instanceof(高级)在本书中,JavaScript 的面向对象编程 (OOP) 风格分四个步骤介绍。本章涵盖步骤 2-4,上一章 涵盖步骤 1。这些步骤是(图 9)
原型是 JavaScript 唯一的继承机制:每个对象都有一个原型,该原型可以是 null 或一个对象。在后一种情况下,该对象继承原型的所有属性。
在对象字面量中,您可以通过特殊属性 __proto__ 设置原型
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);鉴于原型对象本身可以有一个原型,我们得到一个对象链——所谓的 *原型链*。这意味着继承让我们感觉像是在处理单个对象,但实际上是在处理对象链。
图 10 显示了 obj 的原型链是什么样的。
非继承属性称为 *自身属性*。obj 有一个自身属性 .objProp。
某些操作会考虑所有属性(自身属性和继承属性)——例如,获取属性
> const obj = { foo: 1 };
> typeof obj.foo // own
'number'
> typeof obj.toString // inherited
'function'其他操作只考虑自身属性——例如,Object.keys()
> Object.keys(obj)
[ 'foo' ]继续阅读另一个也只考虑自身属性的操作:设置属性。
原型链的一个可能违反直觉的方面是,通过对象(甚至是继承的对象)设置 *任何* 属性只会更改该对象本身,而不会更改任何原型。
考虑以下对象 obj
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};在下一个代码片段中,我们设置了继承的属性 obj.protoProp(A 行)。这通过创建一个自身属性来“更改”它:读取 obj.protoProp 时,首先找到自身属性,其值 *覆盖* 继承属性的值。
// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);
obj.protoProp = 'x'; // (A)
// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');
// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');obj 的原型链如图 11 所示。
__proto__,除非在对象字面量中我建议避免使用伪属性 __proto__:正如我们将在 后面 看到的,并非所有对象都有它。
但是,对象字面量中的 __proto__ 不同。在那里,它是一个内置功能,始终可用。
获取和设置原型的推荐方法是
获取原型的最佳方法是通过以下方法
Object.getPrototypeOf(obj: Object) : Object设置原型的最佳方法是在创建对象时——通过对象字面量中的 __proto__ 或通过
Object.create(proto: Object) : Object如果必须,可以使用 Object.setPrototypeOf() 更改现有对象的原型。但这可能会对性能产生负面影响。
以下是这些功能的使用方法
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);
Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);到目前为止,“p 是 o 的原型”始终意味着“p 是 o 的 *直接* 原型”。但它也可以更宽松地使用,表示 p 在 o 的原型链中。这种更宽松的关系可以通过以下方式检查
p.isPrototypeOf(o)例如
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);考虑以下代码
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
},
};
const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
},
};
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');我们有两个非常相似的对象。两者都有两个属性,名称分别为 .name 和 .describe。此外,方法 .describe() 是相同的。我们如何避免重复该方法?
我们可以将其移动到对象 PersonProto 并使该对象成为 jane 和 tarzan 的原型
const PersonProto = {
describe() {
return 'Person named ' + this.name;
},
};
const jane = {
__proto__: PersonProto,
name: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
name: 'Tarzan',
};原型名称反映了 jane 和 tarzan 都是人。
图 12 说明了这三个对象是如何连接的:底部的对象现在包含特定于 jane 和 tarzan 的属性。顶部的对象包含它们之间共享的属性。
当您进行方法调用 jane.describe() 时,this 指向该方法调用的接收者 jane(在图的左下角)。这就是该方法仍然有效的原因。tarzan.describe() 的工作原理类似。
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');我们现在准备学习类,它基本上是用于设置原型链的紧凑语法。在底层,JavaScript 的类是非传统的。但这是您在使用它们时很少看到的东西。对于使用过其他面向对象编程语言的人来说,它们通常应该很熟悉。
我们之前使用过 jane 和 tarzan,它们是表示人员的单个对象。让我们使用 *类声明* 来实现人员对象的工厂
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}现在可以通过 new Person() 创建 jane 和 tarzan
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
const tarzan = new Person('Tarzan');
assert.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');类 Person 有两种方法
.describe().constructor(),它在新实例创建后立即调用并初始化该实例。它接收传递给 new 运算符的参数(在类名之后)。如果您不需要任何参数来设置新实例,则可以省略构造函数。有两种 *类定义*(定义类的方式)
类表达式可以是匿名的,也可以是命名的
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };命名类表达式的名称的工作方式类似于 命名函数表达式的名称。
这是对类的初步了解。我们很快就会探索更多功能,但首先我们需要学习类的内部机制。
类的底层机制有很多东西。让我们看一下 jane 的图表(图 13)。
类 Person 的主要目的是设置右侧的原型链(jane,后跟 Person.prototype)。有趣的是,类 Person 内部的两个构造(.constructor 和 .describe())都为 Person.prototype 创建了属性,而不是为 Person 创建了属性。
这种略显奇怪的方法的原因是向后兼容性:在类之前,*构造函数*(普通函数,通过 new 运算符调用)通常用作对象的工厂。类主要是构造函数的更好语法,因此与旧代码保持兼容。这解释了为什么类是函数
> typeof Person
'function'在本书中,我交替使用术语 *构造(函数)* 和 *类*。
很容易混淆 .__proto__ 和 .prototype。希望图 13 能清楚地说明它们之间的区别
.__proto__ 是用于访问对象原型的伪属性。.prototype 是一个普通属性,只是由于 new 运算符的使用方式而变得特殊。这个名字并不理想:Person.prototype 并不指向 Person 的原型,而是指向 Person 的所有实例的原型。Person.prototype.constructor(高级)图 13 中有一个细节我们还没有看,那就是:Person.prototype.constructor 指向 Person
> Person.prototype.constructor === Person
true这种设置也是为了向后兼容性而存在的。但它还有两个额外的好处。
首先,类的每个实例都继承属性 .constructor。因此,给定一个实例,您可以使用它创建“类似”的对象
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
// (the instanceof operator is explained later)
assert.equal(cheeta instanceof Person, true);其次,您可以获取创建给定实例的类的名称
const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');以下类声明主体中的所有构造都创建 Foo.prototype 的属性。
class Foo {
constructor(prop) {
this.prop = prop;
}
protoMethod() {
return 'protoMethod';
}
get protoGetter() {
return 'protoGetter';
}
}让我们按顺序检查它们
.constructor() 在创建 Foo 的新实例后调用,以设置该实例。.protoMethod() 是一个普通方法。它存储在 Foo.prototype 中。.protoGetter 是一个存储在 Foo.prototype 中的 getter。以下交互使用类 Foo
> const foo = new Foo(123);
> foo.prop
123
> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'以下类声明主体中的所有构造都创建所谓的 *静态* 属性——Bar 本身的属性。
class Bar {
static staticMethod() {
return 'staticMethod';
}
static get staticGetter() {
return 'staticGetter';
}
}静态方法和静态 getter 的使用方法如下
> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'instanceof 运算符instanceof 运算符告诉您一个值是否是给定类的实例
> new Person('Jane') instanceof Person
true
> ({}) instanceof Person
false
> ({}) instanceof Object
true
> [] instanceof Array
true我们将在 后面 了解子类化之后更详细地探讨 instanceof 运算符。
我建议使用类的原因如下
类是用于对象创建和继承的通用标准,现在在各种框架(React、Angular、Ember 等)中得到广泛支持。与以前几乎每个框架都有自己的继承库相比,这是一个进步。
它们可以帮助 IDE 和类型检查器等工具完成工作,并在其中启用新功能。
如果您从另一种语言转向 JavaScript 并且习惯使用类,那么您可以更快地上手。
JavaScript 引擎会优化它们。也就是说,使用类的代码几乎总是比使用自定义继承库的代码快。
您可以将内置构造函数(例如 Error)子类化。
这并不意味着类是完美的
存在过度使用继承的风险。
存在在类中放置太多功能的风险(而其中一些功能通常最好放在函数中)。
它们在表面上和底层的工作方式截然不同。换句话说,语法和语义之间存在脱节。有两个例子
C 中的方法定义会在对象 C.prototype 中创建一个方法。造成这种脱节的原因是向后兼容性。值得庆幸的是,这种脱节在实践中很少引起问题;如果您同意类的表面行为,通常不会有问题。
练习:编写一个类
exercises/proto-chains-classes/point_class_test.mjs
本节介绍用于对外部隐藏对象某些数据的技术。我们在类的上下文中讨论它们,但它们也适用于直接创建的对象,例如,通过对象字面量创建的对象。
第一种技术通过在属性名称前添加下划线来使其私有化。这不会以任何方式保护属性;它只是向外部发出信号:“您不需要了解此属性。”
在以下代码中,属性 ._counter 和 ._action 是私有的。
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The two properties aren’t really private:
assert.deepEqual(
Object.keys(new Countdown()),
['_counter', '_action']);使用此技术,您不会获得任何保护,并且私有名称可能会发生冲突。从好的方面来说,它易于使用。
另一种技术是使用 WeakMap。关于其工作原理的详细说明,请参阅WeakMap 章节。这是一个预览
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);此技术为您提供了相当大的外部访问保护,并且不会发生任何名称冲突。但使用起来也更复杂。
本书介绍了类中私有数据最重要的技术。而且可能很快就会有内置支持。有关详细信息,请参阅 ECMAScript 提案 “类公共实例字段和私有实例字段”。
Exploring ES6 中介绍了一些其他技术。
类也可以将现有类子类化(“扩展”)。例如,以下类 Employee 是 Person 的子类
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');两点注释
在 .constructor() 方法内部,您必须先通过 super() 调用超类构造函数,然后才能访问 this。这是因为 this 在调用超类构造函数之前不存在(这种现象是类特有的)。
静态方法也会被继承。例如,Employee 继承了静态方法 .logNames()
> 'logNames' in Employee
true 练习:子类化
exercises/proto-chains-classes/color_point_class_test.mjs
上一节中的类 Person 和 Employee 由多个对象组成(图 14)。理解这些对象如何关联的一个关键见解是存在两条原型链
实例原型链从 jane 开始,然后是 Employee.prototype 和 Person.prototype。原则上,原型链到此结束,但我们还有一个对象:Object.prototype。此原型为几乎所有对象提供服务,这就是为什么它也包含在此处的原因
> Object.getPrototypeOf(Person.prototype) === Object.prototype
true在类原型链中,首先是 Employee,然后是 Person。之后,链条继续到 Function.prototype,它之所以存在是因为 Person 是一个函数,而函数需要 Function.prototype 的服务。
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceof(高级)我们还没有看到 instanceof 的真正工作原理。给定表达式
x instanceof Cinstanceof 如何确定 x 是否是 C 的实例(或 C 的子类)?它通过检查 C.prototype 是否在 x 的原型链中来实现。也就是说,以下表达式是等效的
C.prototype.isPrototypeOf(x)如果我们回到图 14,我们可以确认原型链确实引导我们得到了以下正确答案
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true接下来,我们将利用我们对子类化的知识来理解一些内置对象的原型链。以下工具函数 p() 可帮助我们进行探索。
const p = Object.getPrototypeOf.bind(Object);我们提取了 Object 的方法 .getPrototypeOf() 并将其分配给 p。
{} 的原型链让我们从检查普通对象开始
> p({}) === Object.prototype
true
> p(p({})) === null
true图 15 显示了此原型链的示意图。我们可以看到 {} 确实是 Object 的实例 - Object.prototype 在其原型链中。
[] 的原型链数组的原型链是什么样的?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true此原型链(在图 16 中可视化)告诉我们,数组对象是 Array 的实例,而 Array 是 Object 的子类。
function () {} 的原型链最后,普通函数的原型链告诉我们所有函数都是对象
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
trueObject 实例的对象仅当 Object.prototype 在其原型链中时,对象才是 Object 的实例。通过各种字面量创建的大多数对象都是 Object 的实例
> ({}) instanceof Object
true
> (() => {}) instanceof Object
true
> /abc/ug instanceof Object
true没有原型的对象不是 Object 的实例
> ({ __proto__: null }) instanceof Object
falseObject.prototype 终止了大多数原型链。它的原型是 null,这意味着它本身也不是 Object 的实例
> Object.prototype instanceof Object
false.__proto__ 究竟是如何工作的?伪属性 .__proto__ 由类 Object 通过 getter 和 setter 实现。它可以像这样实现
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}这意味着您可以通过创建一个原型链中没有 Object.prototype 的对象来关闭 .__proto__(请参阅上一节)
> '__proto__' in {}
true
> '__proto__' in { __proto__: null }
false让我们研究一下方法调用如何与类一起使用。我们正在重新审视之前的 jane
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const jane = new Person('Jane');图 17 显示了 jane 的原型链图。
普通方法调用是_调度_的 - 方法调用 jane.describe() 分两步进行
调度:在 jane 的原型链中,找到键为 'describe' 的第一个属性并检索其值。
const func = jane.describe;调用:调用该值,同时将 this 设置为 jane。
func.call(jane);这种动态查找方法并调用它的方式称为_动态调度_。
您可以_直接_进行相同的方法调用,而无需调度
Person.prototype.describe.call(jane)这一次,我们通过 Person.prototype.describe 直接指向该方法,并且不在原型链中搜索它。我们还通过 .call() 以不同方式指定了 this。
请注意,this 始终指向原型链的开头。这使得 .describe() 可以访问 .name。
当您使用 Object.prototype 的方法时,直接方法调用变得很有用。例如,Object.prototype.hasOwnProperty(k) 检查 this 是否具有键为 k 的非继承属性
> const obj = { foo: 123 };
> obj.hasOwnProperty('foo')
true
> obj.hasOwnProperty('bar')
false但是,在对象的原型链中,可能存在另一个键为 'hasOwnProperty' 的属性,该属性会覆盖 Object.prototype 中的方法。然后,调度方法调用将不起作用
> const obj = { hasOwnProperty: true };
> obj.hasOwnProperty('bar')
TypeError: obj.hasOwnProperty is not a function解决方法是使用直接方法调用
> Object.prototype.hasOwnProperty.call(obj, 'bar')
false
> Object.prototype.hasOwnProperty.call(obj, 'hasOwnProperty')
true这种直接方法调用通常缩写如下
> ({}).hasOwnProperty.call(obj, 'bar')
false
> ({}).hasOwnProperty.call(obj, 'hasOwnProperty')
true这种模式看起来效率低下,但大多数引擎都对此模式进行了优化,因此性能应该不是问题。
JavaScript 的类系统仅支持_单继承_。也就是说,每个类最多只能有一个超类。解决此限制的一种方法是使用一种称为_mixin 类_(简称:_mixins_)的技术。
其思想如下:假设我们希望类 C 从两个超类 S1 和 S2 继承。那就是_多重继承_,JavaScript 不支持。
我们的解决方法是将 S1 和 S2 转换为_mixins_,即子类的工厂
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };这两个函数中的每一个都返回一个扩展了给定超类 Sup 的类。我们创建类 C 如下
class C extends S2(S1(Object)) {
/*···*/
}我们现在有了一个类 C,它扩展了类 S2,而 S2 扩展了类 S1,而 S1 扩展了 Object(大多数类都隐式地这样做)。
我们实现了一个 mixin Branded,它具有用于设置和获取对象品牌的辅助方法
const Branded = (Sup) => class extends Sup {
setBrand(brand) {
this._brand = brand;
return this;
}
getBrand() {
return this._brand;
}
};我们使用此 mixin 来实现类 Car 的品牌管理
class Car extends Branded(Object) {
constructor(model) {
super();
this._model = model;
}
toString() {
return `${this.getBrand()} ${this._model}`;
}
}以下代码确认 mixin 起作用了:Car 具有 Branded 的方法 .setBrand()。
const modelT = new Car('Model T').setBrand('Ford');
assert.equal(modelT.toString(), 'Ford Model T');Mixins 使我们摆脱了单继承的限制
原则上,对象是无序的。对属性进行排序的主要原因是为了使列出条目、键或值的操作具有确定性。这有助于例如测试。
测验
请参阅测验应用程序。