2

基于类的继承是大多数人所熟悉的,也是比较容易理解的。当我们形成类型继承的思维定势后,再次接触原型继承可能会觉得有些奇怪并难以理解。你更可能会吐槽,原型继承根本就不能叫做继承,一点都不面向对象。本人最初也是这样认为的,但深入仔细的对比后发现,两者其实并没有本质的差别,只是表面有点不一样而已。且看下面的分析。

类型继承

先看一个类型继承的例子,代码如下:

public class A {
    //...
}

public class B extends A {
    //...
}

public class C extends B {
    //...
}

C c = new C();

A、B、C为三个继承关系的类,最后将类C实例化。下面这张图描述了类和实例的对应关系。左边为类,右边为其对应实例。

图片1

我们看到,类C实例化后,内存中不仅存在c对象,同时还有a、b两个对象。因为在java中,当我们在执行new C()操作时,jvm中会发生如下过程:

  1. 创建A的实例a。

  2. 创建B的实例b,并将实例b的super指针指向a。

  3. 创建C的实例c,并将实例c的super指针指向b。

过程1和过程2对用户是透明的,不需要人工干预,引擎会按照“蓝图”把这两个过程完成。通过上图右半部分我们可以看到,super指针将a、b、c三个实例串起来了,这里是实现继承的关键。当我们在使用实例c的某个属性或方法时,若实例c中不存在则会沿着super指针向父类对象查找,直到找到,找不到则出错。这就是继承能够达到复用目的内部机制。看到这里大家或许已经联想到原型链了,super所串起来的这个链几乎和原型链一样,只是叫法不一样而已。下面我们就来看看原型继承。

原型继承

图片2

上面是原型继承的示意图。先看图的右半部分,__proto__指针形成的对象链就是原型链。__proto__是一个私有属性,只能看不准访问(某些浏览器看也不给看)。__proto__的作用和前面的super是一样的,原型链实现复用的机制和类型继承也几乎是一样的,这里不再重复。有一点不一样就是原型继承中的属性写操作只会改变当前对象并不会影响原型链上的对象。

如何去构造原型链呢?看上去要稍微麻烦一些。原型继承里面没有类的概念,我们需要通过代码,手动完成这个过程。上图中的A、B、C在原型继承称作构造器。构造器就是一个普通的函数,但是将new操作符用到构造器上时,它会执行一个叫[[construct]]的过程。大致如下:

  1. 创建一个空对象obj。

  2. 设置obj的内部属性[[Class]]为Object。

  3. 设置obj的内部属性[[Extensible]]为true。

  4. 设置obj的[[__proto__]]属性:如果函数对象prototype的值为对象则直接赋给obj,否则赋予Object的prototype值

  5. 调用函数对象的[[Call]]方法并将结果赋给result。

  6. 如果result为对象则返回result,否则返回obj。

从第4条可以看到,构造器生成的对象的__proto__属性会指向构造器的prototype值,这就是我们构造原型链的关键。下面的代码是上图原型链的构造过程。

function A(){
    //...
}

function B(){
    //...
}

function C(){
    //...
}

var a = new A();
B.prototype = a;
var b = new B();
C.prototype = b;
var c = new C();

上述代码虽然能达到目的,但有点繁琐,我们可以将这个过程封装一下。backbone的实现是这样的:

var extend = function(protoProps, staticProps) {
    var parent = this;
    var child;
    if (protoProps && _.has(protoProps, 'constructor')) {
      child = protoProps.constructor;
    } else {
      child = function(){ return parent.apply(this, arguments); };
    }    
    _.extend(child, parent, staticProps);
    child.prototype = _.create(parent.prototype, protoProps);
    child.prototype.constructor = child;
    child.__super__ = parent.prototype;
    return child;
  }

其中_.extend(child, parent, staticProps)是将staticProps和parent对象的属性复制给child。_.create方法的实现大概如下。

_.create = function(prototype, protoProps){
    var F = function(){};
    F.prototype = prototype;
    var result = new F();
    return _.extend(result, protoProps);
}

有了extend方法,我们的代码就可以写成:

    A.extend = extend;
    var B = A.extend({
        //...
    );
    var C = B.extend({
        //...
    );
    var c = new C();

这段代码和类型继承的代码十分相似,通过原型继承我们也可以达到类型继承的效果。但是通过前面的比较我们发现,继承的本质就其实就是对象的复用。原型继承本身就是以对象为出发点考虑的,所以大多时候我们并不一定要按照类型继承的思维考虑问题。而且js是弱类型,对象的操作也极其自由,上述的_.create方法可能是js里面实现继承的一个更简单有效的方法。

总结

前面讨论了两种继承方式,可以看到,继承的本质其实就是对象的复用。本人觉得原型继承更加的简单和明确,它直接就是从对象的角度考虑问题。当然,如果你需要一个非常强大的继承体系,你也可以构造出一个类似类型继承的模式。相对来说,本人觉得原型继承更灵活和自由些,也是非常巧妙和独特的。


crossea
1.1k 声望43 粉丝