红宝书8.1

221 阅读7分钟

8.1 理解对象

面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”我们可以把ECMAScript的对象想象成散列表:无非就是一组键值对,其中值可以是数据或函数。

8.1.1 属性的类型

ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义 的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]。

属性分两种:数据属性和访问器属性。

1. 数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为true
  • [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true
  • [[Writable]]:表示能否修改属性的值。默认值为true
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined
let person = {
    name: "Nicholas"
};

这里创建了一个person对象,包含一个值为"Nicholas"的name属性,其中[[Configurable]]、[[Enumerable]]和 [[Writable]]都会被设置为 true,而[[Value]]特性会被设置为"Nicholas"。

要修改属性的默认特性,就必须使用 Object.defineProperty()方法。这个方法接收 3 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。描述符对象上的属性可以包含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。比如:

let person = {};
Object.defineProperty(person, "name", {
  writable: false,
  value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"

这个例子创建了一个名为 name 的属性并给它赋予了一个只读的值"Nicholas"。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。

类似的规则也适用于创建不可配置的属性。比如:

let person = {};
Object.defineProperty(person, "name", {
  configurable: false,
  value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name;
console.log(person.name); // "Nicholas"

这个例子把 configurable 设置为 false,意味着这个属性不能从对象上删除。非严格模式下对这个属性调用 delete 没有效果,严格模式下会抛出错误。此外,无论何种模式下一个属性被定义为不可配置之后,就不能再变回可配置的了,再次调用 Object.defineProperty()并修改任何非 writable 属性会导致错误。

注意:在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为 false

2. 访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。 访问器属性是不能直接定义的,必须使用 Object.defineProperty()。例:
// 定义一个对象,包含伪私有成员year_和公共成员edition
let book = {
  year_: 2017,
  edition: 1
};

Object.defineProperty(book, "year", {
  get() {
    return this.year_;
  },
  set(newValue) {
    if (newValue > 2017) {
      this.year_ = newValue;
      this.edition += newValue - 2017;
    }
  }
});
book.year = 2018;
console.log(book.edition); // 2

在这个例子中,对象 book 有两个默认属性:year_和 edition。year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。另一个属性 year 被定义为一个访问器属性,其中获取函数简单地返回 year_的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把 year 属性修改为 2018 会导致 year_变成 2018,edition 变成 2。这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。

获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。

在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。

注意 在ECMAScript5以前,开发者会使用两个非标准的访问创建访问器属性:__defineGetter__()和__defineSetter__()。这两个方法最早是 Firefox 引入的,后来 Safari、 Chrome 和 Opera 也实现了。

8.1.2 定义多个属性

在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。

let book = {};
Object.defineProperties(book, {
      year_: {
        value: 2017
      },
      edition: {
        value: 1
      },
      year: {
        get() {
          return this.year_;
        },
        set(newValue) {
          if (newValue > 2017) {
            this.year_ = newValue;
            this.edition += newValue - 2017;
          }
        }
      }
});

这段代码在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。 最终的对象跟上一个示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable 和 writable 特性值都是 false。

8.1.3 读取属性的特性

使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含 configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、 writable 和 value 属性。比如:

let book = {};
Object.defineProperties(book, {
  year_: {
    value: 2017
  },
  edition: {
    value: 1
  },
  year: {
    get: function() {
      return this.year_;
    },
    set: function(newValue){
      if (newValue > 2017) {
        this.year_ = newValue;
        this.edition += newValue - 2017;
      }
    }   
  }
});
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"

let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"

对于数据属性 year_,value 等于原来的值,configurable 是 false,get 是 undefined。对 于访问器属性 year,value 是 undefined,enumerable 是 false,get 是一个指向获取函数的指针。

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法。这个方法实际上 会在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。对于前面的例子,使用这个静态方法会返回如下对象:

console.log(Object.getOwnPropertyDescriptors(book));
// {
//   edition: {
//     configurable: false,
//     enumerable: false,
//     value: 1,
//     writable: false
//   },
//   year: {
//     configurable: false,
//     enumerable: false,
//     get: fun(),
//     set: fn(newValue)
//   },
//   year_: {
//     configurable: false,
//     enumerable: false,
//     value: 2017,
//     writable: false
//   }
// }

未完。。。