转载:http://blog.youkuaiyun.com/a153375250/article/details/51083245
JS是一种基于对象的语言,对象的概念在JS体系中十分的重要,因此有必要清楚地了解一下JS中对象创建的常用方法及各自的局限性。
使用Object或对象字面量创建对象
在说工厂模式创建对象之前,我们不妨回顾一下JS中最基本的创建对象的方法,比如说我想创建一个student对象怎么办?最简单地,new一个Object:
这样,一个student对象就创建完毕,拥有2个属性name
以及age
,分别赋值为"easy"
和20
。
如果你嫌这种方法有一种封装性不良的感觉,我们也可以使用对象字面量的方式来创建student对象:
这样看起来似乎就完美了。但是马上我们就会发现一个十分尖锐的问题:当我们要创建同类的student1,student2,…,studentn时,我们不得不将以上的代码重复n次。
能不能像工厂车间那样,有一个车床就不断生产出对象呢?我们看”工厂模式”。
工厂模式创建对象
JS中没有类的概念,那么我们不妨就使用一种函数将以上对象创建过程封装起来以便于重复调用,同时可以给出特定接口来初始化对象:
这样一来我们就可以通过createStudent函数源源不断地”生产”对象了。看起来已经高枕无忧了,但贪婪的人类总有不满足于现状的天性:我们不仅希望”产品”的生产可以像工厂车间一般源源不断,我们还想知道生产的产品究竟是哪一种类型的。
比如说,我们同时又定义了”生产”水果对象的createFruit()函数:
对于以上代码创建的对象v1、v2,我们用instanceof操作符去检测,他们统统都是Object类型。我们的当然不满足于此,我们希望v1是Student类型的,而v2是Fruit类型的。为了实现这个目标,我们可以用自定义构造函数的方法来创建对象。
构造函数模式创建对象
在上面创建Object这样的原生对象的时候,我们就使用过其构造函数:
在创建原生数组Array类型对象时也使用过其构造函数:
在进行自定义构造函数创建对象之前,我们首先了解一下构造函数
和普通函数
有什么区别。
其一,实际上并不存在创建构造函数的特殊语法,其与普通函数唯一的区别在于调用方法。对于任意函数,使用new操作符调用,那么它就是构造函数;不使用new操作符调用,那么它就是普通函数。
其二,按照惯例,我们约定构造函数名以大写字母开头,普通函数以小写字母开头,这样有利于显性区分二者。例如上面的new Array(),new Object()。
其三,使用new操作符调用构造函数时,会经历(1)创建一个新对象;(2)将构造函数作用域赋给新对象(使this指向该新对象);(3)执行构造函数代码;(4)返回新对象;4个阶段。
了解了构造函数
和普通函数
的区别之后,我们使用构造函数将工厂模式
的函数重写,并添加一个方法属性:
这样我们再分别创建Student和Fruit的对象:
这时我们再来用instanceof操作符来检测以上对象类型就可以区分出Student以及Fruit了:
这样我们就解决了工厂模式
无法区分对象类型的尴尬。那么使用构造方法来创建对象是否已经完美了呢?
我们知道在JS中,函数是对象。那么,当我们实例化不止一个Student对象的时候:
其中共同的alertName()
函数也被实例化了n次,我们可以用以下方法来检测不同的Student对象并不共用alertName()
函数:
这无疑是一种内存的浪费。我们知道,this对象是在运行时基于函数的执行环境进行绑定的。在全局函数中,this对象等同于window;在对象方法中,this指向该对象。在上面的构造函数中:
我们在创建对象(执行alertName函数之前)时,就将alertName()函数绑定在了该对象上。我们完全可以在执行该函数的时候再这样做,办法是将对象方法移到构造函数外部:
在调用stu1.alert()
时,this对象才被绑定到stu1上。
我们通过将alertName()函数定义为全局函数,这样对象中的alertName属性则被设置为指向该全局函数的指针。由此stu1和stu2共享了该全局函数,解决了内存浪费的问题。
但是,通过全局函数的方式解决对象内部共享的问题,终究不像一个好的解决方法。如果这样定义的全局函数多了,我们想要将自定义对象封装的初衷便几乎无法实现了。更好的方案是通过原型对象模式来解决。
原型模式创建对象
函数的原型对象
在了解如何使用原型模式创建对象之前,有必要先搞清楚什么是原型对象。
我们创建的每一个函数都有一个prototype属性,该属性是一个指针,该指针指向了一个对象。对于我们创建的构造函数,该对象中包含可以由所有实例共享的属性和方法。如下如所示:
在默认情况下,所有原型对象会自动包含一个constructor属性,该属性也是一个指针,指向prototype所在的函数:
对象实例和原型对象的关联
在调用构造函数创建新的实例时,该实例的内部会自动包含一个[[Prototype]]指针属性,该指针指便指向构造函数的原型对象。注意,这个指针关联的是实例与构造函数的原型对象
而不是实例与构造函数
:
使用原型模型创建对象
直接在原型对象中添加属性和方法
了解了原型对象之后,我们便可以通过在构造函数原型对象中添加属性和方法来实现对象间数据的共享了。例如:
以上代码,我们在Student的protptype对象中添加了name、age属性以及alertName()方法。但创建的stu1和stu2中并不包含name、age属性以及alertName()方法,而只包含一个[[prototype]]指针属性。当我们调用stu1.name
或stu1.alertName()
时,是如何找到对应的属性和方法的呢?
当我们需要读取对象的某个属性时,都会执行一次搜索。首先在该对象中查找该属性,若找到,返回该属性值;否则,到[[prototype]]指向的原型对象中继续查找。
由此我们也可以看出另外一层意思:如果对象实例中包含和原型对象中同名的属性或方法,则对象实例中的该同名属性或方法会屏蔽原型对象中的同名属性或方法。原因就是“首先在该对象中查找该属性,若找到,返回该属性值;”
拥有同名实例属性或方法的示意图:
上图中,我们在访问stu1.name是会得到”EasySir”:
通过对象字面量重写原型对象
很多时候,我们为了书写的方便以及直观上的”封装性”,我们往往采用对象字面量直接重写整个原型对象:
要特别注意,我们这里相当于用对象字面量重新创建了一个Object对象,然后使Student的prototype指针指向该对象。该对象在创建的过程中,自动获得了新的constructor属性,该属性指向Object的构造函数。因此,我们在以上代码中,增加了constructor : Student
使其重新指回Student构造函数。
原型模型创建对象的局限性
原型模型在对象实例共享数据方面给我们带来了很大的便利,但通常情况下不同的实例会希望拥有属于自己单独的属性。我们将构造函数模型和原型模型结合使用即可兼得数据共享和”不共享”。
构造与原型混合模式创建对象
我们结合原型模式在共享方法属性以及构造函数模式在实例方法属性方面的优势,使用以下的方法创建对象:
以上,在构造函数中定义实例属性,在原型中定义共享属性的模式,是目前使用最广泛的方式。通常情况下,我们都会默认使用这种方式来定义引用类型变量。
在JavaScript中创建对象的方式有两种:对象字面量和使用new表示。对象字面量是一种灵活的书写方式:
var o1 = {
p:”I’m in Object literal”,
alertP:function(){
alert(this.p);
}
}
用对象字面量创建了一个对象01,它具有一个成员变量P以及一个成员方法alertP。
这种写法的缺点是,每一个创建新的对象都需要写出完整的定义语句。不便于创建大量相同类型的对象,不利于使用继承高级特性。
new表达式是配合构造函数使用的,例如new String("a string"),调用内置的String函数构造了一个字符串对象,下面我们用构造函数的方式开创建一个对象,首先是定义构造函数,然后是调用new表达式。
function CO(){
this.p = “I’m in constructed object”;
this.alertP = function(){
alert(this.p);
}
}
var o2 = newCO();
那么,在使用new操作符来调用一个构造函数的时候,发生了什么呢?其实很简单,就发生了四件事:
var obj={};
obj._proto_ =CO.prototype;
CO.call(obj);
return obj;
第一行,创建一个空对象obj。
第二行,将这个空对象的__proto__成员指向了构造函数对象的prototype成员对象,这是最关键的一步,具体细节将在下文描述。
第三行,将构造函数的作用域赋给新对象,因此CA函数中的this指向新对象obj,然后再调用CO函数。于是我们就给obj对象赋值了一个成员变量p,这个成员变量的值是” I’min constructed object”。
第四行,返回新对象obj。当构造函数里包含返回语句时情况比较特殊,这种情况会在下文中说到。
正确定义JavaScript函数
JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,他就,他就被称作构造函数,或构造器。
一个函数要作为一个真正意义上的构造函数,需要满足下列条件:
1、在函数内部对新对象(this)的属性进行设置,通常添加属性和方法。
2、构造函数可以包含语句,但是返回值必须是this,或者其他非对象类型的值。
上文定义的构造函数CO就是一个标准的、简单的构造函数。
下面例子定义的函数C1返回了一个对象,我们可以使用new表达式来调用它。该表达式可以正确返回一个对象:
function C1(){
var o = {
p:”I’m p in C1”
}
return o;
}
var o1 = new C1();
alert(o1.p);//I’m p in C1
但这种方式并不是值得推荐的方式,因为对象o1的原型是函数c1内部定义的对象o的原型,也就是object.prototype。这种方式相当于执行了正常new表达式的前三步,而在第四步的时候返回了从c1函数的返回值。该方式同样不便于创建大量相同类型的对象,不利于使用继承高级特性,并且容易造成混乱,应该摒弃。
一个构造函数在某些情况下完全可作为普通的功能函数来使用,这是JavaScript灵活性的一个体现。
以下定义的c2就是一个“多用途”函数:
function C2(a, b){
this.p = a + b;
this.alertP = function(){
alert(this.p);
}
return this.p;//此返回语句在C2作为构造函数时没有意义
}
var c2 = new C2(2,3);
c2.alertP();//结果为5
alert(C2(2, 3)); //结果为5
该函数既可以用构造函数老构造一个对象,也可以作为普通的函数来使用。作为普通函数时,他接收两个参数,并返回两者相加的结果。为了代码的可读性和可维护性,建议作为构造函数的函数不要掺杂出构造作用以外的代码;同样的,一般的功能函数也不要用作构造对象。
为什么要使用构造函数
从表面上看,构造函数似乎只是对一个新创建的对象进行初始化,增加一些成员变量和方法;
然而构造函数的作用远不止这些。为了说明使用构造函数的意义,我们先来回顾一下前文提到的例子。执行var o2 = new CO();创建对象的时候,发生了四件事情:
|
var obj ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return
obj;
|
我们说最重要的是第二步,将新生成的对象的__prop__属性赋值为构造函数的prototype属性,使得通过构造函数创建的所有对象可以共享相同的原型。这意味着同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的对象。
在JavaScript标准中,并没有_prop_这个属性,不过他现在已经是一些主流的JavaScript执行环境默认的一个标准属性,用于指向构造函数的原型。该属性是默认不可见得。而且在各执行环境中实现的细节不尽相同,例如IE浏览器中不存在该属性。我们只要知道JavaScript对象内部存在指向构造函数原型的指针就可以了,这个指针是在调用new表达式的时候自动赋值的,并且我们不应该去修改它。
在构造对象的四个步骤中,我们可以看到,除第二步以外,别的步骤我们无须借助new表达式去实现,因此new表达式不仅仅是对这四个步骤的简化,也是要实现继承的必经之路。
容易混淆的地方
关于JavaScript的构造函数,有一个容易混淆的地方,那就是原型的constructor属性。
在JavaScript中,每一个函数都有默认的原型对象属性prototype,该对象默认包含了两个成员属性:constructor和__proto__。关于原型的细节就不在本文赘述了,我们现在关心的是这个constructor属性。
按照面向对象的习惯性思维,我们说构造函数相当于“类”的定义,从而可能会认为constructor属性就是该类实际意义上的构造函数,在new表达式创建一个对象的时候,会直接调用constructor来初始化对象,那就大错特错了。new表达式执行的实际过程已经在上文中介绍过了(四个步骤),其中用于初始化对象的是第三步,调用的初始化函数正是“类函数”本身,而不是constructor。如果没有考虑过这个问题,这一点可能不太好理解,那就让我们举个例子来说明一下吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function C3(a, b){
this
.p = a + b;
this
.alertP = function(){
alert(
this
.p);
}
}
//我们定义一个函数来覆盖C3原型中的constructor,试图改变属性p的值
function fake(){
this
.p =
100
;
}
C3.prototype.constructor = fake;
//覆盖C3原型中的constructor
var c3 =
new
C3(
2
,
3
);
c3.alertP();
//结果仍然为5
|
上述代码手动改变了C3原型中的constructor函数,然而却没有对c3对象的创建产生实质的影响,可见在new表达式中,起初始化对象作用的只能是构造函数本身。
那么constructor属性的作用是什么呢?一般来说,我们可以使用constructor属性来测试对象的类型:
1
2
|
var myArray = [
1
,
2
,
3
];
(myArray.constructor == Array);
// true
|
这招对于简单的对象是管用的,涉及到继承或者跨窗口等复杂情况时,可能就没那么灵光了:
1
2
3
4
5
6
7
|
function f() {
this
.foo =
1
;}
function s() {
this
.bar =
2
; }
s.prototype =
new
f();
// s继承自f
var son =
new
s();
// 用构造函数s创建一个子类对象
(son.constructor == s);
// false
(son.constructor == f);
// true
|
这样的结果可能跟你的预期不相一致,所以使用constructor属性的时候一定要小心,或者干脆不要用它。