# 對象創建模式
在JavaScript中創建對象很容易——可以通過使用對象直接量或者構造函數。本章將在此基礎上介紹一些常用的對象創建模式。
JavaScript語言本身簡單、直觀,通常也沒有其他語言那樣的語法特性:命名空間、模塊、包、私有屬性以及靜態成員。本章將介紹一些常用的模式,以此實現這些語法特性。
我們將對命名空間、依賴聲明、模塊模式以及沙箱模式進行初探——它們幫助更好地組織應用程序的代碼,有效地減輕全局污染的問題。除此之外,還會對包括:私有和特權成員、靜態和私有靜態成員、對象常量、鏈以及類式函數定義方式在內的話題進行討論。
## 命名空間模式(Namespace Pattern)
命名空間可以幫助減少全局變量的數量,與此同時,還能有效地避免命名沖突、名稱前綴的濫用。
JavaScript默認語法并不支持命名空間,但很容易可以實現此特性。為了避免產生全局污染,你可以為應用或者類庫創建一個(通常就一個)全局對象,然后將所有的功能都添加到這個對象上,而不是到處申明大量的全局函數、全局對象以及其他全局變量。
看如下例子:
// BEFORE: 5 globals
// Warning: antipattern
// constructors
function Parent() {}
function Child() {}
// a variable
var some_var = 1;
// some objects
var module1 = {};
module1.data = {a: 1, b: 2};
var module2 = {};
可以通過創建一個全局對象(通常代表應用名)來重構上述這類代碼,比方說, MYAPP,然后將上述例子中的函數和變量都變為該全局對象的屬性:
// AFTER: 1 global
// global object
var MYAPP = {};
// constructors
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// a variable
MYAPP.some_var = 1;
// an object container
MYAPP.modules = {};
// nested objects
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};
這里的MYAPP就是命名空間對象,對象名可以隨便取,可以是應用名、類庫名、域名或者是公司名都可以。開發者經常約定全局變量都采用大寫(所有字母都大寫),這樣可以顯得比較突出(不過,要記住,一般大寫的變量都用于表示常量)。
這種模式是一種很好的提供命名空間的方式,避免了自身代碼的命名沖突,同時還避免了同一個頁面上自身代碼和第三方代碼(比如:JavaScript類庫或者小部件)的沖突。這種模式在大多數情況下非常適用,但也有它的缺點:
* 代碼量稍有增加;在每個函數和變量前加上這個命名空間對象的前綴,會增加代碼量,增大文件大小
* 該全局實例可以被隨時修改
* 命名的深度嵌套會減慢屬性值的查詢
本章后續要介紹的沙箱模式則可以避免這些缺點。
###通用命名空間函數
隨著程序復雜度的提高,代碼會分置在不同的文件中以特定順序來加載,這樣一來,就不能保證你的代碼一定是第一個申明命名空間或者改變量下的屬性的。甚至還會發生屬性覆蓋的問題。所以,在創建命名空間或者添加屬性的時候,最好先檢查下是否存在,如下所示:
// unsafe
var MYAPP = {};
// better
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// or shorter
var MYAPP = MYAPP || {};
如上所示,不難看出,如果每次做類似操作都要這樣檢查一下就會有很多重復性的代碼。比方說,要申明**MYAPP.modules.module2**,就要重復三次這樣的檢查。所以,我們需要一個重用的**namespace()**函數來專門處理這些檢查工作,然后用它來創建命名空間,如下所示:
// using a namespace function
MYAPP.namespace('MYAPP.modules.module2');
// equivalent to:
// var MYAPP = {
// modules: {
// module2: {}
// }
// };
下面是上述namespace函數的實現案例。這種實現是無損的,意味著如果要創建的命名空間已經存在,則不會再重復創建:
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// strip redundant leading global
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
上述實現支持如下使用:
// assign returned value to a local var
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true
// skip initial `MYAPP`
MYAPP.namespace('modules.module51');
// long namespace
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');
圖5-1 展示了上述代碼創建的命名空間對象在Firebug下的可視結果

