黑铁时代
Programing is not only one kind of technology, but also one kind of art.

在很多高级语言中,如C#,ActionScript 3.0,都有两种继承方式,类继承和接口继承。但是JavaScript中没有这些概念,JavaScript是通过原型链这种特别的方式实现继承的。

 

什么是原型链

我们知道原型是每个构造函数自带的一个属性,原型属性保存着一个指针,指向一个对象被称之为原型对象。每一种构造函数都有自己的一个原型对象,而且每种构造函数的所有对象实例也都有一个内部属性指向这种构造函数的原型对象,即所有的对象实例都会共享这个原型对象。

如果我们将一个构造函数A的实例赋值给另一个构造函数B的原型属性,即B.prototype = new A(),然后我们在把B的实例赋值给构造函数C的原型属性,即C.prototype = new B()。好了,现在我们想访问某个C对象实例d的属性name。执行过程是:解析器会先在d的属性中搜索,结果没有找到,之后到d的原型对象中去搜索;d的原型对象就是new B(),所以就会在B的实例中搜索,结果也没有找到;但是解析器不会放弃,它会继续搜索B实例的原型对象,即new A(),依次类推,直到链条的最末端。这样就形成了一条由原型组成的链条,所以叫原型链。

其实所有原型链的最末端都应该是Object的prototype指向的原型对象,因为Object对象是所有对象的基类,即所有构造函数的默认原型都是Object的实例

 

关于原型继承的例子

  function Parent( name ) {
    this.name = name;

    this.friends = [];

  }

  Parent.prototype = {
    constructor: Parent,

    // friends: [], 如果放在原型中,那么所有实例都会共享这个属性

    introduce: function () {
      alert( 'Hello, my name is ' + this.name );
    }
  }

  function Child( name, age ) {

    Parent.call( this, name ); // 这行代码之后在详细解释

    this.age = age;
  }
  Child.prototype = new Parent(); // 将父类的实例赋值给子类的原型属性
  Child.prototype.introduce = function () {
    alert( 'Hello, my name is '+ this.name + ', and my age is ' + this.age );
  }

  var child1 = new Child( 'Jack', 24 );
  child1.introduce(); // 弹出 Hello, my name is Jack, and my age is 24

  var child2 = new Child( 'Lion', 21 );

  child2.introduce(); // 弹出 Hello, my name is Lion, and my age is 21

  其实在JavaScript中没有类的概念,我用类来称呼,会更符合大家对面向对象的理解。父类的定义应该比较清楚了,父类定义了一个name属性,并且将方法定义在父类的原型中。子类定义了一个age属性,继承是通过Child.prototype = new Parent();这句代码来实现的,我们将父类的实例赋值给子类的原型,那么当父类的name属性在子类中没有定义的时候,解析器就会跑到原型对象中去搜索,原型对象就是父类的一个实例,所以就找到了父类的name属性。我们还看到子类有一个自己的introduce方法,这个方法会屏蔽父类原型中定义的introduce方法,如果我们不重写这个方法,那么解析器就会使用父类原型中的introduce方法,因为父类中的方法是可以通过原型链搜索到的。这就是原型链继承最基本的方式了,只要理解了原型的原理,应该很容易理解。

 

注意:关于引用类型的属性不应该放在原型上,就上面的例子,Parent有一个friends属性(Array类型的),如果将其放在原型中的,会出现下面的情况:

  child1.friends.push( 'Leo' );

  child2.firends.push( 'Sara' );

  console.log( child1.friends ); // 打印 Leo, Sara

  console.log( child2.friends ); // 打印 Leo, Sara

  因为放在原型对象中的属性都是共享的,所以任意一个实例改变它,其他实例都会受到影响。如果你需要所有实例都共享某个属性,这样做就没有问题,否则还是将其移到构造函数中,像父类的name属性那样比较合理。

