天天看點

說說JS中的淺拷貝與深拷貝

在JavaScript中對象的淺拷貝和深拷貝有如下差別:

淺拷貝:僅僅複制對象的引用,而不是對象本身。

深拷貝:複制對象所引用的全部對象。

我在平常練習時,常使用的2種淺拷貝和三種深拷貝的方法。

淺拷貝:

1.自定義實作

function simpleClone(obj) {
                var simpleCloneObj = {};
                for (var i in obj) {
                    simpleCloneObj[i] = obj[i];
                }
                return simpleCloneObj;
            }
           

2.使用Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目标對象,然後傳回目标對象。但是 Object.assign() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。

var simpleClone= Object.assign({}, obj);
           

深拷貝:

1.使用 JSON.parse() 方法

function deepClone(obj) {
                var deepCloneObj = {};
                try {
                    deepCloneObj = JSON.parse(JSON.stringify(obj));
                }
                catch (e) {
                }
                return deepCloneObj;
            }
           

這種方法雖然簡單,但是有如下的問題,它會抛棄對象的constructor,也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。

這種方法能正确處理的對象隻有 Number, String, Boolean, Array, 扁平對象(自己百度一下),即那些能夠被 json 直接表示的資料結構。RegExp對象是無法通過這種方式深拷貝。

  1. 遞歸拷貝

    第二個參數可以用來實作追加。

function deepClone(initalObj, finalObj) {
                var deepCloneObj = finalObj || {};
                for (var i in initalObj) {
                    var prop = initalObj[i];

                    // 避免互相引用對象導緻死循環,如initalObj.a = initalObj的情況
                    if (prop === deepCloneObj) {
                        continue;
                    }

                    if (typeof prop === 'object') {
                        deepCloneObj[i] = (prop.constructor === Array) ? [] : {};
                        arguments.callee(prop, deepCloneObj[i]);
                    } else {
                        deepCloneObj[i] = prop;
                    }
                }
                return deepCloneObj;
            }
           

3.使用Object.create()方法

function deepClone(initalObj, finalObj) {
                var deepCloneObj = finalObj || {};
                for (var i in initalObj) {
                    var prop = initalObj[i];

                    // 避免互相引用對象導緻死循環,如initalObj.a = initalObj的情況
                    if (prop === deepCloneObj) {
                        continue;
                    }

                    if (typeof prop === 'object') {
                        deepCloneObj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
                    } else {
                        deepCloneObj[i] = prop;
                    }
                }
                return deepCloneObj;
            }
           

當直接使用下面的方法也可以達到深拷貝的效果。

var deepCloneObj= Object.create(oldObj);
           

那麼現在來測試一下淺拷貝的方法。

var cloneObj = simpleClone(obj);

                console.log(cloneObj.name);
                console.log(cloneObj.val);

                cloneObj.name = "simpleCloneTest2";
                cloneObj.val = [, , ];

                console.log(cloneObj.val);
                console.log(obj.name);
                console.log(obj.val);
           

執行結果如下:

simpleCloneTest
 [0, 1, 2]

 simpleCloneTest2
 [3, 4, 5]

 simpleCloneTest
 [0, 1, 2]
           

我們發現在cloneObj 更改name和val時,obj的值并沒有更改。這是為什麼呢?我們稍後說明。

我們再來另一種測試。

var obj = {
                    name: "simpleCloneTest",
                    val: [, , ]
                };
                var cloneObj = simpleClone(obj);

                console.log(cloneObj.name);
                console.log(cloneObj.val);

                //cloneObj.name = "simpleCloneTest2";
                //cloneObj.val = [, , ];
                //console.log(cloneObj.name);
                //console.log(cloneObj.val);

                //console.log(obj.name);
                //console.log(obj.val);

                  cloneObj.name = "simpleCloneTest3";
                cloneObj.val[] = ;

                console.log(cloneObj.name);
                console.log(cloneObj.val[]);

                console.log(obj.name);
                console.log(obj.val[]);
           