圖5-1 MYAPP命名空間在Firebug下的可視結果
## 聲明依賴
JavaScript庫往往是模塊化而且有用到命名空間的,這使用你可以只使用你需要的模塊。比如在YUI2中,全局變量YAHOO就是一個命名空間,各個模塊作為全局變量的屬性,比如YAHOO.util.Dom(DOM模塊)、YAHOO.util.Event(事件模塊)。
將你的代碼依賴在函數或者模塊的頂部進行聲明是一個好主意。聲明就是創建一個本地變量,指向你需要用到的模塊:
var myFunction = function () {
// dependencies
var event = YAHOO.util.Event,
dom = YAHOO.util.Dom;
// use event and dom variables
// for the rest of the function...
};
這是一個相當簡單的模式,但是有很多的好處:
- 明確的聲明依賴是告知你代碼的用戶,需要保證指定的腳本文件被包含在頁面中。
- 將聲明放在函數頂部使得依賴很容易被查找和解析。
- 本地變量(如dom)永遠會比全局變量(如YAHOO)要快,甚至比全局變量的屬性(如YAHOO.util.Dom)還要快,這樣會有更好的性能。使用了依賴聲明模式之后,全局變量的解析在函數中只會進行一次,在此之后將會使用更快的本地變量。
- 一些高級的代碼壓縮工具比如YUI Compressor和Google Closure compiler會重命名本地變量(比如event可能會被壓縮成一個字母,如A),這會使代碼更精簡,但這個操作不會對全局變量進行,因為這樣做不安全。
下面的代碼片段是關于是否使用依賴聲明模式對壓縮影響的展示。盡管使用了依賴聲明模式的test2()看起來復雜,因為需要更多的代碼行數和一個額外的變量,但在壓縮后它的代碼量卻會更小,意味著用戶只需要下載更少的代碼:
function test1() {
alert(MYAPP.modules.m1);
alert(MYAPP.modules.m2);
alert(MYAPP.modules.m51);
}
/*
minified test1 body:
alert(MYAPP.modules.m1);alert(MYAPP.modules.m2);alert(MYAPP.modules.m51)
*/
function test2() {
var modules = MYAPP.modules;
alert(modules.m1);
alert(modules.m2);
alert(modules.m51);
}
/*
minified test2 body:
var a=MYAPP.modules;alert(a.m1);alert(a.m2);alert(a.m51)
*/
## 私有屬性和方法
JavaScript不像Java或者其它語言,它沒有專門的提供私有、保護、公有屬性和方法的語法。所有的對象成員都是公有的:
var myobj = {
myprop: 1,
getProp: function () {
return this.myprop;
}
};
console.log(myobj.myprop); // `myprop` is publicly accessible console.log(myobj.getProp()); // getProp() is public too
當你使用構造函數創建對象的時候也是一樣的,所有的成員都是公有的:
function Gadget() {
this.name = 'iPod';
this.stretch = function () {
return 'iPad';
};
}
var toy = new Gadget();
console.log(toy.name); // `name` is public console.log(toy.stretch()); // stretch() is public
### 私有成員
盡管語言并沒有用于私有成員的專門語法,但你可以通過閉包來實現。在構造函數中創建一個閉包,任何在這個閉包中的部分都不會暴露到構造函數之外。但是,這些私有變量卻可以被公有方法訪問,也就是在構造函數中定義的并且作為返回對象一部分的那些方法。我們來看一個例子,name是一個私有成員,在構造函數之外不能被訪問:
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
}
var toy = new Gadget();
// `name` is undefined, it's private
console.log(toy.name); // undefined
// public method has access to `name`
console.log(toy.getName()); // "iPod"
如你所見,在JavaScript創建私有成員很容易。你需要做的只是將私有成員放在一個函數中,保證它是函數的本地變量,也就是說讓它在函數之外不可以被訪問。
### 特權方法
特權方法的概念不涉及到任何語法,它只是一個給可以訪問到私有成員的公有方法的名字(就像它們有更多權限一樣)。
在前面的例子中,getName()就是一個特權方法,因為它有訪問name屬性的特殊權限。
### 私有成員失效
當你使用私有成員時,需要考慮一些極端情況:
- 在Firefox的一些早期版本中,允許通過給eval()傳遞第二個參數的方法來指定上下文對象,從而允許訪問函數的私有作用域。比如在Mozilla Rhino(譯注:一個JavaScript引擎)中,允許使用`__parent__`來訪問私有作用域。現在這些極端情況并沒有被廣泛應用到瀏覽器中。
- 當你直接通過特權方法返回一個私有變量,而這個私有變量恰好是一個對象或者數組時,外部的代碼可以修改這個私有變量,因為它是按引用傳遞的。
我們來看一下第二種情況。下面的Gadget的實現看起來沒有問題:
function Gadget() {
// private member
var specs = {
screen_width: 320,
screen_height: 480,
color: "white"
};
// public function
this.getSpecs = function () {
return specs;
};
}
這里的問題是getSpecs()返回了一個specs對象的引用。這使得Gadget的使用者可以修改貌似隱藏起來的私有成員specs:
var toy = new Gadget(),
specs = toy.getSpecs();
specs.color = "black";
specs.price = "free";
console.dir(toy.getSpecs());
在Firebug控制臺中打印出來的結果如圖5-2:

