0%

JavaScript 核心 (26) - 物件 - 淺層複製及深層複製

淺層複製 Shallow Copy

複製一個或多個物件自身所有可數的屬性到另一個目標物件。回傳的值為該目標物件。
上面不容易理解,直接上範例比較清楚:

1
2
3
4
5
6
7
8
9
/* 問題 */
var a = {
name: 'Cloud',
};

var b = a;

b.name = '測試';
console.log(a.name); /* 測試 */
  • Object.assign
1
2
3
4
5
6
7
8
9
var a = {
name: 'Cloud',
};

var b = Object.assign({}, a);

b.name = '測試 Object.assign';
console.log(a.name); /* Cloud */
console.log(b.name); /* 測試 Object.assign */

Object.assign 也可改寫成 ES6:

1
2
3
4
5
6
7
8
9
var a = {
name: 'Cloud',
};

var b = { ... a };

b.name = '測試 Object.assign';
console.log(a.name); /* Cloud */
console.log(b.name); /* 測試 Object.assign */

然後淺層複製只會複製第一層物件,若是修改到內層物件屬性,仍會改變原本物件的屬性值

1
2
3
4
5
6
7
8
9
10
11
12
var a = {
name: 'Cloud',
age: {
number: 26,
}
};

var b = { ... a };

b.age.number = '測試';
console.log(b.age.number); /* 測試 */
console.log(a.age.number); /* 測試 */

這是因為內層的 age 仍指向原本的參考,並不會一起複製勒。

深層複製 Deep Copy

只需要將物件轉換成純值,在轉換成物件即可

  • JSON.parse() with JSON.stringify()
1
2
3
4
5
6
7
8
9
10
11
12
var a = {
name: 'Cloud',
age: {
number: 26,
}
};

var b = JSON.parse(JSON.stringify(a));

b.age.number = '測試';
console.log(b.age.number); /* 測試 */
console.log(a.age.number); /* 26 */

然而此方法僅限用於 json 格式,屬性值不能是undefinedNaN或是function,否則會無法轉換。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = {
name: 'Cloud',
age: {
number: 26
},
fn: function() { /* 消失 */
console.log('測試1');
},
un: undefined, /* 消失 */
nan: NaN, /* 變成 null */
};

var b = JSON.parse(JSON.stringify(a)); /* {name: "Cloud", age: {…}, nan: null} */

console.log(b);

深層複製完美解法

一般會參考第三方工具的深層複製寫法,當然如果是高手等級,就乾脆自己寫一個插件也不是不行。

  • Underscore — _.clone()
    此方法只能算是半個深層複製,也就是說不會完全複製物件內的參考。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 請先引入 Underscore,否則會出錯 */
var a = {
name: 'Cloud',
age: {
number: 26
},
fn: function() {
console.log('1');
},
un: undefined,
nan: NaN,
};

var b = _.clone(a);

console.log(b); /* {name: "Cloud", age: {…}, un: undefined, nan: NaN, fn: ƒ} */

/* 檢測是否為 深層複製 */
var x = {
a: 1,
b: { z: 0 }
};
var y = _.clone(x);
x.b.z = 100;
y.b.z // 100
console.log(x === y); /* false */
console.log(x.b === y.b); /* true 物件內層屬性依然指向相同參考 */
  • jQuery — $.extend()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 請先引入 jQuery,否則會出錯 */
var a = {
name: 'Cloud',
age: {
number: 26
},
fn: function() {
console.log('1');
},
un: undefined,
nan: NaN,
};

var b = $.extend(true, {}, a);

console.log(b); /* {name: "Cloud", age: {…}, nan: NaN, fn: ƒ} */

注意,undefined 不會被複製到哩。

  • lodash — .clone() or.cloneDeep()
    _.clone(obj, true) 等於 _.cloneDeep(obj)
    lodash 在深層複製上更優於 jQuery、Underscore,畢竟原始碼上就多了幾百行啊。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 請先引入 lodash,否則會出錯 */
var a = {
name: 'Cloud',
age: {
number: 26
},
fn: function() {
console.log('1');
},
un: undefined,
nan: NaN,
};

var b = _.cloneDeep(a);

console.log(b); /* {name: "Cloud", age: {…}, un: undefined, nan: NaN, fn: ƒ} */

lodash的_.cloneDeep()幾乎完美複製原本的物件內容。

效能&功能比較

特性jQuerylodashJSON.parse
瀏覽器兼容性IE6+ (1.x) & IE9+ (2.x)IE6+ (Compatibility) & IE9+ (Modern)IE8+
能夠深複製內層所有屬性值回傳異常 RangeError: Maximum call stack size exceeded支持回傳異常 TypeError: Converting circular structure to JSON
對 Date, RegExp 的深層複製支持×支持×
對 ES6 新引入的標準對象的深層複製支持×支持×
複製陣列的属性×僅支持RegExp#exec返回的陣列结果×
是否保留非原生對象的類型×××
複製不可枚舉元素×××
複製函數×××
方法jQuerylodashJSON.parse
平均478293656

倘若需要使用到深層複製的話,還是會先以 loadsh 為優先哩。

參考資料

六角學院 - JavaScript 核心篇
JavaScript 核心觀念(30)-物件-淺層複製及深層複製
深入剖析 JavaScript 的深复制