概述

在JavaScript中,存在六种继承方式,分别是基本继承(即前文js基础之原型链提到的最原始的方式,暂未查到官方称谓)、借用构造函数、组合继承、原型式继承、寄生式继承和寄生式组合继承。其中组合继承是基本继承和借用构造函数的组合版本,也是最常用的继承;原型式继承是基本继承的“干净”版本(只继承原型);寄生式继承是原型式继承的工厂化版本;寄生组合式继承是组合继承融入原型式继承思想后的优化版本,也是目前认为最理想的继承方式。它们的关系如下图:

下面我们就依次来介绍这六种继承方式。

1. 基本继承

该继承方式的实现原理主要是原型链那篇文章中讲到的基于原型链的继承,简单回顾一下:

function Child(){}
function Parent(){}
Parent.prototype.run = function(){}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

通过将Parent的一个实例直接作为子类Child的原型对象,所有Child的实例都拥有一个内部指针__proto__指向这个Parent实例。借助浏览器的原型链查找机制,当调用的某个方法或属性在Child实例中不存在时,执行引擎就会去Child的原型对象(即上述Parent实例)中查找,如果仍然找不到,就会继续向上,到Parent的原型对象中查找。由于在JavaScript中,所有的对象都继承自Object,因此该查找过程会一直持续到Object的原型对象,如果仍然没有找到,就会报属性或方法不存在。

前文也提到了该继承方式的两大弊端,一个是原型对象中所有引用类型的值都会被子类共享,如(承接上面的代码):

//父类原型对象上新增一个引用类型的数据
Parent.prototype.color = ['black', 'green'];
//构造两个子类对象
var child1 = new Child();
var child2 = new Child();
//通过child1向上述属性中添加一个元素
child1.color.push('white');console.log(child2.color);  //['black', 'green', 'white']

通过上述代码可以看出,我们通过child1修改了Parent原型中的color,但是此时child2引用到的color值也发生了变化(除非真的要共享该属性,否则这种行为是无法接受的)。这其中的原因也很简单,我们通过上述方式实现的继承,并没有把父类的方法和属性复制到子类实例中,而是借助原型链的查找机制上溯查找到的,因此在内存中,color属性只在Parent的原型中存在一份,显然对它的操作会影响所有引用它的实例(如果修改的是基本数据类型的属性,则会在子类实例上创建该属性(因为基本数据类型的值是无法改变的,参考
V8引擎的内存管理分析),因此不会影响其他实例)。

另一个弊端就是无法在构造子类的时候向父类的构造函数动态传值。比如上面的代码中,如果我们执行:

var child3 = new Child('夕山雨');

那么很显然,“夕山雨”这个参数将用于构造子类实例,是无法传递给Parent构造函数的,我们只能在实现继承的时候,向父类构造函数传入固定的参数,即:

Child.prototype = new Parent("夕山雨");

这样所有Child的实例的原型对象值都是完全一样的,不能满足我们需要动态向父类构造函数传值的实际需要。

由于存在上述两个弊端,这种继承方式几乎很少单独使用(但当父类只是定义了一些方法时,仍然可以使用该方式,因为我们不会去修改方法本身,也不需要在构造父类时对方法赋值)。基本继承的原理图可以在前文js基础之原型链中找到。

下面就来看第二种继承方式 - 借用构造函数,它以另外一种思路来解决上述两个问题。

2. 借用构造函数

顾名思义,借用构造函数就是去借用别人的构造函数来构造自己的实例对象。在JavaScript中,构造函数实际上是定义了一组构造对象的规则,我们使用该规则,根据传入的参数不同,批量地构造大同小异的实例对象。如:

function Parent(name, age){this.name = name;this.age = age;...
}

我们现在定义的规则就是,如果你传给我name和age两个参数,我就在this对象上添加name和age属性来接收这两个参数(该过程就是在构造this对象)。调用这组规则有两种方式:

  1. 如果你使用new来调用该函数,那么浏览器就先创建一个空对象,让this指向这个空对象,再去构造这个空对象。
  2. 如果你以对象为调用者调用Parent,那么它不会自动生成空对象,而是以调用者作为this,去构造这个调用者。

对于第二种情况,当我们使用对象调用一个函数时,其内部的this指的是调用该函数的对象。如果该函数没有被任何对象显式地调用,那么在浏览器中,它就被认为是被全局对象window调用的。

//此时浏览器会先创建一个空对象,然后将函数内的this指向这个空对象,并构造它
var parent = new Parent("夕山雨", 24);
//现在我们在window上调用Parent,name和age属性将被添加到window上,
//于是变成了全局变量
Parent("夕山雨", 24);
window.age === 24;  //true

