原型链继承

首先我们简单回忆一下构造函数、原型、原型链之间的关系:每个构造函数有一个prototype 属性,它指向原型对象,而原型对象都有一个指向构造函数的指针 constructor,实例对象都包含指向原型对象的内部指针__proto__。如果我们让原型对象等于另一个构造函数的实例,那么此原型对象就会包含一个指向另一个原型的指针。这样一层一层,逐级向上,就形成了原型链。

根据上面的回顾,我们可以写出 原型链继承。

function Vehicle(powerSource) {this.powerSource = powerSource;this.components = ['座椅', '轮子'];
}Vehicle.prototype.run = function() {console.log('running~');
};function Car(wheelNumber) {this.wheelNumber = wheelNumber;
}Car.prototype.playMusic = function() {console.log('sing~');
};// 将父构造函数的实例赋值给子构造函数的原型
Car.prototype = new Vehicle();const car1 = new Car(4);

上面这个例子中,首先定义一个叫做 交通工具 的构造函数,它有两个属性分别是是 驱动方式 和 组成部分,还有一个原型方法是 跑;接下来定义叫做 汽车 的构造函数,它有 轮胎数量 属性和 播放音乐 方法。我们将 Vehicle 的实例赋值给 Car 的原型,并创建一个名叫 car1 的实例。

但该继承方式有几个缺点:

  • 多个实例对引用类型的操作会被篡改

  • 子类型的原型上的 constructor 属性被重写了

  • 给子类型原型添加属性和方法必须在替换原型之后

  • 创建子类型实例时无法向父类型的构造函数传参

  • 缺点 1:父类的实例属性被添加到了实例的原型中,当原型的属性为引用类型时,就会造成数据篡改。
    我们新增一个实例叫做 car2,并给car2.components 追加一个新元素。打印car1,发现 car1.components 也发生了变化。这就是所谓多个实例对引用类型的操作会被篡改。

const car2 = new Car(8);car2.components.push('灯具');car2.components; // ['座椅', '轮子', '灯具']
car1.components; // ['座椅', '轮子', '灯具']
  • 缺点 2 :原型链继承导致 Car.prototype.constructor被重写,它指向的是 Vehicle 而非 Car。因此你需要手动将 Car.prototype.constructor 指回Car
Car.prototype = new Vehicle();
Car.prototype.constructor === Vehicle; // true// 重写 Car.prototype 中的 constructor 属性,指向自己的构造函数 Car
Car.prototype.constructor = Car;
  • 缺点 3:因为 Car.prototype = new Vehicle();重写了 Car 的原型对象,所以导致playMusic方法被覆盖掉了,因此给子类添加原型方法必须在替换原型之后。
function Car(wheelNumber) {this.wheelNumber = wheelNumber;
}Car.prototype = new Vehicle();// 给子类添加原型方法必须在替换原型之后
Car.prototype.playMusic = function() {console.log('sing~');
};
  • 缺点 4::显然,创建 car 实例时无法向父类的构造函数传参,也就是无法初始化powerSource属性。
const car = new Car(4);// 只能创建实例之后再修改父类的属性
car.powerSource = '汽油';

借用构造函数继承

该方法又叫 伪造对象 或 经典继承。它的实质是 在创建子类实例时调用父类的构造函数。

function Vehicle(powerSource) {this.powerSource = powerSource;this.components = ['座椅', '轮子'];
}Vehicle.prototype.run = function() {console.log('running~');
};function Car(wheelNumber) {this.wheelNumber = wheelNumber;// 继承父类属性并且可以传参Vehicle.call(this, '汽油');
}Car.prototype.playMusic = function() {console.log('sing~');
};const car = new Car(4);

使用经典继承的好处是可以给父类传参,并且该方法不会重写子类的原型,故也不会损坏子类的原型方法。此外,由于每个实例都会将父类中的属性复制一份,所以也不会发生多个实例篡改引用类型的问题(因为父类的实例属性不在原型中了)。

然而缺点也是显而易见的,我们丝毫找不到run方法的影子,这是因为该方式只能继承父类的实例属性和方法,不能继承原型上的属性和方法。

为了将公有方法放到所有实例都能访问到的地方,我们一般将它们放到构造函数的原型中。而如果让 借用构造函数继承 运作下去,显然需要将 公有方法
写在构造函数里而非其原型,这在创建多个实例时势必造成浪费。

组合继承

组合继承吸收上面两种方式的优点,它使用原型链实现对原型方法的继承,并借用构造函数来实现对实例属性的继承。

