JavaScript中继承的那些事

引言

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

假如如今有一个“人类”的构造函数:app

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属性的副本了。

不过借用构造函数进行继承,不免会有方法都在构造函数中定义,没法实现函数的复用。而且在超类型的原型中定义的方法对于子类型是不可见的,结果全部类型都要使用构造函数进行继承,因此单独使用构造函数的状况比较少。this

 

 

经过原型链

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

若是Man的原型指向的是Human的实例,那么Man原型对象中将会包括一个指向Human的指针,那么全部Man的实例就均可以继承Human了。.net

        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的一个实例。prototype

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属性,让这个属性指回原来的构造函数。

 

 

组合继承(伪经典继承)

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

那么上面让“男人”继承“人类”就能够经过组合继承实现:对象

        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();

 

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

 

以上~

相关文章
相关标签/搜索