JavaScript进阶之继承

简介: JavaScript进阶之继承

前言


文章最开始先来带大家回忆一下构造函数、原型和实例的关系: 《JavaScript高级程序设计》中讲道:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。


上面的话听起来有几分难以理解,咱们用武侠视角来形象一下三者的关系。以武侠宗门宗主为例,构造函数相当于宗主本身,原型相当于宗主的分身,宗主心疼自己的弟子,生成一个投影分身(实例)来保护弟子,当弟子遇到危险时,可以通过投影分身调用宗主法术来御敌。


image.png

JavaScript 继承初学有几分难以理解,因此小包本文就以前端宗门传承角度来讲述继承,图文并茂,以比较浅显易懂的例子,带大家生动形象的理解 JavaScript 继承。


故事背景


随着前端的发展,前端宗的门徒越来越多,如果保证前端的高质量发展和持续性发展成为摆在前端门面前的紧要问题,前端宗高层经过紧急会议,最终决定公诸同好,大开传承之门,以促前端事业的大发展。


但问题来了,这个传承方案应该如何设计那?下面咱们跟随着前端宗门高层的视角,一起来领悟 JavaScript 继承。


传承石——原型链继承


经过高层一致商讨,宗门传承是宗门长久发展的基石,传承要具有一定象征意义,于是宗门初步决定设置一个传承石,将宗主的毕生所悟刻印在传承石中,弟子们从传承石中接受传承。


  1. 宗主神通大成,不止有自身 Metropolit() 知识传承,还具有宗主分身 Metropolit.prototype 存储其他传承。
  2. new Metropolit() 生成宗主所有的知识传承。
  3. 建造传承石 Inherit()Inherit.prototype 存储宗主的所有传承
  4. 弟子们通过 new Inherit() 获得传承


原型链继承的实现思路是将父类的实例作为子类的原型


image.png


转化成代码


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 对传承的理解。


通过上面的分析,我们可以总结出原型链传承的优缺点:


优点: 父类型的方法可以复用


缺点


  1. 父类型的所有引用类型会被子类实例共享,子类实例更改引用类型的值,会影响其他子类实例。
  2. 在创建子类型的实例时,不能向父类型的构造函数中传递参数。


法则传承——借用构造函数


法则传承是传承不可缺少的一部分,缺少法则传承不利于宗门培养高等战力,因此宗门高层决定单独为法则传承打造一个法宝。


  • 宗主将自身法则传承浓缩到 MetropolitInherit 传承法宝中
  • 打造 RuleInherit 法宝,内部使用 MetropolitInherit.call 法宝刻印法则传承。
  • 弟子们通过 new RuleInherit() 获取法则传承


image.png


借用构造函数的实现思路是: 在子类型构造函数的内部调用超类型构造函数


转化成代码


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) ,使用父类的构造函数来增强子类实例,也就是将父类新的属性复制给子类实例一份。


优点


  1. 避免父类引用类型属性被所有实例共享(法则传承冲突)
  2. 在创建子类型的实例时,可以向父类型的构造函数中传递参数(选择法则类型)


缺点


  1. 只能继承父类的实例属性和方法,不能继承父类原型属性和方法(只能继承法宝中的传承,其余传承无法继承)
  2. 方法定义在构造函数中,每个子实例都含有父类函数副本,无法实现函数复用


附带法则传承的传承石——组合继承


传承石与传承法宝模式都各有弊端,但两种传承方式优缺点恰好互补,如果两种传承方案结合,不失为一种完善的传承方案,因此宗门初步决定使用组合继承的方式——将传承石与传承法宝结合。这也是 JavaScript 中最常用的继承模式


  1. 宗主凝结自身传承于 Metropolit(),分身传承于 Metropolit.prototype
  2. 将法则传承刻印在自身 Metropolit()Metropolit 承载法则传承法宝的功能
  3. 设置传承石 Inherit()Inherit 内部法阵调用传承法宝  Metropolit.call(this) ,刻印法则传承。
  4. Inherit.prototype 接受宗主的所有传承,即 Inherit.prototype = new Metropolit()


  • 弟子们通过 new Inherit() 获得传承