function Vehicle(powerSource) {this.powerSource = powerSource;this.components = ['座椅', '轮子'];
}Vehicle.prototype.run = function() {console.log('running~');
};function Car(wheelNumber) {this.wheelNumber = wheelNumber;Vehicle.call(this, '汽油'); // 第二次调用父类
}Car.prototype = new Vehicle(); // 第一次调用父类// 修正构造函数的指向
Car.prototype.constructor = Car;Car.prototype.playMusic = function() {console.log('sing~');
};const car = new Car(4);

虽然该方式能够成功继承到父类的属性和方法,但它却调用了两次父类。第一次调用父类的构造函数时,Car.prototype会得到 powerSourcecomponents两个属性;当调用Car构造函数生成实例时,又会调用一次 Vehicle 构造函数,此时会在这个实例上创建 powerSourcecomponents。根据原型链的规则,实例上的这两个属性会屏蔽原型链上的两个同名属性。

原型式继承

该方式通过借助原型,基于已有对象创建新的对象。

首先创建一个名为object 的函数,然后在里面中创建一个空的函数 F,并将该函数的 prototype指向传入的对象,最后返回该函数的实例。本质来讲,object()对传入的对象做了一次 浅拷贝。

function object(proto) {function F() {}F.prototype = proto;return new F();
}const cat = {name: 'Lolita',friends: ['Yancey', 'Sayaka', 'Mitsuha'],say() {console.log(this.name);},
};const cat1 = object(cat);

虽然这种方式很简洁,但仍然有一些问题。因为 原型式继承 相当于 浅拷贝,所以会导致 引用类型 被多个实例篡改。下面这个例子中,我们给 cat1.friends 追加一个元素,却导致 cat.friends 被篡改了。

cat1.friends.push('Hachi');cat.friends; // ['Yancey', 'Sayaka', 'Mitsuha', 'Hachi']

寄生式继承

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

const cat = {name: 'Lolita',friends: ['Yancey', 'Sayaka', 'Mitsuha'],say() {console.log(this.name);},
};function createAnother(original) {const clone = Object.create(original); // 获取源对象的副本clone.gender = 'female';clone.fly = function() {// 增强这个对象console.log('I can fly.');};return clone; // 返回这个对象
}const cat1 = createAnother(cat);

和 原型式继承 一样,该方式会导致 引用类型 被多个实例篡改,此外,fly 方法存在于 实例 而非 原型 中,因此 函数复用 无从谈起。

寄生组合式继承

上面我们谈到了 组合继承,它的缺点是会调用两次父类,因此父类的实例属性会在子类的实例和其原型上各自创建一份,这会导致实例属性屏蔽原型链上的同名属性。
好在我们有 寄生组合式继承,它本质上是通过 寄生式继承 来继承父类的原型,然后再将结果指定给子类的原型。这可以说是在 ES6 之前最好的继承方式了,面试写它没跑了。

function inheritPrototype(child, parent) {const prototype = Object.create(parent.prototype); // 创建父类原型的副本prototype.constructor = child; // 将副本的构造函数指向子类child.prototype = prototype; // 将该副本赋值给子类的原型
}

然后我们尝试写一个例子。

function Vehicle(powerSource) {this.powerSource = powerSource;this.components = ['座椅', '轮子'];
}Vehicle.prototype.run = function() {console.log('running~');
};function Car(wheelNumber) {this.wheelNumber = wheelNumber;Vehicle.call(this, '汽油');
}inheritPrototype(Car, Vehicle);Car.prototype.playMusic = function() {console.log('sing~');
};

它只调用了一次父类,因此避免了在子类的原型上创建多余的属性,并且原型链结构还能保持不变。

ES6 继承

功利主义来讲,在 ES6 新增 class 语法之后,上述几种方法已沦为面试专用。当然class 仅仅是一个语法糖,它的核心思想仍然是 寄生组合式继承,下面我们看一看怎样用 ES6 的语法实现一个继承。

