而 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
继上一篇介绍了 Class 的语法,今天来看看 ES6 中的继承。
在 ES5 大概有 6 种继承方式:类式继承、构造函数继承、组合式继承、原型继承、寄生式继承、寄生组合式继承,而这些方式都有一些各自的缺点,可看文章:深入 JavaScript 继承原理。
而 ES6 标准提供了 Class(类)的语法来定义类,语法很像传统的面向对象写法。本质上仍然是通过原型实现继承的,可以理解为
class
只是一个语法糖,跟传统面向对象的类又不一样。废话说完了,入正题...那 Class 是怎样实现继承的呢?
一、简介
通过
extends
关键字实现继承,比 ES5 写一长串的原型链,方便清晰很多,对吧。上面的示例中,定义了一个
Student
(子)类,该类通过extends
关键字继承了Person
(父)类的所有属性和方法。但由于两个类中并没有实现什么功能,相当于Student
复制了一个Person
类而已。原因是,ES5 的继承实质是先创建子类的实例对象(即
this
),然后再将父类的方法添加到实例对象this
上(类似Parent.apply(this)
)。而 ES6 继承机制完全不同,它先将父类的实例对象的属性和方法,放到this
上,然后再用子类的构造函数修改this
。因此,在子类使用this
之前,需要调用super()
方法执行父类的constructor()
方法来创建实例对象this
。我们来改下:
实例对象
stu
同时是Student
、Person
类的实例,这点与 ES5 表现一致。二、Object.getPrototypeOf()
使用
Object.getPrototypeOf()
可以通过子类获取其直接父类。提一下,我们一直使用的
Object.prototype.__proto__
并不是 ECMAScript 标准,只是被各大浏览器厂商支持,因此我们才可以使用。现在被推荐使用的是,标准支持的Objec.getPrototypeOf()
、Object.setPrototypeOf()
方法。三、super 关键字
关键字
super
可以作为函数使用,也可以作为对象使用,两种是有区别的。1. super 作为函数
当
super
作为函数,只能在(子类)构造方法中使用,若在非子类或类的其他方法中调用,是会报错的。在构造方法当作函数调用
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
)。小结:
在普通方法内,类似
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 是作为函数,还是对象使用,必须明确指定,否则会报错。
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
属性。关于对象可分为:普通对象和函数对象,区别如下:
而 ES6 的
class
作为构造函数的语法糖,同时有prototype
属性和__proto__
,因此同时存在两条继承链。__proto__
属性,表示构造函数的继承,总是指向父类。prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。上面的示例中,子类
Student
的__proto__
指向父类Person
,子类的Student
的prototype
属性的__proto__
属性指向父类Person
的prototype
属性。因为类的继承,是按照以下模式实现的:
这样去理解:
Student
的原型(__proto__
)是父类Person
;Student
的原型对象(prototype
)是父类Person
的原型对象(prototype
)的实例。因此,理论上
extends
关键字后面的(函数)对象,只要含有prototype
属性就可以了,但Function.prototype
除外。但在做项目的时候应该从实际应用场景考虑,这样去做是否有意义。五、Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具备各个组成成员的接口。它的最简单实现如下:
上面的示例,对象
c
是对象a
和对象b
的合成,具备两者的接口。下面是一个更完备的实现,将多个类的接口“混入”另一个类。
上面示例中的的
mix
函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。The end.