引言

JS是一门面向对象的语言,但是在JS中没有引入类的概念,之前特别疑惑在JS中继承的机制到底是怎样的,一直学了JS的继承这块后才恍然大悟,遂记之。

假如现在有一个“人类”的构造函数:

function Human() {
this.type = '人类';
}

还有一个“男人”的构造函数:

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

现在我们想实现的是,让这个“男人”继承“人类”。

借用构造函数

我们可以通过在子类型的内部调用超类型的构造函数来达到子类型继承超类型的效果。函数是在特定环境中执行的代码对象,因此可以通过call()或者apply()在新创建的对象上执行构造函数。

        function Man(name, age) {
Human.apply(this,arguments);
this.name = name;
this.age = age;
} var man = new Man('shanlei',20);
console.log(man.type);
//输出:人类

在代码中的Human.apply(this,arguments)这一段代码借调用超类型的构造函数,通过apply函数(或call()函数),实际上是在(将要)新创建的Man实例的环境下调用超类型构造函数,这样就会在Man实例对象上执行Human()函数中定义的所有对象初始化代码,这样的话Man中每个实例就会都有自己的type属性的副本了。

不过借用构造函数进行继承,难免会有方法都在构造函数中定义,无法实现函数的复用。并且在超类型的原型中定义的方法对于子类型是不可见的,结果所有类型都要使用构造函数进行继承,所以单独使用构造函数的情况比较少。

通过原型链

原型链的定义机制我在JavaScript中原型链的那些事中已经提过了,说到底,通过原型链实现继承根本是通过prototype属性进行实现的。

如果Man的原型指向的是Human的实例,那么Man原型对象中将会包括一个指向Human的指针,那么所有Man的实例就都可以继承Human了。

        function Man(name,age) {
this.name = name;
this.age = age;
} Man.prototype = new Human();
Man.prototype.constructor = Man;
var man = new Man('shalei',20);
console.log(man.type);

我们知道其实prototype属性实质上就是一个指向函数对象的指针,我们通过改变prototype属性的指向,让他指向Human的一个实例。

Man.prototype.constructor = Man;

我们都知道任意一个原型对象都有一个constructor属性,constructor属性指向了它的构造函数,也就是说,如果没有改变Man的prototype的指向,那么Man.prototype.constructor是指向Man的。

更重要的是,每一个实例也有一个constructor属性,实例的constructor属性默认调用prototype的constructor,即:
console.log(man1.constructor == Man.prototype.constructor);
//输出:true

所以想一下当在执行完Man.prototype = new Human()后,所有的Man实例都指向了Human属性,即:

console.log(man1.constructor == Human);
//输出:true

这样的话会导致原型链的紊乱,也就是说原型链将会中断,所以我们必须手动纠正。

我们改变了Man的prototype的指向,让他等于一个Human的实例对象。即:

Man.prototype.constructor = Man;

这是非常重要的一步,如果我们在代码中更换了prototype对象,那么为了不破坏原型链,下一步必做的就是纠正prototype的constructor属性,让这个属性指回原来的构造函数。

组合继承(伪经典继承)

组合继承的整体思想就是将原型链和借用构造函数同时使用,取两者的长处的一种继承模式。思路是使用原型链实现原型属性和方法的继承,借用构造函数来实现对实例属性的继承。这样做的好处是实现了函数的复用,同时又保证了每个属性都有自己的属性。

那么上面让“男人”继承“人类”就可以通过组合继承实现:

        Human.prototype.go = function() {
console.log('running!');
} function Man(name, age) { Human.apply(this,arguments);
this.name = name;
this.age = age;
} Man.prototype = new Human();
Man.prototype.constructor = Man; var man1 = new Man('shanlei',20);
console.log(man1.type);
man1.type = 'man';
console.log(man1.type);
console.log(man1.name);
console.log(man1.age); var man2 = new Man('zhangkai',18);
console.log(man2.type);
console.log(man2.name);
console.log(man2.age);
man2.go();

输出如下:

人类
man
shanlei
20
人类
zhangkai
18
running!

原型式继承

如果说继承的对象并不是构造函数呢?我们没有办法使用借用构造函数进行继承,这个时候我们就可以使用原型式继承。

这个继承模式是由道格拉斯·克罗克福德提出的。原型式继承并没有使用严格意义上的构造函数。而是借助原型可以在已有的对象上创建新的对象,同时还避免了创建自定义类型。所以道格拉斯·克罗克福德给出了一个函数:

        function object(o){
function F(){}
F.prototype = o;
return new F();
}

