语法

ES5 中的仿类结构

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function() {
  return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
1
2
3
4
5
6
7
8

ES6 中类的声明

基本细节

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
1
2
3
4
5
6
7
8
9
10
11

里面有一个 constructor 方法,这就是构造方法,而 this 关键字则代表实例对象。
Point 类除了构造方法,还定义了一个 toString 方法。注意,定义“类”的方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致。

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff(); // "stuff"
1
2
3
4
5
6
7
8

事实上,类的所有方法都定义在类的 prototype 属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {}
};
1
2
3
4
5
6

b 是 B 类的实例,它的 constructor 方法就是 B 类原型的 constructor 方法。而 prototype 对象的 constructor 属性,直接指向“类”的本身,这与 ES5 的行为是一致的。即为 B.

class B {}
let b = new B();

b.constructor === B.prototype.constructor; // true
1
2
3
4

b 是 B 类的实例,它的 constructor 方法就是 B 类原型的 constructor 方法。

class Point {
  constructor() {
    // ...
  }
}
Object.assign(Point.prototype, {
  toString() {},
  toValue() {}
});
1
2
3
4
5
6
7
8
9

类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这一点与 ES5 的行为不一致。

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
```js
类的属性名,可以采用表达式。
```js
let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上)。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var point = new Point(2, 3);
point.toString(); // (2, 3)
point.hasOwnProperty('x'); // true
point.hasOwnProperty('y'); // true
point.hasOwnProperty('toString'); // false
point.__proto__.hasOwnProperty('toString'); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

默认为严格模式

类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

constructor 方法

constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。

class Point {}

// 等同于
class Point {
  constructor() {}
}
1
2
3
4
5
6

constructor 方法默认返回实例对象(即 this),完全可以指定返回另外一个对象。

class Foo {
  constructor() {
    return Object.create(null);
  }
}
new Foo() instanceof Foo;
// false
1
2
3
4
5
6
7

上面代码中,constructor 函数返回一个全新的对象,结果导致实例对象不是 Foo 类的实例。

与自定义类型的区别

  1. 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。
  2. 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
  3. 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用 Object.defineProperty()才能将方法改变为不可枚举。
  4. 类的所有方法内部都没有[[Construct]],因此使用 new 来调用它们会抛出错误。
  5. 调用类构造器时不使用 new,会抛出错误。
  6. 试图在类的方法内部重写类名,会抛出错误。

类表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};
1
2
3
4
5

需要注意的是,这个类的名字是 MyClass 而不是 Me,Me 只在 Class 的内部代码可用,指代当前类。

let inst = new MyClass();
inst.getClassName(); // Me
Me.name; // ReferenceError: Me is not defined
1
2
3

如果类的内部没用到的话,可以省略 Me,也就是可以写成下面的形式。

const MyClass = class { /* ... */ };
1

被当作值的类

在编程中,能被当作值来使用的就称为一级公民( first-class citizen),意味着它能作为参数传给函数、能作为函数返回值、能用来给变量赋值。JS 的函数就是一级公民(它们有时又被称为一级函数),此特性让 JS 独一无二。
ES6 延续了传统,让类同样成为一级公民。这就使得类可以被多种方式所使用。例如,它能作为参数传入函数:

function createObject(classDef) {
  return new classDef();
}
let obj = createObject(
  class {
    sayHi() {
      console.log('Hi!');
    }
  }
);
obj.sayHi(); //	"Hi!"
1
2
3
4
5
6
7
8
9
10
11

私有方法和私有属性

私有方法现有的方案

私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
一种做法是在命名上加以区别。