因此一个构造函数被用来构造哪个对象,取决于其内部的this指向哪个对象。而这正是借用构造函数的基本思路。比如我们写出下面的代码:

var parent = {};
Parent.call(parent, "夕山雨", 24);

我们手动创建了一个空对象,然后调用Parent函数,并使用call将函数的this手动绑定到该空对象上,这时构造函数Parent将会把构造规则应用在该空对象上,构造出一个与使用new完全一致的Parent实例。

那么我们想一下,既然构造函数可以用来构造空对象,为什么不能用来构造子类实例呢?比如:

function Child(name, age){Parent.call(this, name, age);
}
var child = new Child("夕山雨", 24);

神奇的情况出现了!我们“借用”了Parent构造函数作为构造规则,来构造Child实例(使用new调用Child时,内部的this就是一个Child实例)。现在我们在Parent构造函数中定义的所有属性和方法都会给当前Child实例重新定义一遍,从而实现了继承。

该方式刚好解决了第一种继承方式的两大弊端。其一,引用类型属性的共享。现在我们每个子类的属性都只是我们借用父类的构造函数新定义的,因此相互独立,不存在共享导致的冲突。其二,无法向父类构造函数动态传值。显然,我们现在向Child传入的参数又被传给了父类构造函数,因此该问题也得到了解决。

但这种继承方式的缺点却更为明显,父类的所有属性和方法都需要重新构造一遍。虽然这样对引用类型的属性来说,可以解决共享导致的冲突,却也导致方法无法被共享,每个子类都需要维护一个相同的方法,失去了继承的本质。因此单独使用该继承方式的频率就更低了。

现在让我们回顾以上两种继承方式,我们发现,基本继承成功实现了方法的共享,但却导致了不应被共享的引用属性的共享;而借用构造函数成功解决了引用属性共享的问题,却导致了方法无法被共享。

按照惯例,我们是不是该融合两者的优点,创造一种更好的继承方式了?

3. 组合继承

和它的名字一样直白,组合继承就是组合使用上述两种继承方式,来实现方法的共享和属性的独立构造。使用该继承方式时我们有一个通用习惯,就是把属性放在父类的构造函数中,而把方法放在父类的原型上。首先,我们仍然使用借用构造函数来进行属性的构造:

function Child(name, age){Parent.call(this, name, age);
}

现在每次调用该构造函数,都会先借用父类的构造函数来构造子类,创建独立的子类属性。然后,我们再借助基本继承实现方法的继承:

Child.prototype = new Parent();
//修正Child.constructor的指向
Child.constructor = Child;

现在我们就实现了组合继承。

由于我们把属性放在父类的构造函数中,借用构造函数会以父类构造函数为规则,为子类实例创建这些属性。然后通过将Child.prototype指向父类的对象,我们又借助原型链实现了对父类方法的继承。该继承方式完全解决了上述两种继承方式带来的问题,也是目前最为常用的一种继承方式。

当然了,该继承方式并非完美的,实际上该继承方式需要调用父类构造函数两次,如下:

function Child(name, age){//第二次父类构造函数,来构造子类对象(因为该函数是在执行new时才调用的,//因此调用次序靠后)Parent.call(this, name, age);
}
//第一次调用构造函数,目的是借助该父类实例继承其原型
Child.prototype = new Parent();  //我们将这个对象暂即为temp
//修正Child的constructor指向
Child.constructor = Child;var child = new Child("夕山雨", 24);

作为子类原型对象的那个temp父类实例拥有父类所有的属性(以及可能在构造函数中定义的方法)。但是当我们借用父类构造函数构造子类后,子类实例上同样拥有所有的父类属性。根据原型链查找规则,子类自身的属性优先级高于原型上的同名属性,结果就是我们访问到的永远是子类的属性,原型上的属性被“屏蔽”了。既然这些属性根本访问不到,那为什么要浪费时间和内存去构造它呢?这就是组合继承的问题所在。为了解决这个问题,又衍生出下面的继承方式。

4. 原型式继承

看到这个名字,我们可能想到,基本继承不也是一种原型继承吗?没错,原型式继承的思想就来自于基本继承,但它是一种更“干净”的实现。它的“干净”之处在于,它只继承父类的原型,而不继承父类构造函数中的属性(及可能存在的方法)。看下面的例子:

function createObject( prototype ){//创建一个空构造函数function F(){};//将该构造函数的原型替换为传入的原型对象F.prototype = prototype;//创建并返回一个空对象,但该对象的__proto__指向传入的prototypereturn new F();
}
//child是个空对象,但可以借助原型链访问Parent的原型属性和方法
var child = createObject( Parent.prototype );

