javascript
JavaScript 中的对象拷贝(深拷贝、浅拷贝)
對象是 JavaScript 的基本塊。對象是屬性的集合,屬性是鍵值對。JavaScript 中的幾乎所有對象都是位于原型鏈頂部?Object?的實例。
介紹
如你所知,賦值運算符不會創(chuàng)建一個對象的副本,它只分配一個引用,我們來看下面的代碼:
let obj = {a: 1,b: 2, }; let copy = obj;obj.a = 5; console.log(copy.a); // 結(jié)果 // copy.a = 5;—>?Edit on JS Bin
obj?變量是一個新對象初始化的容器。copy?變量指向同一個對象,是對該對象的引用。所以現(xiàn)在有兩種方式可以訪問這個?{ a: 1, b: 2, }?對象。你必須通過?obj?變量或?copy?變量,無論你是通過何種方式對這個對象進行的任何操作都會影響該對象。
不變性(Immutability)最近被廣泛地談?wù)?#xff0c;這個很重要!上面示例的方法消除了任何形式的不變性,如果原始對象被你的代碼的另一部分使用,則可能導致bug。
復制對象的原始方式
復制對象的原始方法是循環(huán)遍歷原始對象,然后一個接一個地復制每個屬性。我們來看看這段代碼:
function copy(mainObj) {let objCopy = {}; // objCopy 將存儲 mainObj 的副本let key;for (key in mainObj) {objCopy[key] = mainObj[key]; // 將每個屬性復制到objCopy對象}return objCopy; }const mainObj = {a: 2,b: 5,c: {x: 7,y: 4,}, }console.log(copy(mainObj));—>?Edit on JS Bin
存在的問題
——— 愚人碼頭注,開始 ——
關(guān)于第2點中的 writable 屬性:
當?writable?設(shè)置為false時,表示不可寫,也就是說屬性不能被修改。
var o = {}; // Creates a new objectObject.defineProperty(o, 'a', {value: 37,writable: false });console.log(o.a); // logs 37 o.a = 25; // No error thrown // (it would throw in strict mode, // even if the value had been the same) console.log(o.a); // logs 37. The assignment didn't work.// strict mode (function() {'use strict';var o = {};Object.defineProperty(o, 'b', {value: 2,writable: false});o.b = 3; // throws TypeError: "b" is read-onlyreturn o.b; // returns 2 without the line above }());正如上例中看到的,修改一個 non-writable 的屬性不會改變屬性的值,同時也不會報異常。
詳細查看:MDN 文檔
——— 愚人碼頭注,開始 ——
淺拷貝對象
當拷貝源對象的頂級屬性被復制而沒有任何引用,并且拷貝源對象存在一個值為對象的屬性,被復制為一個引用時,那么我說這個對象被淺拷貝。如果拷貝源對象的屬性值是對象的引用,則只將該引用值復制到目標對象。
淺層復制將復制頂級屬性,但是嵌套對象將在原始(源)對象和副本(目標)對象之間是共享。
使用 Object.assign() 方法
Object.assign() 方法用于將從一個或多個源對象中的所有可枚舉的屬性值復制到目標對象。
let obj = {a: 1,b: 2, }; let objCopy = Object.assign({}, obj); console.log(objCopy); // Result - { a: 1, b: 2 }—>?Edit on JS Bin
到目前為止。我們創(chuàng)建了一個?obj?的副本。讓我們看看是否存在不變性:
let obj = {a: 1,b: 2, }; let objCopy = Object.assign({}, obj);console.log(objCopy); // result - { a: 1, b: 2 } objCopy.b = 89; console.log(objCopy); // result - { a: 1, b: 89 } console.log(obj); // result - { a: 1, b: 2 }—>?Edit on JS Bin
在上面的代碼中,我們將?objCopy?對象中的屬性?b?的值更改為?89?,并且當我們在控制臺中 log 修改后的?objCopy?對象時,這些更改僅應(yīng)用于?objCopy?。我們可以看到最后一行代碼檢查?obj對象并沒有被修改。這意味著我們已經(jīng)成功地創(chuàng)建了拷貝源對象的副本,而且它沒有引用。
Object.assign()的陷阱
不要高興的太早! 雖然我們成功地創(chuàng)建了一個副本,一切似乎都正常工作,記得我們討論了淺拷貝? 我們來看看這個例子:
let obj = {a: 1,b: {c: 2,}, } let newObj = Object.assign({}, obj); console.log(newObj); // { a: 1, b: { c: 2} }obj.a = 10; console.log(obj); // { a: 10, b: { c: 2} } console.log(newObj); // { a: 1, b: { c: 2} }newObj.a = 20; console.log(obj); // { a: 10, b: { c: 2} } console.log(newObj); // { a: 20, b: { c: 2} }newObj.b.c = 30; console.log(obj); // { a: 10, b: { c: 30} } console.log(newObj); // { a: 20, b: { c: 30} }// 注意: newObj.b.c = 30; 為什么呢..—>?Edit on JS Bin
obj.b.c = 30 ?
這就是?Object.assign()?的陷阱。Object.assign?只是淺拷貝。?newObj.b?和?obj.b?都引用同一個對象,沒有單獨拷貝,而是復制了對該對象的引用。任何對對象屬性的更改都適用于使用該對象的所有引用。我們?nèi)绾谓鉀Q這個問題?繼續(xù)閱讀…我們會在下一節(jié)給出修復方案。
注意:原型鏈上的屬性和不可枚舉的屬性不能復制。 看這里:
let someObj = {a: 2, }let obj = Object.create(someObj, { b: {value: 2, },c: {value: 3,enumerable: true, }, });let objCopy = Object.assign({}, obj); console.log(objCopy); // { c: 3 }—>?Edit on JS Bin
someObj?是在?obj?的原型鏈,所以它不會被復制。
屬性?b?是不可枚舉屬性。
屬性?c?具有 可枚舉(enumerable) 屬性描述符,所以它可以枚舉。 這就是為什么它會被復制。
深度拷貝對象
深度拷貝將拷貝遇到的每個對象。副本和原始對象不會共享任何東西,所以它將是原件的副本。以下是使用?Object.assign()?遇到問題的修復方案。讓我們探索一下。
使用 JSON.parse(JSON.stringify(object));
這可以修復了我們之前提出的問題。現(xiàn)在?newObj.b?有一個副本而不是一個引用!這是深度拷貝對象的一種方式。 這里有一個例子:
let obj = { a: 1,b: { c: 2,}, }let newObj = JSON.parse(JSON.stringify(obj));obj.b.c = 20; console.log(obj); // { a: 1, b: { c: 20 } } console.log(newObj); // { a: 1, b: { c: 2 } } (一個新的對象)不可變性: ?
—>?Edit on JS Bin
陷阱
不幸的是,此方法不能用于復制用戶定義的對象方法。 見下文。
復制對象方法
方法是一個對象的屬性,它是一個函數(shù)。在以上的示例中,我們還沒有復制對象的方法。現(xiàn)在讓我們嘗試一下,使用我們學過的方法來創(chuàng)建副本。
let obj = {name: 'scotch.io',exec: function exec() {return true;}, }let method1 = Object.assign({}, obj); let method2 = JSON.parse(JSON.stringify(obj));console.log(method1); //Object.assign({}, obj) /* result {exec: function exec() {return true;},name: "scotch.io" } */console.log(method2); // JSON.parse(JSON.stringify(obj)) /* result {name: "scotch.io" } */—>?Edit on JS Bin
結(jié)果表明,Object.assign()?可以用于復制對象的方法,而使用?JSON.parse(JSON.stringify(obj))?則不行。
復制循環(huán)引用對象
循環(huán)引用對象是具有引用自身屬性的對象。讓我們使用已學的復制對象的方法來復制一個循環(huán)引用對象的副本,看看它是否有效。
使用 JSON.parse(JSON.stringify(object))
讓我們嘗試使用?JSON.parse(JSON.stringify(object)):
// 循環(huán)引用對象 let obj = { a: 'a',b: { c: 'c',d: 'd',}, }obj.c = obj.b; obj.e = obj.a; obj.b.c = obj.c; obj.b.d = obj.b; obj.b.e = obj.b.c;let newObj = JSON.parse(JSON.stringify(obj));console.log(newObj);—>?Edit on JS Bin
結(jié)果是:
很明顯,JSON.parse(JSON.stringify(object))?不能用于復制循環(huán)引用對象。
使用 Object.assign()
讓我們嘗試使用?Object.assign():
// 循環(huán)引用對象 let obj = { a: 'a',b: { c: 'c',d: 'd',}, }obj.c = obj.b; obj.e = obj.a; obj.b.c = obj.c; obj.b.d = obj.b; obj.b.e = obj.b.c;let newObj2 = Object.assign({}, obj);console.log(newObj2);—>?Edit on JS Bin
結(jié)果是:
Object.assign()?適用于淺拷貝循環(huán)引用對象,但不適用于深度拷貝。隨意瀏覽瀏覽器控制臺上的循環(huán)引用對象樹。我相信你會發(fā)現(xiàn)很多有趣的工作在那里。
使用展開操作符(…)
ES6已經(jīng)有了用于數(shù)組解構(gòu)賦值的 rest 元素,和實現(xiàn)的數(shù)組字面展開的操作符。看一看這里的數(shù)組的展開操作符的實現(xiàn):
const array = ["a","c","d", {four: 4}, ]; const newArray = [...array]; console.log(newArray); // 結(jié)果 // ["a", "c", "d", { four: 4 }]—>?Edit on JS Bin
對象字面量的展開操作符目前是ECMAScript 的第 3 階段提案。對象字面量的展開操作符能將源對象中的可枚舉的屬性復制到目標對象上。下面的例子展示了在提案被接受后復制一個對象是多么的容易。
let obj = {one: 1,two: 2, }let newObj = { ...z };// { one: 1, two: 2 }注意:這將只對淺拷貝有效
結(jié)論
在 JavaScript 中復制對象可能是相當艱巨的,特別是如果您剛開始使用 JavaScript 并且不了解該語言的方式。希望本文幫助您了解并避免您可能遇到復制對象的陷阱。如果您有任何庫或一段代碼可以獲得更好的結(jié)果,歡迎與社區(qū)分享。
國外原文:https://scotch.io/bar-talk/copying-objects-in-javascript
國內(nèi)原文翻譯:https://juejin.im/entry/5a28ec86f265da43163cf720
總結(jié)
以上是生活随笔為你收集整理的JavaScript 中的对象拷贝(深拷贝、浅拷贝)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决 /usr/share/git-co
- 下一篇: Web前端性能优化——编写高效的Java