重新回顾JS的笔记系列(七)
1.对象
对象是用来存储键值对和更复杂的实体
我们可以通过使用带有可选 属性列表 的花括号 {…}
来创建对象。一个属性就是一个键值对(“key: value”),其中键(key
)是一个字符串(也叫做属性名),值(value
)可以是任何值。
- 创建对象:
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
属性有键(或者也可以叫做“名字”或“标识符”),位于冒号 ":"
的前面,值在冒号的右边。
例子:
let user = { // 一个对象
name: "John", // 键 "name",值 "John"
age: 30 // 键 "age",值 30
};
同时我们可以增加任意类型的属性
user.isAdmin = true;
也可以删除属性
delete user.age;
我们也可以用多字词语来作为属性名,但必须给它们加上引号:
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
但是,多次属性是不可以使用点操作的:
// 这将提示有语法错误
user.likes birds = true
正确的操作如下:
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
此外,在对象使用方括号的时候,可以使用key来作为变量进行属性的访问(不用方括号会出现undefined)
let user = {
name: "John",
age: 30
};
let key = prompt("What do you want to know about the user?", "name");
// 访问变量
alert( user[key] ); // John(如果输入 "name")
- 计算属性
当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性。
例如:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"
本质上,这跟下面的语法效果相同:
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// 从 fruit 变量中获取值
bag[fruit] = 5;
- 属性值简写
在实际开发中,我们通常用已存在的变量当做属性名。
例如:
function makeUser(name, age) {
return {
name: name,
age: age,
// ……其他的属性
};
}
let user = makeUser("John", 30);
alert(user.name); // John
注意,对象的属性名对保留字是没有限制的,你可以使用‘let’作为属性名。
- 判断属性存在
相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!
读取不存在的属性只会得到 undefined
。所以我们可以很容易地判断一个属性是否存在:
let user = {};
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
这里还有一个特别的,检查属性是否存在的操作符 "in"
。
语法是:
"key" in object
例如:
let user = { name: "John", age: 30 };
alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。
请注意,in
的左边必须是 属性名。通常是一个带引号的字符串。
如果我们省略引号,就意味着左边是一个变量,它应该包含要判断的实际属性名。例如:
let user = { age: 30 };
let key = "age";
alert( key in user ); // true,属性 "age" 存在
为何会有 in
运算符呢?与 undefined
进行比较来判断还不够吗?
确实,大部分情况下与 undefined
进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in
运算符的判断结果仍是对的。
那就是属性存在,但存储的值是 undefined
的时候:
let obj = {
test: undefined
};
alert( obj.test ); // 显示 undefined,所以属性不存在?
alert( "test" in obj ); // true,属性存在!
在上面的代码中,属性 obj.test
事实上是存在的,所以 in
操作符检查通过。
- for···in循环
语法:
for (key in object) {
// 对此对象属性中的每个键执行的代码
}
例如,让我们列出 user
所有的属性:
let user = {
name: "John",
age: 30,
isAdmin: true
};
for (let key in user) {
// keys
alert( key ); // name, age, isAdmin
// 属性键的值
alert( user[key] ); // John, 30, true
}
注意:
如果我们遍历一个对象,简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。
例如:
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
2.对象的引用和复制
简单的来说,我们构建对象的时候所设定的变量存储的是对象本体所在的地址,也就是对对象的“引用”,所以如果直接用另一个变量来复制以前的对象变量,那么复制到的是对象的地址,相当于,对象本体是一把锁,对象变量是一把钥匙,用另一个变量复制对象变量,就是“配一把钥匙”。
3.对象相等
仅当两个对象为同一对象时,两者才相等。
例如,这里 a
和 b
两个变量都引用同一个对象,所以它们相等:
let a = {};
let b = a; // 复制引用
alert( a == b ); // true,都引用同一对象
alert( a === b ); // true
而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
对于类似 obj1 > obj2
的比较,或者跟一个原始类型值的比较 obj == 5
,对象都会被转换为原始值。我们很快就会学到对象是如何转换的,但是说实话,很少需要进行这样的比较 —— 通常是在编程错误的时候才会出现这种情况。
4.对象克隆
那么,拷贝一个对象变量会又创建一个对相同对象的引用。
但是,如果我们想要复制一个对象,那该怎么做呢?
我们可以创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。
就像这样:
let user = {
name: "John",
age: 30
};
let clone = {}; // 新的空对象
// 将 user 中所有的属性拷贝到其中
for (let key in user) {
clone[key] = user[key];
}
// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据
alert( user.name ); // 原来的对象中的 name 属性依然是 John
我们也可以使用 Object.assign 方法来达成同样的效果。
语法是:
Object.assign(dest, [src1, src2, src3...])
- 第一个参数
dest
是指目标对象。 - 更后面的参数
src1, ..., srcN
(可按需传递多个参数)是源对象。 - 该方法将所有源对象的属性拷贝到目标对象
dest
中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。 - 调用结果返回
dest
。
例如,我们可以用它来合并多个对象:
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);
// 现在 user = { name: "John", canView: true, canEdit: true }
如果被拷贝的属性的属性名已经存在,那么它会被覆盖:
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // 现在 user = { name: "Pete" }
我们也可以用 Object.assign
代替 for..in
循环来进行简单克隆:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
它将 user
中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
还有其他克隆对象的方法,例如使用 spread 语法 clone = {...user}
,在后面的章节中我们会讲到。
5.深度拷贝
到现在为止,我们都假设 user
的所有属性均为原始类型。但属性可以是对其他对象的引用。
例如:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
现在这样拷贝 clone.sizes = user.sizes
已经不足够了,因为 user.sizes
是个对象,它会以引用形式被拷贝。因此 clone
和 user
会共用一个 sizes:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true,同一个对象
// user 和 clone 分享同一个 sizes
user.sizes.width++; // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个获取到变更后的结果
为了解决这个问题,并让 user
和 clone
成为两个真正独立的对象,我们应该使用一个拷贝循环来检查 user[key]
的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”
Last modified on 2023-03-09