JavaScript中继承的那些事

时间:2023-12-21 13:34:08

引言

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!

JavaScript中继承的那些事

原型式继承

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

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

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

JavaScript中继承的那些事

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

在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;
  }

JavaScript中继承的那些事

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

寄生式继承

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

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

使用时:

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

JavaScript中继承的那些事

在主要实现对象是自定义类型而不是构造函数的情况下,寄生式继承是一种有用的继承模式,其中使用的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();

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

以上~