Object
属性的类型
在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不 指定,则都默认为 false
数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。
let person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name; //报错
console.log(person.name); // "Nicholas"访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访 问器属性有 4 个特性描述它们的行为。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017,
// get: function () { // 报错,数据属性不能和访问器属性同时使用
// return this.edition;
// },
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
})
console.log(Object.getOwnPropertyDescriptors(book));
// {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// },
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// },
// year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
// }
// }
const dest = {
set a(val) {
console.log(`Invoked dest setter with param ${ val }`);
}
};
const src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
console.log(dest);
console.log(src.a); 调用 src 的获取方法 调用 dest 的设置方法并传入参数"foo" 因为这里的设置函数不执行赋值操作 所以实际上并没有把值转移过来 console.log(dest); // { set a(val) {...} }
对象解构
解构在内部使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这 意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),null 和 undefined 不能被解构,否则会抛出错误。
嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样:
参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在 函数签名中声明在函数体内使用局部变量:
创建对象
工厂模式
构造函数模式
比如,前面的例子使用构造函数模式可以这样写:
在这个例子中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部 的代码跟 createPerson()基本是一样的,只是有如下区别
没有显式地创建对象。
属性和方法直接赋值给了 this。
没有 return。
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。 (1) 在内存中创建一个新对象。 (2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。 (3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。 (4) 执行构造函数内部的代码(给新对象添加属性)。 (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
构造函数的问题 构造函数的主要问题在于,其定义的方法会在每个实例上 都创建一遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方 法不是同一个 Function 实例
要解决这个问题,可以把函数定义转移到构造函数外部:
Object.keys() vs Object.getOwnPropertyNames()
理解原型
虽然不是所有实现都对外暴露了[[Prototype]],但可以使用 isPrototypeOf()方法确定两个对 象之间的这种关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时 返回 true,如下所示:
这里通过原型对象调用 isPrototypeOf()方法检查了 person1 和 person2。因为这两个例子内 部都有链接指向 Person.prototype,所以结果都返回 true。 ECMAScript 的 Object 类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值。使用 Object.getPrototypeOf()可以 方便地取得一个对象的原型 例如:
Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一 个新值。这样就可以重写一个对象的原型继承关系:
使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创 建一个新对象,同时为其指定原型:
原型的动态性
虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两 回事。实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同 的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。 记住,实例只有指向原型的指针,没有指向构造函数的指针。来看下面的例子:
在这个例子中,Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()的时 候,会导致错误。这是因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。图 8-3 展示了这里面的原因。

重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最
初的原型。
原型的问题
原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默 认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共 享特性。
我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性 也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题 来自包含引用值的属性。来看下面的例子:
这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建 了两个 Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个 friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个 数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一 般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因
继承
原型链
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用 类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
实现原型链涉及如下代码模式:

默认原型
实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实 现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向 Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默 认方法的原因。因此前面的例子还有额外一层继承关系。图 8-5 展示了完整的原型链。

原型继承
Object.create()与这里的 object()方法效果相同
Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增 属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。比如:
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
原型式继承比较接近的一种继承方式是寄生式继承(parasitic inheritance),也是 Crockford 首倡的 一种模式。寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种 方式增强对象,然后返回这个对象。基本的寄生继承模式如下:
在这段代码中,createAnother()函数接收一个参数,就是新对象的基准对象。这个对象 original 会被传给 object()函数,然后将返回的新对象赋值给 clone。接着给 clone 对象添加一个新方法 sayHi()。最后返回这个对象。可以像下面这样使用 createAnother()函数:
这个例子基于 person 对象返回了一个新对象。新返回的 anotherPerson 对象具有 person 的所 有属性和方法,还有一个新方法叫 sayHi()。 寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式 继承所必需的,任何返回新对象的函数都可以在这里使用。 注意 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
在上面的例子中,我们定义了 Animal 构造函数作为父类,它有一个属性 name 和一个方法 sayName。然后我们定义了 Dog 构造函数作为子类,它通过借用 Animal 构造函数来继承它的属性 name,并添加了自己的属性 age 和方法 sayAge。最后,我们使用 Object.create() 方法创建了一个 Animal.prototype 的副本,并将其赋值给 Dog.prototype,实现了对父类方法的继承。
Object.fromEntries()
ECMAScript 2019又给Object类添加了一个静态方法fromEntries(),用于通过键/值对数组的 集合构建对象。这个方法执行与 Object.entries()方法相反的操作。来看下面的例子:
此静态方法接收一个可迭代对象参数,该可迭代对象可以包含任意数量的大小为 2 的可迭代对象。 这个方法可以方便地将 Map 实例转换为 Object 实例,因为 Map 迭代器返回的结果与 fromEntries() 的参数恰好匹配:
Last updated