JavaScript七大继承解析
原型链继承
首先我们简单回忆一下构造函数、原型、原型链之间的关系:每个构造函数有一个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
会得到 powerSource
和components
两个属性;当调用Car
构造函数生成实例时,又会调用一次 Vehicle
构造函数,此时会在这个实例上创建 powerSource
和 components
。根据原型链的规则,实例上的这两个属性会屏蔽原型链上的两个同名属性。
原型式继承
该方式通过借助原型,基于已有对象创建新的对象。
首先创建一个名为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七大继承解析相关推荐
- JavaScript 七大继承全解析
继承作为基本功和面试必考点,必须要熟练掌握才行.小公司可能仅让你手写继承(一般写 寄生组合式继承 即可),大厂就得要求你全面分析各个继承的优缺点了.这篇文章深入浅出,让你全面了解 JavaScript ...
- 一文梳理JavaScript中常见的七大继承方案
阐述JavaScript中常见的七大继承方案
- 深入解析JavaScript 原型继承
JavaScript 原型继承,学习js面向对象的朋友可以看看.十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不足之处,欢迎批评指正. Object.prototype Ja ...
- JavaScript实现继承的方式,不正确的是:
JavaScript实现继承的方式,不正确的是:DA.原型链继承 B.构造函数继承 C.组合继承 D.关联继承 解析 javaScript实现继承共6种方式: 原型链继承.借用构造函数继承.组合继承. ...
- JavaScript重难点解析5(对象高级、浏览器内核与事件循环模型(js异步机制))
JavaScript重难点解析5(对象高级.浏览器内核与事件循环模型(js异步机制) 对象高级 对象创建模式 Object构造函数模式 对象字面量模式 工厂模式 自定义构造函数模式 构造函数+原型的组 ...
- 白话解释 Javascript 原型继承(prototype inheritance)
来源: 个人博客 白话解释 Javascript 原型继承(prototype inheritance) 什么是继承? 学过"面向对象"的同学们是否还记得,老师整天挂在嘴边的面向对 ...
- JavaScript面向对象--继承 (超简单易懂,小白专属)...
JavaScript面向对象--继承 (超简单易懂,小白专属) 一.继承的概念 子类共享父类的数据和方法的行为,就叫继承. 二.E55如何实现继承?探索JavaScript继承的本质 2.1构造函数之 ...
- JavaScript重难点解析6(Promise)
JavaScript重难点解析6(Promise 概念 为什么要使用Promise Promise 的状态 Promise 对象的值 Promise工作流程 基本用法 Promise其他方法 asyn ...
- JavaScript重难点解析4(作用域与作用域链、闭包详解)
JavaScript重难点解析4(作用域与作用域链.闭包详解) 作用域与作用域链 作用域 作用域与执行上下文 作用域链 闭包 闭包理解 将函数作为另一个函数的返回值 将函数作为实参传递给另一个函数调用 ...
最新文章
- Aooms_基于SpringCloud的微服务基础开发平台实战_002_工程构建
- 100本名著浓缩成了100句话
- 甜、酸、苦、辣、咸与健康
- java泛型中<?>和<T>有什么区别?
- MS UC 2013-2-Deploy Microsoft Exchange Server 2013-4-Post-Installation Tasks
- python如果想测试变量的类型、可以使用_python里测试变量类型用什么
- Asp.net MVC 3 Framework: SportsStore源码
- 如何用纯 CSS 创作背景色块变换的按钮特效
- mysql之delete删除记录后数据库大小不变
- Machine Learning---LMS 算法
- 虚拟现实果真来了吗?
- 从零开始开发标准的s57电子海图第三篇--ECDIS标准(共一百篇)
- 测试用例之黑盒测试方法
- c语言 数据结构面试题及答案,数据结构c语言版试题大全(含答案).docx
- 浅谈一下前后端分离(什么是前后端分离以及前后端分离的原理)
- 昆冶金计算机高考录取分数线,昆明冶金高等专科学校2020年录取分数线(附2018-2020年分数线)...
- 1987年,国际C语言混乱代码大赛
- Ajax库-认识服务器,URL地址,axios基本用法,响应状态码,业务状态码,接口测试工具
- CCCP(convex-concave procedure)优化算法的一些理解
- yolov3原理+训练损失
热门文章
- 迈普光彩北区销售部签订辽宁某会议室55寸LCD液晶拼接显示屏项目
- php相机拍照太大,光比太大,拍照片不是曝光不足就是过曝,一招帮你解决
- arrayQualityMetrics包常用函数
- Unity/C# Socket框架学习遇到的相关方法
- makop勒索病毒|勒索病毒解密|勒索病毒恢复|数据库修复
- 图神经网络时间序列预测,时间序列神经网络预测
- 用java自己实现代码阻塞的几种方式
- 树叶贴画机器人_学生手工论文,关于对学前教育手工课教学相关参考文献资料-免费论文范文...
- Java JDK8新特性Stream流
- 抓包工具哪家强(暴力窃取前戏)