我们可以创建一个新的临时性的对象来保存超类型上所有属性和方法,用来给子类型的继承。而这个就是这个函数要做的事。

在object函数内部先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。本质上其实就是object对传入的对象进行了一次浅复制。

现在有一个“男人”对象:

        var man = {
name: 'shanlei',
age:20
}

还有一个“人类”对象:

        var human = {
type:'人类'
}

现在我想让这个man继承human,也就是说这个“男人”,他是一个“人类”。

这里需要注意的是,这两个对象现在时普通的对象,而不是构造函数,所以我们无法使用上面的方法。

我们可以通过原型式继承,如下面的例子:

        var human = {
type:'人类'
} function object(o){
function F(){}
F.prototype = o;
return new F();
} var ano_man = object(human);
ano_man.name = 'shanlei';
ano_man.age = 20;
console.log(ano_man.type);

原型式继承要求必须有一个对象可以作为另一个对象的基础,如果有的话,那么只需要将它传给object函数,然后在根据需求对得到的对象进行修改就行了。在上面的例子中object函数返回一个新对象,这个新对象以human为原型,所以它的原型中就包含一个基本类型值属性。

在ECMAScript中通过函数Object.create()规范了原型式继承,该方法接收两个参数,一个用于新对象原型的对象,第二个参数用于为新对象定义额外属性的对象,在传入一个参数的情况下,Object.create()和上面的object()函数作用相同。

拷贝继承

我们可以想一下其实继承的意思就是子类型把超类型的所有属性和方法拿过来放在自己身上。 那么我们可以将超类型的属性和方法全部拷贝给子类型,从而实现继承。

浅拷贝

我们可以实现一个方法,将超类型的对象传入方法,然后将对象的属性添加到子类型上并返回,具体代码如下:

        function extendCopy(val) {
var c = [];
for(var i in val) {
c[i] = val[i];
}
c.uber = val;
return c;
}

具体使用可以这样:

        var ano_man = extendCopy(human);
ano_man.name = 'shanlei';
console.log(ano_man.type);

使用方法类似于上面介绍的原型式继承。但是这样实现继承有一个很大的问题,那就是当对象的属性是引用类型值(数组,对象等)时,在拷贝过程中,子对象获得的只是一个内存地址,而不是真正的属性拷贝。

深拷贝

我们可以在浅拷贝的基础上进行深拷贝。我们知道,当在拷贝基本类型值时是在内存中新开辟了一块区域用于拷贝对象属性的存储,所以我们只需要递归下去调用浅拷贝就行了。

    function deepCopy(p, c) {
     var c = c || {};
    for (var i in p) {
      if (typeof p[i] === 'object') {
        c[i] = (p[i].constructor === Array) ? [] : {};
        deepCopy(p[i], c[i]);
      } else {
         c[i] = p[i];
      }
    }
    return c;
  }

使用方法和浅拷贝类似,这里就不举例了。

寄生式继承

寄生式继承的思路就是创建一个用于封装继承过程的函数,在该函数内部以某种方式来增强对象,最后再向真的它做了所有工作一样返回对象。还是上面man和human两个对象间实现继承的例子:

function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}

使用时:

var clone = createAnother(human);
clone.sayHi();

在主要实现对象是自定义类型而不是构造函数的情况下,寄生式继承是一种有用的继承模式,其中使用的object函数不是必须的,任何能实现该功能的函数都可以。

寄生组合式继承

组合继承是JS中一种非常常用的继承模式,可是这个方式实现继承有一个问题,就是无论在任何情况下,都会调用两次超类型构造函数。一次是在创建子类型原型的时候,第二次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但是我们不得不在调用子类型构造函数时重写这些属性。如此在继承非常频繁的情况下就会造成内存过度损耗的情况了。这个时候,我们可以使用寄生组合式继承!

寄生组合式继承,就是借用构造函数来继承属性,通过原型链的混成形式来继承方法。具体思路是不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已,本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

基本模式如下:

        function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}

该函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步时创建一个超类型原型对象的副本 。第二步为创建的副本添加constructor属性,从而弥补因重写原型而失去默认的constructor属性。最后一步将新创建的副本对象复制给子类型的原型。

让我们回到第一个问题:有一个“男人”的构造函数和“人类”的构造函数,我现在想让男人继承人类!

        function Human() {
this.type = '人类';
} Human.prototype.sayHi = function() {
console.log('hi');
} function Man(name,age) {
Human.apply(this,arguments);
this.name = name;
this.age = age;
} inheritPrototype(Man, Human); var man = new Man('shalei',20); console.log(man.type)
man.sayHi();

寄生组合式继承是引用类型最理想的的继承范式!

