ES6 引入了一个类的概念,其实类是个特殊的函数,可以看做是一个语法糖,它的大部分功能,ES5都能做到。新的 class 写法,只是让对象原型的写法更加清晰、更像面对想象编程的语法而已。
Class
类声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function() { return `(${this.x},${this.y})`; }
class Point { constructor(x, y) { this.x = x; this.y = y; }
toString() { return `(${this.x},${this.y})`; } }
|
函数声明和类声明之间的一个重要区别是 函数声明会提升,类声明不会。需要先声明类,才能访问它。
在类的实例上调用方法,其实就是调用原型上的方法。
1 2 3
| class B {} let b = new Class B(); b.constructor === B.prototype.constructor;
|
b是B类的实例,它的 constructor 方法就是B类原型的 constructor 方法。
类内部的方法,是不可枚举的
constructor 方法
constructor 方法是类的默认方法,通过 new 命令生成实例对象时,自动调用该方法。一个类必须有 constructor 方法,如果没有显示定义,一个空的 constructor 方法会被默认添加。
constructor 方法默认会返回实例对象(即 this),当然也可以指定返回另一个对象。
1 2 3 4 5 6
| class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo;
|
**类必须使用 new 调用,否则会报错,这也是它与普通函数的一个主要区别,后者不用 new 也可以执行。
类的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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(); point.hasOwnProperty('x'); point.hasOwnProperty('y'); point.hasOwnProperty('toString'); point.__proto__.hasOwnProperty('toString');
|
x 和 y 都是实例对象 point 自身的属性(因为定义在 this 上),所以 hasOwnProperty 方法返回 true,而 toString是原型对象的属性(因为定义在 point 类上),所以 hasOwnProperty 方法返回 false。
1 2 3
| var p1 = new Point(2, 3); var p2 = new Point(3, 2); p1.__prototype === p2.__prototype__;
|
Class表达式
1 2 3 4 5
| const MyClass = class Me { getClassName() { return Me.name; } }
|
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
1 2 3 4 5 6 7 8 9
| class Foo { static classMethod() { return 'hello'; } } Foo.classMethod();
var foo = new Foo(); foo.classMethod();
|
如果静态方法中包含 this 关键字,这个 this 指的是类,而不是实例。
1 2 3 4 5 6 7 8 9 10 11 12
| class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); } } Foo.bar();
|
上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
父类的静态方法,可以被子类继承
1 2 3 4 5 6 7 8
| class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo {} Bar.classMethod(); }
|
静态方法也可以从 super 对象上调用。
1 2 3 4 5 6 7 8 9 10 11 12
| class Foo { static classMethod() { return 'hello'; } }
class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod();
|
实例属性的新写法
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
| class IncreasingCounter { constructor() { this._count = 0; } get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
class IncreasingCounter { _count = 0; get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
|
静态属性
静态属性指的是 Class 本身属性,即 Class.propName, 而不是定义在实例对象(this) 上的属性。
1 2 3
| class Foo {} Foo.prop = 1; Foo.prop;
|
Class 的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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); this.color = color; } toString() { return this.color + ' ' + super.toString(); } }
|
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
1 2 3 4 5 6 7 8 9
| class ColorPoint extends Point {
}
class ColorPoint extends Point { constructor(...args) { super(...args); } }
|
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
1 2 3
| let cp = new ColorPoint(25, 8, 'red'); cp instanceof ColorPoint; cp instanceof Point;
|
父类的静态方法,也会被子类继承。
Object.gerPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
1
| Object.getPrototypeOf(ColorPoint) === Point;
|
super
super 关键字既可以当函数使用,也可以当做对象使用。
super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
1 2 3 4 5 6
| class A {} class B extends A { constructor() { super(); } }
|
子类B的构造函数之中的 super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,super 虽然代表了父类A的构造函数,但是返回的是子类B的实例,即 super 内部的 this 指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
1 2 3 4 5 6 7 8 9 10 11 12
| class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A(); new B();
|
new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
super 作为对象时,在普通方法中,指向父类的原型对象,在静态方法中,指向父类。
1 2 3 4 5 6 7 8 9 10 11
| class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); } }
|
子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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);
var child = new Child(); child.myMythod(2);
|
super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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();
|
静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。
类的 prototype 和 proto
- 子类的 proto 属性,表示构造函数的继承,总是指向父类。
- 子类的 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。
1 2 3 4 5
| class A {} class B extends A {}
B.__proto__ === A; B.prototype.__proto__ === A.prototype;
|
之所以会产生这样的结果是因为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class A {} class B {}
Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
const b = new B();
Object.setPrototypeOf = function(obj, proto) { obj.__proto__ = proto; return obj; }
Object.setPrototypeOf(B.prototype, A.prototype);
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
B.__proto__ = A;
|