JavaScript 创建对象的方式
文章来源:《JavaScript 实战》(ISBN:9787115189158)P23
JavaScript 的语法比较多,提供了不止一种的方法来创建对象。
简单的对象创建
可能最简单的创建对象的方法是用一个新的 Object 开始,然后向其中添加内容。想要创建一个新 Object,你只要这么做:
var newObject = new Object();
变量 newObject 现在指向一个 Object 的实例,Object 是 JavaScript 中所有对象的基类。要给它增加一个元素,比如说:增加一个叫 firstName 的元素,你要做的只是:
newObject.firstName = "frank";
从代码的这个位置开始,newObject.firstName 就将含有一个值 “frank”,除非它在后面被修改了。你也可以很容易地添加函数:
newObject.sayName = function(){
alert(this.firstName);
}
现在 newObject.sayName () 调用的结果,是弹出一个 “frank” 的警告消息。与大多数成熟的面向对象语言不同的是,在 JavaScript 中,你不必为一个对象实例创建类或者蓝图(类的结构)。你可以像这里所写的那样,在运行时创建它。在对象的整个生命周期中都可以这么做。在网页中,这就意味着你可以在任何时候给对象添加属性和方法。
事实上,JavaScript 实现只是把所有对象当作关联数组。然后给数组加上了一个面具,使它的语法看起来更像 Java 或者 C++,使用点分隔表示法。为了强调这一点,你可以像这样检索 newObject 中 firstName 字段的值:
var theFirstName = newObject["firstName"];
同样,可以这样调用 sayName () 函数:
newObject["sayName"]();
这个简单的功能可以算是很多强大功能的基础。比如,假如你想基于某种逻辑(这里是判断)调用某个对象的方法又如何呢?哦,你可以这么做:
var whatFunction;
if(whatVolume == 1){
whatFunction = "sayName";
}
if(whatVolume == 2){
whatFunction = "sayLoudly";
}
newObject["whatFunction"](); //根据上面的赋值,将会调用不同的函数
假设我们已经将函数 sayLoudly () 添加到 newObject,这个函数在 firstName 字段中的 alert () 之前调用 toUpperCase ()。然后我们可以基于一个变量的值,来控制对象 “大声”(全部大写)地或者 “温柔”(如上所示,全部小写)地说出名字。
当向一个对象添加函数的时候,你可以使用已存在的函数。作为例子,让我们来继续添加那个 sayLoudly () 函数:
function sayLoudly() {
alert(this.firstName.toUpperCase());
}
newObject.sayLoudly = sayLoudly;
请注意这里 this 关键字的用法。可以说,它指向的对象将会在运行时动态计算。因此,在这个例子中,this 指向的是函数 sayLoudly () 作为其成员的那个类 —— 这里是 newObject。有意思的是,如果 sayLoudly () 完全是另一个对象的一部分,关键字 this 就会引用另一个对象。这种运行时绑定也是 JavaScript 面向对象实现的一个非常强大的特性,因为它允许代码共享,本质上来说,是一种继承的形式。
使用 JSON 创建对象
由于 JSON(JavaScript Object Notation,JavaScript 对象表示法)在 Ajax 请求中的使用而被越来越广泛的关注,所以很多人了解它。然而,一些人还不知道 JSON 实际上是 JavaScript 规范中的一个核心部分,而且它在 Ajax 登台之前就已经存在了。它最初的目的是为了快速简便地定义复杂的对象关系图,也就是那些嵌套于其他对象中的对象的实例。虽然它看上去如此的简单,但它允许了创建对象的另外一种方式。
还记得我们曾经说过,JavaScript 里的对象只是隐藏在面具下的关联数组吗?这就是 JSON 可运转的因素。让我们来看看如何使用 JSON 来创建前面的例子中的 newObject:
function sayLoudly() {
alert(this.firstName.toUpperCase());
}
var newObject = {
firstName : "frank",
sayName : function() { alert(this.firstName); },
sayLoudly : sayLoudly
};
使用 JSON 和定义一个数组非常类似,除了你需要使用花括号而不是方括号。注意函数可以是内联的,也可以引用外部函数。(看到 sayLoudly : sayLoudly 可能有一些困惑,但是 JavaScript 理解为第一个 sayLoudly 是对象的一个成员,而第二个 sayLoudly 是对一个已存在的对象的引用。)
在 JSON 中,你可以随意地嵌套对象定义来创建对象的层级关系。例如,我们向 newObject 中添加一个名为 LastName 的对象:
function sayLoudly(){
alert(this.firstName.toUpperCase());
}
var newObject = {
firstName : "frank",
sayName : function() { alert(this.firstName); },
sayLoudly : sayLoudly,
LastName : {
lastName : "Zammetti",
sayName : function() { alert(this.lastName); }
}
};
然后,你可以通过下面的调用来显示姓的部分:
newObject.LastName.sayName();
类的定义
在 JavaScript 中,其实所有的东西都是对象。这是事实,只有一小部分例外,比如一些内置的原语。这部分讨论的最重要的一点是,函数本身就是对象。你已经了解到如何创建一个 Object 的实例并向其中添加属性和方法了,但是那意味着每次创建对象的一个新的实例时,你都要从零开始创建,这里肯定有更好的方法,不是吗?当然有:创建一个类。
在 JavaScript 中,类实际上就是一个函数。这个函数同样被当作类的构造函数来提供服务。那么,例如,让我们把 newObject 写作一个类,重命名为 newClass:
function newClass {
alert("constructor");
this.firstName = "frank";
this.sayName = function() {
alert(this.firstName);
}
}
var nc = new newClass();
nc.sayName();
执行这段代码的时候,你会先后看到两个警告信息:首先,当执行到 var nc = new newClass (); 这行的时候,弹出 “constructor”, 然后,当执行到 nc.sayName (); 这行的时候弹出 “frank”。你想要创建多少 newClass 的实例就可以创建多少,而且它们将会含有同样的属性和方法。一旦创建,它们还将弹出同样的警告信息,firstName 含有同样的初始值。简而言之,你为创建 newClass 对象创建了一个蓝图,你定义了一个类。
但是,由此引出的一个问题是,newClass 的每一个实例都含有 firstName 的一个副本和 sayName () 方法的一个副本,那么每个实例都将占用了更多的内存。newClass 的每个副本都有各自的 firstName 副本,这个可能是你想要的,但是如果所有的实例可以共享相同的 sayName () 副本的话,是不是更好了,能否节省些内存?很明显,在这个例子中,我们并不能有大量的内存节省,但是你可以设想一些更大的代码的情况,那些场合下,很可能差别就比较大了。幸运的是,有一种方法可以这样做。
原型
JavaScript 中的每一个独立的对象都有一个与之关联的原型 (prototype) 属性。在我所知道的其他语言中,并没有完全与 prototype 相等的东西,但是它可以被看作一个简化的继承形式。基本上,它的工作方式是:当你创建一个对象的新实例时候,定义在对象的原型中的所有属性和方法,在运行时都会附着在那个新的实例上。
我知道,第一次看到这个概念时,可能有一些难以理解,但是幸运的是,它很容易演示:
function newClass(){
this.firstName = "frank";
}
newClass.prototype.sayName = function() {
alert(this.firstName);
}
var nc = new newClass();
nc.sayName();
执行的时候,这个代码会弹出同样的警告信息说 “frank”。与前面的例子不同的是,无论你创建了多少个 newClass 的实例,在内存中 sayName () 函数只会有一个单独的实例。这个方法实际上是附加在每个实例上,而且 this 关键字还是在运行时被计算的。所以 this 通常指向它所属的那个特定的 newClass 实例。例如,如果你有两个 newClass 的实例分别叫做 nc1 和 nc2,然后一个对 nc1.sayName () 的调用将 this 指向 nc1,一个对 nc2.sayName () 的调用将 this 指向 nc2。
你应该使用哪种方法呢
前面所说的每个方法都有各自的优点和缺点,而且我怀疑是否存在谁好谁坏的共识。它们在功能上都是相同的,所以很大程度上它取决于你喜欢代码看起来像什么样子。也就是说,我想有一些常用的准则来帮助你做决定。
最重要的一个可能是,如果你要创建一个类,这个类非常大,而且它可能会有复杂的实例,那么几乎可以肯定要使用原型的方法。这样可以带来最好的内存使用效率,这通常是一个重要的目标。
如果你要创建一个单独的类而且知道这个类将只有一个实例,我个人倾向定义一个类。对我来说,这样的代码逻辑化最强、最类似于更全面的面向对象语言的,因此可能更易于项目中的新开发人员理解。
如果你的对象层级关系嵌套层次很多或者你需要在一个动态方式中定义一个对象(一段逻辑代码的输出结果),那么,JSON 方法可能是一个好的选择。如果需要将对象序列化并且通过网络进行传输,JSON 也几乎非常明显是首选。如果你需要重构一个从服务器传送过来的对象时也是如此。我怀疑做这些事情的时候,没有比 JSON 更简单的方法,这不是小范围的怀疑,而是因为这些事情就是设计 JSON 的目的。
面向对象的好处
无论你选择哪种方法,将代码面向对象化都有很多好处。一个重要的好处就是,每一个对象本质上就是一个命名空间。可以用此来模拟 Java 和 C# 的包。
另一个好处是可以使用对象来隐藏数据。看看下面的代码:
function newClass(){
this.firstName = "Frank";
lastName = "Zend";
}
var nc = new newClass();
alert(nc.firstName);
alert(nc.lastName);
这段代码的执行会有两个警告信息:第一个说 “Frank”,第二个说 undefined“。这是因为 lastName 字段在 newClass 的实例外是不可访问的。请注意字段定义的不同。任何使用 this 关键字定义的字段,都只能在类的内部访问。这个规则对于方法也同样适用。
同样,不要忘记 JavaScript 的内置对象可以通过它们的原型来扩展,事实上,JavaScript 的名为 Prototype.js 的库就是在做这个事。但是,如果不小心的话,你真的可能把事情搞乱糟,所以扩展内置对象的时候要警慎。
你可以从其他的对象中” 借 “函数,并把它们添加到自己的对象中。例如,假设你想要通过输出 newClass 本身来显示 newClass 的 firstName 字段。为实现这个目标,你实现了一个 toString () 函数。再假设你还想在它上面使用 String 对象中的 toUpperCase () 函数。很简单,我们可以这么做:
function newClass(){
this.firstName = "frank";
this.toUC = String.toUpperCase();
this.toString = function(){
return this.toUC(this.firstName);
}
}
var nc = new newClass();
alert(nc);
执行这段代码,结果是一个警告提示说”FRANK“。注意调用了 toString () 这个函数,但并不是作为 firstName String 对象的一个方法。取而代之的,是通过指向它的一个引用调用的,这个引用是 newClass 的属性之一,名为 toUC ()。这是一个很好用的功能,尤其是当你创建自己的对象之后,又想创建一个新的,并使用原来的代码。那你不用复制、剪切以及粘贴,只需引用其他类里的已存在的方法就可以了。