在JavaScript中复制对象

来自菜鸟教程
跳转至:导航、​搜索

对象是 JavaScript 的基本块。 对象是属性的集合,属性是键(或名称)和值之间的关联。 JavaScript 中几乎所有的对象都是 Object 的实例,它位于原型链的顶部。

介绍

如您所知,赋值运算符不会创建对象的副本,它只会分配对它的引用,让我们看看以下代码:

let obj = {
  a: 1,
  b: 2,
};
let copy = obj;

obj.a = 5;
console.log(copy.a);
// Result 
// a = 5;

obj 变量是初始化新对象的容器。 copy 变量指向同一个对象,并且是对该对象的引用。 所以基本上这个 { a: 1, b: 2, } 对象是说:现在有两种方法可以访问我。 您必须通过 obj 变量或 copy 变量以任何一种方式到达我,并且您通过这些方式(网关)对我所做的任何事情都会影响我。

这些天来,不变性被广泛谈论,你必须听这个电话! 此方法消除了任何形式的不变性,如果原始对象被代码的另一部分使用,可能会导致错误。

复制对象的简单方法

复制对象的简单方法是遍历原始对象并一个接一个地复制每个属性。 让我们看一下这段代码:

function copy(mainObj) {
  let objCopy = {}; // objCopy will store a copy of the mainObj
  let key;

  for (key in mainObj) {
    objCopy[key] = mainObj[key]; // copies each property to the objCopy object
  }
  return objCopy;
}

const mainObj = {
  a: 2,
  b: 5,
  c: {
    x: 7,
    y: 4,
  },
}

console.log(copy(mainObj));

固有问题

  1. objCopy 对象有一个新的 Object.prototype 方法,不同于 mainObj 对象原型方法,这不是我们想要的。 我们想要原始对象的精确副本。
  2. 不复制属性描述符。 在 objCopy 对象中,值设置为 false 的“可写”描述符将为 true。
  3. 上面的代码只复制了 mainObj 的可枚举属性。
  4. 如果原始对象中的属性之一是对象本身,那么它将在副本和原始对象之间共享,使它们各自的属性指向同一个对象。

浅拷贝对象

当源顶级属性在没有任何引用的情况下被复制并且存在一个其值为对象并被复制为引用的源属性时,就称该对象为浅复制。 如果源值是对对象的引用,它只会将该引用值复制到目标对象。

浅拷贝将复制顶级属性,但嵌套对象在原始(源)和副本(目标)之间共享。

使用 Object.assign() 方法

Object.assign() 方法用于将所有可枚举自身属性的值从一个或多个源对象复制到目标对象。

let obj = {
  a: 1,
  b: 2,
};
let objCopy = Object.assign({}, obj);
console.log(objCopy);
// Result - { a: 1, b: 2 }

好吧,到目前为止,这已经完成了。 我们制作了 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 }

在上面的代码中,我们将 objCopy 对象中的属性 'b' 的值更改为 89 并且当我们在控制台中记录修改后的 objCopy 对象时,更改仅适用于 objCopy。 最后一行代码检查 obj 对象是否仍然完好无损并且没有更改。 这意味着我们已经成功地创建了源对象的副本,而没有对其进行任何引用。

Object.assign() 的陷阱

没那么快! 虽然我们成功创建了一个副本,并且一切似乎都运行良好,但还记得我们讨论过浅拷贝吗? 让我们看一下这个例子:

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} }

// Note: newObj.b.c = 30; Read why..

为什么 obj.bc = 30?

嗯,这是 Object.assign() 的一个陷阱。 Object.assign 只做浅拷贝。 newObj.bobj.b 都共享对对象的相同引用,因为没有进行单独的复制,而是复制了对对象的引用。 对对象的任何属性所做的任何更改都适用于使用该对象的所有引用。 我们如何解决这个问题? 继续阅读……我们在下一节中有一个修复。

注意:不能复制原型链上的属性和不可枚举的属性。 看这里:

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 }
  • someObj 位于 obj 的原型链上,因此不会被复制。
  • property b 是不可枚举的属性。
  • property c 有一个可枚举的属性描述符,允许它是可枚举的。 这就是它被复制的原因。

深度复制对象

深拷贝将复制它遇到的每个对象。 副本和原始对象不会共享任何内容,因此它将是原始对象的副本。 这是我们在使用 Object.assign() 时遇到的问题的解决方法。 让我们探索一下。

使用 JSON.parse(JSON.stringify(object));

这解决了我们之前遇到的问题。 现在 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 } } (New Object Intact!)

不可变:✓

陷阱

不幸的是,此方法不能用于复制用户定义的对象方法。 见下文。

复制对象方法

方法是作为函数的对象的属性。 在到目前为止的示例中,我们还没有使用方法复制对象。 现在让我们尝试一下,并使用我们学到的方法来制作副本。

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"
}
*/

结果表明Object.assign()可以用来复制方法,而JSON.parse(JSON.stringify(obj))不能用。

复制圆形对象

圆形对象是具有引用自身的属性的对象。 让我们使用到目前为止所学的复制对象的方法来复制一个圆形对象,看看它是否有效。

使用 JSON.parse(JSON.stringify(object))

让我们试试 JSON.parse(JSON.stringify(object))

// circular object
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); 

结果如下:

JSON.parse(JSON.stringify(obj)) 显然不适用于圆形物体。

使用 Object.assign()

让我们试试 Object.assign()

// circular object
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); 

结果如下:

Object.assign() 适用于浅拷贝圆形对象,但不适用于深拷贝。 随意在浏览器控制台上探索 circular object tree。 我相信你会在那里找到很多有趣的工作。

使用展开元素 (…)

ES6 已经实现了数组解构赋值的剩余元素和数组字面量的展开元素。 在此处查看数组上的展开元素实现:

const array = [
  "a",
  "c",
  "d", {
    four: 4
  },
];
const newArray = [...array];
console.log(newArray);
// Result 
// ["a", "c", "d", { four: 4 }]

对象字面量的扩展属性目前是 ECMAScript Stage 3 提案。 对象初始值设定项中的扩展属性将自己的可枚举属性从源对象复制到目标对象。 下面的示例显示了在提案被接受后复制对象是多么容易。

let obj = {
  one: 1,
  two: 2,
}

let newObj = { ...obj };

// { one: 1, two: 2 }

注意:这只会对浅拷贝有效

结论

在 JavaScript 中复制对象可能会非常令人生畏,尤其是如果您是 JavaScript 新手并且不了解该语言的使用方法。 希望本文能帮助您理解并避免将来可能遇到的复制对象的陷阱。 如果您有任何库或代码可以达到更好的结果,欢迎与社区分享。 快乐编码!