class Vehicle {constructor(powerSource) {// 用 Object.assign() 会更加简洁Object.assign( this,{ powerSource, components: ['座椅', '轮子'] },// 当然你完全可以用传统的方式// this.powerSource = powerSource;// this.components = ['座椅', '轮子'];);}run() {console.log('running~');}
}class Car extends Vehicle {constructor(powerSource, wheelNumber) {// 只有 super 方法才能调用父类实例super(powerSource, wheelNumber);this.wheelNumber = wheelNumber;}playMusic() {console.log('sing~');}
}const car = new Car('核动力', 3);

JavaScript七大继承解析相关推荐

  1. JavaScript 七大继承全解析

    继承作为基本功和面试必考点,必须要熟练掌握才行.小公司可能仅让你手写继承(一般写 寄生组合式继承 即可),大厂就得要求你全面分析各个继承的优缺点了.这篇文章深入浅出,让你全面了解 JavaScript ...

  2. 一文梳理JavaScript中常见的七大继承方案

    阐述JavaScript中常见的七大继承方案

  3. 深入解析JavaScript 原型继承

    JavaScript 原型继承,学习js面向对象的朋友可以看看.十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不足之处,欢迎批评指正. Object.prototype Ja ...

  4. JavaScript实现继承的方式,不正确的是:

    JavaScript实现继承的方式,不正确的是:DA.原型链继承 B.构造函数继承 C.组合继承 D.关联继承 解析 javaScript实现继承共6种方式: 原型链继承.借用构造函数继承.组合继承. ...

  5. JavaScript重难点解析5(对象高级、浏览器内核与事件循环模型(js异步机制))

    JavaScript重难点解析5(对象高级.浏览器内核与事件循环模型(js异步机制) 对象高级 对象创建模式 Object构造函数模式 对象字面量模式 工厂模式 自定义构造函数模式 构造函数+原型的组 ...

  6. 白话解释 Javascript 原型继承(prototype inheritance)

    来源: 个人博客 白话解释 Javascript 原型继承(prototype inheritance) 什么是继承? 学过"面向对象"的同学们是否还记得,老师整天挂在嘴边的面向对 ...

  7. JavaScript面向对象--继承 (超简单易懂,小白专属)...

    JavaScript面向对象--继承 (超简单易懂,小白专属) 一.继承的概念 子类共享父类的数据和方法的行为,就叫继承. 二.E55如何实现继承?探索JavaScript继承的本质 2.1构造函数之 ...

  8. JavaScript重难点解析6(Promise)

    JavaScript重难点解析6(Promise 概念 为什么要使用Promise Promise 的状态 Promise 对象的值 Promise工作流程 基本用法 Promise其他方法 asyn ...

  9. JavaScript重难点解析4(作用域与作用域链、闭包详解)

    JavaScript重难点解析4(作用域与作用域链.闭包详解) 作用域与作用域链 作用域 作用域与执行上下文 作用域链 闭包 闭包理解 将函数作为另一个函数的返回值 将函数作为实参传递给另一个函数调用 ...

最新文章

  1. Aooms_基于SpringCloud的微服务基础开发平台实战_002_工程构建
  2. 100本名著浓缩成了100句话
  3. 甜、酸、苦、辣、咸与健康
  4. java泛型中<?>和<T>有什么区别?
  5. MS UC 2013-2-Deploy Microsoft Exchange Server 2013-4-Post-Installation Tasks
  6. python如果想测试变量的类型、可以使用_python里测试变量类型用什么
  7. Asp.net MVC 3 Framework: SportsStore源码
  8. 如何用纯 CSS 创作背景色块变换的按钮特效
  9. mysql之delete删除记录后数据库大小不变
  10. Machine Learning---LMS 算法
  11. 虚拟现实果真来了吗?
  12. 从零开始开发标准的s57电子海图第三篇--ECDIS标准(共一百篇)
  13. 测试用例之黑盒测试方法
  14. c语言 数据结构面试题及答案,数据结构c语言版试题大全(含答案).docx
  15. 浅谈一下前后端分离(什么是前后端分离以及前后端分离的原理)
  16. 昆冶金计算机高考录取分数线,昆明冶金高等专科学校2020年录取分数线(附2018-2020年分数线)...
  17. 1987年,国际C语言混乱代码大赛
  18. Ajax库-认识服务器,URL地址,axios基本用法,响应状态码,业务状态码,接口测试工具
  19. CCCP(convex-concave procedure)优化算法的一些理解
  20. yolov3原理+训练损失

热门文章

  1. 迈普光彩北区销售部签订辽宁某会议室55寸LCD液晶拼接显示屏项目
  2. php相机拍照太大,光比太大,拍照片不是曝光不足就是过曝,一招帮你解决
  3. arrayQualityMetrics包常用函数
  4. Unity/C# Socket框架学习遇到的相关方法
  5. makop勒索病毒|勒索病毒解密|勒索病毒恢复|数据库修复
  6. 图神经网络时间序列预测,时间序列神经网络预测
  7. 用java自己实现代码阻塞的几种方式
  8. 树叶贴画机器人_学生手工论文,关于对学前教育手工课教学相关参考文献资料-免费论文范文...
  9. Java JDK8新特性Stream流
  10. 抓包工具哪家强(暴力窃取前戏)