class Widget {
  // 公有方法
  foo(baz) {
    this._bar(baz);
  }
  // 私有方法
  _bar(baz) {
    return (this.snaf = baz);
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

class Widget {
  foo(baz) {
    bar.call(this, baz);
  }
  // ...
}
function bar(baz) {
  return (this.snaf = baz);
}
1
2
3
4
5
6
7
8
9

上面代码中,foo 是公有方法,内部调用了 bar.call(this, baz)。这使得 bar 实际上成为了当前模块的私有方法。
还有一种方法是利用 Symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass {
  // 公有方法
  foo(baz) {
    this[bar](baz);
  }
  // 私有方法
  [bar](baz) {
    return (this[snaf] = baz);
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,bar 和 snaf 都是 Symbol 值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。

私有属性和方法的提案

目前,有一个提案,为 class 加了私有属性。方法是在属性名之前,使用#表示。

class Point {
  #x;

  constructor(x = 0) {
    #x = +x; // 写成 this.#x 亦可
  }

  get x() { return #x }
  set x(value) { #x = +value }
}
1
2
3
4
5
6
7
8
9
10

私有属性可以指定初始值,在构造函数执行时进行初始化。

class Point {
  #x = 0;
  constructor() {
    #x; // 0
  }
}
1
2
3
4
5
6

这种写法不仅可以写私有属性,还可以用来写私有方法。

class Foo {
  #a;
  #b;
  #sum() { return #a + #b; }
  printSum() { console.log(#sum()); }
  constructor(a, b) { #a = a; #b = b; }
}
1
2
3
4
5
6
7

另外,私有属性也可以设置 getter 和 setter 方法。

class Counter {
  #xValue = 0;

  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value;
  }

  constructor() {
    super();
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

私有属性不限于从 this 引用,类的实例也可以引用私有属性。

class Foo {
  #privateValue = 42;
  static getPrivateValue(foo) {
    return foo.#privateValue;
  }
}
Foo.getPrivateValue(new Foo()); // 42
1
2
3
4
5
6
7

但是,直接从实例上引用私有属性是不可以的,只能在类的定义中引用。

class Foo {
  #bar;
}
let foo = new Foo();
foo.#bar; // 报错
1
2
3
4
5

this 的指向

类的方法内部如果含有 this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
1
2
3
4
5
6
7
8
9
10
11
12
13

上面代码中,printName 方法中的 this,默认指向 Logger 类的实例。但是,如果将这个方法提取出来单独使用,this 会指向该方法运行时所在的环境,因为找不到 print 方法而导致报错。
一个比较简单的解决方法是,在构造方法中绑定 this,这样就不会找不到 print 方法了

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}
1
2
3
4
5
6

另一种解决方法是使用箭头函数。

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }
  // ...
}
1
2
3
4
5
6
7
8

还有一种解决方法是使用 Proxy,获取方法的时候,自动绑定 this。

function selfish(target) {
  const cache = new WeakMap();
  const handler = {
    get(target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}
const logger = selfish(new Logger());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

name 属性

class Point {}
Point.name; // "Point"
1
2

name 属性总是返回紧跟在 class 关键字后面的类名。

Class 的取值函数(getter)和存值函数(setter)

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: ' + value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop;
// 'getter'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype,
  'html'
);

'get' in descriptor; // true
'set' in descriptor; // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Class 的 Generator 方法

如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

class Foo {
  constructor(...args) {
    this.args = args;
  }
  *[Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Class 的静态方法 static

如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。静态方法可以与非静态方法重名。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
Foo.classMethod(); // 'hello'
var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function
1
2
3
4
5
6
7
8
9

注意,如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例。

class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}
Foo.bar(); // hello
1
2
3
4
5
6
7
8
9
10
11
12

父类的静态方法,可以被子类继承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
class Bar extends Foo {}
Bar.classMethod(); // 'hello'
1
2
3
4
5
6
7

静态方法也是可以从 super 对象上调用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}
Bar.classMethod(); // "hello, too"
1
2
3
4
5
6
7
8
9
10
11

Class 的静态属性和实例属性

静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。ES6 明确规定,Class 内部只有静态方法,没有静态属性。

class Foo {}
Foo.prop = 1;
Foo.prop; // 1
1
2
3
// 以下两种写法都无效
class Foo {
  // 写法一
  prop: 2;

  // 写法二
  static prop: 2;
}

Foo.prop; // undefined
1
2
3
4
5
6
7
8
9
10

目前有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。
(1)类的实例属性
类的实例属性可以用等式,写入类的定义之中。

class MyClass {
  myProp = 42;

  constructor() {
    console.log(this.myProp); // 42
  }
}
1
2
3
4
5
6
7

为了可读性的目的,对于那些在 constructor 里面已经定义的实例属性,新写法允许直接列出。

class ReactCounter extends React.Component {
  state;
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
}
1
2
3
4
5
6
7
8
9

(2)类的静态属性 类的静态属性只要在上面的实例属性写法前面,加上 static 关键字就可以了。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
1
2
3
4
5
6
7
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
1
2
3
4
5
6
7

new.target 属性

Class 内部调用 new.target,返回当前 Class。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); // 输出 true
1
2
3
4
5
6
7
8
9

需要注意的是,子类继承父类时,new.target 会返回子类。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3); // 输出 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意,在函数外部,使用 new.target 会报错。

Class 的继承

ES6 之前

ES6 之前,实现自定义类型的继承是个繁琐的过程。严格的继承要求有多个步骤。例如,研究以下范例:

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}

Rectangle.prototype.getArea = function() {
  return this.length * this.width;
};

function Square(length) {
  Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    value: Square,
    enumerable: true,
    writable: true,
    configurable: true
  }
});

var square = new Square(3);
console.log(square.getArea()); //	9
console.log(square instanceof Square); //	true
console.log(square instanceof Rectangle); //	true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

ES6

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {}

class ColorPoint extends Point {}
1
2
3

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

class Point {
  /* ... */
}
class ColorPoint extends Point {
  constructor() {}
}
let cp = new ColorPoint(); // ReferenceError
1
2
3
4
5
6
7

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。如果子类没有定义 constructor 方法,这个方法会被默认添加.
另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true
1
2

super 关键字

super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}
class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}
1
2
3
4
5
6
7

当作函数使用

super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}
1
2
3
4
5
6
7

