重新回顾JS的笔记系列(七)

重新回顾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.对象相等

仅当两个对象为同一对象时,两者才相等。

例如,这里 ab 两个变量都引用同一个对象,所以它们相等:

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 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 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,能从另外一个获取到变更后的结果

为了解决这个问题,并让 userclone 成为两个真正独立的对象,我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”


Last modified on 2023-03-09