前言
今天来看看 ES6 中的继承。
在 ES5 大概有 6 种继承方式:类式继承、构造函数继承、组合式继承、原型继承、寄生式继承、寄生组合式继承,而这些方式都有一些各自的缺点
而 ES6 标准提供了 Class(类)的语法来定义类,语法很像传统的面向对象写法。本质上仍然是通过原型实现继承的,可以理解为 class
只是一个语法糖,跟传统面向对象的类又不一样。废话说完了,入正题...
那 Class 是怎样实现继承的呢?
正文
一、简介
通过 extends
关键字实现继承,比 ES5 写一长串的原型链,方便清晰很多,对吧。
class Person {} class Student extends Person {} // 没错,这样就实现了继承
上面的示例中,定义了一个 Student
(子)类,该类通过 extends
关键字继承了 Person
(父)类的所有属性和方法。但由于两个类中并没有实现什么功能,相当于 Student
复制了一个 Person
类而已。
需要注意的是,若子类自实现了
constructor
方法,需在其内部使用super
关键字来调用父类的构造方法,否则当子类进行实例化时会报错。
class Person {} class Student extends Person { constructor() { super() // 相当于调用父类 Person 的构造方法, // 相当于 Person.prototype.constructor.call(this), // 而且,若 constructor 内使用了 this 关键字, // super() 一定要在 this 之前进行调用,否则会报错。 } } const stu = new Student() // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor // 至于为什么上一个示例,实例化时并不会报错,原因如下: // 当 constructor 缺省时,JS 引擎会默认添加一个 constructor 方法,相当于: // // class Person {} // class Student extends Person { // constructor(...args) { // super(...args) // } // }
原因是,ES5 的继承实质是先创建子类的实例对象(即 this
),然后再将父类的方法添加到实例对象 this
上(类似 Parent.apply(this)
)。而 ES6 继承机制完全不同,它先将父类的实例对象的属性和方法,放到 this
上,然后再用子类的构造函数修改 this
。因此,在子类使用 this
之前,需要调用 super()
方法执行父类的 constructor()
方法来创建实例对象 this
。
我们来改下:
class Person { constructor(name, age) { this.name = name this.age = age } sayHi() { console.log(`Hi, my name is ${this.name}.`) } } class Student extends Person { constructor(name, age, stuNo) { super(name, age) this.stuNo = stuNo // this 只能在调用 super() 后使用 } } const stu = new Student('Frankie', 20, 2021001) stu.sayHi() // "Hi, my name is Frankie."
实例对象 stu
同时是 Student
、Person
类的实例,这点与 ES5 表现一致。
stu instanceof Student // true stu instanceof Person // true
二、Object.getPrototypeOf()
使用 Object.getPrototypeOf()
可以通过子类获取其直接父类。
Object.getPrototypeOf(stu) === Student // true Object.getPrototypeOf(Student) === Person // true // 也可以使用非标准的 __proto__ 访问原型 stu.__proto__.constructor === Student // true stu.__proto__ === Student.prototype // true
提一下,我们一直使用的 Object.prototype.__proto__
并不是 ECMAScript 标准,只是被各大浏览器厂商支持,因此我们才可以使用。现在被推荐使用的是,标准支持的 Objec.getPrototypeOf()
、Object.setPrototypeOf()
方法。
三、super 关键字
关键字 super
可以作为函数使用,也可以作为对象使用,两种是有区别的。
1. super 作为函数
当 super
作为函数,只能在(子类)构造方法中使用,若在非子类或类的其他方法中调用,是会报错的。
class Person { constructor(name, age) { this.name = name this.age = age console.log(new.target.name) // super() // 若在此调用,会报错:SyntaxError: 'super' keyword unexpected here } sayHi() { console.log(`Hi, my name is ${this.name}.`) } } class Student extends Person { constructor(name, age, stuNo) { // super(name, age) console.log(super(name, age) === this) // true this.stuNo = stuNo // this 只能在调用 super() 后使用 } getStuNo() { // super() // 若在此调用,会报错:SyntaxError: 'super' keyword unexpected here return this.stuNo } } const stu = new Student('Frankie', 20, 2021001) // 打印:"Student" const person = new Person('Mandy', 18) // 打印:"Person"
在构造方法当作函数调用 super()
,它代表了父类的构造方法。根据 ES6 规定,子类的构造函数必须执行一次 super
函数。当缺省 constructor()
方法时,JS 引擎会帮我们添加一个默认构造方法,里面也包括 super()
的调用。
小结:
super()
只能在子类的constructor()
方法内调用,在getStuNo()
方法内调用会报错。例如,示例中父类Person
并没有继承自其他类,因此在父类Person
的constructor()
方法内调用是会报错的。- 调用
super()
返回当前实例化对象,即this
。 new.target
指向直接被new
执行的类。因此通过new Student()
和new Person()
进行实例化时,new.target
分别指向Student
类和Person
类。
2. super 作为对象
当 super
作为对象使用时,在普通方法内(包括 constructor()
在内的非静态方法),它指向父类的原型对象(即 Parent.prototype
);而在静态方法内,它指向父类(即 Parent
)。
// 父类 class Person { constructor(name, age) { this.name = name this.age = age } sayHi() { console.log(`Hi, my name is ${this.name}.`) } printAge() { console.log(this.age) } static classMethodParent() { console.log(Person.name) } } Object.assign(Person.prototype, { prop: 'hhh' }) // 子类 class Student extends Person { constructor(name, age, stuNo) { super(name, age) this.stuNo = stuNo // 作为对象 super 相当于 Person.prototype super.sayHi() // "Hi, my name is Frankie." console.log(super.name) // undefined console.log(super.prop) // "hhh" // 另外,要注意: super.tmp = 'temporary' // 相当于 this.tmp = 'temporary' console.log(super.tmp) // undefined console.log(this.tmp) // "temporary" } getStuNo() { return this.stuNo } getAge() { // 普通方法内,super 指向父类的原型对象, // 即相当于 Person.prototype.printAge.call(this) super.printAge() } static classMethod() { // 静态方法内,super 指向父类 // 相当于 Person.classMethodParent() super.classMethodParent() } } const stu = new Student('Frankie', 20, 2021001) stu.getAge() // 20 Student.classMethod() // "Person"
小结:
- 在普通方法内,类似
super.xxx
等取值操作,super
均指向父类的原型对象。例如,上述子类构造方法内super.name
打印结果为undefined
,原因是属性name
是挂载到实例对象上的,而不是实例的原型对象上的。即super.name
相当于Person.prototype.name
。 - 在普通方法内,类似
super.xxx = 'xxx'
等赋值操作,相当于this.xxx = 'xxx'
,因此属性xxx
会被挂载到实例对象上,而不是父类原型对象。 - 普通方法内,通过
super.xxx()
调用父类方法,相当于Person.prototype.xxx.call(this)
。 - 在静态方法内,
super
指向父类,因此super.xxx()
相当于Person.classMethodParent()
。
以上提到的普通方法,是指非静态方法。
3. 注意事项
无论 super 是作为函数,还是对象使用,必须明确指定,否则会报错。
class Person {} class Student extends Person { constructor() { super() console.log(super) // SyntaxError: 'super' keyword unexpected here } }
4. 关于 super 总结
- 作为函数时,仅可在子类的
constructor()
内使用。若constructor()
内包括this
的使用,则super()
必须在this
之前进行调用。 - 作为对象时,若在非静态方法内使用,
super.xxx
(super.xxx()
)相当于Parent.prototype.xxx
(Parent.prototype.xxx.call(this)
)。 - 作为对象时,若在静态方法内使用,
super.xxx
(super.xxx()
)相当于Parent.xxx
(Parent.xxx()
)。 - 我们都知道在 JavaScript 访问对象的某个属性(或方法),先从对象本身去查找是否有此属性,再从原型上一层一层的查找,若最终查找不到会返回
undefined
(或抛出 TypeError 错误)。
同样地,在 Class 继承中,若子类、父类存在同名方法,使用实例对象进行调用该方法,若子类查找到了,自然不会再去父类中查找。但我们在设计类的时候,可能仍需要执行父类的同名方法,那么怎么调用呢?
显然通过父类名.方法名()
的方式调用是不合理、不灵活的,道理就跟 JavaScript 要设计this
关键字一样。于是super
就诞生了(最后这句是我猜的,哈哈)。
四、类的 prototype 属性和 __proto__ 属性
在 ES5 之中,每个对象都有 __proto__
属性,它指向对象的构造函数的 prototype
属性。
关于对象可分为:普通对象和函数对象,区别如下:
对象类型 | prototype(原型对象) | __proto__(原型) |
普通对象 | ❌ | ✅ |
函数对象 | ✅ | ✅ |
所有对象都有
__proto__
属性,而只有函数对象才具有prototype
属性。其中构造函数属于函数对象,而实例对象则属于普通对象,因此实例对象是没有prototype
属性的。
而 ES6 的 class
作为构造函数的语法糖,同时有 prototype
属性和 __proto__
,因此同时存在两条继承链。
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。 - 子类的
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class Person { } class Student extends Person { } Student.__proto__ === Person // true Student.prototype.__proto__ === Student.prototype // true
上面的示例中,子类 Student
的 __proto__
指向父类 Person
,子类的 Student
的 prototype
属性的 __proto__
属性指向父类 Person
的 prototype
属性。
因为类的继承,是按照以下模式实现的:
class Person { } class Student extends Person { } // 相当于 class Person { } class Student { } Object.setPrototypeOf(Student, Person) Object.setPrototypeOf(Student.prototype, Person.prototype) // ------------------------------------------------------- // 关于 Object.setPrototypeOf() 内部是这样实现的: // Object.setPrototypeOf = functon(obj, proto) { // obj.__proto__ = proto // return obj // } // // 因此,相当于: // Student.__proto__ = Person // Student.prototype.__proto__ = Person.prototype // -------------------------------------------------------
这样去理解:
- 作为一个对象,子类
Student
的原型(__proto__
)是父类Person
; - 作为一个构造函数,子类
Student
的原型对象(prototype
)是父类Person
的原型对象(prototype
)的实例。
因此,理论上 extends
关键字后面的(函数)对象,只要含有 prototype
属性就可以了,但 Function.prototype
除外。但在做项目的时候应该从实际应用场景考虑,这样去做是否有意义。
// 1. Student 类继承 Object 类 class Student extends Object { } Student.__proto__ === Object // true Student.prototype.__proto__ === Object.prototype // true // 2. Student 不继承 class Student { } Student.__proto__ === Function.prototype Student.prototype.__proto__ === Object.prototype // 因此,相当于: // Object.setPrototypeOf(Student, Function.prototype) // Object.setPrototypeOf(Student.prototype, Object.prototype)
五、Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具备各个组成成员的接口。它的最简单实现如下:
const a = { a: 1 } const b = { b: 2 } const c = { ...a, ...b }
上面的示例,对象 c
是对象 a
和对象 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) } } }
上面示例中的的 mix
函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) { // ... }
The end.