JavaScript 深入理解对象创建方式

     在JS中,为了改进语言熟悉程度,也引入了“构造函数”这样的机制,但是在JS中构造函数本身也是函数,只是可以用来创建对象。在JS中创建对象,也需要用到new操作符,它的实际过程是这样的:

    

1:创建一个对象(这一过程完全在new操作符之前)
2:将构造函数的作用域赋给该对象(因此this也就指向了该对象)
3:执行构造函数(创建属性和方法)
4:返回新对象

 

    值得一提的是在Python的“构造方法”中,机制与这几乎一致(只是Python将这个方法移到类中,并称之为__init__方法)

 

    创建对象有以下几种方式,包括工厂模式、构造函数模式、原型模式等等。

    首先来看工厂模式:

   

function createPerson(person,age){
    var o=new Object();
    o.person=person;
    o.age=age;
    o.sayName=function(){
        alert(o.person);
    }
    return o;
}
var ted=createPerson("ted",15);
var marry=createPerson("mary",11);
ted.sayName();//输出ted
marry.sayName();//输出mary

  

   工厂模式的缺点就是创建了一个对象,但创建了两个引用。同样它并没有解决对象识别的问题,即怎么样知道一个对象的类型。

   再来看构造函数模式:

   

function Person(person,age){	
      this.person=person;
      this.age=age;
      this.sayName=function(){
	alert(this.person);
     }
}
var ted=new Person("ted",15);
var marry=new Person("marry",13);
ted.sayName();//输出ted
marry.sayName();//输出marry
//按照惯例,这里构造函数首字母采用大写

  

   注意在这种模式下,这两个对象都具有一个构造函数属性,称为constructor(实质上是Person构造函数的原型对象的属性,稍后将介绍原型对象),例如:

  

alert(ted.constructor);
alert(marry.constructor);
//不出意外的话,会打印整个Person函数

   而在第一种方式下,尝试打印它们的constructor属性话,会直接打印object的构造函数。

 

   但是这样随之而来,也引出了一个问题,ted和marry都创建了一个具有完全功能的函数对象,但这两个函数对象却不是同一个。可以像下面这样稍微改造下:

  

function Person(person,age){
    this.person=person;
    this.age=age;
    this.sayName=sayName;
}
function sayName(){
     alert(this.person);
}

 

   这样不用创建重复的函数对象了,但还是引来另外一个问题。如果这个对象拥有很多函数,那么这样一一声明全局函数,但这个全局函数实质上只能供Person使用,所以这丝毫没有体会到封装性的好处。

   是时候到重量级“人物”出场了--原型模式!

   先来了解下什么是原型。我们创建的每一个函数都有一个Prototype属性,这一个属性是一个对象,它包括一个constructor属性,这个属性又重新指向了拥有该Prototype属性的对象。是不是感觉晕掉了?来看看它们的图。

   假如我们这样创建了2个对象,其实际内部运行机制是什么样的?先看下下面代码:

  

function Person(){	
}
Person.prototype.name="ted";
Person.prototype.age=11;
Person.prototype.sayName=function(){
	alert(this.name);
}
var ted=new Person();
var marry=new Person();
ted.sayName();//输出ted
marry.sayName();//输出ted

 

   它的基本运行流程是这样的:

   

   

 

 

 

 

   上面图是我自己手绘的,呵呵,请诸兄不要笑话。

   要检测对象和原型之间的关系的方法是:

  

alert(Person.prototype.isPrototypeOf(ted));//弹出true
alert(Person.prototype.isPrototypeOf(marry));//弹出true

  

  每当代码读取对象的一个属性的时候,都会执行一次搜索。目标是具有给定名字的属性。方法是:先查看对应实例里,是否有该名字,如果没有,则查看对应的原型对象里,是否有该属性。例如:

   

  

ted.name="haha";
alert(ted.name);//输出haha
alert(marry.name);//输出ted

 

   从以上代码可以看出,的确实例属性遮盖了原型对象的属性。

   但是可以利用delete操作符删除这个实例属性,例如:

  

delete ted.name;
alert(ted.name);//输出ted

 

    后来的Python,也借鉴了这种做法,不过Python采用的是del操作符,它们的思想都是类似的,都是让对象的引用次数减一。

    要验证一个属性是否是实例属性还是系统属性,请查看:

   

alert(ted.hasOwnProperty("name"));//输出true
alert(marry.hasOwnProperty("name"));//输出false

 

    在Python中则对应hasAttribute()方法,这是顶层类object的方法。

    在JS中可以利用in操作符和hasOwnProperty()两个操作结合起来,来判断一个属性究竟是存在于对象还是实例中。

   

alert(checkNameInClass(ted,"name"));//输出true
alert(checkNameInPrototype(ted,"name"));//输出false
alert(checkNameInClass(marry,"name"));//输出false
alert(checkNameInPrototype(marry,"name"));//输出true
function checkNameInClass(obj,name){
    return obj.hasOwnProperty(name);
}
function checkNameInPrototype(obj,name){
   return !obj.hasOwnProperty(name)&&(name in obj);
}

 

 但是上面的原型方法略显复杂,可以改用对象字面量方法来定义:

  

Person.prototype={
     name:"ted",
     age:11,
     sayName:function(){
          alert(this.name);
     }
}
var ted=new Person();
var marry=new Person();
ted.sayName()//输出ted
marry.sayName();//输出ted

 

   这里同样均可以输出ted。但是这里存在一个问题,就是字面量定义原型的方法会完全覆盖默认的prototyp属性,因此之前讨论的constructor属性也就不再指向Person了,而是指向object了,虽然此时新实例还是原对象的实例,做下测试:

   

alert(ted instanceof Person);//输出true
alert(ted.constructor == Person);//输出false

 

   如果这里的constructor属性确实非常重要(至少我没发现它会有多么重要),那么可以这样修改:

  

Person.prototype={
    constructor:Person,
    name:"ted",
    age:11,
    sayName:function(){
       alert(this.name);
    }
}
//这次可以正确的输出构造函数在Person中了

 

   这里还有另外一个问题,如果先定义了Person实例对象,后定义了原型对象,则会提示方法找不到,理由是:重新覆写了原型对象后,原来的Person所指向的原型对象被改变了,等于切断了原来的构造函数与最初原型之间的联系。例如:

  

function Person(){	
}
var ted=new Person();
Person.prototype={
    constructor:Person,
    name:"ted",
    age:11,
    sayName:function(){
         alert(this.name);
    }
}
ted.sayName();//解释执行的时候会出错

 

  当然这还不是原型对象模式最大的缺点,它最大的缺点是省略了构造函数所传入的初始化参数这一环节,结果所有实例都取得了相同的属性值。例如:

   

function Person(){	
}
Person.prototype={
    constructor:Person,
    name:"ted",
    age:11,
    sayName:function(){
         alert(this.name);
    },
    friends:["a","b"]
}
var ted=new Person();
var marry=new Person();
alert(ted.friends==marry.friends);//输出true
ted.friends.push("c");
alert(ted.friends);//输出a b c
alert(marry.friends);//也输出a b c

 

    如果你的本意就是想设计的让所有对象共享相同属性,那么上述方法没有任何问题,并且在某种程度上还提高了内聚性。但是实例一般有属于自己的单独的属性的,这也是为什么会很少单独只使用原型模式的问题了。

 

posted @ 2011-06-16 20:42  老去的JAVA程序员  阅读(176)  评论(0编辑  收藏  举报