日期:2014-05-16  浏览次数:20352 次

几种JavaScript定义类和对象的方法
几种JavaScript定义类和对象的方法

http://www.nowamagic.net/javascript/js_MethodsToCreateClassObject.php

最近偶然碰到有朋友问我"hoisting"的问题。即在js里所有变量的声明都是置顶的,而赋值则是在之后发生的。可以看看这个例子:
view source
print?
1 var a = 'global';
2 (function () {
3     alert(a);
4     var a = 'local';
5 })();

大家第一眼看到这个例子觉得输出结果是什么?'global'?还是'local'?其实都不是,输出的是undefined,不用迷惑,我的题外话就是为了讲这个东西的。

其实很简单,看一看JavaScript运行机制就会明白。我们可以把这种现象看做"预声明"。但是如果稍微深究一下,会明白得更透彻。

这里其实涉及到对象属性绑定机制。因为所有JavaScript函数都是一个对象。在函数里声明的变量可以看做这个对象的"类似属性"。对象属性的绑定在语言里是有分"早绑定"和"晚绑定"之分的。

【早绑定】是指在实例化对象之前定义其属性和方法。解析程序时可以提前转换为机器代码。通常的强类型语言如C++,java等,都是早绑定机制的。而JavaScript不是强类型语言。它使用的是"晚绑定"机制。

【晚绑定】是指在程序运行前,无需检查对象类型,只要检查对象是否支持特性和方法即可。可以在绑定前对对象执行大量操作而不受任何惩罚。

上面代码出现的"预声明"现象,我们大可用"晚绑定"机制来解释。在函数的作用域中,所有变量都是"晚绑定"的。 即声明是顶级的。所以上面的代码和下面的一致:
view source
print?
1 var a = 'global';
2 (function () {
3     var a;
4     alert(a);
5     a = 'local';
6 })();

在alert(a)之前只对a作了声明而没有赋值。所以结果可想而知。

在JavaScript里,我所知道的几种定义类和对象的方式:
直接量方式

使用直接量构建对象是最基础的方式,但也有很多弊端。
view source
print?
1 var Obj = new Object;
2 Obj.name = 'sun';
3 Obj.showName = function() {
4     alert('this.name');
5 }

我们构建了一个对象Obj,它有一个属性name,一个方法showName。但是如果我们要再构建一个类似的对象呢?难道还要再重复一遍?NO!,我们可以用一个返回特定类型对象的工厂函数来实现。就像工厂一样,流水线的输出我们要的特定类型结果。
工厂方式
view source
print?
01 function createObj(name) {
02     var tempObj = new Object;
03     tempObj.name = name;
04     tempObj.showName = function () {
05         alert(this.name);
06     };
07     return tempObj;
08 }
09 var obj1 = createObj('obj_one');
10 var obj2 = createObj('obj_two');

这种工厂函数很多人是不把他当做构建对象的一种形式的。一部分原因是语义:即它并不像使用了运算符new来构建的那么正规。还有一个更大的原因,是因为这个工厂每次产出一个对象都会创建一个新函数showName(),即每个对象拥有不同的版本,但实际上他们共享的是同一个函数。

有些人把showName在工厂函数外定义,然后通过属性指向该方法,可以避开这个问题:
view source
print?
01 function showName () {
02     alert(this.name);
03 }  
04 function createObj(name) {
05     var tempObj = new Object;
06     tempObj.name = name;
07     tempObj.showName = showName;
08     return tempObj;
09 }
10 var obj1 = createObj('obj_one');
11 var obj2 = createObj('obj_two');

可惜的是,这种方式让showName()这个函数看起来不像对象的一个方法。
构造函数方式

这种方式是为了解决上面工厂函数的第一个问题,即没有new运算符的问题。可是第二个问题它依然不能解决。我们来看看。
view source
print?
1 function Obj(name) {
2     this.name = name;
3     this.showName = function () {
4         alert(this.name);
5     }
6 }
7 var obj1 = new Obj('obj_one');
8 var obj2 = new Obj('obj_two');

它的好处是不用在构造函数内新建一个对象了,因为new运算符执行的时候会自动创建一个对象,并且只有通过this才能访问这个对象。所以我们可以直接通过this来对这个对象进行赋值。而且不用再return,因为this指向默认为构造函数的返回值。同时,用了new关键字来创建我们想要的对象是不是感觉更"正式"了。可惜,它仍然不能解决会重复生成方法函数的问题,这个情况和工厂函数一样。
原型方式

这种方式对比以上方式,有个很大的优势,就是它解决了方法函数会被生成多次的问题。它利用了对象的prototype属性。我们依赖原型可以重写对象实例。
view source
print?
1 var Obj = function () {}
2 Obj.prototype.name = 'me';
3 Obj.prototype.showName = function () {
4     alert(this.name);
5 }
6 var obj1 = new Obj();
7 var obj2 = new Obj();

我们依赖原型对构造函数进行重写,无论是属性还是方法都是通过原型引用的方式给新建的对象,因此都只会被创建一次。可惜的是,这种方式存在两个致命的问题:

   1. 没办法在构建对象的时候就写入想要的属性,因为原型在构造函数作用域外边,没办法通过传递参数的方式在对象创建的时候就写入属性值。只能在对象创建完毕后对值进行重写。
   2. 致命问题在于当属性指向对象时,这个对象会被多个实例所共享。考虑下面的代码:

view source
print?
01 var Ob