instanceof
(高级)在本书中,JavaScript 的面向对象编程 (OOP) 风格分四个步骤介绍。本章涵盖步骤 2-4,上一章 涵盖步骤 1。这些步骤是(图 9)
原型是 JavaScript 唯一的继承机制:每个对象都有一个原型,该原型可以是 null
或一个对象。在后一种情况下,该对象继承原型的所有属性。
在对象字面量中,您可以通过特殊属性 __proto__
设置原型
const proto = {
protoProp: 'a',
;
}const obj = {
__proto__: proto,
objProp: 'b',
;
}
// obj inherits .protoProp:
.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true); assert
鉴于原型对象本身可以有一个原型,我们得到一个对象链——所谓的 *原型链*。这意味着继承让我们感觉像是在处理单个对象,但实际上是在处理对象链。
图 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
.deepEqual(Object.keys(obj), ['objProp']);
assert
.protoProp = 'x'; // (A)
obj
// We created a new own property:
.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
assert
// The inherited property itself is unchanged:
.equal(proto.protoProp, 'a');
assert
// The own property overrides the inherited property:
.equal(obj.protoProp, 'x'); assert
obj
的原型链如图 11 所示。
__proto__
,除非在对象字面量中我建议避免使用伪属性 __proto__
:正如我们将在 后面 看到的,并非所有对象都有它。
但是,对象字面量中的 __proto__
不同。在那里,它是一个内置功能,始终可用。
获取和设置原型的推荐方法是
获取原型的最佳方法是通过以下方法
.getPrototypeOf(obj: Object) : Object Object
设置原型的最佳方法是在创建对象时——通过对象字面量中的 __proto__
或通过
.create(proto: Object) : Object Object
如果必须,可以使用 Object.setPrototypeOf()
更改现有对象的原型。但这可能会对性能产生负面影响。
以下是这些功能的使用方法
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
.equal(Object.getPrototypeOf(obj), proto1);
assert
Object.setPrototypeOf(obj, proto2);
.equal(Object.getPrototypeOf(obj), proto2); assert
到目前为止,“p
是 o
的原型”始终意味着“p
是 o
的 *直接* 原型”。但它也可以更宽松地使用,表示 p
在 o
的原型链中。这种更宽松的关系可以通过以下方式检查
.isPrototypeOf(o) p
例如
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert
.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false); assert
考虑以下代码
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
,
};
}const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
,
};
}
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
我们有两个非常相似的对象。两者都有两个属性,名称分别为 .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()
的工作原理类似。
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
我们现在准备学习类,它基本上是用于设置原型链的紧凑语法。在底层,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');
.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
assert
const tarzan = new Person('Tarzan');
.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
类 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 === Persontrue
这种设置也是为了向后兼容性而存在的。但它还有两个额外的好处。
首先,类的每个实例都继承属性 .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)
.equal(cheeta instanceof Person, true); assert
其次,您可以获取创建给定实例的类的名称
const tarzan = new Person('Tarzan');
.equal(tarzan.constructor.name, 'Person'); assert
以下类声明主体中的所有构造都创建 Foo.prototype
的属性。
class Foo {
constructor(prop) {
this.prop = prop;
}protoMethod() {
return 'protoMethod';
}protoGetter() {
get return 'protoGetter';
} }
让我们按顺序检查它们
.constructor()
在创建 Foo
的新实例后调用,以设置该实例。.protoMethod()
是一个普通方法。它存储在 Foo.prototype
中。.protoGetter
是一个存储在 Foo.prototype
中的 getter。以下交互使用类 Foo
> const foo = new Foo(123);
> foo.prop123
> 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 Persontrue
> ({}) instanceof Personfalse
> ({}) instanceof Objecttrue
> [] instanceof Arraytrue
我们将在 后面 了解子类化之后更详细地探讨 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:
.deepEqual(
assertObject.keys(new Countdown()),
'_counter', '_action']); [
使用此技术,您不会获得任何保护,并且私有名称可能会发生冲突。从好的方面来说,它易于使用。
另一种技术是使用 WeakMap。关于其工作原理的详细说明,请参阅WeakMap 章节。这是一个预览
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
.set(this, counter);
_counter.set(this, action);
_action
}dec() {
let counter = _counter.get(this);
--;
counter.set(this, counter);
_counterif (counter === 0) {
.get(this)();
_action
}
}
}
// The two pseudo-properties are truly private:
.deepEqual(
assertObject.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');
.equal(
assert.describe(),
jane'Person named Jane (CTO)');
两点注释
在 .constructor()
方法内部,您必须先通过 super()
调用超类构造函数,然后才能访问 this
。这是因为 this
在调用超类构造函数之前不存在(这种现象是类特有的)。
静态方法也会被继承。例如,Employee
继承了静态方法 .logNames()
> 'logNames' in Employeetrue
练习:子类化
exercises/proto-chains-classes/color_point_class_test.mjs
上一节中的类 Person
和 Employee
由多个对象组成(图 14)。理解这些对象如何关联的一个关键见解是存在两条原型链
实例原型链从 jane
开始,然后是 Employee.prototype
和 Person.prototype
。原则上,原型链到此结束,但我们还有一个对象:Object.prototype
。此原型为几乎所有对象提供服务,这就是为什么它也包含在此处的原因
> Object.getPrototypeOf(Person.prototype) === Object.prototypetrue
在类原型链中,首先是 Employee
,然后是 Person
。之后,链条继续到 Function.prototype
,它之所以存在是因为 Person
是一个函数,而函数需要 Function.prototype
的服务。
> Object.getPrototypeOf(Person) === Function.prototypetrue
instanceof
(高级)我们还没有看到 instanceof
的真正工作原理。给定表达式
instanceof C x
instanceof
如何确定 x
是否是 C
的实例(或 C
的子类)?它通过检查 C.prototype
是否在 x
的原型链中来实现。也就是说,以下表达式是等效的
.prototype.isPrototypeOf(x) C
如果我们回到图 14,我们可以确认原型链确实引导我们得到了以下正确答案
> jane instanceof Employeetrue
> jane instanceof Persontrue
> jane instanceof Objecttrue
接下来,我们将利用我们对子类化的知识来理解一些内置对象的原型链。以下工具函数 p()
可帮助我们进行探索。
const p = Object.getPrototypeOf.bind(Object);
我们提取了 Object
的方法 .getPrototypeOf()
并将其分配给 p
。
{}
的原型链让我们从检查普通对象开始
> p({}) === Object.prototypetrue
> p(p({})) === nulltrue
图 15 显示了此原型链的示意图。我们可以看到 {}
确实是 Object
的实例 - Object.prototype
在其原型链中。
[]
的原型链数组的原型链是什么样的?
> p([]) === Array.prototypetrue
> p(p([])) === Object.prototypetrue
> p(p(p([]))) === nulltrue
此原型链(在图 16 中可视化)告诉我们,数组对象是 Array
的实例,而 Array
是 Object
的子类。
function () {}
的原型链最后,普通函数的原型链告诉我们所有函数都是对象
> p(function () {}) === Function.prototypetrue
> p(p(function () {})) === Object.prototypetrue
Object
实例的对象仅当 Object.prototype
在其原型链中时,对象才是 Object
的实例。通过各种字面量创建的大多数对象都是 Object
的实例
> ({}) instanceof Objecttrue
> (() => {}) instanceof Objecttrue
> /abc/ug instanceof Objecttrue
没有原型的对象不是 Object
的实例
> ({ __proto__: null }) instanceof Objectfalse
Object.prototype
终止了大多数原型链。它的原型是 null
,这意味着它本身也不是 Object
的实例
> Object.prototype instanceof Objectfalse
.__proto__
究竟是如何工作的?伪属性 .__proto__
由类 Object
通过 getter 和 setter 实现。它可以像这样实现
class Object {
__proto__() {
get return Object.getPrototypeOf(this);
}__proto__(other) {
set 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
。
.call(jane); func
这种动态查找方法并调用它的方式称为_动态调度_。
您可以_直接_进行相同的方法调用,而无需调度
.prototype.describe.call(jane) Person
这一次,我们通过 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');
.equal(modelT.toString(), 'Ford Model T'); assert
Mixins 使我们摆脱了单继承的限制
原则上,对象是无序的。对属性进行排序的主要原因是为了使列出条目、键或值的操作具有确定性。这有助于例如测试。
测验
请参阅测验应用程序。