輸出結果如下:

simpleCloneTest
[0, 1, 2]

simpleCloneTest3
3

simpleCloneTest
3
           

一下内容來自于:https://yq.aliyun.com/articles/35053

這時我們發現clone和原來的obj的值都更改了。這讓我百思不得其解,最終找到一篇很有說服力的文章,進行複制過來(避免文章被删帖)。

outline:

為什麼要說JS中深拷貝與淺拷貝

JS對類型的分類

immutable與mutable

簡單類型檢測

淺拷貝VS深拷貝

為什麼要說JS中深拷貝與淺拷貝

近來在研讀underscore的源碼,發現其中一小段代碼

_.mixin = function(obj) {
    _.each(_.functions(obj), function(name) {
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            var args = [this._wrapped];
            push.apply(args, arguments);
            return result(this, func.apply(_, args));
        };
    });
};
....
_.mixin(_);
           

這段代碼就是要把我們在上綁的很多方法*淺拷貝*一份到.prototype.這裡的淺拷貝引發一些思考.那麼什麼是淺拷貝、什麼是深拷貝?在了解深、淺拷貝之前,我們需要了解下JS中對類型的分類,因為對于不同的類型,我們選擇拷貝的方式也是不一樣的.

**

JS對類型的分類

**

stackoverflow有人提了一個問題:

Stoyan Stefanov in his excellent book ‘Object-Oriented JavaScript’ says:

Any value that doesn’t belong to one of the five primitive types listed above is an object.

Stoyan Stefanov說的這句話,在JS中要麼就是primitive類型,要們就是object類型.

*Primitive

A primitive (primitive value, primitive data type) is data that is not an object and has no methods. In >JavaScript, there are 6 primitive data types: string, number, boolean, null, undefined, symbol (new in ECMAScript 2015).

Most of the time, a primitive value is represented directly at the lowest level of the language implementation.

All primitives are immutable (cannot be changed).*

MDN上指出了JS中的primitive類型一共就是string number boolean null undefined symbol(ES2015)6中類型,其餘的都是object類型.

MDN還說了primitive類型not an object以及has no methods.但是我們平時的使用都是這樣的var str = “hello world”;console.log(str.charAt(0)).這段代碼中明顯str是primitive的變量,按照MDN的說法,str變量應該是not an object并且has no methods的,這裡我們明顯調用了str.charAt方法.是我們錯了還是MDN錯了!!!!那我們再測試下str是不是一個object.Object.prototype.toString.call(str)這段代碼執行的結果居然是[object String].就是說str不僅是object同時還has methods.但是str确實是primitive類型的.

在MDN給出primitive type定義的同時,還給出了Primitive wrapper objects的定義

*Except for null and undefined, all primitive values have object equivalents that wrap around the >primitive values:

String for the string primitive.

Number for the number primitive.

Boolean for the Boolean primitive.

Symbol for the Symbol primitive.

The wrapper’s valueOf() method returns the primitive value.*

也就是說對于這些primitive的類型,确實不是object,并且也沒有methods.執行str.charAt的時候是把string(primitive)類型轉成了String(object)類型.ES5規範中這樣解釋:

說說JS中的淺拷貝與深拷貝

這裡雖然對于一些内部方法的調用我們并不清楚,但是基本也明确當我們在調用str.charAt的時候,JS執行引擎把str變成了String對象,可以執行String上的方法.了解了JS中的類型分類,我們在說一說JS中mutable和immutable.

immutable與mutable

在上一段我們講了JS中的類型分類,總體來說就兩類就是object和primitive,判斷依據就是隻有string、number、boolean、null、undefined、symbol(ES2015)才是primitive的,其餘均為object的.在我們引用MDN的一段話中,還提到了All primitives are immutable (cannot be changed).那麼這句話是什麼意思.所有的primitive都是immutable(不可變的).這句話可能大家看完很不了解.var a = 1;a = 2; a= “hello world”;,這裡a就是primitive的類型,不是可以修改麼,那MDN的這句All primitives are immutable是什麼意思呢.