圖5-2 私有對象被修改了
這個意外的問題的解決方法就是不要將你想保持私有的對象或者數組的引用傳遞出去。達到這個目標的一種方法是讓getSpecs()返回一個新對象,這個新對象只包含對象的使用者感興趣的數據。這也是眾所周知的“最低授權原則”(Principle of Least Authority,簡稱POLA),指永遠不要給出比需求更多的東西。在這個例子中,如果Gadget的使用者關注它是否適應一個特定的盒子,它只需要知道尺寸即可。所以你應該創建一個getDimensions(),用它返回一個只包含width和height的新對象,而不是把什么都給出去。也就是說,也許你根本不需要實現getSpecs()方法。
當你需要傳遞所有的數據時,有另外一種方法,就是使用通用的對象復制函數創建specs對象的一個副本。下一章提供了兩個這樣的函數——一個叫extend(),它會淺復制一個給定的對象(只復制頂層的成員)。另一個叫extendDeep(),它會做深復制,遍歷所有的屬性和嵌套的屬性。
### 對象字面量和私有成員
到目前為止,我們只看了使用構建函數創建私有成員的示例。如果使用對象字面量創建對象時會是什么情況呢?是否有可能含有私有成員?
如你前面所看到的那樣,私有數據使用一個函數來包裹。所以在使用對象字面量時,你也可以使用一個立即執行的匿名函數創建的閉包。例如:
var myobj; // this will be the object
(function () {
// private members
var name = "my, oh my";
// implement the public part
// note -- no `var`
myobj = {
// privileged method
getName: function () {
return name;
}
};
}());
myobj.getName(); // "my, oh my"
還有一個原理一樣但看起來不一樣的實現示例:
var myobj = (function () {
// private members
var name = "my, oh my";
// implement the public part
return {
getName: function () {
return name;
}
};
}());
myobj.getName(); // "my, oh my"
這個例子也是所謂的“模塊模式”的基礎,我們稍后將講到它。
### 原型和私有成員
使用構造函數創建私有成員的一個弊端是,每一次調用構造函數創建對象時這些私有成員都會被創建一次。
這對在構建函數中添加到`this`的成員來說是一個問題。為了避免重復勞動,節省內存,你可以將共用的屬性和方法添加到構造函數的`prototype`(原型)屬性中。這樣的話這些公共的部分會在使用同一個構造函數創建的所有實例中共享。你也同樣可以在這些實例中共享私有成員。你可以將兩種模式聯合起來達到這個目的:構造函數中的私有屬性和對象字面量中的私有屬性。因為`prototype`屬性也只是一個對象,可以使用對象字面量創建。
這是一個示例:
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
}
Gadget.prototype = (function () {
// private member
var browser = "Mobile Webkit";
// public prototype members
return {
getBrowser: function () {
return browser;
}
};
}());
var toy = new Gadget();
console.log(toy.getName()); // privileged "own" method console.log(toy.getBrowser()); // privileged prototype method
### 將私有函數暴露為公有方法
“暴露模式”是指將已經有的私有函數暴露為公有方法。當對對象進行操作時,所有功能代碼都對這些操作很敏感,而你想盡量保護這些代碼的時候很有用。(譯注:指對來自外部的修改很敏感。)但同時,你又希望能提供一些功能的訪問權限,因為它們會被用到。如果你把這些方法公開,就會使得它們不再健壯,你的API的使用者可能修改它們。在ECMAScript5中,你可以選擇凍結一個對象,但在之前的版本中不可用。下面進入暴露模式(原來是由Christian Heilmann創造的模式,叫“暴露模塊模式”)。
我們來看一個例子,它建立在對象字面量的私有成員模式之上:
var myarray;
(function () {
var astr = "[object Array]",
toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return ?1;
}
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());
這里有兩個私有變量和兩個私有函數——`isArray()`和`indexOf()`。在包裹函數的最后,使用那些允許被從外部訪問的函數填充`myarray`對象。在這個例子中,同一個私有函數 `indexOf()`同時被暴露為ECMAScript 5風格的`indexOf`和PHP風格的`inArry`。測試一下myarray對象:
myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2
現在假如有一些意外的情況發生在暴露的`indexOf()`方法上,私有的`indexOf()`方法仍然是安全的,因此`inArray()`仍然可以正常工作:
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2
## 模塊模式
模塊模式使用得很廣泛,因為它可以為代碼提供特定的結構,幫助組織日益增長的代碼。不像其它語言,JavaScript沒有專門的“包”(package)的語法,但模塊模式提供了用于創建獨立解耦的代碼片段的工具,這些代碼可以被當成黑盒,當你正在寫的軟件需求發生變化時,這些代碼可以被添加、替換、移除。
模塊模式是我們目前討論過的好幾種模式的組合,即:
- 命名空間模式
- 立即執行的函數模式
- 私有和特權成員模式
- 依賴聲明模式
第一步是初始化一個命名空間。我們使用本章前面部分的`namespace()`函數,創建一個提供數組相關方法的套件模塊:
MYAPP.namespace('MYAPP.utilities.array');
下一步是定義模塊。使用一個立即執行的函數來提供私有作用域供私有成員使用。立即執行的函數返回一個對象,也就是帶有公有接口的真正的模塊,可以供其它代碼使用:
MYAPP.utilities.array = (function () {
return {
// todo...
};
}());
下一步,給公有接口添加一些方法:
MYAPP.utilities.array = (function () {
return {
inArray: function (needle, haystack) {
// ...
},
isArray: function (a) {
// ...
}
};
}());
如果需要的話,你可以在立即執行的函數提供的閉包中聲明私有屬性和私有方法。函數頂部也是聲明依賴的地方。在變量聲明的下方,你可以選擇性地放置輔助初始化模塊的一次性代碼。函數最終返回的是一個包含模塊公共API的對象:
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties
array_string = "[object Array]",
ops = Object.prototype.toString;
// private methods
// ...
// end var
// optionally one-time init procedures
// ...
// public API
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},
isArray: function (a) {
return ops.call(a) === array_string;
}
// ... more methods and properties
};
}());
模塊模式被廣泛使用,這是一種值得強烈推薦的模式,它可以幫助組織代碼,尤其是代碼量在不斷增長的時候。
### 暴露模塊模式
我們在本章中討論私有成員模式時已經討論過暴露模式。模塊模式也可以用類似的方法來組織,將所有的方法保持私有,只在最后暴露需要使用的方法來初始化API。
上面的例子可以變成這樣:
MYAPP.utilities.array = (function () {
// private properties
var array_string = "[object Array]",
ops = Object.prototype.toString,
// private methods
inArray = function (haystack, needle) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return ?1;
},
isArray = function (a) {
return ops.call(a) === array_string;
};
// end var
// revealing public API
return {
isArray: isArray,
indexOf: inArray
};
}());
### 創建構造函數的模塊
前面的例子創建了一個對象`MYAPP.utilities.array`,但有時候使用構造函數來創建對象會更方便。你也可以同樣使用模塊模式來做。唯一的區別是包裹模塊的立即執行的函數會在最后返回一個函數,而不是一個對象。
看下面的模塊模式的例子,創建了一個構造函數`MYAPP.utilities.Array`:
MYAPP.namespace('MYAPP.utilities.Array');
MYAPP.utilities.Array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties and methods...
Constr;
// end var
// optionally one-time init procedures
// ...
// public API -- constructor
Constr = function (o) {
this.elements = this.toArray(o);
};
// public API -- prototype
Constr.prototype = {
constructor: MYAPP.utilities.Array,
version: "2.0",
toArray: function (obj) {
for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
a[i] = obj[i];
}
return a;
}
};
// return the constructor
// to be assigned to the new namespace return Constr;
}());
像這樣使用這個新的構造函數:
var arr = new MYAPP.utilities.Array(obj);
### 在模塊中引入全局上下文
作為這種模式的一個常見的變種,你可以給包裹模塊的立即執行的函數傳遞參數。你可以傳遞任何值,但通常會傳遞全局變量甚至是全局對象本身。引入全局上下文可以加快函數內部的全局變量的解析,因為引入之后會作為函數的本地變量:
MYAPP.utilities.module = (function (app, global) {
// references to the global object
// and to the global app namespace object
// are now localized
}(MYAPP, this));
## 沙箱模式
沙箱模式主要著眼于命名空間模式的短處,即:
- 依賴一個全局變量成為應用的全局命名空間。在命名空間模式中,沒有辦法在同一個頁面中運行同一個應用或者類庫的不同版本,在為它們都會需要同一個全局變量名,比如`MYAPP`。
- 代碼中以點分隔的名字比較長,無論寫代碼還是解析都需要處理這個很長的名字,比如`MYAPP.utilities.array`。
顧名思義,沙箱模式為模塊提供了一個環境,模塊在這個環境中的任何行為都不會影響其它的模塊和其它模塊的沙箱。
這個模式在YUI3中用得很多,但是需要記住的是,下面的討論只是一些示例實現,并不討論YUI3中的消息箱是如何實現的。
### 全局構造函數
在命名空間模式中 ,有一個全局對象,而在沙箱模式中,唯一的全局變量是一個構造函數,我們把它命名為`Sandbox()`。我們使用這個構造函數來創建對象,同時也要傳入一個回調函數,這個函數會成為代碼運行的獨立空間。
使用沙箱模式是像這樣:
new Sandbox(function (box) {
// your code here...
});
`box`對象和命名空間模式中的`MYAPP`類似,它包含了所有你的代碼需要用到的功能。
我們要多做兩件事情:
- 通過一些手段(第3章中的強制使用new的模式),你可以在創建對象的時候不要求一定有new。
- 讓`Sandbox()`構造函數可以接受一個(或多個)額外的配置參數,用于指定這個對象需要用到的模塊名字。我們希望代碼是模塊化的,因此絕大部分`Sandbox()`提供的功能都會被包含在模塊中。
有了這兩個額外的特性之后,我們來看一下實例化對象的代碼是什么樣子。
你可以在創建對象時省略`new`并像這樣使用已有的“ajax”和“event”模塊:
Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});
下面的例子和前面的很像,但是模塊名字是作為獨立的參數傳入的:
Sandbox('ajax', 'dom', function (box) {
// console.log(box);
});
使用通配符“*”來表示“使用所有可用的模塊”如何?為了方便,我們也假設沒有任何模塊傳入時,沙箱使用“*”。所以有兩種使用所有可用模塊的方法:
Sandbox('*', function (box) {
// console.log(box);
});
Sandbox(function (box) {
// console.log(box);
});
下面的例子展示了如何實例化多個消息箱對象,你甚至可以將它們嵌套起來而互不影響:
Sandbox('dom', 'event', function (box) {
// work with dom and event
Sandbox('ajax', function (box) {
// another sandboxed "box" object
// this "box" is not the same as
// the "box" outside this function
//...
// done with Ajax
});
// no trace of Ajax module here
});
從這些例子中看到,使用沙箱模式可以通過將代碼包裹在回調函數中的方式來保護全局命名空間。
如果需要的話,你也可以利用函數也是對象這一事實,將一些數據作為靜態屬性存放到`Sandbox()`構造函數。
最后,你可以根據需要的模塊類型創建不同的實例,這些實例都是相互獨立的。
現在我們來看一下如何實現`Sandbox()`構造函數和它的模塊來支持上面講到的所有功能。
### 添加模塊
在動手實現構造函數之前,我們來看一下如何添加模塊。
`Sandbox()`構造函數也是一個對象,所以可以給它添加一個`modules`靜態屬性。這個屬性也是一個包含名值(key-value)對的對象,其中key是模塊的名字,value是模塊的功能實現。
Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
box.getElement = function () {};
box.getStyle = function () {};
box.foo = "bar";
};
Sandbox.modules.event = function (box) {
// access to the Sandbox prototype if needed:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {};
box.dettachEvent = function () {};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {};
box.getResponse = function () {};
};
在這個例子中我們添加了`dom`、`event`和`ajax`模塊,這些都是在每個類庫或者復雜的web應用中很常見的代碼片段。
實現每個模塊功能的函數接受一個實例`box`作為參數,并給這個實例添加屬性和方法。
### 實現構造函數
最后,我們來實現`Sandbox()`構造函數(你可能會很自然地想將這類構造函數命名為對你的類庫或者應用有意義的名字):
function Sandbox() {
// turning arguments into an array
var args = Array.prototype.slice.call(arguments),
// the last argument is the callback
callback = args.pop(),
// modules can be passed as an array or as individual parameters
modules = (args[0] && typeof args[0] === "string") ? args : args[0], i;
// make sure the function is called
// as a constructor
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}
// add properties to `this` as needed:
this.a = 1;
this.b = 2;
// now add modules to the core `this` object
// no modules or "*" both mean "use all modules"
if (!modules || modules === '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}
// initialize the required modules
for (i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}
// call the callback
callback(this);
}
// any prototype properties as needed
Sandbox.prototype = {
name: "My Application",
version: "1.0",
getName: function () {
return this.name;
}
};
這個實現中的一些關鍵點:
- 有一個檢查`this`是否是`Sandbox`實例的過程,如果不是(也就是調用`Sandbox()`時沒有加`new`),我們將這個函數作為構造函數再調用一次。
- 你可以在構造函數中給`this`添加屬性,也可以給構造函數的原型添加屬性。
- 被依賴的模塊可以以數組的形式傳遞,也可以作為單獨的參數傳遞,甚至以`*`通配符(或者省略)來表示加載所有可用的模塊。值得注意的是,我們在這個示例實現中并沒有考慮從外部文件中加載模塊,但明顯這是一個值得考慮的事情。比如YUI3就支持這種情況,你可以只加載最基本的模塊(作為“種子”),其余需要的任何模塊都通過將模塊名和文件名對應的方式從外部文件中加載。
- 當我們知道依賴的模塊之后就初始化它們,也就是調用實現每個模塊的函數。
- 構造函數的最后一個參數是回調函數。這個回調函數會在最后使用新創建的實例來調用。事實上這個回調函數就是用戶的沙箱,它被傳入一個`box`對象,這個對象包含了所有依賴的功能。
## 靜態成員
靜態屬性和方法是指那些在所有的實例中保持一致的成員。在基于類的語言中,表態成員是用專門的語法來創建,使用時就像是類自己的成員一樣。比如`MathUtils`類的`max()`方法會被像這樣調用:`MathUtils.max(3, 5)`。這是一個公有靜態成員的示例,即可以在不實例化類的情況下使用。同樣也可以有私有的靜態方法,即對類的使用者不可見,而在類的所有實例間是共享的。我們來看一下如何在JavaScript中實現公有和私有靜態成員。
### 公有靜態成員
在JavaScript中沒有專門用于靜態成員的語法。但通過給構造函數添加屬性的方法,可以擁有和基于類的語言一樣的使用語法。之所有可以這樣做是因為構造函數和其它的函數一樣,也是對象,可以擁有屬性。前一章討論過的Memoization模式也使用了同樣的方法,即給函數添加屬性。
下面的例子定義了一個構造函數`Gadget`,它有一個靜態方法`isShiny()`和一個實例方法`setPrice()`。`isShiny()`是一個靜態方法,因為它不需要指定一個對象才能工作(就像你不需要先指定一個工具(gadget)才知道所有的工具是不是有光澤的(shiny))。但setPrice()卻需要一個對象,因為工具可能有不同的定價:
// constructor
var Gadget = function () {};
// a static method
Gadget.isShiny = function () {
return "you bet";
};
// a normal method added to the prototype Gadget.prototype.setPrice = function (price) {
this.price = price;
};
現在我們來調用這些方法。靜態方法`isShiny()`可以直接在構造函數上調用,但其它的常規方法需要一個實例:
// calling a static method
Gadget.isShiny(); // "you bet"
// creating an instance and calling a method
var iphone = new Gadget();
iphone.setPrice(500);
使用靜態方法的調用方式去調用實例方法并不能正常工作,同樣,用調用實例方法的方式來調用靜態方法也不能正常工作:
typeof Gadget.setPrice; // "undefined"
typeof iphone.isShiny; // "undefined"
有時候讓靜態方法也能用在實例上會很方便。我們可以通過在原型上加一個新方法來很容易地做到這點,這個新方法作為原來的靜態方法的一個包裝:
Gadget.prototype.isShiny = Gadget.isShiny;
iphone.isShiny(); // "you bet"
在這種情況下,你需要很小心地處理靜態方法內的`this`。當你運行`Gadget.isShiny()`時,在`isShiny()`內部的`this`指向`Gadget`構造函數。而如果你運行`iphone.isShiny()`,那么`this`會指向`iphone`。
最后一個例子展示了同一個方法被靜態調用和非靜態調用時明顯不同的行為,這取決于調用的方式。這里的`instanceof`用于獲方法是如何被調用的:
// constructor
var Gadget = function (price) {
this.price = price;
};
// a static method
Gadget.isShiny = function () {
// this always works
var msg = "you bet";
if (this instanceof Gadget) {
// this only works if called non-statically
msg += ", it costs $" + this.price + '!';
}
return msg;
};
// a normal method added to the prototype
Gadget.prototype.isShiny = function () {
return Gadget.isShiny.call(this);
};
測試一下靜態方法調用:
Gadget.isShiny(); // "you bet"
測試一下實例中的非靜態調用:
var a = new Gadget('499.99');
a.isShiny(); // "you bet, it costs $499.99!"
### 私有靜態成員
到目前為止,我們都只討論了公有的靜態方法,現在我們來看一下如何實現私有靜態成員。所謂私有靜態成員是指:
- 被所有由同一構造函數創建的對象共享
- 不允許在構造函數外部訪問
我們來看一個例子,`counter`是`Gadget`構造函數的一個私有靜態屬性。在本章中我們已經討論過私有屬性,這里的做法也是一樣,需要一個函數提供的閉包來包裹私有成員。然后讓這個包裹函數立即執行并返回一個新的函數。將這個返回的函數賦值給`Gadget`作為構造函數。
var Gadget = (function () {
// static variable/property
var counter = 0;
// returning the new implementation
// of the constructor
return function () {
console.log(counter += 1);
};
}()); // execute immediately
這個`Gadget`構造函數只簡單地增加私有的`counter`的值然后打印出來。用多個實例測試的話你會看到`counter`在實例之間是共享的:
var g1 = new Gadget();// logs 1
var g2 = new Gadget();// logs 2
var g3 = new Gadget();// logs 3
因為我們在創建每個實例的時候`counter`的值都會加1,所以它實際上成了唯一標識使用`Gadget`構造函數創建的對象的ID。這個唯一標識可能會很有用,那為什么不把它通用一個特權方法暴露出去呢?(譯注:其實這里不能叫ID,只是一個記錄有多少個實例的數字而已,因為如果有多個實例被創建的話,其實已經沒辦法取到前面實例的標識了。)下面的例子是基于前面的例子,增加了用于訪問私有靜態屬性的`getLastId()`方法:
// constructor
var Gadget = (function () {
// static variable/property
var counter = 0,
NewGadget;
// this will become the
// new constructor implementation
NewGadget = function () {
counter += 1;
};
// a privileged method
NewGadget.prototype.getLastId = function () {
return counter;
};
// overwrite the constructor
return NewGadget;
}()); // execute immediately
測試這個新的實現:
var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId(); // 2
var ipad = new Gadget();
ipad.getLastId(); // 3
靜態屬性(包括私有和公有)有時候會非常方便,它們可以包含和具體實例無關的方法和數據,而不用在每次實例中再創建一次。當我們在第七章中討論單例模式時,你可以看到使用靜態屬性實現類式單例構造函數的例子。
## 對象常量
JavaScript中是沒有常量的,盡管在一些比較現代的環境中可能會提供`const`來創建常量。
一種常用的解決辦法是通過命名規范,讓不應該變化的變量使用全大寫。這個規范實際上也用在JavaScript原生對象中:
Math.PI; // 3.141592653589793
Math.SQRT2; // 1.4142135623730951
Number.MAX_VALUE; // 1.7976931348623157e+308
你自己的常量也可以用這種規范,然后將它們作為靜態屬性加到構造函數中:
// constructor
var Widget = function () {
// implementation...
};
// constants
Widget.MAX_HEIGHT = 320;
Widget.MAX_WIDTH = 480;
同樣的規范也適用于使用字面量創建的對象,常量會是使用大寫名字的普通名字。
如果你真的希望有一個不能被改變的值,那么可以創建一個私有屬性,然后提供一個取值的方法(getter),但不給賦值的方法(setter)。這種方法在很多可以用命名規范解決的情況下可能有些矯枉過正,但不失為一種選擇。
下面是一個通過的`constant`對象的實現,它提供了這些方法:
- set(name, value)
定義一個新的常量
- isDefined(name)
檢查一個常量是否存在
- get(name)
取常量的值
在這個實現中,只允許基本類型的值成為常量。同時還要使用`hasOwnproperty()`小心地處理那些恰好是原生屬性的常量名,比如`toString`或者`hasOwnProperty`,然后給所有的常量名加上一個隨機生成的前綴:
var constant = (function () {
var constants = {},
ownProp = Object.prototype.hasOwnProperty,
allowed = {
string: 1,
number: 1,
boolean: 1
},
prefix = (Math.random() + "_").slice(2);
return {
set: function (name, value) {
if (this.isDefined(name)) {
return false;
}
if (!ownProp.call(allowed, typeof value)) {
return false;
}
constants[prefix + name] = value;
return true;
},
isDefined: function (name) {
return ownProp.call(constants, prefix + name);
},
get: function (name) {
if (this.isDefined(name)) {
return constants[prefix + name];
}
return null;
}
};
}());
測試這個實現:
// check if defined
constant.isDefined("maxwidth"); // false
// define
constant.set("maxwidth", 480); // true
// check again
constant.isDefined("maxwidth"); // true
// attempt to redefine
constant.set("maxwidth", 320); // false
// is the value still intact?
constant.get("maxwidth"); // 480
## 鏈式調用模式
使用鏈式調用模式可以讓你在一對個象上連續調用多個方法,不需要將前一個方法的返回值賦給變量,也不需要將多個方法調用分散在多行:
myobj.method1("hello").method2().method3("world").method4();
當你創建了一個沒有有意義的返回值的方法時,你可以讓它返回this,也就是這些方法所屬的對象。這使得對象的使用者可以將下一個方法的調用和前一次調用鏈起來:
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
alert(this.value);
}
};
// chain method calls
obj.increment().add(3).shout(); // 5
// as opposed to calling them one by one
obj.increment();
obj.add(3);
obj.shout(); // 5
### 鏈式調用模式的利弊
使用鏈式調用模式的一個好處就是可以節省代碼量,使得代碼更加簡潔和易讀,讀起來就像在讀句子一樣。
另外一個好處就是幫助你思考如何拆分你的函數,創建更小、更有針對性的函數,而不是一個什么都做的函數。長時間來看,這會提升代碼的可維護性。
一個弊端是調用這樣寫的代碼會更困難。你可能知道一個錯誤出現在某一行,但這一行要做很多的事情。當鏈式調用的方法中的某一個出現問題而又沒報錯時,你無法知曉到底是哪一個出問題了。《代碼整潔之道》的作者Robert Martion甚至叫這種模式為“train wreck”模式。(譯注:直譯為“火車事故”,指負面影響比較大。)
不管怎樣,認識這種模式總是好的,當你寫的方法沒有明顯的有意義的返回值時,你就可以返回`this`。這個模式應用得很廣泛,比如jQuery庫。如果你去看DOM的API的話,你會發現它也會以這樣的形式傾向于鏈式調用:
document.getElementsByTagName('head')[0].appendChild(newnode);
## method()方法
JavaScript對于習慣于用類來思考的人來說可能會比較費解,這也是很多開發者希望將JavaScript代碼變得更像基于類的語言的原因。其中的一種嘗試就是由Douglas Crockford提出來的`method()`方法。其實,他也承認將JavaScript變得像基于類的語言是不推薦的方法,但不管怎樣,這都是一種有意思的模式,你可能會在一些應用中見到。
使用構造函數主須Java中使用類一樣。它也允許你在構造函數體的`this`中添加實例屬性。但是在`this`中添加方法卻是不高效的,因為最終這些方法會在每個實例中被重新創建一次,這樣會花費更多的內存。這也是為什么可重用的方法應該被放到構造函數的`prototype`屬性(原型)中的原因。但對很多開發者來說,`prototype`可能跟個外星人一樣陌生,所以你可以通過一個方法將它隱藏起來。
> 給語言添加一個使用起來更方便的方法一般叫作“語法糖”。在這個例子中,你可以將`method()`方法稱為一個語法糖方法。
使用這個語法糖方法`method()`來定義一個“類”是像這樣:
var Person = function (name) {
this.name = name;
}.
method('getName', function () {
return this.name;
}).
method('setName', function (name) {
this.name = name;
return this;
});
注意構造函數和調用`method()`是如何鏈起來的,接下來又鏈式調用了下一個`method()`方法。這就是我們前面討論的鏈式調用模式,可以幫助我們用一個語句完成對整個“類”的定義。
`method()`方法接受兩個參數:
- 新方法的名字
- 新方法的實現
然后這個新方法被添加到`Person`“類”。新方法的實現也只是一個函數,在這個函數里面`this`指向由`Person`創建的對象,正如我們期望的那樣。
下面是使用`Person()`創建和使用新對象的代碼:
var a = new Person('Adam');
a.getName(); // 'Adam'
a.setName('Eve').getName(); // 'Eve'
同樣地注意鏈式調用,因為`setName()`返回了`this`就可以鏈式調用了。
最后是`method()`方法的實現:
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function (name, implementation) {
this.prototype[name] = implementation;
return this;
};
}
在`method()`的實現中,我們首先檢查這個方法是否已經被實現過,如果沒有則繼續,將傳入的參數`implementation`加到構造函數的原型中。在這里`this`指向構造函數,而我們要增加的功能正在在這個構造函數的原型上。
## 小結
在本章中你看到了好幾種除了字面量和構造函數之外的創建對象的方法。
你看到了使用命名空間模式來保持全局空間干凈和幫助組織代碼。看到了簡單而又有用的依賴聲明模式。然后我們詳細討論了有關私有成員的模式,包括私有成員、特權方法以及一些涉及私有成員的極端情況,還有使用對象字面量創建私有成員以及將私有方法暴露為公有方法。所有這些模式都是搭建起現在流行而強大的模塊模式的積木。
然后你看到了使用沙箱模式作為長命名空間的另一種選擇,它可以為你的代碼和模塊提供獨立的環境。
在最后,我們深入討論了對象常量、靜態成員(公有和私有)、鏈式調用模式,以及神奇的`method()`方法。