上面的方式同样实现了继承,但并不是完整的继承,而是只继承父类的原型对象,丢弃了父类构造函数中的所有属性和方法。该继承方式在ES5中得到了原生实现,即Object.create()方法,你可以传入一个原型对象进去,构造一个以该原型为原型的空对象,这样在某些轻量级的继承场景中是十分便捷的。如:

var child = Object.create( Parent.prototype );
child.name = "夕山雨";
child.age = 24;

不难发现,如果我们给Object.create传入一个父类的实例对象(即Object.create( new Parent() ) ),那么它就是基本继承(所以说它是基本继承更“干净”的版本)。

该继承方式显然有着自己的缺陷,但它却为解决组合继承的重复构造问题提供了思路。在介绍如何通过原型式继承来解决组合继承遇到的问题之前,我们再介绍另外一种继承方式 - 寄生式继承,它是原型式继承的工厂化版本。

5. 寄生式继承

在上面的原型式继承中,我们创建了一个空的子类对象之后,需要手动为其添加自有的属性和方法,如:

var child = Object.create( Parent.prototype );
child.name = "夕山雨";
child.age = 24;

上述三条语句都是用于构造子类对象的,但却是独立的,我们认为这样耦合性较差,封装程度不够,因此我们通常会将其封装为一个函数:

function createChild( Parent ){var child = Object.create( Parent.prototype );child.name = "夕山雨";child.age = 24;
}var child = createChild( Parent );

当我们定义了上述函数之后,每次构造一个子类对象,只需要写下面的一行语句即可,代码看上去也优雅了很多,这就是所谓的寄生式继承。所以,寄生式继承就是原型式继承的一个工厂化(将一系列流程封装在一起,进行快速批量生产)版本。

显然寄生式继承并不是为了解决原型式继承的问题而存在的。接下来我们就来了解一种更加优雅的继承方式 - 寄生式组合继承。

6. 寄生组合式继承

该继承方式的主要思路就是,用原型式继承替换组合继承中的基本继承。

Child.prototype = new Parent();  //该父类实例记为temp

这里生成的中间父类对象temp是一个完整的父类对象,但是由于子类仍会调用一次父类的构造函数,生成同名的属性和方法。这样在访问这些属性和方法时,实际上都来自于子类自身,而temp中的则不会被访问(除非通过delete删除了子类的同名属性)。那么我们何不在这里通过原型式继承创建一个“干净”的对象作为子类的原型呢?代码如下:

Child.prototype = Object.create( Parent.prototype );

这里Object.create创建的是一个空对象,但是以Parent的原型为原型,我们将这个对象作为Child的prototype,就避免了在这里调用父类的构造函数,因此组合继承的性能问题也得到了解决。下面是完整的寄生组合式继承的实现过程:

function Parent(name, age){this.name = name;this.age = age;
}
Parent.prototype.run = function(){ ... };function Child(name, age){//第二次父类构造函数,来构造子类对象(因为该函数是在执行new时才调用的,//因此调用次序靠后)Parent.call(this, name, age);
}
//将Child.prototype替换为一个继承了父类原型的空对象
Child.prototype = Object.create( Parent.prototype );
//修正Child的constructor指向
Child.prototype.constructor = Child;var child = new Child("夕山雨", 24);

目前,寄生组合式继承被认为是JavaScript实现继承的一种比较理想的方式。

总结

介绍完所有的继承方式,我们可以重新梳理一下它们之前的关系。这六种继承方式中,基本继承和借用构造函数可以认为是最基础的版本,前者借助原型链实现方法共享,后者借用构造函数实现属性的独立构造,结合两者的优势衍生出组合继承。原型式继承是基本继承的“干净”版本,将其工厂化就是寄生式继承,而将它与借用构造函数结合就得到了理想的继承实现 - 寄生组合式继承。

现在我们来思考一下,为什么React中组件在继承时需要在子组件的构造函数中写super(props)?很简单,就是将借用构造函数的过程封装在super函数中,通过传入参数,以父类定义的构造规则去构造子类对象。而ES6的extends关键字,其实就是原型式继承的语法糖。如:

//React的继承
class Child extends Parent{constructor(name, age){super(name, age);}
}
//寄生组合式继承
Child.prototype = Object.create( Parent.prototype );
function Child(name, age){Parent.call(name, age);
}

