对象

不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为 object 类型,null的二进制表示是全0,自然前三位也是0,所以执行 typeof 时会返回 "object"

对象可以通过两种形式定义:字面量 和 构造形式。两者生成的对象是一样的,唯一的区别是字面量可以添加多个键/值对,但是在构造形式中必须逐个添加属性。

1// 字面量【推荐】
2let obj = {
3  key: valye,
4};
5
6// 构造形式
7let obj = new Object();
8obj.key = value;

函数是对象的一个子类型,JavaScript中的函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数

数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般对象要稍微复杂一些。

内置对象

JavaScript中还有一些对象子类型,通常被称为内置对象。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

这些内置对象可以当做构造函数来使用,从而可以构造一个对应子类型的新对象。

. 操作符和 [] 操作符

. 语法通常被称为「属性访问」,要求属性名满足标识符的命名规范

[] 语通常被称为「键访问」,可以接受任意 UTF-8/Unicode 字符串作为属性名。

在对象中,属性名用于都是字符串。如果使用string字面量以外的其它值作为属性名,那么它首先会被转换成一个字符串。

数组

如果试图像数组添加一个属性,但是属性看起来像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)

1let arr = [1, 2, 3];
2arr["3"] = 4;
3console.log(arr.length); // 4
4console.log(arr[3]); // 4

属性描述符

ES5开始,所有的属性都具备了属性描述符

  • writable(可写):决定是否可以修改属性的值。
  • enumerable(可枚举):决定属性是否出现在对象的属性枚举中,默认是 true。
  • configurable(可配置):只要属性是可配置的,就可以使用 definedProperty() 方法来修改属性描述符。注意:把 configurable 修改成 false 是单向操作,无法撤销!除了无法修改,configurable: false 还会禁止删除这个属性。
1const myObject = {
2  a: 2,
3};
4console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
5// { value: 2, writable: true, enumerable: true, configurable: true }

不变性

1 对象常量

结合 writable: falseconfigurable: false 就可以创建一个真正的常量属性。(不可修改、重定义或者删除)

2 禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions() ,非严格模式下,创建新属性会静默失败,在严格模式下,将会抛出 TypeError 错误。

3 密封

Object.seal() 会创建一个密封对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions() 并把所有现在有属性标记为 configurable: false

所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

4 冻结

Object.freeze() 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal() 并把所有 “数据访问” 属性标记为 writable: false,这样就无法修改它们的值。

这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(这个对象引用的其他对象是不受影响的)

你可以 “深度冻结” 一个对象,具体方法为,首先在这个对象上调用 Object.freeze() 然后遍历它引用的所有对象并在这些对象上调用 Object.freeze()。但是一定要小心,因为这样做有可能会在无意中冻结其它(共享)对象。

[[get]]

获取属性值

访问属性时,引擎实际上会调用内部的默认[[get]]操作(在设置属性值时是[[put]]),[[get]]操作会检查对象本身是否包含这个属性,如果没找到的话还会查找[[prototype]]链。

[[put]]

设置属性值

[[put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)

如果已经存在这个属性,[[put]] 算法大致会检查下面这些内容:

  1. 属性是否有访问描述符?如果是并且存在 setter 就调用 setter
  2. 属性的数据描述符中 writable 是否是 false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[put]] 操作会更加复杂。

Getter和Setter

在ES5中可以使用 getter 和 setter 部分改写默认操作,但是只能引用在单个属性上,无法应用在整个对象上。

getter是一个隐藏函数,会在获取属性值时调用。

setter也是一个隐藏函数,会在设置属性值时调用。

当你给一个属性定义 getter、setter或者两者都有时,这个属性会被定义为「访问描述符」(和数据描述符相对),对于访问描述符来说,JavaScript会忽略它们的value和writable特性,取而代之的是get和set。

  • get
  • set
  • configurable
  • enumable
1const myObject = {
2  // 给a定义一个getter
3  get a() {
4    return this._a_;
5  },
6  // 给a定义一个setter
7  set a(val) {
8    this._a_ = val * 2;
9  },
10};
11myObject.a = 3;
12console.log(myObject.a);

setter会覆盖单个属性默认的[[put]]操作。

通常来说 getter 和 setter 是成对出现的(只定义一个的话,通常会产生意料之外的行为)

存在性

1const myObj = {
2  a: 2,
3};
4// 注意属性都是字符串
5console.log("a" in myObj); // true
6console.log("b" in myObj); // false
7console.log(myObj.hasOwnProperty("a")); // true
8console.log(myObj.hasOwnProperty("b")); // false
9
10// 有的对象可能没有连接到 Object.prototype,则可以使用这种方式
11console.log(Object.prototype.hasOwnProperty.call(myObj, "a")); // true
12console.log(Object.prototype.hasOwnProperty.call(myObj, "b")); // false

in 操作符会检查属性名是否在对象及其[[prototype]]原型链中。

hasOwnProperty() 只会检查属性是否在当前对象中,不会检查[[prototype]]链。

所有普通对象都可以通过对于 Object.prototype 的委托来访问 hasOwnProperty()