Class的继承
本系列属于阮一峰老师所著的ECMAScript 6 入门学习笔记
简洁
Class可以通过extends
关键字来实现继承,对比ES5中通过修改原型链实现继承,要清晰和方便很多
// 用法
class Point{}
class ColorPoint extends Points{}
// super关键字,用来表示父类的构造函数,用来新建父类的this对象
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方法中调用super方法,否则新建实例会报错。这是因为子类没有自己的this对象,而是继承父类的对象,然后对其加工。如果不调用super方法,子类就得不到this对象
class ColorPoint extends Point{
constructor(){}
}
let cp = new ColorPoint() // ReferenceError
ES5的继承实质是先创造子类的实例对象this
,然后将父类的方法添加到this
上(Parent.apply(this)
)。ES6的继承机制完全不同,实质是先创造父类的实例对象this
(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
// 若子类没有定义constructor方法,该方法会被默认添加
class ColorPoint extends Point{}
//等同于
class ColorPoint extends Point{
constructor(...args){
super(...args)
}
}
// 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建是基于对父类实例的加工,只有super方法才能返回父类实例
class ColorPoint extends Point{
constructor(x,y,color){
this.color = color // ReferenceError
super(x,y)
this.color = color // 正确
}
}
// 子类创建的实例,同时是父类的实例
let cp = new ColorPoint(23,2,'green')
cp instanceof ColorPoint // true
cp instanceof Point // true
// 父类的静态方法,也会被子类继承
class A{
static hello(){
console.log('hello world')
}
}
class B extends A{}
B.hello() // hello world
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用来从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point // true
super关键字
super
关键字,即可以当做函数使用,也可以当做对象使用
// super作为函数调用,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。作为函数时,super()只能用在子类的构造函数中,用在其他地方会报错
class A{}
class B extends A{
constructor(){
super()
}
}
// 虽然super代表了父类A的构造函数,但是返回的是子类B的实例
class A{
constructor(){
console.log(new.taget.name)
}
}
class B extends A{
constructor(){
super()
}
}
new A() // A
new B() // B
// super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类
class A{
p(){
return 2
}
}
class B extends A{
constructor(){
super()
console.log(super.p())
}
}
// super.p()相当于A.prototype.p()
let b = new B() // 2
// 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。如果定义在父类的原型对象上,super就可以取到
class A{}
A.prototype.x = 2
class B extends A{
constructor(){
super()
console.log(super.x)
}
}
let b = new B() // 2
// ES6规定,通过super调用父类的方法时,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()
}
}
// super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2,而不是1。实际上执行的是super.print.call(this)
let b = new B()
b.m() // 2
// 由于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
}
}
// super.x赋值3等同于this.x赋值3,而读取super.x时读取的是A.prototype.x,返回undefined
// 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)
}
}
// super在静态方法中指向父类,在普通方法中指向父类的原型对象
Child.myMethod(1) // static 1
var child = new Child()
child.myMethod(2) // instance 2
类的prototype属性和__ proto __属性
大多数浏览器在ES5的实现中,都有一个__proto__
属性,指向对应构造函数的prototype
属性。Class作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性
class A{}
class B extends A{}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
extends的继承目标
extends
关键字后面可以跟多种类型的值,只要是一个具有prototype
属性的函数,就能被继承。
以下讨论三种特殊情况:
// 第一种:子类继承Object类。
class A extends Object{}
// 这时A就是构造函数Object的复制,A的实例就是Object实例
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
// 第二种:不存在任何继承。
class A{}
// A作为一个基类(即不存在任何继承),就是一个普通函数,直接继承Function.prototype。A调用后返回一个空对象(即Object实例)
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
// 第三种:继承null
class A extends null{}
// 和第二种情况类似,A是一个普通函数,直接继承Function.prototype
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
// A调用后返回的对象不继承任何方法,__proto__指向Function.prototype,执行以下代码
class C extends null{
constructor(){ return Object.create(null) }
}
实例的__ 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
// 以上我们可以通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为
p2.__proto__.__proto__.printName = function(){
console.log('Angus')
}
p1.printName() // 'Angus'
原生构造函数的继承
ES5原生构造函数(Boolean()
、Number()
、String()
、Array()
、Date()
、Function()
、RegExp()
、Error()
、Object()
)是不允许继承的,ES6运行原生构造函数定义子类。因为ES6是先新建父类的实例对象this
,然后再用子类的构造函数修饰this
,使得父类所有行为都可以继承。
class MyArray extends Array{
constructor(...args){
super(...args)
}
}
var arr = new MyArray()
arr[0] = 12
arr.length // 1
// 注意,继承Objcet的子类,有一个行为差异
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构造函数会忽略参数
Mixin模式的实现
Mixin指的是多个对象合成一个新的对象,新对象具有各个组成员的接口
// 最简单的实现方法
const a = {
a: 'a'
}
const b = {
b: 'b'
}
const c = {...a,...b}
// 比较完备的实现,将多个类的接口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 DistributedEdit extends mix(Loggable, Serializable) {
// ...
}