class 是如何实现继承的?
我相信时至今日,大部分同学看完题目都能很快的写出答案。
使用 ES 6 提供的类,能够很快的实现继承。
class Parent {
constructor() {
this.name = '爸爸';
this.books = ['JAVA']
}
showName() {
console.log(this.name)
}
}
class Child extends Parent {
constructor() {
super();
this.name = '儿子';
this.school = 'high school';
}
showSchool() {
console.log(this.school);
}
}
但是,其实 JavaScript 本身是没有类这个概念的,class 只是一个语法糖,Parent 与 Child 本质上还是函数。所以 class 提供的继承是基于 JavaScript 的原型链实现的。那么如何通过原型链实现继承呢?
ES5中的继承
JavaScript 中继承的本质就是子类共享父类的原型对象。
ps:仅个人观点,有什么不妥请大家及时反驳。
类式继承
类式继承通过子类的原型对象指向父类的实例,完成继承。
代码
function Parent() {
this.name = '爸爸';
this.books = ['JAVA'];
}
Parent.prototype.showName = function() {
console.log(this.name);
}
function Child() {
this.name = '儿子';
this.school = 'high school'
}
Child.prototype = new Parent(); // 子类的原型对象指向父类的实例,子类就能访问 Parent 原型对象上的方法了
Child.prototype.showSchool = function() {
console.log(this.school);
}
验证
const c = new Child();
// 调用父类方法
c.showName(); // 儿子
// 调用子类方法
c.showSchool(); // high school
不足
看起来类式继承很完美的完成了工作,Child 的实例化的对象正常调用了父类的方法。但是我们看看 c 到底长什么样子。
{
name: '儿子',
school: 'high school',
__proto__: {
books: ['JAVA'],
name: '爸爸',
showSchool: f (),
__proto__: {
showName: f(),
constructor: f Parent()
// ...
}
}
}
可以看出,类式继承具有以下几个问题:
- 父类的属性只实例化了一次,如果所有实例共享 books 这个属性,任意一个修改了实例修改了 books 的话,而其他实例不知情,这种情况是很危险的。由于信息不对称,很容易出现bug。
- 对象 c 的 proto 没有 constructor。
- 有两个name变量,节约内存,可以去掉一个。
寄生组合式继承
- 通过每次调用父类,传入this,使引用类型的值不共享。
- 创建一个中间对象,显式的设置 constructor。
- 子类的原型对象指向这个中间对象,中间对象的__proto__指向父类的原型对象。
ps:提个问题,这里的中间对象其实可以不用创建,直接指定子类的原型对象的__proto__为父类的原型对象(Object.setPrototypeOf)是否可行?文末说下一我的看法。
代码
function Parent() {
this.name = '爸爸';
this.books = ['JAVA'];
}
Parent.prototype.showName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this); // 像不像 class 中的 super()
this.name = '儿子';
this.school = 'high school'
}
// Object.create 创建一个中间对象
// 中间对象的__proto__指向父类的原型对象
// 子类的原型对象指向这个中间对象
Child.prototype = Object.create(Parent.prototype, {
// 显示的指定 constructor
constructor: {
value: Child,
enumerable: false, // 不可遍历
writable: true, // 可改写
configurable: true
}
})
Child.prototype.showSchool = function() {
console.log(this.school);
}
验证
const c = new Child();
// 调用父类方法
c.showName(); // 儿子
// 调用子类方法
c.showSchool(); // high school
可以看一下 c 的结构
{
name: "儿子",
books: ['JAVA'],
school: "high school",
__proto__: {
showSchool: ƒ (),
constructor: ƒ Child(),
__proto__: {
showName: ƒ (),
constructor: ƒ Parent(),
// ...
}
}
}
上面提到的问题都得到了解决,有兴趣的同学可以去对比一下与class实例化后的继承的结构是否有区别。
小结
- JavaScript 本身是没有类的概念,继承也是通过原型链实现的,
- JavaScript 中继承的本质就是子类共享父类的原型对象。
ps: 上面的问题,我觉得是可行的。但是
由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。 ——摘自 MDN
参考资料:
- 【JavaScript 设计模式】 —— 张容铭
- MDN