淺談JavaScript的淺拷貝與深拷貝

數據類型

在開始拷貝之前,我們從JavaScript的數據類型和內存存放地址講起。
數據類型分爲基本數據類型引用數據類型

基本數據類型主要包括undefined,boolean,number, string,null。

基本數據類型主要存放在棧(stack),存放在棧中的數據簡單,大小確定。存放在棧內存中的數據是直接按值存放的,是可以直接訪問的。

基本數據類型的比較是值的比較,只要它們的值相等就認爲它們是相等的。

let a = 1;
let b = 1;
console.log(a === b); //true

這裏使用嚴格相等,主要是爲了==會進行類型轉換。

let a = true;
let b = 1;
console.log(a == b); //true

引用數據類型也就是對象類型Object type,比如object,array, function,date等。
引用數據類型是存放在堆(heap)內存中的,變量實際上是一個存放在棧內存的指針,這個指針指向堆內存中的地址。

引用數據類型的比較是引用的比較
所以我們每次對js中的引用類型進行操作的時候,都是操作其保存在棧內存中的指針,所以比較兩個引用數據類型,是看它們的指針是否指向同一個對象。

let foo = {a: 1, b: 2};
let bar = {a: 1, b: 2};
console.log(foo === bar); //false

雖然變量foo和變量bar所表示的內容是一樣的,但是其在內存中的位置不一樣,也就是變量foo和bar在棧內存中存放的指針指向的不是堆內存中的同一個對象,所以它們是不相等的。

棧和堆的區別

其實淺拷貝和深拷貝的主要區別就是數據在內存中的存儲類型不同。
棧和堆都是內存中劃分出來用來存儲的區域。
棧(stack) 是自動分配的內存空間,由系統自動釋放;
堆(heap) 則是動態分配的內存,大小不定也不會自動釋放。

淺拷貝

如果你的對象只有值類型的屬性,可以用ES6的新語法Object.assign(...)實現拷貝

//淺拷貝
let obj = {foo: 'foo', bar: "bar"};

let shallowCopy = { ...obj };  //{foo: 'foo', bar: "bar"}
//淺拷貝
let obj = {foo: "foo", bar: "bar"};

let shallowCopy = Object.assign({}, obj); //{foo: 'foo', bar: "bar"}

我們接着來看下淺拷貝和賦值(=) 的區別

let obj = {foo: "foo", bar: "bar"};
let shallowCopy = { ...obj }; //{foo: 'foo', bar: "bar"}
let obj2= obj; //{foo: 'foo', bar: "bar"}
shallowCopy.foo = 1;
obj2.bar = 1
console.log(obj); //{foo: "foo", bar: 1};

可以看出賦值得到的obj2和最初的obj指向的是同一對象,改變數據會使原數據一同改變。
而淺拷貝得到的shallowCopy則將obj的第一層數據對象拷貝到了,和源數據不指向同一對象,改變不會使原數據一同改變。

深拷貝

但是Object.assign(...)方法只能進行對象的一層拷貝。對於對象的屬性是對象的對象,他不能進行深層拷貝。
迷糊了吧?直接代碼解釋

let foo = {a: 0, b: {c: 0}};
let copy = { ...foo };
copy.a = 1;
copy.b.c = 1;
console.log(copy); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 1}};

可以看到,使用Object.assign(...)方法拷貝的copy對象的二層對象發生改變的時候,依然會使原數據一同改變。
這裏,對存在子對象的對象進行拷貝的時候,就是深拷貝了。

淺拷貝:將B對象拷貝到A對象中,不包括B裏面的子對象
深拷貝:將B對象拷貝到A對象中,包括B裏面的子對象

深拷貝實現的方法:

這裏只說幾種常用方法,
1.JSON.parse(JSON.stringify( ));

    let foo = {a: 0, b: {c: 0}};
       let copy = JSON.parse(JSON.stringify(foo));
       copy.a = 1;
       copy.b.c = 1;

       console.log(copy); //{a: 1, b: {c: 1}};
       console.log(foo); //{a: 0, b: {c: 0}};

