[筆記] Javascript 深拷貝與淺拷貝


這就得先來談談 Javascript 的基本型別(Primitive Type) vs 物件(Object)

Data Type分別為:
  • Primitive Type: Number, String, Boolean, Null, Undefined
  • Object: Object, Array, Function, Date, Regx
基本型別和物件的傳值方式不同:
  • 基本型別是 by Value
  • 物件是by reference

如果是 Primitive Type
var copyMe = 1234;

function copy(o) {
    var copyObj = o;

    // 直接給值
    copyObj = 55;

    console.log(o);      //1234 不變
    console.log(copyObj);//55

}
copy(copyMe);
如果是 By Value 的方式,不會改變原本的。

假如有個需要複製的物件(接下來都用這個來舉例)
var copyThis = {a:11, b:21, c:33};
以下舉例
function copy1(o) {
    var copyObj = o;

    // 直接給值
    copyObj = 55;

    console.log(o);      //{a:11, b:21, c:33} 
    console.log(copyObj);//55
//-----------------------------------------------
    copyObj2 = o;
    copyObj2.a = 60;

    console.log(o);       //{a:60, b:21, c:33} 被改變
    console.log(copyObj2);//{a:60, b:21, c:33}

}
copy1(copyThis);
當直接給值55的時候,又重新定義了 copyObj,所以不影響;
但是在下半段,修改 copyObj2.a 連動到 o.a,因為他們指向同一個節點位置,所以原本的也被改變了。

So...
  • 淺拷貝:就是物件指向某個指標(Node),但還是共用同一塊記憶體。
  • 深拷貝:就是另外建立新的物件,有同樣的內容,且不共用記憶體。

但還是有些例外
function copy2(o) {
    var copyObj = o;

    // 直接給值
    o = {a:33, b:55, c:77};

    console.log(o);      //{a:33, b:55, c:77} 被改變
    console.log(copyObj);//{a:11, b:21, c:33} 不變
}
copy2(copyThis);
object literal 的方式指定物件的值,那麼就會是 by value

咦!什麼是 object literal?
請參考 [筆記] 談談JavaScript中的物件建立(Object) - Part 2 | 利用大括號{}建立物件

如果不更動到原本的呢?
function copy3(o) {
    var copyObj = {};
    copyObj = {
        a:o.a,
        b:o.b,
        c:o.c
    };
    copyObj.a = 14;

    console.log(o); // {a:11, b:21, c:33}
    console.log(copyObj); //{a:14, b:21, c:33}

}
copy3(copyThis);
雖然這樣可以避免,但這樣一個個列出太麻煩,且這樣不是deep copy

那麼,如果使用Object.create()??
function copy6(o) {
    var copyObj = Object.create(o);
    copyObj.a = 77;
    copyObj.b = 66;
    console.log(o); // {a:11, b:21, c:33}
    console.log(copyObj.a); // {a: 77, b: 66, c: 33}

}
copy6(copyThis);
在這邊請注意,可能發生以下狀況:當Object.create()遇上 Array
var copyArr = [{a: 11, b: 21, c: 33}];
function copy7(o) {
    var copyObj = Object.create(o);
    copyObj[0].a = 88;

    console.log(o); // {a: 88, b: 21, c: 33} //被改變了...
    console.log(copyObj); // a: 88, b: 21, c: 33}

}
copy7(copyArr);
為什麼呢?Javascript Arrays created with Object.create - not real Arrays?
總結就是,Object.create()而言,是建立一個Array屬性的「物件」,這又回到 by reference 的問題了。

恩⋯⋯那如果反過來,Array.form()Object.create()合併使用?
var copyArr7_1 = [11, 21, 33];
function copy7_1(o) {
    var copyObj = Array.from(Object.create(o));
    copyObj[0] = 88;

    console.log(o); // [11, 21, 33]
    console.log(copyObj); // [88, 21, 33]

}
copy7_1(copyArr7_1);
此時發現,原本的不會被改變,但是如果遇到以下狀況⋯⋯
var copyArr7_2 = [{a:11}, {b:21}, {c:33}];
function copy7_2(o) {
    var copyObj = Array.from(Object.create(o));
    copyObj[0].a = 88;

    console.log(o); // {a: 88, b: 21, c: 33} //被改變了...
    console.log(copyObj); // {a: 88, b: 21, c: 33}

}
copy7_2(copyArr7_2);
這時就得探討Array.form()

解法:先JSON.stringify轉成 JSON 格式,再JSON.parse解開。
function copy4(o) {
    var copyObj = JSON.parse(JSON.stringify(o));

    copyObj.a = 14;

    console.log(o); // {a:11, b:21, c:33}
    console.log(copyObj); // {a: 14, b: 21, c: 33}

}

copy4(copyThis);
解法:使用 ES6 新的函式 Object.assign()
function copy5(o) {
    var copyObj = Object.assign({},o);

    copyObj.a = 14;

    console.log(o); // {a:11, b:21, c:33}
    console.log(copyObj); // {a: 14, b: 21, c: 33}

}

copy5(copyThis);
Object.assign({},o){}意思是另建立一個空物件,再將o屬性質複製過去,因此此方法只針對一層的物件有用。可以用在一些簡單的需求。

當然,運用 library 也可以快速達到 deep copy 效果:
  • lodash 的 cloneDeep()
  • jQuery 的 extend()

最後,放上小小的實作⋯⋯
//訂單結帳
var shoppingCar = [
    {usr_id:'AB123',ord_id:'NA20171212',items:[
        {item:'Pants', it_price:'23'},
        {item:'Shirt', it_price:'21'},
        {item:'Top', it_price:'15'},
         function contactMethod () {
            return 'AAAAA';
        }]
    },
    {name:'Aero',mobile:'+8869000000',add:'Taipei, Taiwan'}
];

function checkout(order, rate) {
    var output = {};

    for (var key in order) {
        var o = order[key];

        if (key === "it_price"){
            o = discount(rate, o);
        }

        output[key] = o;

        if (typeof o === 'function') {
            output[key] = new Function("return "+o.toString());
        }

        if (typeof o === 'object') {
            output[key] = checkout(o, rate);
        }
    }

    return output;
}

function discount(rate, price) {
    var output = price * rate/100;
    return output;
}

var myOrder = checkout(shoppingCar, 70);
console.log(myOrder);
console.log(shoppingCar);

參考資料
https://pjchender.blogspot.tw/2016/03/javascriptby-referenceby-value.html
https://www.codementor.io/avijitgupta/deep-copying-in-js-7x6q8vh5d
http://larry850806.github.io/2016/09/20/shallow-vs-deep-copy/
https://github.com/wengjq/Blog/issues/3