另一点就是为子类的原型添加属性和方式要放在重写原型之后,即Child.prototype = new Parent(); 之后,因为所有的构造函数都有一个自己的原始的原型对象,当重置原型对象之后,就等于指向了新的原型对象了,但是之前添加的属性和方法都还在原始的原型对象上,所以就会失效。

 

  原型链说完之后,我们就来继续看Parent.call( this, name ); 这句神一样的代码。如果注释掉这句代码的话,我们调用introduce会出现什么情况呢?会弹出"Hello,my name is undefined, and my age is 25"。现在明白了这句代码的主要目的就是为了向父类的构造函数传递参数(其实实质目的是将父类的属性添加到子类中,并通过参数赋值),还记得call和apply的用法吗?他们的作用都是将某个函数的执行作用域变成第一个参数的执行作用域,这里就是将Parent的执行作用域变成this的执行作用域(this就是当前Child的实例),上面的例子就相当于调用了this.name = name;和this.friends = [];,相当于为Child的实例添加自己的name属性和friends属性了。

  竟然call和apply可以实现将父类的属性转换成子类的属性,那么为什么还需要使用子类的原型属性?原因还是因为原型是可以共享的,我们需要将方法放在原型上面,然后共享,这样可以大大减少内存消耗,提高性能。而且为每个对象初始化自己的方法副本,逻辑上也是不太合理。如果100年后,我们的内存和CPU都超级NB的时候,这点消耗完全可以忽略不计,那再另当别论。所以我们还是需要使用原型来共享方法,使用call来继承属性。

  这种组合模式实现了封装,继承,也可以用instanceof操作符和isPrototypeOf方法解决对象识别问题,但是还是有一个缺点,就是两次调用构造函数。一次是我们用call调用父类构造函数的时候,将父类的属性转化成子类的属性,完成了属性继承;另一次是在实现原型继承的时候,我们使用了new Parent()。这就导致了子类的原型对象和子类的实例都存在相同名字的属性,只是子类实例的属性将原型对象中的屏蔽了而已。虽然有这样的缺点,但是并不影响组合继承成为最经典的继承方式,也是最流行的方式。

 

寄生组合模式

这种模式就是为了解决组合模式两次调用构造函数的缺点。不过我需要先了解一下什么是原型式继承寄生式继承

原型式继承最先有道格拉斯提出,不需要创建自己的构造函数,在已经存在的某个对象的基础上创建一个新对象:

function createObject( object ) {

  function F() {};

  F.prototype = object;

  return new F();

}

寄生式继承在原型式基础上的一种改进,它有点类似工厂函数,用于加工原型式继承返回的对象,返回一个功能更多的新对象:

function createObject( object ) {

  function F() {};

  F.prototype = object;

  

  var newObject = new F();

  newObject.fun = function() {

    // Code...

  }

 

  return newObject;

}

 

当我们已经存在一个对象的情况下,我们不需要再使用自己定义构造函数,我们只需要加工这个对象的就可以使用了。所以我们就可以讲这个思想用在组合继承中,于是就形成了寄生组合式继承

function inheritPrototype( child, parent ) {

  function F() {};

  F.prototype = parent.prototype;

  var o = new F();

  o.constructor = parent;

  child.prototype = o;

}

我们从父类取出原型对象,作为我们原型式继承需要的基础对象,因为我们只需要父类的原型对象。我们使用空的F构造函数实例化一个新对象o,于是o的原型属性就指向父类的原型对象了,这时候要重写o的constructor,否则o的constructor就是F。最后,我们将子类child的prototype属性指向新对象o。

这样做的目的有两个:

  1 子类child的prototype指向的是对象o,而不再是parent的实例,就不存在再次调用父类构造函数的问题了,因为F构造函数中没有任何属性;

  2 竟然我们在原型继承的时候只需要使用父类的原型对象,那么我们直接将父类的原型对象丢给子类的prototype属性不就行了吗?这样其实是不行的,因为这样就相当于子类可以直接引用父类的原型,子类的prototype属性直接指向父类的原型对象而不再是一个对象的实例。当我们为子类的prototype添加方法的时候,就会修改到父类的原型对象,其实就相当于添加到了父类的原型对象中。子类的方法,竟然父类也有,这就违背了面向对象的概念。更糟糕的是如果还有其他类型也是继承这个父类,那么也受到影响,因为原型对象是共享的。

 

关于继承,还有很多变种的方式,许多优秀的框架中都有自己的实现方法,不管是那种方式,利用原型链进行继承应该是根本。

 

posted on 2012-07-04 20:44  黑铁时代  阅读(206)  评论(0编辑  收藏  举报