Class的继承
简介
比起 ES5 修改原型链实现继承的方式,ES6 的 class
通过 extends
关键字实现继承,更加清晰和方便。子类继承父类的所有属性和方法,需要用 super
方法,继承父类的 this
对象,否则会因为得不到 this
对象新建实例报错。
class A {
x = 1
y = 2
}
class B extends A {
constructor() {
super()
}
}
class C extends A {
constructor() {
}
}
let b = new B() // B {x: 1, y: 2}
let c = new C() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
ES5 继承实质是先创建子类的实例对象 this
,然后再将父类的方法添加到 this
上(Parent.apply(this)
)。ES6 继承实质是先创建父类的实例对象 this
(所以必须先调用 super
方法),然后再用子类的构造函数修改 this
。
如果子类没有定义 constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor
方法。且只有调用了 super
方法,子类实例的 this
才会被初始化,才能使用 this
。
class A {}
class B extends A {}
// 等同于
class B extends A {
constructor(...args) {
super(...args)
}
}
class C extends A {
constructor(x) {
this.x = x // Error
super()
this.x = x // OK
}
}
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用来从子类上获取父类。因此可以用这个方法判断某个子类是否继承了另一个类作为父类。
Object.getPrototypeOf(B) === A // true
super关键字
super
关键字既可以当作函数使用,也可以当作对象使用。
在作为函数使用时,代表父类的构造函数
ES6 要求,子类的构造函数必须执行一次
super
函数,否则新建实例会报错。super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,即super
内部的this
指的是B
的实例,因此super()
在子类B
的构造函数中执行时,它内部的this
指向的是B
的实例。另外,
super
函数只能用在子类的构造函数之中,用在其他地方会报错。jsclass A { constructor() { console.log(new.target.name) } } class B extends A { constructor() { super() } fn() { super() // 报错 } } new A() // A new B() // B
在作为对象使用时,指向父类的原型对象
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。⚠ 注意
由于
super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。把属性定义在父类的原型对象上super
才能拿到。jsclass A { constructor() { this.foo = 1 } p() { return 2 } } A.prototype.bar = 3 class B extends A { constructor() { super() console.log(super.p()) // 2 console.log(super.bar) // 3 } get m() { return super.foo // undefined } } let b = new B()
ES6规定,通过 super
调用父类方法时,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
赋值相当于 this.x = 3
,当读取 super.x
时,读的是 A.prototype.x
,所以结果为 undefined
。
如果 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
let child = new Child()
child.myMethod(2) // instance 2
⚠ 注意
使用 super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
类的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
上面代码中,子类 B
的 __proto__
属性指向父类 A
,子类 B
的 prototype
属性的 __proto__
属性指向父类 A
的 prototype
属性。
extends的继承目标
extends
关键字后面可以跟多种类型的值。
class B extends A {}
上面代码的 A
可以是任意值。只要这个值有 prototype
属性即可。由于函数都有 prototype
属性,因此 A
可以是任意函数。
不过,有三种特殊情况:
子类继承
Object
类jsclass A extends Object {} A.__proto__ === Object // true A.prototype.__proto__ === Object.prototype // true
这种情况下,
A
其实就是构造函数Object
的复制,A
的实例就是Object
的实例。不存在任何继承
jsclass A {} A.__proto__ === Function.prototype // true A.prototype.__proto__ === Object.prototype // true
这种情况下,
A
作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype
。但是,A
调用super
方法时,返回的是undefined
,因为A
作为一个普通函数,没有任何继承。子类继承
null
jsclass A extends null {} A.__proto__ === Function.prototype // true A.prototype.__proto__ === undefined // true
这种情况下,
A
也是一个普通函数,所以直接继承Function.prototype
。但是,A
调用super
方法时,返回的是undefined
,因为没有继承任何东西。
实例的 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
原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array
的子类。
function MyArray() {
Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})
var arr = new MyArray()
arr[0] = 12
arr.length // 0
上面代码中,MyArray
并不是 Array
的实例,所以 arr.length
属性等于0。这显然不符合继承的意图,所以 ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this
,然后再用子类的构造函数修饰 this
,使得父类的所有行为都可以继承。
Mixin模式的实现
由于 ES6 没有 mixin
的语法,但是通过 extends
关键字,可以很方便地实现 mixin
模式。具体做法如下。
首先,实现一些 mixin
函数。
function mix(...mixins) {
class Mix {}
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 MyClass extends mix(AnotherClass, YetAnotherClass) {
/* ... */
}
上面代码中,MyClass
就是 AnotherClass
和 YetAnotherClass
的混合,继承了它们的全部接口。
总结
ES6 的 class
语法通过 extends
关键字实现了继承,使得子类可以继承父类的属性和方法。子类的构造函数中必须首先调用 super()
方法,以确保父类的构造函数被正确执行,从而正确地初始化子类的 this
对象。如果子类没有显式定义构造函数,那么会默认添加一个调用了 super()
的空构造函数。
super
关键字在子类中具有双重作用:作为函数时,它代表父类的构造函数;作为对象时,在普通方法中指向父类的原型对象,而在静态方法中指向父类本身。通过 super
,子类可以调用父类的方法,同时 super
会绑定子类的 this
,使得对属性的赋值会反映到子类实例上。
ES6 的继承机制同时存在两条继承链:一条是子类的 __proto__
属性指向父类,表示构造函数的继承;另一条是子类 prototype
属性的 __proto__
属性指向父类的 prototype
属性,表示方法的继承。这种设计使得子类可以继承父类的实例属性和原型上的方法。
extends
关键字后可以跟任意具有 prototype
属性的函数,包括原生构造函数。ES6 允许继承原生构造函数,解决了 ES5 中无法通过原型链继承原生构造函数的问题。继承原生构造函数时,子类可以正确地继承和扩展原生对象的行为。
除了传统的继承模式,ES6 还可以实现 Mixin
模式,即通过 extends
和一些辅助函数,使得一个类可以继承多个类的属性和方法。这种模式提高了代码的复用性,允许开发者创建更加灵活和功能丰富的类结构。