image.png


组合继承的实现思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承


转化成代码


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 为例,我们来看一下为什么会有两份相同的属性和方法。


image.png


  • 第一次调用 new Metropolit() ,给 Inherit.prototype 原型添加 vuerule 属性
  • 调用new Inherit(),内部第二次调用 Metropolit.call(this),给生成的实例对象添加 vuerule 属性


传功殿——原型式继承


虽然上面的方案看起来已经非常完善,但还是有很多长老提出了自己的想法,传承石主要刻印宗主的知识,宗主是个战斗天才,但并非全能型人才,长此以往,宗门的风格会越来越偏激,副职业会衰落掉。这很不利于宗门的发展。


综合全面的发展才是正确的发展方向,但不能单独为每个长老都设置传承石吧,劳民伤财。高层又商讨了一番,初步计划建立传承殿,传承殿接受长老、宗主的传承刻印,如果有弟子需要传承某位长老,就为该长老创建传承接口。


传承殿功能: 接受长老或宗主 O 的传承知识


  • 内部存在虚拟法阵(即创建构造函数 F )作为过渡
  • 虚拟法阵接受传承知识(F.prototype -> 长老或宗主O的传承知识)
  • 返回 new F() 传承接口


function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}
复制代码


image.png


原型式继承的实现思路是对参数对象的一种浅复制


转化成代码


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() 方法规范化了原型式继承。


缺点


有原型链继承的基础,我们可以很轻松的发现,原型式继承的缺点与原型链是相同的。


  1. 子类实例共享父类引用类型属性(法则传承)
  2. 在创建子类型的实例时,不能向父类型的构造函数中传递参数。


定制化传承殿——寄生式继承


设立传承殿后,宗主发现如此大动干戈、大动土木一方面解决不了法则传承的问题;另一方面感觉使用个法宝就能实现这些功能,是否值得那?


长老灵机一动,不如我们将传承部分设计为法宝,传承殿定制化的提供传承接口。例如 vue 长老除了提供必要的知识传承外,还可以提供 vue 实战训练;webpack 长老可以提供 webpack 源码解析等。


  1. 设计传承法宝 object( ES5 后使用 Object.create 规范化)
  2. 为传承法宝返回值添加定制化方法,例如 vueTraining
  3. 弟子们通过 inheritPalace 接受传承


function inheritPalace(vue) {
    const vueKnowledge = object(vue);
    vueKnowledge.vueTraining = function() {
        console.log("vue实战训练");
    }
    return vueKnowledge;
}
复制代码


image.png


寄生式继承的实现思路是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象


转化成代码


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); // 道存在吗?
复制代码


缺点: 寄生式传承为构造函数新增属性和方法,增强了函数属性。这个功能看起来很诱人,但它并没有修复原型式继承的问题——法则传承和选择法则传承的类型。并且新添加函数方法无法实现函数复用


寄生组合式继承


虽然上述长老提供的功能非常诱人,但依旧没有解决法则传承的问题,法则传承的问题可是重中之重,这是宗主灵机一动,能不能将传承法宝(组合继承)集合到传承殿中,这样不就两全其美了吗?


  1. 首先宗主决定优化 inheritPalace 传承殿功能,注重其传承功能


function inheritPalace(inheritKnow, patriarch) {
    const knowledge = Object.create(patriarch.prototype);
    // 修复构造函数原型上constructor丢失问题
    knowledge.constructor = inheritKnow;
    inheritKnow.prototype = knowledge;
}
复制代码


  1. 对于 vue 长老来说,将自身传承存储在构造函数 vueKnowledge()vueKnowledge 存放法则传承及自身传承,vueKnowledge.prorotype 存取分身传承。
  2. 建立 vue 长老传承接口, vueInheritvueInherit 内部调用 vueKnowledge.call 将法则传承刻印在 vueInherit
  3. 通过 inheritPalacevueInheritvueKnowledge 链接,获取其余传承信息。
  4. 弟子们通过 new vueInherit() 获取 vue 长老的所有传承

image.png


寄生组合式继承的实现思路是不必为了指定子类型的原型而调用超类型的构造函数


