也來談一談js的淺複製和深複製

1.淺複製VS深複製

本文中的複製也能夠稱爲拷貝,在本文中認爲複製和拷貝是相同的意思。另外,本文只討論js中複雜數據類型的複製問題(Object,Array等),不討論基本數據類型(null,undefined,string,number和boolean),這些類型的值自己就存儲在棧內存中(string類型的實際值仍是存儲在堆內存中的,可是js把string當作基本類型來處理 ),不存在引用值的狀況。javascript

淺複製和深複製均可以實如今已有對象的基礎上再生一份的做用,可是對象的實例是存儲在堆內存中而後經過一個引用值去操做對象,由此複製的時候就存在兩種狀況了:複製引用和複製實例,這也是淺複製和深複製的區別所在。java

淺複製:淺複製是複製引用,複製後的引用都是指向同一個對象的實例,彼此之間的操做會互相影響jquery

深複製:深複製不是簡單的複製引用,而是在堆中從新分配內存,而且把源對象實例的全部屬性都進行新建複製,以保證深複製的對象的引用圖不包含任何原有對象或對象圖上的任何對象,複製後的對象與原來的對象是徹底隔離的正則表達式

由深複製的定義來看,深複製要求若是源對象存在對象屬性,那麼須要進行遞歸複製,從而保證複製的對象與源對象徹底隔離。然而還有一種能夠說處在淺複製和深複製的粒度之間,也是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. 深複製

深複製的狀況比較複雜一些,咱們先從一些比較簡單的狀況提及:oop

3.1 Array的slice和concat方法

Array的slice和concat方法都會返回一個新的數組實例,可是這兩個方法對於數組中的對象元素卻沒有執行深複製,而只是複製了引用了,所以這兩個方法並非真正的深複製,經過如下代碼進行理解:this

    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返回的不一樣的數組實例,這與直接的引用複製是不一樣的。prototype

    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;
    }
相關文章
相關標籤/搜索