以上~

JavaScript中继承的那些事的更多相关文章

  1. 探讨 JS 的面向对象中继承的那些事

    最近学了 JS 的面向对象,这篇文章主要是探讨 JS 的面向对象中继承的那些事. JS中继承的特点: 1.子类继承父类: 2.子类可以用父类的方法和属性 3.子类的改变可以不影响父类 下面用一个例子来 ...

  2. Javascript中继承

    Javascript中继承 构造函数继承 原型继承 call和apply继承 组合继承

  3. javascript中继承方式及优缺点(三)

    文以<JavaScript高级程序设计>上的内容为骨架,补充了ES6 Class的相关内容,从我认为更容易理解的角度将继承这件事叙述出来,希望大家能有所收获. 1. 继承分类 先来个整体印 ...

  4. javascript 中继承实现方式归纳

    转载自:http://sentsin.com/web/1109.html 不同于基于类的编程语言,如 C++ 和 Java,javascript 中的继承方式是基于原型的.同时由于 javascrip ...

  5. javascript中继承(一)-----原型链继承的个人理解

    [寒暄]好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解. 总的说来, ...

  6. 实现JavaScript中继承的三种方式

    在JavaScript中,继承可以通过三种手法实现原型链继承 使用apply.call方法 对象实例间的继承.     一.原型链继承 在原型链继承方面,JavaScript与java.c#等语言类似 ...

  7. 浅谈JavaScript中继承的实现

    谈到js中的面向对象编程,都有一个共同点,选择原型属性还是构造函数,两者各有利弊,而就片面的从js的对象创建以及继承的实现两个方面来说,官方所推荐的是两个相结合,各尽其责,各取其长,在前面的例子中,我 ...

  8. JavaScript中继承机制的模仿实现

    首先,我们用一个经典例子来简单阐述一下ECMAScript中的继承机制. 在几何学上,实质上几何形状只有两种,即椭圆形(是圆形的)和多边形(具有一定数量的边).圆是椭圆的一种,它只有一个焦点.三角形. ...

  9. JavaScript中继承的实现方法--详解

    最近看<JavaScript王者归来>中关于实现继承的方法,做了一些小总结: JavaScript中要实现继承,其实就是实现三层含义:1.子类的实例可以共享父类的方法:2.子类可以覆盖父类 ...

随机推荐

  1. Linux 进程通信方式

    转载文章 进程通信的方式 管道( pipe ): 管道包括三种: 普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用. 流管道s_pipe: 去除了第一种限制 ...

  2. Cobbler安装CentOS7系统时报错 curl:(7)Failed connect to 10.0.0.201:80;Connection refused

    问题原因: 其他涉及到http服务的端口全部都改成了81端口.只有 /etc/cobbler/settings 这里没有改. [root@mage-monitor- ~/]#grep -E " ...

  3. gcc 与 g++的区别

    原文: http://www.cnblogs.com/wb118115/p/5969775.html ------------------------------------------------- ...

  4. CuratorBarrier

    一.DistributedDoubleBarrier 同时开始,同时结束 package bjsxt.curator.barrier; import java.util.Random; import ...

  5. MYSQL(Mariadb)

    CentOS7下安装MariaDB 添加 MariaDB yum 仓库(官网的,也可以直接用aliyun云的) vi /etc/yum.repos.d/MariaDB.repo [mariadb] n ...

  6. 交叉编译bash

    1 下载bash版本:[version 4.2.53]地址:http://ftp.gnu.org/gnu/bash/ 2 解压将下载的bash压缩包解压,命令: # mkdir /home/carri ...

  7. linux/windows转mac的习惯设置

    外接键盘 常用快捷键设置 中英文快捷键名都重新设置一遍 使用ctrl替代command(对 内置 键盘操作) https://jingyan.baidu.com/article/6f2f55a1465 ...

  8. AutoCAD开发3--修改文字图层,颜色

    Dim pText As AcadText Dim pColor1 As AcadAcCmColor Set pColor1 = Application.GetInterfaceObject(&quo ...

  9. C# Form Chart X刻度左右多余一格怎么去掉

    如上图所示:形成的chart,1和8时y没有值,我实际给的也是2~7的数,可视1和8的刻度却在,怎么去掉,谢谢. 解决方法:chart1.ChartAreas[0].AxisX.IsMarginVis ...

  10. href=#与href=javascript:void(0)的区别

    #"包含了一个位置信息,默认的锚点是#top 也就是网页的上端 而javascript:void(0)  仅仅表示一个死链接 这就是为什么有的时候页面很长浏览链接明明是#可是跳动到了页首,而 ...