转化成代码


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); // 道可道,非常道
复制代码


寄生组合式继承是目前继承最成熟的方案,它囊括了所有的优点:


  • 只调用一次父类构造函数
  • 在创建子类型的实例时,可以向父类型的构造函数中传递参数
  • 父类方法可以复用
  • 避免父类引用类型属性被所有实例共享


总结


原型链继承


  • 核心: 将父类的实例作为子类的原型
  • 优点: 父类方法可以复用
  • 缺点:父类型的所有引用类型会被子类实例共享,子类更改引用类型的值,会影响其他子类。 在创建子类型的实例时,不能向父类型的构造函数中传递参数。


借助构造函数继承


  • 核心:在子类型构造函数的内部调用超类型构造函数
  • 优点:避免父类引用类型属性被所有实例共享(法则传承冲突)。在创建子类型的实例时,可以向父类型的构造函数中传递参数(选择法则类型)
  • 缺点:只能继承父类的实例属性和方法,不能继承父类原型属性和方法(只能继承法宝中的传承,其余传承无法继承)。方法定义在构造函数中,每个子实例都含有父类函数副本,无法实现函数复用


组合继承


  • 核心: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
  • 优点:结合了原型链继承及借助构造函数继承的优点
  • 缺点: 调用两次父类构造函数


原型式继承


  • 核心:对参数对象的一种浅复制
  • 优缺点与原型链继承相同


寄生式继承


  • 核心:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
  • 缺点: 无法实现函数复用


寄生组合式继承


组合继承最大的问题在于执行两次父类构造函数,寄生组合式继承就是为了降低调用父类构造函数的开销而出现的


  • 核心:不必为了指定子类型的原型而调用超类型的构造函数
  • 优点:
  • 只调用一次父类构造函数
  • 在创建子类型的实例时,可以向父类型的构造函数中传递参数
  • 父类方法可以复用
  • 避免父类引用类型属性被所有实例共享



相关文章
|
1天前
|
缓存 JavaScript 前端开发
【JavaScript 技术专栏】DOM 操作全攻略:从基础到进阶
【4月更文挑战第30天】本文深入讲解JavaScript与DOM交互,涵盖DOM基础、获取/修改元素、创建/删除元素、事件处理结合及性能优化。通过学习,开发者能掌握动态改变网页内容、结构和样式的技能,实现更丰富的交互体验。文中还讨论了DOM操作在实际案例、与其他前端技术结合的应用,助你提升前端开发能力。
|
1天前
|
JavaScript 前端开发 算法
JavaScript 中的 if 判断:深入理解、实战应用与进阶技巧
【4月更文挑战第7天】探索 JavaScript 中的 if 判断语句,它是构建逻辑清晰程序的基础。了解其概念、语法、应用示例及编程技巧,包括条件控制、else if 结构、三目运算符。注意条件表达式简洁性,避免 falsy 值陷阱,利用逻辑运算符优化,并减少 if 嵌套。实践这些技巧将提升编程能力和代码质量。
24 0
|
1天前
|
JavaScript 前端开发
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点
15 0
|
1天前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(三)
深入JS面向对象(原型-继承)
31 0
|
1天前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
32 0
|
1天前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
22 0
|
1天前
|
JavaScript 前端开发
JavaScript 中最常用的继承方式
【5月更文挑战第9天】JavaScript中的继承有多种实现方式:1) 原型链继承利用原型查找,但属性共享可能引发问题;2) 借用构造函数避免共享,但方法复用不佳;3) 组合继承结合两者优点,是最常用的方式;4) ES6的class继承,是语法糖,仍基于原型链,提供更直观的面向对象编程体验。
17 1
|
1天前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念
【5月更文挑战第9天】JavaScript继承有优点和缺点。优点包括代码复用、扩展性和层次结构清晰。缺点涉及深继承导致的复杂性、紧耦合、单一继承限制、隐藏父类方法以及可能的性能问题。在使用时需谨慎,并考虑其他设计模式。
15 2
|
1天前
|
JavaScript 前端开发 开发者
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点
12 0
|
1天前
|
JavaScript 前端开发
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点