对象与基本类型之间的一个基本区别在于,对象是“按引用”存储和复制的,而基本类型值:字符串、数字、布尔值等 - 总是“作为一个整体值”被复制。
如果我们稍微了解一下复制值时发生的情况,就很容易理解这一点。
让我们从一个基本类型开始,比如一个字符串。
在这里,我们将 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
),我们仍然会打开同一个橱柜,并且可以访问已更改的内容。
按引用比较
只有当两个对象是同一对象时,它们才相等。
例如,这里a
和b
引用同一对象,因此它们相等
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 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
是一个对象,将通过引用进行复制,因此 clone
和 user
将共享相同的尺寸
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
为了解决这个问题并使 user
和 clone
真正成为独立的对象,我们应该使用克隆循环检查 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)。
评论
<code>
标记,要插入多行代码,请将它们包装在<pre>
标记中,要插入 10 行以上的代码,请使用沙盒(plnkr、jsbin、codepen…)