目前這個部落格是用 jekyll 架在 github page 上,但因為 jekyll 功能不多,所以決定把部落格遷移到 Medium 上,以後這邊就不會再發表新文章,歡迎大家到 Medium 上追蹤我~

[Javascript] 關於 JS 中的淺拷貝和深拷貝

因為前幾天在寫專案的時候要用到深拷貝
所以就去查了一些相關的資料
趁這個機會順便做點筆記


基本型別(Primitive Type) VS 物件(Object)

在 JS 中有一些基本型別像是NumberStringBoolean
而物件就是像這樣的東西{ name: 'Larry', skill: 'Node.js' }
物件跟基本型別最大的不同就在於他們的傳值方式


基本型別是傳 value,像是這樣

var a = 10;
var b = a;
b = 20;

console.log(a); // 10
console.log(b); // 20

在修改b時並不會改到a


但物件就不同,物件傳的是 reference

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = obj1;
obj2.b = 100;

console.log(obj1); // { a: 10, b: 100, c: 30 } <-- b 被改到了
console.log(obj2); // { a: 10, b: 100, c: 30 }

複製一份obj1叫做obj2
然後把obj2.b改成100
但卻不小心改到obj1.b
因為他們根本是同一個物件
這就是所謂的淺拷貝


要避免這樣的錯誤發生就要寫成這樣

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
obj2.b = 100;

console.log(obj1); // { a: 10, b: 20, c: 30 } <-- b 沒被改到
console.log(obj2); // { a: 10, b: 100, c: 30 }

這樣就是深拷貝
不會改到原本的obj1


淺拷貝(Shallow Copy) VS 深拷貝(Deep Copy)

淺拷貝只複製指向某個物件的指標
而不複製物件本身
新舊物件還是共用同一塊記憶體

但深拷貝會另外創造一個一模一樣的物件
新物件跟原物件不共用記憶體
修改新物件不會改到原物件


如何做到 Deep Copy

要完全複製又不能修改到原物件
這時候就要用 Deep Copy
這裡會介紹幾種 Deep Copy 的方式

自己手動複製

像上面的範例
obj1的屬性一個個複製到obj2

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
obj2.b = 100;

console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2); // { a: 10, b: 100, c: 30 }

但這樣很麻煩要自己慢慢複製
而且這樣其實不是 Deep Copy
如果像下面這個狀況

var obj1 = { body: { a: 10 } };
var obj2 = { body: obj1.body };
obj2.body.a = 20;

console.log(obj1); // { body: { a: 20 } } <-- 被改到了
console.log(obj2); // { body: { a: 20 } }
console.log(obj1 === obj2); // false
console.log(obj1.body === obj2.body); // true

雖然obj1obj2是不同物件
但他們會共用同一個obj1.body
所以修改obj2.body.a時也會修改到舊的


Object.assign

Object.assign是 ES6 的新函式
可以幫助我們達成跟上面一樣的功能

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;

console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2); // { a: 10, b: 100, c: 30 }

Object.assign({}, obj1)的意思是先建立一個空物件{}
接著把obj1中所有的屬性複製過去
所以obj2會長得跟obj1一樣
這時候再修改obj2.b也不會影響obj1

因為Object.assign跟我們手動複製的效果相同
所以一樣只能處理深度只有一層的物件
沒辦法做到真正的 Deep Copy
不過如果要複製的物件只有一層的話可以考慮使用他


轉成 JSON 再轉回來

JSON.stringify把物件轉成字串
再用JSON.parse把字串轉成新的物件

var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;

console.log(obj1); // { body: { a: 10 } } <-- 沒被改到
console.log(obj2); // { body: { a: 20 } }
console.log(obj1 === obj2); // false
console.log(obj1.body === obj2.body); // false

這樣做是真正的 Deep Copy
但只有可以轉成JSON格式的物件才可以這樣用
function沒辦法轉成JSON

var obj1 = { fun: function(){ console.log(123) } };
var obj2 = JSON.parse(JSON.stringify(obj1));

console.log(typeof obj1.fun); // 'function'
console.log(typeof obj2.fun); // 'undefined' <-- 沒複製

要複製的function會直接消失
所以這個方法只能用在單純只有資料的物件


jquery

相信大家應該都很熟悉 jquery 這個 library
jquery 有提供一個$.extend可以用來做 Deep Copy

var $ = require('jquery');

var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};

var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

lodash

另外一個很熱門的函式庫 lodash
也有提供_.cloneDeep用來做 Deep Copy

var _ = require('lodash');

var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};

var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

這是我比較推薦的方法
效能還不錯
使用起來也很簡單


如果有人有更好的方法也可以跟我說
我會把它補充上去讓更多人知道~

GitHub:@Larry850806
FaceBook 粉專:賴瑞的程式筆記
如果有新文章或是看到好的文章也會分享在粉專