JavaScript中的对象赋值与原始赋值初探

133 阅读5分钟

简介

我希望在我的JavaScript编程生涯的早期就能理解一些东西,那就是对象赋值是如何工作的,以及它与原始赋值有什么不同。这是我试图用最简洁的方式来表达这种区别。

基元与对象

作为回顾,让我们回顾一下JavaScript中不同的原始类型和对象。

原始类型:布尔(Boolean),空(Null),未定义(Undefined),数字(Number),大Int(BigInt)(你可能不会经常看到这个),字符串(String),符号(Symbol)(你可能不会经常看到这个

对象类型:对象、数组、日期、[许多其他类型]

原始赋值和对象赋值有何不同

原始赋值

当你把一个基元值赋给一个变量时,该值在内存中被创建,变量则指向内存中的该值。基元不能被修改;换句话说,它们是不可改变的

让我们看一下下面的代码。

const a = 'hello';
let b = a;

在这种情况下,字符串hello 是在内存中创建的,a 指向该字符串。当我们给b 赋值时,ab 也指向内存中的同一个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" }

这就对了!由于ab 被分配给了内存中的同一个对象的引用,改变b 的属性实际上只是改变了内存中ab 都指向的对象的属性。

彻底地说,我们也可以看到这一点在数组中的作用。

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.stringifyJSON.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',
//   },
// }

但这确实有其缺点。序列化和反序列化不能保留复杂的对象,如函数。

一个深度拷贝库

引入一个深度拷贝库来完成这项任务是相当常见的,特别是当你的对象有一个未知的或特别深的层次结构时。这些库通常是执行上述浅层拷贝方法之一的函数,沿着对象树递归。

总结

虽然这看起来是个复杂的话题,但如果你保持对原始类型和对象的不同分配方式的认识,你就会很顺利。玩一玩这些例子,如果你愿意的话,可以尝试编写你自己的深拷贝函数。