深入理解JavaScript之实现继承的7种方式
1.原型链继承
核心:将父类的实例作为子类的原型
首先,要知道构造函数、原型和实例之间的关系:构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
function Father(){ this.name = '父类的名字'; } Father.prototype.getFatherName = function(){ console.log('父类的方法'); } function Son(){ this.name = '子类的名字'; } // 如果此时有Son的原型对象有方法或属性,下面Son.prototype = new Father(),由于原型重定向,原型上的方法和属性会丢失 Son.prototype.getAge = function(){ console.log('子类的年龄') } Son.prototype = new Father(); // 核心:创建父类的实例,并将该实例赋值给子类的prototype Son.prototype.getSonName = function(){ console.log('子类的方法'); } var son = new Son(); son.getFatherName(); // 父类的方法 Son.prototype.__proto__.getFatherName = function(){ // 缺点:如果有多个实例对其父类原型,则会互相影响 console.log('子类改变父类的方法'); } son.getFatherName(); // 子类改变父类的方法
缺点:
父类使用this声明的属性(私有属性和公有属性)被所有实例共享,在多个实例之间对引用类型数据操作会互相影响。
创建子类实例时,无法向父类构造函数传参。
2.借用构造函数继承(call)
核心:使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类
function Father(name, age){ this.name = name; this.age = age; } Father.prototype.getFatherName = function(){ console.log('父类的方法'); } function Son(name, age, job){ Father.call(this,name, age); // 继承自Father this.job = job; } , '前端开发'); //son.getFatherName(); // Uncaught TypeError: son.getFatherName is not a function
优点:
- 可以向父类传递参数,而且解决了原型链继承中:父类属性使用this声明的属性会在所有实例共享的问题。
缺点:
- 只能继承父类通过this声明的属性/方法,不能继承父类prototype上的属性/方法。
- 每次子类实例化都要执行父类函数,重新声明父类this里所定义的方法,因此父类方法无法复用。
3.组合继承
核心:组合上述两种方法,用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
function Father(name, age){ this.name = name; this.age = age; this.sex = 'man'; } Father.prototype.getFatherName = function(){ console.log('父类的方法') } function Son(name, age, job){ Father.call(this,name,age); // 第二次调用:创建子类型实例的时候 this.job = job; } Son.prototype = new Father(); // 第一次调用:设置子类型实例的原型的时候 Son.prototype.constructor = Son; // prototype构造器指回自己 , '前端开发'); son.getFatherName(); console.log(son)
优点:
- 可以继承父类原型上的属性,可以传参,可复用。
- 每个新子类对象实例引入的构造函数属性是私有的。
缺点:
- 两次调用父类函数(new fatherFn()和fatherFn.call(this)),造成一定的性能损耗。
- 在使用子类创建实例对象时,其原型中会存在两份相同属性/方法的问题。
拓展:
constructor的作用
返回创建实例对象的Object构造函数的引用。
当我们只有实例对象没有构造函数的引用时: 某些场景下,我们对实例对象经过多轮导入导出,我们不知道实例是从哪个函数中构造出来或者追踪实例的构造函数,较为艰难。(它主要防止一种情况下出错,就是你显式地去使用构造函数。比如,我并不知道instance是由哪个函数实例化出来的,但是我想clone一个,这时就可以这样——>instance.constructor) 这个时候就可以通过实例对象的constructor属性来得到构造函数的引用
let instance = new sonFn() // 实例化子类 export instance; // 多轮导入+导出,导致sonFn追踪非常麻烦,或者不想在文件中再引入sonFn let fn = instance.constructor
因此每次重写函数的prototype都应该修正一下constructor的指向,以保持读取constructor指向的一致性
4.原型式继承(Object.create())
核心:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。
/* Object.create() 的实现原理 */ // cloneObject()对传入其中的对象执行了一次浅拷贝,将构造函数F的原型直接指向传入的对象。 function cloneObject(obj){ function F(){} F.prototype = obj; // 将传进来obj对象作为空函数的prototype return new F(); // 此对象的原型为被继承的对象, 通过原型链查找可以拿到被继承对象的属性 } var father = { name: 'jacky', age: , courses: ['前端'] } // var son1 = Object.create(father); // 效果一样 var son1 = cloneObject(father); son1.courses.push('后端'); var son2 = cloneObject(father); son2.courses.push('全栈'); console.log(father.courses); // ["前端", "后端", "全栈"]
优点:
从已有对象衍生新对象,不需要创建自定义类型
缺点:
与原型链继承一样。多个实例共享被继承对象的属性,存在篡改的可能;也无法传参。
5.寄生式继承
核心:在原型式继承的基础上,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象(增加了一些新的方法和属性),最后返回对象。
使用场景:专门为对象来做某种固定方式的增强。
function createAnother(obj){ var clone = Object.create(obj); clone.skill = function(){ // 以某种方式来增强这个对象 console.log('run'); }; return clone; } var animal = { eat: 'food', drink: 'water' } var dog = createAnother(animal); dog.skill();
优点:没有创建自定义类型,因为只是套了个壳子增加特定属性/方法返回对象,以达到增强对象的目的
缺点:
同原型式继承:原型链继承多个实例的引用类型属性指向相同,存在篡改的可能,也无法传递参数
6.寄生组合式继承
核心:结合借用构造函数传递参数和寄生模式实现继承
- 通过借用构造函数(call)来继承父类this声明的属性/方法
- 通过原型链来继承方法
function Father(name, age){ this.name = name; this.age = age; } Father.prototype.getFatherName = function(){ console.log('父类的方法') } function Son(name, age, job){ Father.call(this,name,age); // 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上 this.job = job; } // 寄生式继承:封装了son.prototype对象原型式继承father.prototype的过程,并且增强了传入的对象。 function inheritPrototype(son,father){ var clone = Object.create(father.prototype); // 原型式继承:浅拷贝father.prototype对象 clone.constructor = son; // 增强对象,弥补因重写原型而失去的默认的constructor 属性 son.prototype = clone; // 指定对象,将新创建的对象赋值给子类的原型 } inheritPrototype(Son,Father); // 将父类原型指向子类 // 新增子类原型属性 Son.prototype.getSonName = function(){ console.log('子类的方法') } ,'前端开发'); console.log(son);
- 寄生组合式继承相对于组合继承有如下优点:
- 只调用一次父类Father构造函数。不必为了指定子类的原型而调用构造函数,而是间接的让 Son.prototype 访问到 Father.prototype。
- 避免在子类prototype上创建不必要多余的属性。 使用原型式继承父类的prototype,保持了原型链上下文不变, instanceof 和isPrototypeOf()也能正常使用。 3.寄生组合式继承是最成熟的继承方法, 也是现在最常用的继承方法,众多JS库采用的继承方案也是它。
缺点:
硬要说的话,就是给子类原型添加属性和方法的时候,一定要放在inheritPrototype()方法之后
7.ES6 extends继承(最优方式)
核心: 类之间通过extends关键字实现继承,清晰方便。 class 仅仅是一个语法糖,它的核心思想仍然是寄生组合式继承。
class Father { constructor(name, age) { this.name = name; this.age = age; } skill() { console.log('父类的技能'); } } class Son extends Father { constructor(name, age, job){ super(name, age); // 调用父类的constructor,只有调用super之后,才可以使用this关键字 this.job = job; } getInfo() { console.log(this.name, this.age, this.job); } } let son = ,'前端开发'); son.skill(); // 父类的技能 son.getInfo(); // jacky 22 前端开发
- 如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
ES5继承与ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,再将父类的方法添加到this上( Father.call(this) )。
- ES6的继承是先创建父类的实例对象this,再用子类的构造函数修改this。
- 因为子类没有自己的this对象,所以必须先调用父类的super()方法。
内容转自掘金社区JackySummer,原链接 https://juejin.im/post/5dd55918e51d4536db238a19
深入理解JavaScript之实现继承的7种方式的更多相关文章
- javascript中实现继承的几种方式
javascript中实现继承的几种方式 1.借用构造函数实现继承 function Parent1(){ this.name = "parent1" } function Chi ...
- javascript 面向对象(实现继承的几种方式)
1.原型链继承 核心: 将父类的实例作为子类的原型 缺点: 父类新增原型方法/原型属性,子类都能访问到,父类一变其它的都变了 function Person (name) { this.name ...
- 前端知识体系:JavaScript基础-原型和原型链-实现继承的几种方式以及他们的优缺点
实现继承的几种方式以及他们的优缺点(参考文档1.参考文档2.参考文档3) 要搞懂JS继承,我们首先要理解原型链:每一个实例对象都有一个__proto__属性(隐式原型),在js内部用来查找原型链:每一 ...
- javascript(js)创建对象的模式与继承的几种方式
1.js创建对象的几种方式 工厂模式 为什么会产生工厂模式,原因是使用同一个接口创建很多对象,会产生大量的重复代码,为了解决这个问题,产生了工厂模式. function createPerson(na ...
- js 实现继承的6种方式(逐渐优化)
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...
- js实现继承的5种方式 (笔记)
js实现继承的5种方式 以下 均为 ES5 的写法: js是门灵活的语言,实现一种功能往往有多种做法,ECMAScript没有明确的继承机制,而是通过模仿实现的,根据js语言的本身的特性,js实现继承 ...
- [转]javascript指定事件处理程序包括三种方式:
javascript指定事件处理程序包括三种方式: (1):DOM0级事件处理程序 如: 代码如下: var btn=document.getElementById("mybtn" ...
- JavaScript对象属性访问的两种方式
JavaScript对象属性访问的两种方式 object.attribute object["attribute"] 例如: var employees = [ { "f ...
- js 实现继承的几种方式
//js中实现继承的几种方式 //实现继承首先要有一个父类,先创造一个动物的父类 function Animal(name){ this.name = name; this.shoot = funct ...
随机推荐
- 用pytorch1.0快速搭建简单的神经网络
用pytorch1.0搭建简单的神经网络 import torch import torch.nn.functional as F # 包含激励函数 # 建立神经网络 # 先定义所有的层属性(__in ...
- CMakeLists 添加 -pthread 编译选项 undefined reference to pthread_atfork
在与 main() 函数同级的 CMakeLists 中添加如下内容(根据项目实际情况修改): cmake_minimum_required (VERSION 2.6) find_package (T ...
- Linux 实现回收站功能脚本
#!/bin/bash function z-trash() { # 判断参数是否为空 if [ ! $1 ] then echo "z-trash error: file name of ...
- Python【变量和赋值】
name = '千变万化' #把“千变万化”赋值给了“name”这个[变量] >>> name = '一'>>> name = '二'>>> pr ...
- AVR单片机教程——点亮第一个LED
做了这么多准备,我们终于可以开始用开发板做点事了. 单片机编程与计算机编程有一些不同点.程序都要有零个或多个输入.一个或多个输出,这是两者都有的,但是计算机编程的输入输出主要靠控制台,而单片机没有. ...
- (转)高效线程池之无锁化实现(Linux C)
本文链接:https://blog.csdn.net/xhjcehust/article/details/45844901 笔者之前照着通用写法练手写过一个小的线程池版本,最近几天复习了一下,发现大多 ...
- JMeter-03-元件的作用域与执行顺序
JMeter元件的作用域与执行顺序 元件的作用域 先来讨论一下元件有作用域.<JMeter基础元件介绍>一节中,我们介绍了8类可被执行的元件(测试计划与线程组不属于元件),这些元件中,取样 ...
- 升级win10 1903版后,vmware打开虚拟机黑屏的解决办法
按照网上给的方法(1-3),又增加了几步(从4开始,只在我自己电脑上实践过): 1. 打开cmd,执行以下命令 netsh winsock reset 2. 重启电脑 3. 以管理员身份执行vmwar ...
- TextBox 显示横线
public class Xtxt3 : TextBox { private bool m_underLine; public bool UnderLine { get { return m_unde ...
- 【转载】 C#中PadRight函数以特定字符在字符串结尾补足位数
在C#开发过程中字符串String类处理过程中,有时字符串长度不够时,需要在右侧侧指定特定的字符来补足字符串长度,此时可以使用String类下的PadRight方法对字符串结尾按特定的字符补足位数.M ...