Class

严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

name属性

class Point {}
Point.name // "Point"
1
2
const MyClass = class Me {
};
MyClass.name // "Me"
1
2
3

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

constructor方法

  • 类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加
  • constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象
  • 类必须使用new调用,否则会报错
class Point {
}

// 等同于
class Point {
  constructor() {}
}
1
2
3
4
5
6
7
class Foo {
  constructor() {
    return Object.create(null);
  }
}

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

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
1
2
3
4
5
6
7
8

Class 表达式

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
1
2
3
4
5
6
7
8
9

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

// 如果类的内部没用到的话,可以省略 Me,也就是可以写成下面的形式
const MyClass = class { /* ... */ };
1
2

立即执行的 Class

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"
1
2
3
4
5
6
7
8
9
10
11

方法

  • 类的所有方法都是定义在类的prototype属性上面
  • 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
  • 类的属性名,可以采用表达式
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let methodName = 'getArea';

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

  [methodName]() {
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11

getter/setter

定义在 Class 里的getter/setter方法,实际上定义在类的原型上的,通过属性的 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

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class Foo {
}

Foo.prop = 1;
Foo.prop // 1
1
2
3
4
5

只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。

目前有一个静态属性的提案open in new window,对实例属性和静态属性都规定了新的写法。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

  • 如果静态方法包含this关键字,这个this指的是类,而不是实例
  • 静态方法可以与非静态方法重名
  • 父类的静态方法,可以被子类继承,也可以从super对象上调用
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
13
class Foo {
  static classMethodHello() {
    return 'hello';
  }
  static classMethodWorld() {
    return 'world';
  }
}

class Bar extends Foo {
  static classMethodHello() {
    return super.classMethodHello() + ', from Bar';
  }
}

Bar.classMethodHello() // 'hello, from Bar'
Bar.classMethodWorld() // 'world'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

不存在变量提升

// 类不存在变量提升(hoist),这一点与 ES5 完全不同
new Foo(); // ReferenceError
class Foo {}
1
2
3

new.target 属性

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • Class 内部调用new.target,返回当前 Class
  • 子类继承父类时,new.target会返回子类
class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
  }
}

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

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

Class 的 Generator 方法

详见Class 的 Generator 方法open in new window

私有方法

ES6 不提供私有方法,只能通过变通方法open in new window模拟实现。

私有属性

ES6 不支持私有属性,但目前有提案open in new window

Class 的继承

constructor

  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错
    • 子类若没有定义constructor方法,constructor方法会被默认添加
  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错
    • 调用super之前,子类还没有自己的实例对象,调用super之后,基于父类的this对象创建出子类的实例
  • ES 5 继承实质
    • 先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this)
  • ES 6 继承实质
    • 先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this
class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

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

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}
1
2
3
4
5
6
7
8
9

静态方法的继承

父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world
1
2
3
4
5
6
7
8
9
10

Object.getPrototypeOf()

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

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

// 注意 __proto__ 不是标准属性
ColorPoint.__proto__ === Point // true
1
2
3
4

super关键字

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

当作函数使用

  • super作为函数调用时,代表父类的构造函数
  • super()只能用在子类的构造函数之中,用在其他地方就会报错
class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B
1
2
3
4
5
6
7
8
9
10
11
12

super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)

当作对象使用

super作为对象时,

  • 在普通方法中,指向父类的原型对象
    • 通过super调用父类的方法时,方法内部的this指向子类
    • 通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
  • 在静态方法中,指向父类
// 普通方法里
class A {
  constructor() {
    this.p = 3;
    this.x = 4;
  }
  getNum() {
    return 2;
  }
  getP() {
    console.log(this.p);
  }
}
A.prototype.proX = 1;

class B extends A {
  constructor() {
    super();

    // 普通方法中:
    // super.getNum() 就相当于 A.prototype.getNum()
    // super.proX 相当于 A.prototype.proX
    console.log(super.getNum()); // 2
    console.log(super.proX) // 1

    this.p = 33;

    // 通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性
    // super.x 读取的是 A.prototype.x,因此是 undefined
    this.x = 44;
    super.x = 444;
    console.log(super.x) // undefined
    console.log(this.x) // 444
  }
  getP() {
    super.getP()
  }
}

let b = new B();
b.getP() // 33
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
29
30
31
32
33
34
35
36
37
38
39
40
41
// 静态方法里
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
20
21
22
23
24
25

其中,super关键字还可以用在普通对象的方法之中,表示原型对象,详见“对象”一节。

显式指定super类型

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

class A {}

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

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();
1
2
3
4
5
6
7
8
9
10

上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B,所以super.valueOf()返回的是一个B的实例。

任意对象里使用super

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

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]
1
2
3
4
5
6
7

prototype属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类。

  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
1
2
3
4
5
6
7
8

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}

// B 的实例继承 A 的实例(用于方法、属性的继承)
Object.setPrototypeOf(B.prototype, A.prototype);

// B 的实例继承 A 的静态属性(用于静态方法、属性的继承)
Object.setPrototypeOf(B, A);

const b = new B();
1
2
3
4
5
6
7
8
9
10
11
12
13

这两条继承链,可以这样理解:

  • 作为一个对象,子类的原型(__proto__属性)是父类;
  • 作为一个构造函数,子类的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

原生构造函数的继承

ES5 无法继承原生的构造函数,ES 6 可以继承原生的构造函数。

原生构造函数的继承open in new window