2022 年 10 月 1 日

对象引用和复制

对象与基本类型之间的一个基本区别在于,对象是“按引用”存储和复制的,而基本类型值:字符串、数字、布尔值等 - 总是“作为一个整体值”被复制。

如果我们稍微了解一下复制值时发生的情况,就很容易理解这一点。

让我们从一个基本类型开始,比如一个字符串。

在这里,我们将 message 的副本放入 phrase

let message = "Hello!";
let phrase = message;

结果,我们有两个独立的变量,每个变量都存储着字符串 "Hello!"

一个显而易见的结果,对吧?

对象不像那样。

分配给对象的变量存储的不是对象本身,而是其“内存地址”——换句话说,是它的“引用”。

我们来看一个这样的变量示例

let user = {
  name: "John"
};

以下是它在内存中的实际存储方式

对象存储在内存中的某个位置(图片右侧),而user变量(左侧)对其有“引用”。

我们可以将对象变量(如user)视为一张纸,上面写着对象的地址。

当我们对对象执行操作时,例如获取属性user.name,JavaScript 引擎会查看该地址上的内容,并在实际对象上执行操作。

现在,这就是它重要的原因。

当复制对象变量时,会复制引用,但不会复制对象本身。

例如

let user = { name: "John" };

let admin = user; // copy the reference

现在我们有两个变量,每个变量都存储对同一对象的引用

如你所见,仍然只有一个对象,但现在有两个引用它的变量。

我们可以使用任一变量来访问对象并修改其内容

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // changed by the "admin" reference

alert(user.name); // 'Pete', changes are seen from the "user" reference

这就像我们有一个带两个钥匙的橱柜,并使用其中一个钥匙(admin)进入其中进行更改。然后,如果我们稍后使用另一个钥匙(user),我们仍然会打开同一个橱柜,并且可以访问已更改的内容。

按引用比较

只有当两个对象是同一对象时,它们才相等。

例如,这里ab引用同一对象,因此它们相等

let a = {};
let b = a; // copy the reference

alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true

这里有两个独立的对象不相等,即使它们看起来很相似(都是空的)

let a = {};
let b = {}; // two independent objects

alert( a == b ); // false

对于obj1 > obj2之类的比较或针对基元obj == 5的比较,对象将转换为基元。我们很快就会研究对象转换的工作原理,但说实话,很少需要进行此类比较——通常它们是编程错误的结果。

Const 对象可以修改

将对象存储为引用的一个重要副作用是,声明为const的对象可以修改。

例如

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

(*)行似乎会导致错误,但事实并非如此。user的值是常量,它必须始终引用同一对象,但该对象的属性可以自由更改。

换句话说,只有当我们尝试将user=...作为一个整体设置时,const user才会给出错误。

也就是说,如果我们真的需要创建常量对象属性,也是可能的,但使用完全不同的方法。我们将在属性标志和描述符一章中提到这一点。

克隆和合并,Object.assign

因此,复制对象变量会创建对同一对象的另一个引用。

但是,如果我们需要复制一个对象,该怎么办?

我们可以创建一个新对象并复制现有对象的结构,方法是遍历其属性并在基元级别复制它们。

像这样

let user = {
  name: "John",
  age: 30
};

let clone = {}; // the new empty object

// let's copy all user properties into it
for (let key in user) {
  clone[key] = user[key];
}

// now clone is a fully independent object with the same content
clone.name = "Pete"; // changed the data in it

alert( user.name ); // still John in the original object

我们还可以使用 Object.assign 方法。

语法为

Object.assign(dest, ...sources)
  • 第一个参数 dest 是目标对象。
  • 后面的参数是源对象列表。

它将所有源对象的属性复制到目标 dest 中,然后返回它作为结果。

例如,我们有 user 对象,让我们向其添加几个权限

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }
alert(user.name); // John
alert(user.canView); // true
alert(user.canEdit); // true

如果复制的属性名已存在,则会被覆盖

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // now user = { name: "Pete" }

我们还可以使用 Object.assign 来执行简单的对象克隆

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

alert(clone.name); // John
alert(clone.age); // 30

这里它将 user 的所有属性复制到空对象中并返回它。

还有其他克隆对象的方法,例如使用 展开语法 clone = {...user},将在本教程后面介绍。

嵌套克隆

到目前为止,我们假设 user 的所有属性都是原始的。但属性可以是对其他对象的引用。

像这样

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

现在仅复制 clone.sizes = user.sizes 不够,因为 user.sizes 是一个对象,将通过引用进行复制,因此 cloneuser 将共享相同的尺寸

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, same object

// user and clone share sizes
user.sizes.width = 60;    // change a property from one place
alert(clone.sizes.width); // 60, get the result from the other one

为了解决这个问题并使 userclone 真正成为独立的对象,我们应该使用克隆循环检查 user[key] 的每个值,如果它是一个对象,则复制其结构。这称为“深度克隆”或“结构化克隆”。有一个 structuredClone 方法实现深度克隆。

structuredClone

调用 structuredClone(object) 克隆 object 及其所有嵌套属性。

以下是如何在我们的示例中使用它

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = structuredClone(user);

alert( user.sizes === clone.sizes ); // false, different objects

// user and clone are totally unrelated now
user.sizes.width = 60;    // change a property from one place
alert(clone.sizes.width); // 50, not related

structuredClone 方法可以克隆大多数数据类型,例如对象、数组、原始值。

它还支持循环引用,即对象属性引用对象本身(直接或通过链或引用)。

例如

let user = {};
// let's create a circular reference:
// user.me references the user itself
user.me = user;

let clone = structuredClone(user);
alert(clone.me === clone); // true

如您所见,clone.me 引用 clone,而不是 user!因此,循环引用也已正确克隆。

不过,在某些情况下,structuredClone 会失败。

例如,当一个对象具有函数属性时

// error
structuredClone({
  f: function() {}
});

不支持函数属性。

为了处理此类复杂情况,我们可能需要结合使用克隆方法、编写自定义代码,或者为了不重复造轮子,采用现有的实现,例如 JavaScript 库 _.cloneDeep(obj) 中的 lodash

总结

对象通过引用进行赋值和复制。换句话说,变量存储的不是“对象值”,而是该值的“引用”(内存地址)。因此,复制这样的变量或将其作为函数参数传递时,复制的是该引用,而不是对象本身。

通过复制的引用执行的所有操作(例如添加/删除属性)都在同一个对象上执行。

要进行“真实复制”(克隆),我们可以对所谓的“浅复制”(嵌套对象通过引用复制)使用 Object.assign,或使用“深克隆”函数 structuredClone,或使用自定义克隆实现,例如 _.cloneDeep(obj)

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,要插入多行代码,请将它们包装在 <pre> 标记中,要插入 10 行以上的代码,请使用沙盒(plnkrjsbincodepen…)