薪火相传——用武侠风来解读JavaScript继承 原创 精华
春节不停更,此文正在参加「星光计划-春节更帖活动」
前言
文章最开始先来带大家回忆一下构造函数、原型和实例的关系:
《JavaScript高级程序设计》中讲道:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
上面的话听起来有几分难以理解,咱们用武侠视角来形象一下三者的关系。以武侠宗门宗主为例,宗主本身相当于构造函数,宗主的分身相当于原型,宗主心疼自己的弟子,生成一个投影分身(实例)给弟子提供包子,弟子通过投影分身就可以调用宗主法术来御敌。
JavaScript
继承初学有几分难以理解,因此小包本文就以前端宗门传承角度来讲述继承,以比较浅显易懂的粒子,带大家生动形象的理解 JavaScript
传承。
故事背景
随着前端的发展,前端宗的门徒越来越多,如果保证前端的高质量发展和持续性发展成为摆在前端门面前的紧要问题,前端宗高层经过紧急会议,最终决定公诸同好,大开传承之门,以促前端事业的大发展。
但问题来了,这个传承方案应该如何设计那?下面咱们跟随着前端宗门高层的视角,一起来领悟 JavaScript
继承。
传承石——原型链继承
经过高层一致商讨,宗门传承是宗门长久发展的基石,传承要具有一定象征意义,于是宗门决定设置一个传承石,将宗主的毕生所悟刻印在传承石中,弟子们从传承石中接受传承。
- 宗主神通大成,不止有自身
Metropolit()
知识传承,还具有一个宗主分身Metropolit.prototype
存储其他传承。 new Metropolit()
生成宗主所有的知识传承。- 建造传承石
Inherit()
,Inherit.prototype
存储宗主的所有传承 - 弟子们通过
new Inherit()
获得传承
原型链继承的实现思路是将父类的实例作为子类的原型
转化成代码
function Metropolit() {
this.vue = 'vue2.x';
}
Metropolit.prototype.getVueFromMetropolit = function () {
return this.vue; // 宗主刻印的vue知识
}
function Inherit() {
this.newVue = 'vue3.x'; // 其他弟子刻印的vue知识
}
Inherit.prototype = new Metropolit(); // 将毕生感悟传入传承之物
Inherit.prototype.getVueFromInherit = function() {
return this.newVue;
}
const stu1 = new Inherit();
const stu2 = new Inherit();
console.log(stu1.getVueFromMetropolit()); // vue2.x
console.log(stu2.getVueFromInherit()); // vue3.x
但天地间的传承分为两种: 普通传承与法则传承。法则传承绝非传承石可以承载,因此宗主将法则传承存放在堆内存中,传承石中存储了法则传承的地址。
法则传承只有一份,弟子们一起刻印,就会对法则感悟造成污染,产生不可逆的后果。比如下面案例:
// rule 属性存储宗主的法则感悟
function Metropolit() {
this.rule = {
truth: '道可道,非常道',
percent: 0.1
};
}
function Inherit() {
this.newVue = 'vue3.x'; // 其他弟子传入的知识
}
Inherit.prototype = new Metropolit(); // 将毕生感悟传入传承之物
const stu1 = new Inherit();
const stu2 = new Inherit();
// stu1 修改法则传承为狗屁道,我要享福
stu1.rule.truth = "狗屁道,我要享福";
// stu2 接受的法则传承发生改变
console.log(stu2.rule.truth); // 狗屁道,我要享福
从上面的代码可以看出,由于引用类型(法则传承)使用地址形式存放在传承石中,stu1
对传承的修改会影响 stu2
对传承的理解。
通过上面的分析,我们可以总结出原型链传承的优缺点:
优点: 父类型的方法可以复用
缺点
- 父类型的所有引用类型会被子类实例共享,子类更改引用类型的值,会影响其他子类。
- 在创建子类型的实例时,不能向父类型的构造函数中传递参数。
传承法宝——借用构造函数
法则传承是传承不可缺少的一部分,缺少法则传承不利于宗门培养高等战力,因此宗门高层决定单独为法则传承打造一个法宝。
- 宗主将自身法则传承浓缩到
MetropolitInherit
传承法宝中 - 打造
RuleInherit
法宝,内部使用MetropolitInherit.call
法宝刻印法则传承。 - 弟子们通过
new RuleInherit()
获取传承
借用构造函数的实现思路是: 在子类型构造函数的内部调用超类型构造函数
转化成代码
function MetropolitInherit() {
this.rule = {
truth: '道可道,非常道',
percent: 0.1
};
}
function RuleInherit() {
// 调用父类构造函数
MetropolitInherit.call(this);
}
const stu1 = new RuleInherit();
const stu2 = new RuleInherit();
stu.rule.truth = "道可道,非常不到";
console.log(stu.rule.truth); //道可道,非常不到
// stu1 修改法则传承不会影响 stu2 的传承
console.log(stu2.rule.truth);//道可道,非常道
宗主神威无敌,掌握的法则成千上百,但对弟子来说,掌握一门就非常消耗精力,因此宗主为了长久的发展,为 RuleInherit
法宝添加法则类型 ruleType
属性,输入什么样的法则类型,就可以返回什么样的法则传承。
function MetropolitInherit(ruleType) {
this.rule = {
truth: '道可道,非常道',
percent: 0.1
};
this.ruleType = ruleType;
this.vue = 'vue2.x';
}
MetropolitInherit.prototype.getVueFromMetropolit = function () {
return this.vue;
}
function RuleInherit(ruleType) {
MetropolitInherit.call(this, ruleType);
}
const stu = new RuleInherit("火道");
// 法则类型: 火道--法则感悟: 道可道,非常道
console.log(`法则类型: ${stu.ruleType}--法则感悟: ${stu.rule.truth}`)
// Uncaught TypeError: stu.getVueFromMetropolit is not a function
// stu 无法获取 MetropolitInherit.prototype 的方法
console.log(stu.getVueFromMetropolit())
通过上面的分析,我们可以发现借用创造函数继承核心在于调用 MetropolitInherit.call(this, ruleType)
,使用父类的构造函数来增强子类实例,也就是将父类新的属性复制给子类实例一份。
优点
- 避免父类引用类型属性被所有实例共享(法则传承冲突)
- 在创建子类型的实例时,可以向父类型的构造函数中传递参数(选择法则类型)
缺点
- 只能继承父类的实例属性和方法,不能继承父类原型属性和方法(只能继承法宝中的传承,其余传承无法继承)
- 方法定义在构造函数中,每个子实例都含有父类函数副本,无法实现函数复用
传承石与传承法宝——组合继承
传承石与传承法宝模式都各有弊端,并且两种传承方式优缺点恰好互补,因此宗门初步决定使用组合继承的方式——将传承石与传承法宝结合。这也是 JavaScript 中最常用的继承模式。
- 宗主凝结自身传承于
Metropolit()
,分身传承于Metropolit.prototype
。 - 将法则传承刻印在自身
Metropolit()
,Metropolit
承载法则传承法宝的功能 - 设置传承石
Inherit()
,Inherit
内部法阵调用传承法宝Metropolit.call(this)
,刻印法则传承。 Inherit.prototype
接受宗主的所有传承,即Inherit.prototype = new Metropolit()
- 弟子们通过
new Inherit()
获得传承
组合继承的实现思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承
转化成代码
function Metropolit() {
this.rule = {
truth: '道可道,非常道',
percent: 0.1
};
this.vue = 'vue2.x'
}
Metropolit.prototype.getVueFromMetropolit = function () {
return this.vue;
}
function Inherit() {
Metropolit.call(this);
this.newVue = 'vue3.x'; // 其他弟子传入的知识
}
Inherit.prototype = new Metropolit();
Inherit.prototype.constructor = Inherit;
Inherit.prototype.getVueFromInherit = function() {
return this.newVue;
}
const stu1 = new Inherit();
const stu2 = new Inherit();
console.log(stu1.getVueFromMetropolit()); // vue2.x
console.log(stu2.getVueFromInherit()); // vue3.x
stu1.rule.truth = "我想享福";
console.log(stu1.rule.truth); // 我想享福
console.log(stu2.rule.truth); // 道可道,非常道
通过上面例子,我们可以发现,组合继承避免了原型链和借用构造函数继承的缺陷,融合了它们的优点。
缺点
但也是存在缺陷的,会调用两次父类型构造函数,实例原型中会存在两份相同的属性或方法,比如以 stu1
为例,我们来看一下为什么会有两份相同的属性和方法。
-
调用
new Metropolit()
,给Inherit.prototype
原型添加vue
和rule
属性 -
调用
new Inherit()
,内部调用Metropolit.call(this)
,给生成的实例对象添加vue
和rule
属性
传功殿——原型式继承
虽然上面的方案看起来已经非常完善,但还是有很多长老提出了自己的想法,传承石主要刻印宗主的知识,宗主是个战斗天才,但并非全能型人才,长此以往,宗门的风格会越来越偏激,副职业会衰落掉。这很不利于宗门的发展。
综合全面的发展才是正确的发展方向,但不能单独为每个长老都设置传承石吧,劳民伤财。高层又商讨了一番,初步计划建立传承殿,传承殿接受长老、宗主的传承刻印,如果有弟子需要传承某位长老,就为该长老创建传承接口。
传承殿功能: 接受长老或宗主 O
的传承知识
- 内部存在虚拟法阵(即创建构造函数
F
)作为过渡 - 虚拟法阵接受传承知识(
F.prototype
-> 长老或宗主O
的传承知识) - 返回
new F()
传承接口
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
原型式继承的实现思路是对参数对象的一种浅复制
转化成代码
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
// 还是以宗主传承为例子
const Metropolit = {
rule: {
truth: '道可道,非常道',
percent: 0.1
},
vue: 'vue2.x'
}
const stu1 = object(Metropolit);
const stu2 = object(Metropolit);
stu1.vue = 'vue3.x';
stu1.rule.truth = '朝闻道,夕死可矣';
console.log(stu1.vue); // vue3.x
console.log(stu1.rule.truth); // 朝闻道,夕死可矣
console.log(stu2.vue); // vue2.x
// 可见原型式继承依旧无法解决法则传承问题
console.log(stu2.rule.truth); // 朝闻道,夕死可矣
原型式继承是道格拉斯•克罗克福德在 2006
年提出,这种原型式传承,要求必须有一个对象可以作为另一个对象的基础。
ECMAScript5
新增Object.create()
方法规范化了原型式继承。
缺点
有原型链继承的基础,我们可以很轻松的发现,原型式继承的缺点与原型链是相同的。
- 子类实例共享父类引用类型属性(法则传承)
- 在创建子类型的实例时,不能向父类型的构造函数中传递参数。
定制化传承殿——寄生式继承
设立传承殿后,宗主发现如此大动干戈、大动土木一方面解决不了法则传承的问题;另一方面感觉使用个法宝就能实现这些功能,是否值得那?
长老灵机一动,不如我们将传承部分设计为法宝,传承殿定制化的提供传承接口。例如 vue
长老除了提供必要的知识传承外,还可以提供 vue
实战训练;webpack
长老可以提供 webpack
源码解析等。
- 设计传承法宝
object
(ES5
后使用Object.create
规范化) - 为传承法宝返回值添加定制化方法,例如
vueTraining
- 弟子们通过
inheritPalace
接受传承
function inheritPalace(vue) {
const vueKnowledge = object(vue);
vueKnowledge.vueTraining = function() {
console.log("vue实战训练");
}
return vueKnowledge;
}
寄生式继承的实现思路是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象
转化成代码
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function inheritPalace(vue) {
const vueKnowledge = object(vue);
vueKnowledge.vueTraining = function() {
console.log("vue实战训练");
}
return vueKnowledge;
}
const vue = {
rule: {
truth: '道可道,非常道',
percent: 0.1
},
vue: 'vue2.x'
}
const stu1 = inheritPalace(vue);
const stu2 = inheritPalace(vue);
stu1.rule.truth = '道存在吗?'
stu1.vueTraining();// vue实战训练
console.log(stu1.rule.truth); // 道存在吗?
console.log(stu2.rule.truth); // 道存在吗?
寄生式传承为构造函数新增属性和方法,增强了函数属性。这个功能看起来很诱人,但它并没有修复原型式继承的问题——法则传承和选择法则传承的类型。并且新添加函数方法无法实现函数复用。
寄生组合式继承
虽然上述长老提供的功能非常诱人,但还是没有解决法则传承的问题,这是宗主灵机一动,能不能将传承法宝(组合继承)集合到传承殿中,这样不就两全其美了吗?
- 首先宗主决定优化
inheritPalace
传承殿功能,注重其传承功能
function inheritPalace(inheritKnow, patriarch) {
const knowledge = Object.create(patriarch.prototype);
// 修复构造函数原型上constructor丢失问题
knowledge.constructor = inheritKnow;
inheritKnow.prototype = knowledge;
}
- 对于
vue
长老来说,将自身传承存储在构造函数vueKnowledge()
,vueKnowledge
存放法则传承及自身传承,vueKnowledge.prorotype
存取分身传承。 - 建立
vue
长老传承接口,vueInherit
,vueInherit
内部调用vueKnowledge.call
将法则传承刻印在vueInherit
内 - 通过
inheritPalace
将vueInherit
与vueKnowledge
链接,获取其余传承信息。 - 弟子们通过
new vueInherit()
获取vue
长老的所有传承
寄生组合式继承的实现思路是不必为了指定子类型的原型而调用超类型的构造函数
转化成代码
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function vueKnowledge(ruleType) {
this.rule = {
truth: '道可道,非常道',
percent: 0.1
};
this.ruleType = ruleType;
this.vue = 'vue2.x';
}
vueKnowledge.prototype.getVue = function() {
return this.vue;
}
function VueInherit(ruleType, webpack) {
vueKnowledge.call(this, ruleType);
this.webpack = webpack;
}
inheritPrototype(VueInherit, vueKnowledge);
VueInherit.prototype.getWebpack = function() {
return this.webpack;
}
const stu1 = new VueInherit('火道', 'webpack5');
const stu2 = new VueInherit('水道', 'webpack4');
// 实例可以使用 VueInherit.prototype 和 vueKnowledge.prototype 的方法
console.log(stu1.getVue()); // vue2.x
console.log(stu1.getWebpack()); // webpack5
// 子类不共享父类引用类型实例
stu1.rule.truth = "水火不容,火道为王";
console.log(stu1.rule.truth); // 水火不容,火道为王
console.log(stu2.rule.truth); // 道可道,非常道
寄生组合式继承是目前继承最成熟的方案,它囊括了所有的优点:
- 只调用一次父类构造函数
- 在创建子类型的实例时,可以向父类型的构造函数中传递参数
- 父类方法可以复用
- 避免父类引用类型属性被所有实例共享
总结
原型链继承
- 核心: 将父类的实例作为子类的原型
- 优点: 父类方法可以复用
- 缺点:父类型的所有引用类型会被子类实例共享,子类更改引用类型的值,会影响其他子类。 在创建子类型的实例时,不能向父类型的构造函数中传递参数。
借助构造函数继承
- 核心:在子类型构造函数的内部调用超类型构造函数
- 优点:避免父类引用类型属性被所有实例共享(法则传承冲突)。在创建子类型的实例时,可以向父类型的构造函数中传递参数(选择法则类型)
- 缺点:只能继承父类的实例属性和方法,不能继承父类原型属性和方法(只能继承法宝中的传承,其余传承无法继承)。方法定义在构造函数中,每个子实例都含有父类函数副本,无法实现函数复用
组合继承
- 核心: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
- 优点:结合了原型链继承及借助构造函数继承的优点
- 缺点: 调用两次父类构造函数
原型式继承
- 核心:对参数对象的一种浅复制
- 优缺点与原型链继承相同
寄生式继承
- 核心:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
- 缺点: 无法实现函数复用
寄生组合式继承
组合继承最大的问题在于执行两次父类构造函数,寄生组合式继承就是为了降低调用父类构造函数的开销而出现的
- 核心:不必为了指定子类型的原型而调用超类型的构造函数
- 优点:
- 只调用一次父类构造函数
- 在创建子类型的实例时,可以向父类型的构造函数中传递参数
- 父类方法可以复用
- 避免父类引用类型属性被所有实例共享