1. 类的承继extends
简介
Class 能够经过extends
关键字完成承继,让子类承继父类的特点和办法。extends 的写法比 ES5 的原型链承继,要清晰和方便许多。
class Point {
}
class ColorPoint extends Point {
}
上面示例中,Point
是父类,ColorPoint
是子类,它经过extends
关键字,承继了Point
类的一切特点和办法。可是由于没有布置任何代码,所以这两个类彻底一样,等于复制了一个Point
类。
下面,咱们在ColorPoint
内部加上代码。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
上面示例中,constructor()
办法和toString()
办法内部,都呈现了super
关键字。super
在这儿标明父类的结构函数,用来新建一个父类的实例目标。
1.1 子类有必要在constructor()
办法中调用super()
ES6 规则,子类有必要在constructor()
办法中调用super()
,否则就会报错。
这是由于子类自己的
this
目标,有必要先经过父类的结构函数完结塑造,得到与父类同样的实例特点和办法,然后再对其进行加工,增加子类自己的实例特点和办法。假如不调用super()
办法,子类就得不到自己的this
目标。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
上面代码中,ColorPoint
承继了父类Point
,可是它的结构函数没有调用super()
,导致新建实例时报错。
1.2 ES6 的承继机制,与 ES5 彻底不同
为什么子类的结构函数,一定要调用super()
?原因就在于 ES6 的承继机制,与 ES5 彻底不同。
- ES5 的承继机制,是先创造一个独立的子类的实例目标,然后再将父类的办法增加到这个目标上面,即“实例在前,承继在后”。
- ES6 的承继机制,则是先将父类的特点和办法,加到一个空的目标上面,然后再将该目标作为子类的实例,即“承继在前,实例在后”。
这便是为什么 ES6 的承继有必要先调用
super()
办法,由于这一步会生成一个承继父类的this
目标,没有这一步就无法承继父类。
留意,这意味着新建子类实例时,父类的结构函数必定会先运行一次。
class Foo {
constructor() {
console.log(1);
}
}
class Bar extends Foo {
constructor() {
super();
console.log(2);
}
}
const bar = new Bar();
// 1
// 2
上面示例中,子类 Bar 新建实例时,会输出1和2。原因便是子类结构函数调用super()
时,会履行一次父类结构函数。
1.3 子类的结构函数中,调用super()
之后,才能够运用this
另一个需求留意的地方是,在子类的结构函数中,只需调用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; // 正确
}
}
上面代码中,子类的constructor()
办法没有调用super()
之前,就运用this
关键字,成果报错,而放在super()
之后便是正确的。
假如子类没有界说constructor()
办法,这个办法会默许增加,而且里边会调用super()
。也便是说,不管有没有显式界说,任何一个子类都有constructor()
办法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
有了子类的界说,就能够生成子类的实例了。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
上面示例中,实例目标cp
一起是ColorPoint
和Point
两个类的实例,这与 ES5 的行为彻底一致。
2. 父类私有特点和私有办法无法承继
父类一切的特点和办法,都会被子类承继,除了私有的特点和办法。
子类无法承继父类的私有特点,或者说,私有特点只能在界说它的 class 里边运用。
class Foo {
#p = 1;
#m() {
console.log('hello');
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.#p); // 报错
this.#m(); // 报错
}
}
上面示例中,子类 Bar 调用父类 Foo 的私有特点或私有办法,都会报错。
假如父类界说了私有特点的读写办法,子类就能够经过这些办法,读写私有特点。
class Foo {
#p = 1;
getP() {
return this.#p;
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.getP()); // 1
}
}
上面示例中,getP()
是父类用来读取私有特点的办法,经过该办法,子类就能够读到父类的私有特点。
3. 子类能够承继父类自身静态特点和静态办法
父类的静态特点和静态办法,也会被子类承继。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
上面代码中,hello()
是A
类的静态办法,B
承继A
,也承继了A
的静态办法。
留意,静态特点是经过软复制完成承继的。
class A { static foo = 100; }
class B extends A {
constructor() {
super();
B.foo--;
}
}
const b = new B();
B.foo // 99
A.foo // 100
上面示例中,foo
是 A 类的静态特点,B 类承继了 A 类,因而也承继了这个特点。可是,在 B 类内部操作B.foo
这个静态特点,影响不到A.foo
,原因便是 B 类承继静态特点时,会选用浅复制,复制父类静态特点的值,因而A.foo
和B.foo
是两个互相独立的特点。
可是,由于这种复制是浅复制,假如父类的静态特点的值是一个目标,那么子类的静态特点也会指向这个目标,由于浅复制只会复制目标的内存地址。
class A {
static foo = { n: 100 };
}
class B extends A {
constructor() {
super();
B.foo.n--;
}
}
const b = new B();
B.foo.n // 99
A.foo.n // 99
上面示例中,A.foo
的值是一个目标,浅复制导致B.foo
和A.foo
指向同一个目标。所以,子类B
修改这个目标的特点值,会影响到父类A
。
4. Object.getPrototypeOf()——从子类上获取父类
Object.getPrototypeOf()
办法能够用来从子类上获取父类。
class Point { /*...*/ }
class ColorPoint extends Point { /*...*/ }
Object.getPrototypeOf(ColorPoint) === Point
// true
因而,能够运用这个办法判别,一个类是否承继了另一个类。
5. super
关键字——既能够当作函数运用,也能够当作目标运用
super
这个关键字,既能够当作函数运用,也能够当作目标运用。在这两种状况下,它的用法彻底不同。
5.1 super
作为函数调用——代表父类的结构函数
第一种状况,super
作为函数调用时,代表父类的结构函数。ES6 要求,子类的结构函数有必要履行一次super
函数。
class A {}
class B extends A {
constructor() {
super();
}
}
上面代码中,子类B
的结构函数之中的super()
,代表调用父类的结构函数。这是有必要的,否则 JavaScript 引擎会报错。
留意,
super
虽然代表了父类A
的结构函数,可是回来的是子类B
的实例,即super
内部的this
指的是B
的实例,因而super()
在这儿相当于A.prototype.constructor.call(this)
。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
上面代码中,new.target
指向当时正在履行的函数。能够看到,在super()
履行时,它指向的是子类B
的结构函数,而不是父类A
的结构函数。也便是说,super()
内部的this
指向的是B
。
作为函数时,super()
只能用在子类的结构函数之中,用在其他地方就会报错。
class A {}
class B extends A {
m() {
super(); // 报错
}
}
上面代码中,super()
用在B
类的m
办法之中,就会造成语法错误。
5.2 super
作为目标—在一般办法中,指向父类的原型目标
第二种状况,super
作为目标时,在一般办法中,指向父类的原型目标;在静态办法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
上面代码中,子类B
傍边的super.p()
,便是将super
当作一个目标运用。这时,super
在一般办法之中,指向A.prototype
,所以super.p()
就相当于A.prototype.p()
。
5.2.1 super
无法调用界说在父类实例上的办法或特点
这儿需求留意,由于super
指向父类的原型目标,所以界说在父类实例上的办法或特点,是无法经过super
调用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
上面代码中,p
是父类A
实例的特点,super.p
就引证不到它。
假如特点界说在父类的原型目标上,super
就能够取到。
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
上面代码中,特点x
是界说在A.prototype
上面的,所以super.x
能够取到它的值。
5.2.2 super
调用父类的办法时,父类办法内部的this
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
上面代码中,super.print()
虽然调用的是A.prototype.print()
,可是A.prototype.print()
内部的this
指向子类B
的实例,导致输出的是2
,而不是1
。也便是说,实际上履行的是super.print.call(this)
。
5.2.3 经过super
对子类实例某个特点赋值
由于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();
上面代码中,super.x
赋值为3
,这时等同于对this.x
赋值为3
。而当读取super.x
的时分,读的是A.prototype.x
,所以回来undefined
。
5.3 super
作为目标,用在静态办法之中——super`将指向父类
假如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
上面代码中,super
在静态办法之中指向父类,在一般办法之中指向父类的原型目标。
5.3.1 父类静态的办法内部的this
指向
别的,在子类的静态办法中经过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
上面代码中,静态办法B.m
里边,super.print
指向父类的静态办法。这个办法里边的this
指向的是B
,而不是B
的实例。
留意,运用
super
的时分,有必要显式指定是作为函数、还是作为目标运用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
上面代码中,console.log(super)
傍边的super
,无法看出是作为函数运用,还是作为目标运用,所以 JavaScript 引擎解析代码的时分就会报错。这时,假如能清晰地标明super
的数据类型,就不会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
上面代码中,super.valueOf()
标明super
是一个目标,因而就不会报错。一起,由于super
使得this
指向B
的实例,所以super.valueOf()
回来的是一个B
的实例。
5.4 能够在恣意一个目标中,运用super
关键字
最终,由于目标总是承继其他目标的,所以能够在恣意一个目标中,运用super
关键字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
6. 类的 prototype 特点和__proto__特点
6.1 Class类的承继一起存在两条承继链
大多数浏览器的 ES5 完成之中,每一个目标都有__proto__
特点,指向对应的结构函数的prototype
特点。Class 作为结构函数的语法糖,一起有prototype
特点和__proto__
特点,因而一起存在两条承继链。
(1)子类的__proto__
特点,标明结构函数的承继,总是指向父类。——子类承继父类的静态特点
(2)子类prototype
特点的__proto__
特点,标明办法的承继,总是指向父类的prototype
特点。——子类实例承继父类的实例办法
class A {
}
class B extends A {
}
// B 承继 A 的静态特点
B.__proto__ === A // true
// B 的实例承继 A 的实例
B.prototype.__proto__ === A.prototype // true
上面代码中,子类B
的__proto__
特点指向父类A
,子类B
的prototype
特点的__proto__
特点指向父类A
的prototype
特点。
这样的成果是由于,类的承继是依照下面的模式完成的。
class A {
}
class B {
}
// B 的实例承继 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 承继 A 的静态特点
Object.setPrototypeOf(B, A);
const b = new B();
ES6 目标的扩展一章给出过Object.setPrototypeOf
办法的完成。
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;
这两条承继链,能够这样了解:作为一个目标,子类(
B
)的原型(__proto__
特点)是父类(A
);作为一个结构函数,子类(B
)的原型目标(prototype
特点)是父类的原型目标(prototype
特点)的实例。
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
6.2 extends
关键字后边能够是恣意函数。
extends
关键字后边能够跟多种类型的值。
class B extends A {
}
上面代码的A
,只需是一个有prototype
特点的函数,就能被B
承继。由于函数都有prototype
特点(除了Function.prototype
函数),因而A
能够是恣意函数。
6.2.1 子类承继Object
类
下面,讨论两种状况。第一种,子类承继Object
类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种状况下,A
其实便是结构函数Object
的复制,A
的实例便是Object
的实例。
6.2.2 不运用extends
第二种状况,不存在任何承继。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种状况下,A
作为一个基类(即不存在任何承继),便是一个一般函数,所以直接承继Function.prototype
。可是,A
调用后回来一个空目标(即Object
实例),所以A.prototype.__proto__
指向结构函数(Object
)的prototype
特点。
6.3 子类实例的 proto 特点
子类实例的__proto__
特点的__proto__
特点,指向父类实例的__proto__
特点。也便是说,子类实例的原型目标的原型目标,是父类实例的原型目标。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
上面代码中,ColorPoint
承继了Point
,导致前者原型的原型是后者的原型。
因而,经过子类实例的__proto__.__proto__
特点,能够修改父类实例的行为。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
上面代码在ColorPoint
的实例p2
上向Point
类增加办法,成果影响到了Point
的实例p1
。
7. Js语言内置原生结构函数的承继
原生结构函数是指语言内置的结构函数,一般用来生成数据结构。ECMAScript 的原生结构函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
7.1 ES5 原生结构函数无法承继
曾经,这些原生结构函数是无法承继的,比方,不能自己界说一个Array
的子类。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
上面代码界说了一个承继 Array 的MyArray
类。可是,这个类的行为与Array
彻底不一致。
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
之所以会产生这种状况,是由于子类无法获得原生结构函数的内部特点,经过Array.apply()
或者分配给原型目标都不行。原生结构函数会疏忽apply
办法传入的this
,也便是说,原生结构函数的this
无法绑定,导致拿不到内部特点。
ES5 是先新建子类的实例目标this
,再将父类的特点增加到子类上,由于父类的内部特点无法获取,导致无法承继原生的结构函数。比方,Array
结构函数有一个内部特点[[DefineOwnProperty]]
,用来界说新特点时,更新length
特点,这个内部特点无法在子类获取,导致子类的length
特点行为不正常。
下面的比如中,咱们想让一个一般目标承继Error
目标。
var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []
上面代码中,咱们想经过Error.call(e)
这种写法,让一般目标e
具有Error
目标的实例特点。可是,Error.call()
彻底疏忽传入的第一个参数,而是回来一个新目标,e
自身没有任何改变。这证明了Error.call(e)
这种写法,无法承继原生结构函数。
7.2 ES6 能够自界说原生数据结构(比方Array
)的子类
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
上面代码界说了一个MyArray
类,承继了Array
结构函数,因而就能够从MyArray
生成数组的实例。这意味着,ES6 能够自界说原生数据结构(比方Array
、String
等)的子类,这是 ES5 无法做到的。
上面这个比如也阐明,extends
关键字不仅能够用来承继类,还能够用来承继原生的结构函数。因而能够在原生数据结构的基础上,界说自己的数据结构。下面便是界说了一个带版别功能的数组。
7.3 界说一个带版别功能的数组
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]
上面代码中,VersionedArray
会经过commit
办法,将自己的当时状态生成一个版别快照,存入history
特点。revert
办法用来将数组重置为最新一次保存的版别。除此之外,VersionedArray
依然是一个一般数组,一切原生的数组办法都能够在它上面调用。
7.4 自界说Error
子类
下面是一个自界说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
// ...
7.5 承继Object
的子类有一个特殊状况
留意,承继
Object
的子类,有一个行为差异。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj
承继了Object
,可是无法经过super
办法向父类Object
传参。这是由于 ES6 改变了Object
结构函数的行为,一旦发现Object
办法不是经过new Object()
这种方式调用,ES6 规则Object
结构函数会疏忽参数。
8.Mixin 模式的完成——多个目标组成一个新的目标
Mixin 指的是多个目标组成一个新的目标,新目标具有各个组成成员的接口。它的最简略完成如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
上面代码中,c
目标是a
目标和b
目标的组成,具有两者的接口。
下面是一个更完备的完成,将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 复制实例特点
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 复制静态特点
copyProperties(Mix.prototype, mixin.prototype); // 复制原型特点
}
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);
}
}
}
上面代码的mix
函数,能够将多个目标组成为一个类。运用的时分,只需承继这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
class A {
constructor(a) {
this.a = a;
}
static _a = 10;
logA() {
console.log(A._a);
}
}
class B {
constructor(b) {
this.b = b;
}
static _b = 20;
logB() {
console.log(B._b);
}
}
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 复制实例特点
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 复制静态特点
copyProperties(Mix.prototype, mixin.prototype); // 复制原型特点
}
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);
}
}
}
class DistributedEdit extends mix(A, B) {
// ...
}
new DistributedEdit();