JS基本原理:对象类型赋值和原生类型赋值
介绍
在本文中,我试图以最简洁的方式来阐明JavaScript编程原理中对象类型赋值和原生类型赋值之间的区别,以及它们各自是如何工作的。这也是我希望在我的JavaScript编程生涯早期就已经理解的东西。
JS中的原生类型和对象类型
首先,让我们回顾一下JavaScript中不同的原生类型和对象类型。
原生类型:Boolean,Null,Undefined,Number,BigInt(可能不常见),String,Symbol(可能不常见)。
对象类型:Object,Array,Date,以及许多其它类型。
原生类型赋值和对象类型赋值有什么不同?
原生类型赋值
将一个原生类型的值赋值给一个变量非常简单,看下面的例子:
const a = 'hello';
const b = a;
在这个示例中,a的值被设置为hello,b的值也被设置为hello。如果我们将b的值修改,a的值不会发生变化。a和b之间不存在任何关联。
const b = 'foobar'; console.log(a); // "hello" console.log(b); // "foobar"
对象类型赋值
对象类型赋值的工作方式则不同。将一个对象类型的值赋值给一个变量会执行下面这两步操作:
- 在内存中创建一个对象
- 将该对象的引用赋值给变量
这会发生什么呢?让我们看一下:
const a = { name: 'Joe' };
const b = a;
第一行在内存中创建了一个对象{ name: 'Joe' },然后将该对象的引用赋值给变量a。第二行将同一对象的引用又赋值给了变量b。
接下来,让我们修改分配给b的对象的一个属性:
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" }
对传递给函数的对象的值进行修改时要小心,除非这是有意而为之的(但我相信在多数情况下你并不希望这样)。
防止意外的修改
在多数情况下,这种方式是符合预期的。这可以让我们非常方便地将同一对象的引用赋值给不同的变量,不过有时候也会引起一些意想不到的结果,当我们改变对象时,这种行为可能会导致一些非常令人困惑的bug。
有几种方法可以防止这种行为的发生,这里我会介绍其中的几个,但肯定不止这几种方法。
JavaScript展开操作符(...)
展开操作符是对对象或数组进行浅拷贝的一个好方法。
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', // }, // }
展开操作符只实现了对象的一层深度的拷贝,但是第二层的属性在内存中仍然指向了同一对象。为了解决这个问题,人们想到了一些方法来进行“深拷贝”,例如第三方库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.stringify(a)); b.name = 'Eva'; b.dog.name = 'Jojo'; console.log(a); // { // name: 'Joe', // dog: { // name: 'Daffodil', // }, // } console.log(b); // { // name: 'Eva', // dog: { // name: 'Jojo', // }, // }
不过这种方法也存在缺点,序列化和反序列化不会保留复杂类型的对象例如函数。
实现深拷贝的第三方库
使用第三方库来实现深拷贝在实际工作中很常见,特别是当你对要操作的对象的结构和层级不是很清楚时。这些库提供的函数通常都是通过递归操作的方式来实现上述对象浅拷贝的功能。
结论
也许本文讨论的这些细节看起来有点复杂,但如果你对JS中原生类型赋值和对象类型赋值有所了解的话,这对你而言就是小菜一碟了。你可以尝试一下上文提到的这些例子,如果愿意的话,你也完全可以尝试着自己实现一个对象深拷贝的函数。
原文地址:JS Fundamentals: Object Assignment vs. Primitive Assignment