1. 程式人生 > >說說JS中的淺拷貝與深拷貝

說說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 = [3, 4, 5];

                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: [0, 1, 2]
                };
                var cloneObj = simpleClone(obj);

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

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

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

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

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

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

輸出結果如下:

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規範中這樣解釋:
這裡寫圖片描述

這裡雖然對於一些內部方法的呼叫我們並不清楚,但是基本也明確當我們在呼叫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 = 0;

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

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

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

                return newId;
            };
        })();
        var a = 1;
        console.log(a.id());//0
        a = 2;
        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 = 0;

    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 = 0;

    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 = 0;

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

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

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

        return newId;
    };
})();
var o1 = {
  number: 1,                                      
  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 = 0;

    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: 1,                                      
  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 == "就不改你"