对象

方括号

对于多词属性,就不能使用悬挂或者尾随逗号,例如:

// 这将提示有语法错误
user.likes birds = true

JavaScript认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。
点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)。
有另一种方法,就是使用方括号,可用于任何字符串:

let user = {};

// 设置
user["likes birds"] = true;

// 读取
alert(user["likes birds"]); // true

// 删除
delete user["likes birds"];

现在一切都可行了。请注意方括号中的字符串要放在引号中,单引号或双引号都可以。

方括号同样提供了一种可以通过任意表达式来获取属性名的方式 —— 与文本字符串不同 —— 例如下面的变量:

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;

在这里,变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。

计算属性

当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性。

例如:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert( bag.apple ); // 5 如果 fruit="apple"

计算属性的含义很简单:[fruit] 含义是属性名应该从 fruit 变量中获取。

所以,如果一个用户输入 "apple",bag 将变为 {apple: 5}。

本质上,这跟下面的语法效果相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

“in” 操作符

相比于其他语言,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 的左边必须是 属性名。通常是一个带引号的字符串。
至于为什么要使用in运算符而不是与undefined进行比较,是因为大部分情况下与 undefined 进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in 运算符的判断结果仍是对的。

"for..in" 循环

为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in。这跟我们在前面学到的 for(;;) 循环是完全不一样的东西。

语法:

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
}

对象可用于面向用户的建议选项列表。如果我们的网站主要面向德国观众,那么我们可能希望 49 排在第一。

但如果我们执行代码,会看到完全不同的现象:

  • USA (1) 排在了最前面
  • 然后是 Switzerland (41) 及其它。

因为这些电话号码是整数,所以它们以升序排列。所以我们看到的是 1, 41, 44, 49。
“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。
所以,"49" 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了:

// Number(...) 显式转换为数字
// Math.trunc 是内建的去除小数部分的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

……此外,如果属性名不是整数,那它们就按照创建时的顺序来排序,例如:

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 增加一个

// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
  alert( prop ); // name, surname, age
}

所以,为了解决电话号码的问题,我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+" 前缀就行了。

像这样:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for (let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

对象引用和复制

赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。
当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。

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",
  sizes: {
    height: 182,
    width: 50
  }
};

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

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝,因此 clone 和 user 会共用一个 sizes。
正确的方法是使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。这样可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。
除此之外,使用 const 声明的对象也是可以被修改的。
例如:

const user = {
  name: "John"
};

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

alert(user.name); // Pete

看起来 (*) 行的代码会触发一个错误,但实际并没有。user 的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改。

换句话说,只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。

垃圾回收和可达性(Reachability)

简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。

比方说:

  • 当前执行的函数,它的局部变量和参数。
  • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
  • 全局变量。
  • (还有一些内部的)

这些值被称作 根(roots)。

  1. 如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。

比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则 该 对象被认为是可达的。而且它引用的内容也是可达的。下面是详细的例子。
在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

this

在没有对象的情况下调用:this == undefined:

function sayHi() {
  alert(this);
}

sayHi(); // undefined

在这种情况下,严格模式下的 this 值为 undefined。如果我们尝试访问 this.name,将会报错。
在非严格模式的情况下,this 将会是 全局对象(浏览器中的 window,我们稍后会在 全局对象 一章中学习它)。这是一个历史行为,"use strict" 已经将其修复了。
通常这种调用是程序出错了。如果在一个函数内部有 this,那么通常意味着它是在对象上下文环境中被调用的。
除此之外,箭头函数没有自己的 “this”。
例如:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

这里的 arrow() 使用的 this 来自于外部的 user.sayHi() 方法。