2.遞歸拷貝

    function deepCopy(initialObj, finalObj){
            let obj = finalObj || {};
            for(let i in initialObj) {
                if(typeof initialObj[i] === "object") {
                    obj[i] = (initialObj[i].constructor === Array) ? [] : {};
                    arguments.callee(initialObj[i], obj[i]);
                }else{
                    obj[i] = initialObj[i];
                }
            }
            return obj;
        }


        var foo = {a: 0, b: {c: 0}};
        var str = {};

        deepCopy(foo, str);

        str.a = 1;
        str.b.c = 1;

        console.log(str); //{a: 1, b: {c: 1}};
        console.log(foo); //{a: 0, b: {c: 0}};

上述代碼確實可以實現深拷貝,但是當遇到兩個互相引用的對象,會出現死循環的情況。

爲了避免相互引用的對象導致死循環的情況,則應該在遍歷的時候判斷是否相互引用對象,如果是則退出循環。

改進版代碼如下:

        function deepCopy(initialObj, finalObj) {
            let obj = finalObj || {};
            for(let i in initialObj){
                let prop = initialObj[i];//避免相互引用導致死循環,如initialObj.a = initialObj的情況
                if(prop === obj) {
                    continue;
                }
                if(typeof prop === 'object'){
                    obj[i] = (prop.constructor === Array) ? [] : {};
                    arguments.callee(prop, obj[i]);
                }else{
                    obj[i] = prop;
                }
            }
            return obj;
        }


        var foo = {a: 0, b: {c: 0}};
        var str = {};

        deepCopy(foo, str);

        str.a = 1;
        str.b.c = 1;

        console.log(str); //{a: 1, b: {c: 1}};
        console.log(foo); //{a: 0, b: {c: 0}};

3.使用Object.create( )方法

        function deepCopy(initialObj, finalObj){
            let obj = finalObj || {};
            for(let i in initialObj){
                let prop = initialObj[i];  //避免相互引用對象導致死循環,如initialObj[i].a = initialObj的情況
                if(prop === obj){
                    continue;
                }
                if(typeof prop === "object"){
                    obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
                }else{
                    obj[i] = prop;
                }
            }
            return obj;
        }


        let foo = {a: 0, b: {c: 0}};
        let str = {};

        deepCopy(foo, str);

        str.a = 1;
        str.b.c = 1;

        console.log(str); //{a: 1, b: {c: 1}};
        console.log(foo); //{a: 0, b: {c: 0}};

4.jQuery

jQuery提供了一個$.extend可以實現深拷貝

var $ = require('jquery);

let foo = {a: 0, b: {c: 0}};
let str = $.extend(true, {}, foo);
str.a = 1;
str.b.c = 1;

console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};

5.lodash
另一個很熱門的函數庫lodash,也有提供_.cloneDeep用來深拷貝

var _ = require('lodash);

let foo = {a: 0, b: {c: 0}};
let str = _.cloneDeep(foo);
str.a = 1;
str.b.c = 1;

console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};

侷限性
所有深拷貝的方法並不適用於所有類型的對象。當然還有其他的坑,像是如何拷貝原型鏈上的屬性?如何拷貝不可枚舉屬性等等。
雖然lodash是最安全的通用深拷貝方法,但如果你自己動手,可能會依據需求寫出最適合你的更高效的深拷貝的方法:

//適用於日期的簡單深拷貝的例子
        function deepCopy(obj) {
            let copy;

            //處理三種簡單的引用數據類型加上undefined和null
            if(obj == null || typeof obj != "object") return obj;

            //處理Date
            if(obj instanceof Date){
                copy = new Date();
                copy.setTime(obj.getTime());
                return copy;
            }

            //處理Array
            if(obj instanceof Array) {
                copy = [];
                for(let i = 0; i < obj.length; i++) {
                    copy[i] = deepCopy(obj[i]);
                }
                return copy;
            }

            //處理Function
            if(obj instanceof Function) {
                copy = function() {
                    return obj.apply(this, arguments);
                }
                return copy;
            }

            //處理Object
            if(obj instanceof Object) {
                copy = {};
                for(let attr in obj) {
                    if(obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr]);
                }
                return copy;
            }


            throw new Error("無法深拷貝" +obj.constructor+ "類型的數據")
        }

快樂拷貝😁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章