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