1.淺複製VS深複製 本文中的複製也可以稱為拷貝,在本文中認為複製和拷貝是相同的意思。另外,本文只討論js中複雜數據類型的複製問題(Object,Array等),不討論基本數據類型(null,undefined,string,number和boolean),這些類型的值本身就存儲在棧記憶體中(stri ...
1.淺複製VS深複製
本文中的複製也可以稱為拷貝,在本文中認為複製和拷貝是相同的意思。另外,本文只討論js中複雜數據類型的複製問題(Object,Array等),不討論基本數據類型(null,undefined,string,number和boolean),這些類型的值本身就存儲在棧記憶體中(string類型的實際值還是存儲在堆記憶體中的,但是js把string當做基本類型來處理 ),不存在引用值的情況。
淺複製和深複製都可以實現在已有對象的基礎上再生一份的作用,但是對象的實例是存儲在堆記憶體中然後通過一個引用值去操作對象,由此複製的時候就存在兩種情況了:複製引用和複製實例,這也是淺複製和深複製的區別所在。
淺複製:淺複製是複製引用,複製後的引用都是指向同一個對象的實例,彼此之間的操作會互相影響
深複製:深複製不是簡單的複製引用,而是在堆中重新分配記憶體,並且把源對象實例的所有屬性都進行新建複製,以保證深複製的對象的引用圖不包含任何原有對象或對象圖上的任何對象,複製後的對象與原來的對象是完全隔離的
由深複製的定義來看,深複製要求如果源對象存在對象屬性,那麼需要進行遞歸複製,從而保證複製的對象與源對象完全隔離。然而還有一種可以說處在淺複製和深複製的粒度之間,也是jQuery的extend方法在deep參數為false時所謂的“淺複製”,這種複製只進行一個層級的複製:即如果源對象中存在對象屬性,那麼複製的對象上也會引用相同的對象。這不符合深複製的要求,但又比簡單的複製引用的複製粒度有了加深。
2. 淺複製
本文認為淺複製就是簡單的引用複製,這種情況較很簡單,通過如下代碼簡單理解一下:
var src = { name:"src" } //複製一份src對象的應用 var target = src; target.name = "target"; console.log(src.name); //輸出target
target對象只是src對象的引用值的複製,因此target的改變也會影響src。
3. 深複製
深複製的情況比較複雜一些,我們先從一些比較簡單的情況說起:
3.1 Array的slice和concat方法
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等)只是複製了引用
3.2 JSON對象的parse和stringify
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等類型的值也沒有實現真正的深度複製,但是這些類型的值在重新定義時一般都是直接覆蓋,所以也不會對源對象造成影響,因此一定程度上也符合深複製的條件
3.4 自己實現一個copy方法
根據以上的思路,自己實現一個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; }