在 ECMAScript 6 ( ES6 )中,类像这样定义:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
这个类使用起来就像一个 ES5 的构造器函数一样:
> var p = new Point(25, 8);
> p.toString()
'(25, 8)'
实际上,类定义的结果就是一个函数:
> typeof Point
'function'
然而,只能通过 new
调用类,不能通过函数调用(原理在后面解释):
> Point()
TypeError: Classes can’t be function-called
在规范中,类以函数调用方式使用的时候,会被函数对象内部的方法 [[Call]] 阻止。
函数声明会被提升:当进入一个作用域的时候,声明在里面的函数马上就可用了 - 不管函数声明在哪个位置。这意味着可以在函数声明之前调用:
foo(); // works, because `foo` is hoisted
function foo() {}
相对地,类声明不会提升。因此,仅在执行到类定义的地方,并且执行完类定义代码,类才会存在。在类声明之前访问类会抛出 ReferenceError
错误:
new Foo(); // ReferenceError
class Foo {}
这种限制的原因是类可以有一个继承
子句,子句的值是任意表达式。这个表达式必须在正确的“地方”被执行,它的执行不能被提升。
没有提升功能导致的限制可能比你想象要少。例如,在类声明前面的函数依然能够访问那个类,但是必须要等到类声明已经被执行掉之后才能调用这个函数。
function functionThatUsesBar() {
new Bar();
}
functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK
与函数类似,有两种类定义,两种方式定义一个类:类声明和类表达式。
同样地类似于函数,类表达式的标识符仅在当前表达式中可见:
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me is not defined
类的主体只能包含方法,不能有数据属性。原型上面的数据属性一般会被认为是反模式,因此这种做法仅强制执行了一种最佳实践。
让我们检测一下类定义中常见的三种方法:
class Foo {
constructor(prop) {
this.prop = prop;
}
static staticMethod() {
return 'classy';
}
prototypeMethod() {
return 'prototypical';
}
}
let foo = new Foo(123);
该类声明的对象图看起来像下面这样。理解此图的小提示: [[Prototype]]
在对象之间是继承关系, prototype
是一个普通的值为对象的属性。属性 prototype
仅仅是比较特殊,因为 new
操作符使用它的值作为新创建实例的原型。
**首先,伪方法 constructor
。**这个方法很特殊,它定义了代表这个类的函数:
> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'
有时称它为类构造器
。它有一些普通构造器函数所不具备的特性(主要是能通过 super()
调用父构造器,这在后面讲解)。
**其次,静态方法。**静态属性(或者说是类属性)就是 Foo
自身上面的属性。如果定义方法的时候在前面加上 static
,就创建了一个类方法:
> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'
第三点,原型方法。 Foo
的原型属性就是 Foo.prototype
的属性。它们是常用的方法,并被 Foo
的实例继承。
> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'
到目前为止,类只允许创建静态方法,不能创建静态数据属性。有两种方法可以解决这个问题。
第一个,可以手动地添加一个静态属性:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Point.ZERO = new Point(0, 0);
第二个,创建一个静态的 getter :
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static get ZERO() {
return new Point(0, 0);
}
}
在两种场景中,都得到一个能读取的属性 Point.ZERO
。在第一种方法中,可以使用 Object.defineProperty()
创建一个只读的属性,但是我喜欢赋值的简洁性。
getters 和 setters 的语法和 ECMAScritp 5 中的对象字面量语法是类似的:
class MyClass {
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
你可以像下面这样使用 MyClass
。
> let inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'
可以通过表达式定义方法名字,需要把表达式放在一对中括号中。例如,下面定义 Foo
的所有方式都是等价的。
class Foo() {
myMethod() {}
}
class Foo() {
['my'+'Method']() {}
}
const m = 'myMethod';
class Foo() {
[m]() {}
}
ECMAScript 6 中有几个特殊的方法,这些方法的键是 Symbol 。计算方法名字让你能够定义这种方法。例如,如果一个对象有一个方法,这个方法的键是 Symbol.iterator
,那么这个对象就是可迭代的。这意味着它的内容可以通过 for-of
循环和其它语言机制来迭代。
class IterableClass {
[Symbol.iterator]() {
···
}
}
如果在方法定义的时候加上一个星号前缀,那么这个方法就会变成生成器方法。在其它方面,生成器对于定义键是 Symbol.iterator
的方法是很有用的。下面的代码展示了如何运作。
class IterableArguments {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new IterableArguments('hello', 'world')) {
console.log(x);
}
// Output:
// hello
// world
继承子句让你能够创建已有构造器(这个构造器可能是通过类的形式创建的,也可能不是)的子类:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // (A)
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color; // (B)
}
}
该类的使用和想象的一样:
> let cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
有两种类:
Point
是一个基类,因为它没有继承子句。ColorPoint
是一个继承类。
有两种使用 super
的方式:
- 在类构造器(类定义中的伪方法
constructor
)中使用它就像方法调用一样(super(...)
),用于调用父构造器(行 A )。 - 在方法定义(在对象字面量或者类中,带有或者不带
static
)中使用它就像属性引用(super.prop
或者方法调用(super.method(...)
)),用于访问父属性(行 B )。
在 ECMAScript 6 中,子类的原型是父类:
> Object.getPrototypeOf(ColorPoint) === Point
true
这意味着静态属性是会被继承的:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
甚至可以通过 super
调用父类中的静态方法:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod(); // 'hello, too'
在一个继承类里面, super()
调用必须先于 this
的使用:
class Foo {}
class Bar extends Foo {
constructor(num) {
let tmp = num * 2; // OK
this.num = num; // ReferenceError
super();
this.num = num; // OK
}
}
在继承的构造器中不调用 super()
也会造成错误:
class Foo {}
class Bar extends Foo {
constructor() {
}
}
let bar = new Bar(); // ReferenceError
就像在 ES5 中一样,可以通过显示地返回一个对象来覆盖构造器的结果:
class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo); // false
如果这样做了,那么 this
是否已经被初始化都没关系了。换句话说,如果用这种方式覆盖结果,那么在继承的构造函数中没必要调用 super()
方法了。
如果没有为基类指定构造器
,那么会使用下面的定义:
constructor() {}
对于继承类,会使用下面的默认构造器:
constructor(...args) {
super(...args);
}
在 ECMAScript 6 中,可以继承所有内置的构造器( ES5 中有相应的变通手段,但是有值得注意的限制)。
例如,现在可以创建自己的异常类(在大多数引擎中会继承堆栈特性):
class MyError extends Error {
}
throw new MyError('Something happened!');
也可以创建 Array
的子类,该子类也会正确地处理 length
:
class MyArray extends Array {
constructor(len) {
super(len);
}
}
// Instances of of `MyArray` work like real Arrays:
let myArr = new MyArray(0);
console.log(myArr.length); // 0
myArr[0] = 'foo';
console.log(myArr.length); // 1
注意,子类化内置构造器需要引擎本地化的支持,不能从转换器中得到这个特性。