本文中的復制也可以稱為拷貝,在本文中認為復制和拷貝是相同的意思。另外,本文只討論js中復雜數據類型的復制問題(Object,Array等),不討論基本數據類型(null,undefined,string,number和boolean),這些類型的值本身就存儲在棧內存中(string類型的實際值還是存儲在堆內存中的,但是js把string當做基本類型來處理 ),不存在引用值的情況。
淺復制和深復制都可以實現在已有對象的基礎上再生一份的作用,但是對象的實例是存儲在堆內存中然後通過一個引用值去操作對象,由此復制的時候就存在兩種情況了:復制引用和復制實例,這也是淺復制和深復制的區別所在。
淺復制:淺復制是復制引用,復制後的引用都是指向同一個對象的實例,彼此之間的操作會互相影響
深復制:深復制不是簡單的復制引用,而是在堆中重新分配內存,並且把源對象實例的所有屬性都進行新建復制,以保證深復制的對象的引用圖不包含任何原有對象或對象圖上的任何對象,復制後的對象與原來的對象是完全隔離的
由深復制的定義來看,深復制要求如果源對象存在對象屬性,那麼需要進行遞歸復制,從而保證復制的對象與源對象完全隔離。然而還有一種可以說處在淺復制和深復制的粒度之間,也是jQuery的extend方法在deep參數為false時所謂的“淺復制”,這種復制只進行一個層級的復制:即如果源對象中存在對象屬性,那麼復制的對象上也會引用相同的對象。這不符合深復制的要求,但又比簡單的復制引用的復制粒度有了加深。
本文認為淺復制就是簡單的引用復制,這種情況較很簡單,通過如下代碼簡單理解一下:
var src = { name:"src" } //復制一份src對象的應用 var target = src; target.name = "target"; console.log(src.name); //輸出target
target對象只是src對象的引用值的復制,因此target的改變也會影響src。
深復制的情況比較復雜一些,我們先從一些比較簡單的情況說起:
Array的slice和concat方法都會返回一個新的數組實例,但是這兩個方法對於數組中的對象元素卻沒有執行深復制,而只是復制了引用了,因此這兩個方法並不是真正的深復制,通過以下代碼進行理解:
var array = [1,2,3]; var array_shallow = array; var array_concat = array.concat(); var array_slice = array.slice(0); console.log(array === array_shallow); //true console.log(array === array_slice); //false console.log(array === array_concat); //false
可以看出,concat和slice返回的不同的數組實例,這與直接的引用復制是不同的。
var array = [1, [1,2,3], {name:"array"}]; var array_concat = array.concat(); var array_slice = array.slice(0); //改變array_concat中數組元素的值 array_concat[1][0] = 5; console.log(array[1]); //[5,2,3] console.log(array_slice[1]); //[5,2,3] //改變array_slice中對象元素的值 array_slice[2].name = "array_slice"; console.log(array[2].name); //array_slice console.log(array_concat[2].name); //array_slice
通過代碼的輸出可以看出concat和slice並不是真正的深復制,數組中的對象元素(Object,Array等)只是復制了引用
JSON對象是ES5中引入的新的類型(支持的浏覽器為IE8+),JSON對象parse方法可以將JSON字符串反序列化成JS對象,stringify方法可以將JS對象序列化成JSON字符串,借助這兩個方法,也可以實現對象的深復制。
var source = { name:"source", child:{ name:"child" } } var target = JSON.parse(JSON.stringify(source)); //改變target的name屬性 target.name = "target"; console.log(source.name); //source console.log(target.name); //target //改變target的child target.child.name = "target child"; console.log(source.child.name); //child console.log(target.child.name); //target child
從代碼的輸出可以看出,復制後的target與source是完全隔離的,二者不會相互影響。
這個方法使用較為簡單,可以滿足基本的深復制需求,而且能夠處理JSON格式能表示的所有數據類型,但是對於正則表達式類型、函數類型等無法進行深復制(而且會直接丟失相應的值),同時如果對象中存在循環引用的情況也無法正確處理
3.3 jQuery中的extend復制方法
jQuery中的extend方法可以用來擴展對象,這個方法可以傳入一個參數:deep(true or false),表示是否執行深復制(如果是深復制則會執行遞歸復制),我們首先看一下jquery中的源碼(1.9.1)
jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( length === i ) { target = this; --i; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; };
這個方法是jQuery中重要的基礎方法之一,可以用來擴展jQuery對象及其原型,也是我們編寫jQuery插件的關鍵方法,事實上這個方法基本的思路就是如果碰到array或者object的屬性,那麼就執行遞歸復制,這也導致對於Date,Function等引用類型,jQuery的extend也無法支持。下面我們大致分析一下這個方法:
(1)第1-6行定義了一些局部變量,這些局部變量將在以後用到,這種將函數中可能用到的局部變量先統一定義好的方式也就是“單var”模式
(2)第9-13行用來修正deep參數,jQuery的這個方法是將deep作為第一個參數傳遞的,因此這裡就判斷了第一個參數是不是boolean類型,如果是,那麼就調整target和i值,i值表示第一個source對象的索引
(3)第17-19行修正了target對象,如果target的typeof操作符返回的不是對象,也不是函數,那麼說明target傳入的是一個基本類型,因此需要修正為一個空的對象字面量{}
(4)第22-25行來處理只傳入了一個參數的情況,這個方法在傳入一個參數的情況下為擴展jQuery對象或者其原型對象
(5)從27行開始使用for in去遍歷source對象列表,因為extend方法是可以傳入多個source對象,取出每一個source對象,然後再嵌套一個for in循環,去遍歷某個source對象的屬性
(6)第32行分別取出了target的當前屬性和source的當前屬性,35-38行的主要作用在於防止深度遍歷時的死循環。然而如果source對象本身存在循環引用的話,extend方法依然會報堆棧溢出的錯誤
(7)第41行的if用來處理深復制的情況,如果傳入的deep參數為true,並且當前的source屬性值是plainObject(使用對象字面量創建的對象或new Object()創建的對象)或數組,則需要進行遞歸深復制
(8)第42-48根據copy的類型是plainObject還是Array,對src進行處理:如果copy是數組,那麼src如果不是數組,就改寫為一個空數組;如果copy是chainObject,那麼src如果不是chainObject,就改寫為{}
(9)如果41行的if條件不成立,那麼直接把target的src屬性用copy覆蓋
jQuery的extend方法使用基本的遞歸思路實現了深度復制,但是這個方法也無法處理source對象內部循環引用的問題,同時對於Date、Function等類型的值也沒有實現真正的深度復制,但是這些類型的值在重新定義時一般都是直接覆蓋,所以也不會對源對象造成影響,因此一定程度上也符合深復制的條件
根據以上的思路,自己實現一個copy,可以傳入deep參數表示是否執行深復制:
//util作為判斷變量具體類型的輔助模塊 var util = (function(){ var class2type = {}; ["Null","Undefined","Number","Boolean","String","Object","Function","Array","RegExp","Date"].forEach(function(item){ class2type["[object "+ item + "]"] = item.toLowerCase(); }) function isType(obj, type){ return getType(obj) === type; } function getType(obj){ return class2type[Object.prototype.toString.call(obj)] || "object"; } return { isType:isType, getType:getType } })(); function copy(obj,deep){ //如果obj不是對象,那麼直接返回值就可以了 if(obj === null || typeof obj !== "object"){ return obj; } //定義需要的局部變臉,根據obj的類型來調整target的類型 var i, target = util.isType(obj,"array") ? [] : {},value,valueType; for(i in obj){ value = obj[i]; valueType = util.getType(value); //只有在明確執行深復制,並且當前的value是數組或對象的情況下才執行遞歸復制 if(deep && (valueType === "array" || valueType === "object")){ target[i] = copy(value); }else{ target[i] = value; } } return target; }