浏览器在遇到extends关键字时,会以原型式继承的方式为子类的prototype赋值,然后借用父类构造函数构造子类。因此这种继承方式虽然看上去与java的继承方式类似,但实现原理却完全不同。

js基础之六种继承方式相关推荐

  1. JavaScript六种继承方式的递进推演

    1. 原型链继承 function Parent1() {this.name = "Parent1"this.son = [1] } // 需要继承的子类 function Chi ...

  2. 理解JS的6种继承方式

    [转]重新理解JS的6种继承方式 写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面 ...

  3. JS基础 原型与继承

    阅读目录 原型基础 原型对象 使用数组原型对象的 concat 方法完成连接操作 默认情况下创建的对象都有原型. 以下 x.y 的原型都为元对象 Object,即JS中的根对象 创建一个极简对象(纯数 ...

  4. js的5种继承方式——前端面试

    js主要有以下几种继承方式:对象冒充,call()方法,apply()方法,原型链继承以及混合方式.下面就每种方法就代码讲解具体的继承是怎么实现的. 1.继承第一种方式:对象冒充 1 function ...

  5. JavaScript 常见的六种继承方式

    方式一.原型链继承 这种方式关键在于:子类型的原型为父类型的一个实例对象. -------------------------------------------------------------- ...

  6. js常见的的6种继承方式

    继承是面向对象的,继承可以帮助我们更好的复用以前的代码,缩短开发周期,提高开发效率:继承也常用在前端工程技术库的底层搭建上,在整个js的学习中尤为重要 常见的继承方式有以下的六种 一.原型链继承 原型 ...

  7. Js理解之路:Js常见的6中继承方式

    目录 一.JS 实现继承的几种方式 第一种:原型链继承 二.构造函数继承(借助call方法) 三.组合继承(原型链继承+构造函数继承) 第四种:原型式继承(借助Object.create) 第五种:寄 ...

  8. 探究JS常见的6种继承方式

    先看以下百科对(面向对象的继承)的解释! 通过以上精炼实用的解释,我们可以了解到继承的基本作用和功能!即可以使得子类具有父类的属性和方法或者重新定义.追加属性和方法等. 广告:帮忙点击>> ...

  9. js 怎么使一个absolute覆盖在父类上面_JS基础-完美掌握继承知识点

    前言 上篇文章详细解析了原型.原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行, ...

  10. 前端进击的巨人(七):走进面向对象,原型与原型链,继承方式

    "面向对象" 是以 "对象" 为中心的编程思想,它的思维方式是构造. "面向对象" 编程的三大特点:"封装.继承.多态" ...

最新文章

  1. MySQL 快速入门教程
  2. 比较正宗的验证邮箱的正则表达式js代码详解
  3. win7 安装mysql 5.7.9记录
  4. 后端技术:MyBatis 知识点整理,值得收藏!
  5. 4.Transfer Learning
  6. python中递归函数写法_Python之递归函数
  7. 重磅分享:一份关于车贷的政策性文件分享
  8. ssm项目中使用拦截器加上不生效解决方案
  9. Linux安装redis数据库
  10. SSO之CAS+LDAP实现单点登录认证
  11. Oracle grant connect, resource to user语句中的权限
  12. 如何爬取猫眼全部信息(电影信息、演员信息)
  13. 设计c语言程序,输出形状为直角三角形的九九乘法表,c语言题库(全国c语言二级考试题库)...
  14. Unity 实现人物移动
  15. 如何用excel筛选相似内容_如何excel中筛选两个表中相同的数据
  16. 基于ThreeJS的3D地球
  17. 计算机一级演示文稿知识点,计算机一级考试ppt演示文稿及上网题考点
  18. readxmls r语言_R语言批量爬取NCBI基因注释数据
  19. C语言结构体实现简单通讯录管理系统
  20. 激活MDI中已经打开过的文件

热门文章

  1. Hadoop HA HDFS启动错误之org.apache.hadoop.ipc.Client: Retrying connect to server问题解决
  2. 物联网空气质量监测系统
  3. Safari浏览器兼容性问题处理
  4. 批处理 文件注释_批处理文件注释
  5. Spark3.0核心调优参数小总结
  6. 计算机自动关机命令,怎么设置电脑自动关机的命令
  7. matlab求一维热传导方程数值解代码,一维热传导方程数值解法及matlab实现
  8. 高仿城通网盘php,PHP代码提取城通网盘直链跳过广告下载
  9. 摩托罗拉为什么要限制自家linux手机,很明显,这是一款配备Linux系统的智能手机,但摩托罗拉将其变成了功能机...
  10. Java精品项目源码第111期小蜜蜂扩音器网上商城系统