注意,super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super()在这里相当于 A.prototype.constructor.call(this)。
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}
class B extends A {
  m() {
    super(); // 报错
  }
}
1
2
3
4
5
6

super 作为对象时

在普通方法中

super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}
class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}
let b = new B();
1
2
3
4
5
6
7
8
9
10
11
12

这里需要注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。

class A {
  constructor() {
    this.p = 2;
  }
}
class B extends A {
  get m() {
    return super.p;
  }
}
let b = new B();
b.m; // undefined
1
2
3
4
5
6
7
8
9
10
11
12

如果属性定义在父类的原型对象上,super 就可以取到。

class A {}
A.prototype.x = 2;
class B extends A {
  constructor() {
    super();
    console.log(super.x); // 2
  }
}
let b = new B();
1
2
3
4
5
6
7
8
9

ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}
let b = new B();
b.m(); // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

由于 this 指向子类实例,所以如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}
let b = new B();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用在静态方法中

如果 super 作为对象,用在静态方法之中,这时 super 将指向父类,而不是父类的原型对象.

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }
  myMethod(msg) {
    console.log('instance', msg);
  }
}
class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }
  myMethod(msg) {
    super.myMethod(msg);
  }
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

另外,在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}
B.x = 3;
B.m(); // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在任意一个对象中

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。

var obj = {
  toString() {
    return 'MyObject: ' + super.toString();
  }
};
obj.toString(); // MyObject: [object Object]
1
2
3
4
5
6

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

ES5

以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array 的子类。

function MyArray() {
  Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});
var colors = new MyArray();
colors[0] = 'red';
colors.length; // 0
colors.length = 0;
colors[0]; // "red"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply()或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。

ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array 构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正常。

ES6

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。下面是一个继承 Array 的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}
var arr = new MyArray();
arr[0] = 12;
arr.length; // 1

arr.length = 0;
arr[0]; // undefined
1
2
3
4
5
6
7
8
9
10
11

这意味着,ES6 可以自定义原生数据结构(比如 Array、String 等)的子类,这是 ES5 无法做到的。
下面是一个自定义 Error 子类的例子,可以用来定制报错时的行为。

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message;
    this.stack = new Error().stack;
    this.name = this.constructor.name;
  }
}
class MyError extends ExtendableError {
  constructor(m) {
    super(m);
  }
}
var myerror = new MyError('ll');
myerror.message; // "ll"
myerror instanceof Error; // true
myerror.name; // "MyError"
myerror.stack;
// Error
//     at MyError.ExtendableError
//     ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

注意,继承 Object 的子类,有一个行为差异。

class NewObj extends Object {
  constructor() {
    super(...arguments);
  }
}
var o = new NewObj({ attr: true });
o.attr === true; // false
1
2
3
4
5
6
7

上面代码中,NewObj 继承了 Object,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object()这种形式调用,ES6 规定 Object 构造函数会忽略参数。

class B extends A {
}
1
2

上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = { ...a, ...b }; // {a: 'a', b: 'b'}
1
2
3
4
5
6
7

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {}
  for (let mixin of mixins) {
    copyProperties(Mix.prototype, mixin); // 拷贝实例属性
    copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷贝原型属性
  }
  return Mix;
}
function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}
1
2
3

TOC