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

  在探讨原型模式之前,必须先了解什么是原型。在JavaScript中,每当创建了一个函数的时候,这个函数自身就会有一个prototype的属性,prototype就是我们所说的原型了。函数的这个prototype属性存放的是一个指针,而这个指针指向了一个对象,即我们所说的原型对象了。原型对象有什么用呢?原型对象存放的是所有对象实例共享的属性和方法,换句话说就是,如果我们为某个类型的原型对象添加的属性和方法,可以让所有的这个类型的实例共享。我们先来看个例子:

  function Person() {

  }

  Person.prototype.name = 'Leo';

  Person.prototype.age = 25;

  Person.prototype.introduce = function() {

    alert( 'Hello, my name is ' + this.name );

  }

  var p1 = new Person();

  var p2 = new Person();

  p1.introduce(); // Hello, my name is Leo

  p2.introduce(); // Hello, my name is Leo

  我们可以在原型对象上添加任意多的属性和方法,然后所有的实例都会共享他们,这就是原型的特点。现在你可能对原型对象有了一定初步的了解,接下来我们再更加深入的讨论一下原型对象。

  每个函数都有会有一个原型对象,这个原型对象包含一个constructor属性,它也是存放的一个指针,指向该原型的构造函数,例子中的Person.prototype.constructor就指向的是Person构造函数;

  alert( Person.prototype.constructor === Peson ) // true

  p1和p2实例也存在一个对Person.prototype的引用,在很多浏览器中都实现了一个非标准的内部属性__proto__,这个属性保存了指向原型对象的指针。

  说了这么多,我们来理清一下它们之间的关系:

  Person 的 prototype 属性指向 Person的原型对象P(假设我们用P表示这个原型对象);

  p1 和 p2 的一个内部属性(根据浏览器的实现而异)也指向 原型对象P;

  p1 和 p2 的 constructor 属性指向 Person(构造函数);

  原型对象p 的 constructor 属性也指向 Person(构造函数);

  注意:由于内部属性并不是ECMAScript制定的标准,所以无法用于开发,但是ECMA提供了一个方法可以使用,那就是isPrototypeOf

  alert( Person.prototype.isPrototypeOf(p1) ); // true,因为p1存在一个指向Person原型对象的指针

 

实例属性和原型属性

  对于一些可以共享的方法和属性放在原型对象上的确非常合适,但是每个实例都应该有自己的属性和自己一些特有的方法,这个时候就不能再放在原型对象上面了。如果Leo想把名字改成Jack,于是他这么做了:

  p1.name = 'Jack';

  p1.introduce(); // Hello, my name is Jack

  名字修改成功了,那是不是原型对象上的name也变成了Jack?其实通过这种方式并不会改变原型上的任何属性,即原型对象上叫Leo的name属性依然存在。为什么显示的是Jack而不是Leo?这里又涉及到了JavaScript访问属性的方式,当我们要访问p1对象的name属性的时候,解析器会先在p1自身的属性中进行查找,如果找到了name属性即叫Jack的这个name属性,那么就停止搜索了。如果没有在自身实例中找到name属性,那么就会继续到原型对象中去查找,只有在原型对象中搜索才会查找到叫Leo的这个name属性。如果在原型对象中依然没有搜索到name属性,就会返回undefined,表示没有定义这个属性。做个实验就明白了:

  p1.name = 'Jack';
  p1.introduce(); // Hello, my name is Jack

  delete p1.name;
  p1.introduce(); // Hello, my name is Leo

  delete Person.prototype.name;
  p1.introduce(); // Hello, my name is undefined

  我们用delete操作符删除p1的name属性之后,就返回原型对象上的name属性;我们在继续删除原型对象上的name属性,就返回undefined了。

  我们如何知道那些属性是来自原型,而那些又来自自身实例呢?方法很简单:

  for ( var index in p1 ) { // in操作符可以用来枚举出对象的所有属性,包括原型对象上的属性(当然这些属性名也包括方法名)

    if ( p1.hasOwnProperty( index ) ) { // hasOwnProperty方法是从Ojbect继承过来的,表示某个属性是不是来自自身,如果是就返回true

      console.log( index + ' is the own property.' );  

    }

    else {

      console.log( index + ' is the prototype property.' )

    }

  }

 

重写原型对象

  通常情况下我们在定义好构造函数之后,我们会重写原型对象,如下:

  function Person() {

  }

  Person.prototype = {

    constructor: Person,

    name: 'Leo',

    age: 25,

    introduce: function() {

      ...

    }

  }

  这样做的目的有两个:一是将原型对象的所有属性和方法都包在一个对象中,体现了封装的原则;二是让简化代码,更易于维护。注意我们显示定义了constructor属性,并将其指向Person构造函数。这是因为原来的Person.prototype.construcotr属性是在创建Person构造函数的时候就自己动初始化了,并指向了Person;但是当我们重写prototype的时候,Person.prototype指向了新的对象,而新对象的构造函数是Object(),所以新对象的constructor就是Object,即Person.prototype的constructor属性也是Object。因此,我们需要显示的重新定义constructor,让其指向Person,这样才是合理的。

 

混合模式

  将所有的方法放在原型上,这个没有问题,因为我们就是希望让所有对象实例都共享方法;但是如果将所有属性也放在原型上面,不仅不合理,还会出现一些问题。我们可以讲一些共享的属性放在原型上面,这些属性一旦初始化后就不会再被修改,而对于一些随时会被修改的属性,就不要放在原型上面,因为没有什么意义,你还是要为实例对象添加属性来完成修改。而且不能在原型上面放像Array这种引用类型的属性,一旦某个对象实例修改了引用类型的属性,其他所有实例都会发生同样的改变,造成严重的后果。

  混合模式就出现了,混合模式是目前最佳的解决方案,其实很简单。混合模式就是将属性放在构造函数中,而方法和共有属性放在原型中。

  function Person( name, age ) {

    this.name = name;

    this.age = age;

  }

  Person.prototype = {

    constructor: Person,

    introduce: function () {

      // Code...

    }

  }

  好了,这就是混合了构造函数模式和原型模式的情况,发挥了两种模式的优势。之后还出现了一些变种的写法,比如动态混合模式。

  function Person( name, age ) {

    this.name = name;

    this.age = age;

    if  ( typeof this.introduce != 'function' ) {

      Person.prototye.introduce = function () { Code... };

    }

  }

  // 如果introduce没有定义,实例化第一个对象的时候就会往原型上添加,由于原型上的东西是共享的,所以只要添加一次后就不需要再添加了。这样写的目的其实只是为了让代码结构看起来像被封装在了一起而已,效果都是一样的。

posted on 2012-07-01 17:25  黑铁时代  阅读(309)  评论(0编辑  收藏  举报