MDN的這句話其實是沒錯誤的.碰到這種問題,查記憶體位址是最好的辦法,可惜查記憶體位址難度太大,在chrome和nodejs上我都嘗試了,都沒有找個有一個比較直覺的方式去看記憶體位址,如果有讀者了解如何看記憶體可以和我聯系.這裡我們借用JS中的原型鍊來做一個小實驗,也可以間接達到檢視記憶體位址的目的.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>immutalbe&mutable</title>
</head>
<body>
    <script>
/*
* 這裡的關鍵還是這個立即執行函數.立即執行函數與function的定義夾出了一個不可回收的區域,也就是var id = 0;
* (不明白的可以參考我的http://warjiang.github.io/devcat/2016/04/16/JSLecture/關于閉包的文章)
* 然後我們定義一個函數generateId,負責給id自增.
* 下面是關鍵
* 我們在Object.prototype上擴充了一個id的方法.
* 由于JS中的原型鍊,給Object.prototype擴充方法等于說給所有的對象都擴充了id的這個方法.
* 當某個對象調用id的方法會自動順着原型鍊回溯到Object.prototype上的id方法.
* 調用這個方法的時候,方法中的this指向調用這個方法的對象.也就是給這個調用者擴充了id這個方法.
* 由于id在立即執行函數内,generateId和Object.prototype.id外,
* 是以id在執行過程中并不會被釋放,而是從0開始不斷加1
* 參考自http://stackoverflow.com/questions/2020670/javascript-object-id
*/
        (function() {
            var id = ;

            function generateId() { return id++; console.log(id)};

            Object.prototype.id = function() {
                var newId = generateId();

                this.id = function() { return newId; };

                return newId;
            };
        })();
        var a = ;
        console.log(a.id());//0
        a = ;
        console.log(a.id());//1
        a = "hello world";
        console.log(a.id());//2
    </script>
</body>
</html>
           

這裡id從0變為1、2就是說,我們的a指派的過程并不是給a指向的記憶體指派,而是說a重新指向了一個新的值.基于此,MDN所謂的primitive是immutable的,說的是primitive類型的value是immutable的,而variable是mutable的.是以說,對于primitive類型的變量,為其指派,本質上就是讓變量指向新的記憶體.

那麼對于object類型的變量呢.我們也來做一個實驗:

(function() {
    var id = ;

    function generateId() { return id++; console.log(id)};

    Object.prototype.id = function() {
        var newId = generateId();

        this.id = function() { return newId; };

        return newId;
    };
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = o1;
console.log(o2.id());//0
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "o2"}
o2 = {name:'xx'}
console.log(o2.id())//1
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "xx"}
           

從這個例子我們可以看出,對于Object類型的變量,直接指派過程等于說讓變量指向右值記憶體位址.如var o2 = o1,o2就是指向o1指向的記憶體空間.但是當我們修改對象的屬性的時候,就會修改原來記憶體中對象的屬性值.如果o2.name = “o2”會令o1.name ==”o2”.這裡就會引發一個深拷貝、淺拷貝的問題.比如這裡的o2 = o1就是一次淺拷貝.淺拷貝的時候,由于指向的記憶體位址是一樣的,如果直接給對象指派是不存在任何問題的比如var o2 = o1;o2 = {name:’xx’}此時o1.id()傳回0,o2.id()傳回1.但是如果修改對象上的屬性時,就會觸發對象指向的記憶體中的對象的屬性修改.

我們在來看另外一個例子:

