简介
我希望在我的JavaScript编程生涯的早期就能理解一些东西,那就是对象赋值是如何工作的,以及它与原始赋值有什么不同。这是我试图用最简洁的方式来表达这种区别。
基元与对象
作为回顾,让我们回顾一下JavaScript中不同的原始类型和对象。
原始类型:布尔(Boolean),空(Null),未定义(Undefined),数字(Number),大Int(BigInt)(你可能不会经常看到这个),字符串(String),符号(Symbol)(你可能不会经常看到这个
对象类型:对象、数组、日期、[许多其他类型]
原始赋值和对象赋值有何不同
原始赋值
当你把一个基元值赋给一个变量时,该值在内存中被创建,变量则指向内存中的该值。基元不能被修改;换句话说,它们是不可改变的。
让我们看一下下面的代码。
const a = 'hello';
let b = a;
在这种情况下,字符串hello 是在内存中创建的,a 指向该字符串。当我们给b 赋值时,a ,b 也指向内存中的同一个hello 字符串。现在,如果我们给b 赋予一个新的值呢?
b = 'foobar';
console.log(a); // "hello"
console.log(b); // "foobar"
这是有道理的;一个新的字符串(foobar )在内存中被创建,现在b 指向这个字符串。a 仍然指向内存中的hello 。重要的是,我们对b 所做的任何事情都不会影响a 的指向,因为原始值是不可改变的。
对象赋值
对象赋值的工作原理与此类似,对象在内存中被创建,变量指向该对象。然而,有一个关键的区别:对象是可以被变异的!我们来看看这个例子。让我们来看看一个新的例子。
const a = { name: 'Joe' };
const b = a;
第一行在内存中创建对象{ name: 'Joe' } ,然后将该对象的引用分配给变量a 。第二行将内存中同一对象的引用分配给b 。
同样,这对对象来说是个大问题,因为它们是可变的。现在让我们改变b 所指向的对象的name 属性。
b.name = 'Jane';
console.log(b); // { name: "Jane" }
console.log(a); // { name: "Jane" }
这就对了!由于a 和b 被分配给了内存中的同一个对象的引用,改变b 的属性实际上只是改变了内存中a 和b 都指向的对象的属性。
彻底地说,我们也可以看到这一点在数组中的作用。
const a = ['foo'];
const b = a;
b[0] = 'bar';
console.log(b); // ["bar"]
console.log(a); // ["bar"]
这也适用于函数参数!
这些赋值规则也适用于你向函数传递对象的时候!请看下面的例子。
const a = { name: 'Joe' };
function doSomething(val) {
val.name = 'Bip';
}
doSomething(a);
console.log(a); // { name: "Bip" }
这个故事的寓意是:当你传递给函数的对象发生突变时,除非是有意为之(我认为你真正想这样做的情况不多)。
防止非故意的突变
在很多情况下,这种行为是需要的。指向内存中的同一个对象有助于我们传递引用并做一些聪明的事情。然而,这并不总是我们所期望的行为,当你开始无意地突变对象时,你可能会出现一些非常混乱的错误。
有几种方法可以确保你的对象是独一无二的。我将在这里介绍其中的一些,但请放心,这个列表并不全面。
展开运算符(...)
传播操作符是对一个对象或数组进行浅层复制的好方法。让我们用它来复制一个对象。
const a = { name: 'Joe' };
const b = { ...a };
b.name = 'Jane';
console.log(b); // { name: "Jane" }
console.log(a); // { name: "Joe" }
关于 "浅层 "复制的说明
了解浅层复制和深层复制是很重要的。浅层复制对于只有一层的对象来说效果很好,但是嵌套的对象就有问题了。让我们使用下面的例子。
const a = {
name: 'Joe',
dog: {
name: 'Daffodil',
},
};
const b = { ...a };
b.name = 'Pete';
b.dog.name = 'Frenchie';
console.log(a);
// {
// name: 'Joe',
// dog: {
// name: 'Frenchie',
// },
// }
我们成功地复制了a 一层深度的对象,但是第二层的属性仍然在内存中引用相同的对象!这就是浅层复制。由于这个原因,人们发明了一些方法来进行 "深度 "复制,比如使用像deep-copy 这样的库,或者将一个对象序列化和反序列化。
使用Object.assign
Object.assign 可以用来在另一个对象的基础上创建一个新的对象。其语法是这样的。
const a = { name: 'Joe' };
const b = Object.create({}, a);
注意;这仍然是一个浅层的拷贝!
序列化和反序列化
一种可以用来深度复制对象的方法是对对象进行序列化和反序列化。一种常见的方法是使用JSON.stringify 和JSON.parse 。
const a = {
name: 'Joe',
dog: {
name: 'Daffodil',
},
};
const b = JSON.parse(JSON.serialize(a));
b.name = 'Eva';
b.dog.name = 'Jojo';
console.log(a);
// {
// name: 'Joe',
// dog: {
// name: 'Daffodil',
// },
// }
console.log(b);
// {
// name: 'Eva',
// dog: {
// name: 'Jojo',
// },
// }
但这确实有其缺点。序列化和反序列化不能保留复杂的对象,如函数。
一个深度拷贝库
引入一个深度拷贝库来完成这项任务是相当常见的,特别是当你的对象有一个未知的或特别深的层次结构时。这些库通常是执行上述浅层拷贝方法之一的函数,沿着对象树递归。
总结
虽然这看起来是个复杂的话题,但如果你保持对原始类型和对象的不同分配方式的认识,你就会很顺利。玩一玩这些例子,如果你愿意的话,可以尝试编写你自己的深拷贝函数。