对象是JavaScript的基本块。对象是属性的集合,属性是键(或名称)和值之间的关联。几乎所有的对象都是位于原型链顶端的Object
的实例。
简介
正如您所知道的,赋值操作符并不创建对象的副本,它只为对象赋值一个引用,让我们看一下下面的代码:
1let obj = {
2 a: 1,
3 b: 2,
4};
5let copy = obj;
6
7obj.a = 5;
8console.log(copy.a);
9// Result
10// a = 5;
obj
变量是初始化的新对象的容器。Copy‘变量指向相同的对象,并且是对该对象的引用。所以基本上这个
{a:1,b:2,}‘对象是在说:现在有两种方法可以访问我。
如今,不变性被广泛谈论,你必须倾听这一呼唤!此方法消除了任何形式的不变性,如果原始对象被代码的另一部分使用,可能会导致错误。
复制对象的幼稚方式
复制对象的简单方法是遍历原始对象,然后一个接一个地复制每个属性。让我们来看看这段代码:
1function copy(mainObj) {
2 let objCopy = {}; // objCopy will store a copy of the mainObj
3 let key;
4
5 for (key in mainObj) {
6 objCopy[key] = mainObj[key]; // copies each property to the objCopy object
7 }
8 return objCopy;
9}
10
11const mainObj = {
12 a: 2,
13 b: 5,
14 c: {
15 x: 7,
16 y: 4,
17 },
18}
19
20console.log(copy(mainObj));
固有问题
1.objCopy
对象有一个不同于mainObj
对象原型方法的新的Object.Prototype
方法,这不是我们想要的。我们想要一份与原始物品一模一样的复制品。
2.不复制属性描述符。在objCopy
对象中,值为FALSE的可写描述符将为TRUE。
3.上面的代码只复制了mainObj
的可枚举属性。
4.如果原始对象中的属性之一是对象本身,则它将在副本和原始对象之间共享,从而使其各自的属性指向同一对象。
浅层复制对象
如果在没有任何引用的情况下复制源顶层属性,并且存在值为对象并作为引用复制的源属性,则称对象为浅层复制。如果源值是对对象的引用,则它只将该引用值复制到目标对象。
浅层复制将复制顶级属性,但原始(源)和复制(目标)之间共享嵌套对象。
使用对象分配()方法
方法的作用是:将所有可枚举的OWN属性的值从一个或多个源对象复制到目标对象。
1let obj = {
2 a: 1,
3 b: 2,
4};
5let objCopy = Object.assign({}, obj);
6console.log(objCopy);
7// Result - { a: 1, b: 2 }
好了,到目前为止,这已经完成了工作。我们已经复制了一份《Objj》。让我们来看看是否存在不变性:
1let obj = {
2 a: 1,
3 b: 2,
4};
5let objCopy = Object.assign({}, obj);
6
7console.log(objCopy); // result - { a: 1, b: 2 }
8objCopy.b = 89;
9console.log(objCopy); // result - { a: 1, b: 89 }
10console.log(obj); // result - { a: 1, b: 2 }
在上面的代码中,我们将objCopy
对象中的属性‘b’
的值更改为89
,当我们将修改后的objCopy
对象记录到控制台时,这些更改只适用于objCopy
。最后一行代码检查obj
对象是否仍然完好无损并且没有更改。这意味着我们已经成功地创建了源对象的副本,而没有任何对它的引用。
Object.Assign()的缺陷
别这么快!虽然我们成功地创建了一个副本,一切似乎都工作正常,还记得我们讨论过浅复制吗?让我们来看看这个例子:
1let obj = {
2 a: 1,
3 b: {
4 c: 2,
5 },
6}
7let newObj = Object.assign({}, obj);
8console.log(newObj); // { a: 1, b: { c: 2} }
9
10obj.a = 10;
11console.log(obj); // { a: 10, b: { c: 2} }
12console.log(newObj); // { a: 1, b: { c: 2} }
13
14newObj.a = 20;
15console.log(obj); // { a: 10, b: { c: 2} }
16console.log(newObj); // { a: 20, b: { c: 2} }
17
18newObj.b.c = 30;
19console.log(obj); // { a: 10, b: { c: 30} }
20console.log(newObj); // { a: 20, b: { c: 30} }
21
22// Note: newObj.b.c = 30; Read why..
为什么obj.b.c=30?
这就是Object.assign()
的一个缺陷。Object.assign
只制作浅层副本。newObj.b
和obj.b
共享对该对象的相同引用,因为没有创建单独的副本,而是复制了对该对象的引用。对对象的任何属性所做的任何更改都会应用于使用该对象的所有引用。我们怎么才能解决这个问题?继续阅读..。我们在下一节中有了解决办法。
注意:不能复制原型链上的属性和不可复制的属性。请看这里:
1let someObj = {
2 a: 2,
3}
4
5let obj = Object.create(someObj, {
6 b: {
7 value: 2,
8 },
9 c: {
10 value: 3,
11 enumerable: true,
12 },
13});
14
15let objCopy = Object.assign({}, obj);
16console.log(objCopy); // { c: 3 }
omeObj
在obj的原型链上,所以不会被复制。属性b
为不可枚举属性。属性c
有一个可枚举的属性描述符,允许它是可枚举的。这就是它被复制的原因。
深度复制对象
深度复制将复制它遇到的每个对象。副本和原始对象不会共享任何内容,因此它将是原始对象的副本。以下是我们使用Object.assign()
时遇到的问题的修复方法。让我们来探索一下。
使用JSON.parse(JSON.stringify(Object));
这解决了我们之前遇到的问题。现在newObj.b
有了副本,没有引用!这是一种深度复制对象的方法。下面是一个例子:
1let obj = {
2 a: 1,
3 b: {
4 c: 2,
5 },
6}
7
8let newObj = JSON.parse(JSON.stringify(obj));
9
10obj.b.c = 20;
11console.log(obj); // { a: 1, b: { c: 20 } }
12console.log(newObj); // { a: 1, b: { c: 2 } } (New Object Intact!)
不可变:✓
瀑布
遗憾的是,此方法不能用于复制用户定义的对象方法。请参见下面的内容。
复制对象方法
方法是作为函数的对象的属性。在到目前为止的示例中,我们还没有使用方法复制对象。现在让我们试一试,用我们学过的方法来复制。
1let obj = {
2 name: 'scotch.io',
3 exec: function exec() {
4 return true;
5 },
6}
7
8let method1 = Object.assign({}, obj);
9let method2 = JSON.parse(JSON.stringify(obj));
10
11console.log(method1); //Object.assign({}, obj)
12/* result
13{
14 exec: function exec() {
15 return true;
16 },
17 name: "scotch.io"
18}
19*/
20
21console.log(method2); // JSON.parse(JSON.stringify(obj))
22/* result
23{
24 name: "scotch.io"
25}
26*/
结果表明,Object.assign()
可以复制方法,而JSON.parse(JSON.stringify(Obj))
不能复制方法。
复制圆形对象
圆形对象是具有引用自身特性的对象。让我们使用到目前为止学到的复制对象的方法来复制圆形对象,并看看它是否有效。
使用JSON.parse(JSON.stringify(Object))
我们来试试JSON.parse(JSON.stringify(Object))
:
1// circular object
2let obj = {
3 a: 'a',
4 b: {
5 c: 'c',
6 d: 'd',
7 },
8}
9
10obj.c = obj.b;
11obj.e = obj.a;
12obj.b.c = obj.c;
13obj.b.d = obj.b;
14obj.b.e = obj.b.c;
15
16let newObj = JSON.parse(JSON.stringify(obj));
17
18console.log(newObj);
结果如下:
JSON.parse(JSON.stringify(Obj))
显然不适用于循环对象。
使用Object.assign()
让我们试试Object.assign()
:
1// circular object
2let obj = {
3 a: 'a',
4 b: {
5 c: 'c',
6 d: 'd',
7 },
8}
9
10obj.c = obj.b;
11obj.e = obj.a;
12obj.b.c = obj.c;
13obj.b.d = obj.b;
14obj.b.e = obj.b.c;
15
16let newObj2 = Object.assign({}, obj);
17
18console.log(newObj2);
结果如下:

Object.assign()
适用于浅层复制圆形对象,但不适用于深层复制。您可以在浏览器控制台上随意浏览圆形对象树
。我相信你会发现那里有很多有趣的工作正在进行。
使用扩散元素(...)
ES6已经实现了用于数组分解赋值的REST元素和用于数组文字的扩展元素。下面我们来看一下数组上的扩展元素实现:
1const array = [
2 "a",
3 "c",
4 "d", {
5 four: 4
6 },
7];
8const newArray = [...array];
9console.log(newArray);
10// Result
11// ["a", "c", "d", { four: 4 }]
对象字面量的spread属性目前是ECMAScript的第3阶段提案。对象初始化器中的扩展属性将自己的可复制属性从源对象复制到目标对象上。下面的示例显示了一旦提案被接受,复制对象是多么容易。
1let obj = {
2 one: 1,
3 two: 2,
4}
5
6let newObj = { ...obj };
7
8// { one: 1, two: 2 }
注意:这将仅对浅层复制有效
结论
在JavaScript中复制对象可能非常困难,特别是如果您是新接触JavaScript并且不了解该语言的话。希望本文能帮助您理解并避免将来复制对象时可能遇到的陷阱。如果您有更好的库或代码,欢迎与社区分享。编码快乐!