(function() {
    var id = ;

    function generateId() { return id++; console.log(id)};

    Object.prototype.id = function() {
        var newId = generateId();

        this.id = function() { return newId; };

        return newId;
    };
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = {}
o2.name = o1.name;
console.log(o2.id());//1
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o1"}
console.log('o2',o2);//o2 Object {name: "o2"}
           

在這個例子中我們對于o2的指派沒有采用o2 = o1;而是采用了o2={},o2.name = o1.name.那麼這樣夠不夠.結合我麼之前說的immutable和mutable,由于name對應的值是string類型的,是immutable的,是以這裡我們拷貝到name是完全夠的,是屬于深拷貝.

看到這裡,相信大家後面我們要做的深、淺拷貝可能有一定的想法了.淺拷貝就是直接指派,或者說不完全的指派(對于對象而言,後面我們會舉例),淺拷貝對于primitive類型的或者說不會直接修改屬性的對象而言比如Function是無害的,但是對于淺拷貝{k1:v1}或者說是[v1,v2]的對象,會出現嚴重的問題,即由于指向同一個記憶體對象,修改屬性等于修改了所有指向該記憶體對象的屬性.

那麼下面我們就需要做類型檢測,對于做深拷貝需要檢測的情況很簡單,如果檢測出來是淺拷貝有害的,我們就做深拷貝,否則直接淺拷貝.

簡單類型檢測

這裡我們隻需要做Object和Array的類型檢測,對于Function、Date等類型的我們都不是很需要.類型檢測我們采用Object.prototype.toString方法

var isType = function(type){
    return function(obj){
        return Object.prototype.toString.call(obj) === '[object '+ type +']';
    }
}
var is = {
    isArray : isType('Array'),
    isObject : isType('Object'),
}
           

有了類型檢測函數,下面我們就可以開心的做深拷貝了.

淺拷貝VS深拷貝

淺拷貝我們之前也說了,這裡直接舉個例子,說明其危害.

(function() {
    var id =;

    function generateId() { return id++; console.log(id)};

    Object.prototype.id = function() {
        var newId = generateId();

        this.id = function() { return newId; }; return newId; };
})();
var o1 = {
  number,                                      
  string: "I am a string",                     
  object: { test1: "Old value" },
  arr: [ "a string", { test2: "Try changing me" } ]
};


var extend = function(result, source) {
    for (var key in source)
        result[key] = source[key];
    return result;
}

var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3

console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//2
console.log('o2',o2.arr.id());//3
           

從id的值上看,o2和o1的内部屬性值,number、string是采用的兩個副本,但是object和arr确實采用的同一個副本.這種情況下如果我們修改o2.object = {name:’o2’}是沒有問題的,由于直接複制本質上上記憶體指向修改的問題.但是如果我們修改o2.object.test1 = “New value”,此時o1和o2會一起變!!!這種情況是我們不想看到的.對于object、array類型的最好做深拷貝(是否深拷貝看應用場景,讀者需要斟酌),結合我們上面的類型檢測,我們把extend函數修改一下

(function() {
    var id =;

    function generateId() { return id++; console.log(id)};

    Object.prototype.id = function() {
        var newId = generateId();

        this.id = function() { return newId; }; return newId; };
})();
var isType = function(type){
    return function(obj){ return Object.prototype.toString.call(obj) === '[object '+ type +']'; }
}
var is = {
    isArray : isType('Array'),
    isObject : isType('Object'),
}

var o1 = {
  number,                                      
  string: "I am a string",                     
  object: { test1: "Old value" },
  arr: [ "a string", { test2: "Try changing me" } ]
};


var extend = function(result, source) {
    for (var key in source){ var copy = source[key]; if(is.isArray(copy)){ //Array deep copy result[key] = extend(result[key] || [], copy); }else if(is.isObject(copy)){ //Object deep copy result[key] = extend(result[key] || {}, copy); }else{ result[key] = copy; } }
    return result;
}

var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3

console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//6
console.log('o2',o2.arr.id());//7

o2.object.test1 = "new Value";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Old value"
console.log(o2,JSON.stringify(o2))//o2.object.test1 == "new Value"

o2.arr[1].test2 = "就不改你";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Try changing me"
console.log(o2,JSON.stringify(o2))//o2.arr[1].test2 == "就不改你"
           

上面内容來自于:

https://yq.aliyun.com/articles/35053

繼續閱讀