摘要:因?yàn)榭赡艽嬖谝粋€(gè)同名的構(gòu)造函數(shù),當(dāng)你調(diào)用的時(shí)候,解析器需要順著作用域鏈從當(dāng)前作用域開(kāi)始查找,直到找到全局構(gòu)造函數(shù)為止。
簡(jiǎn)介
在軟件開(kāi)發(fā)過(guò)程中,模式是指一個(gè)通用問(wèn)題的解決方案。一個(gè)模式不僅僅是一個(gè)可以用來(lái)復(fù)制粘貼的代碼解決方案,更多地是提供了一個(gè)更好的實(shí)踐經(jīng)驗(yàn)、有用的抽象化表示和解決一類(lèi)問(wèn)題的模板。
對(duì)象有兩大類(lèi):
本地對(duì)象(Native):由ECMAScript標(biāo)準(zhǔn)定義的對(duì)象
宿主對(duì)象(Host):由宿主環(huán)境創(chuàng)建的對(duì)象(比如瀏覽器環(huán)境)
本地對(duì)象也可以被歸類(lèi)為內(nèi)置對(duì)象(比如Array、Date)或自定義對(duì)象(var o = {})。
宿主對(duì)象包含window和所有DOM對(duì)象。如果你想知道你是否在使用宿主對(duì)象,將你的代碼遷移到一個(gè)非瀏覽器環(huán)境中運(yùn)行一下,如果正常工作,那么你的代碼就只用到了本地對(duì)象。
“GoF”的書(shū)中提到一條通用規(guī)則,“組合優(yōu)于繼承”。
console 對(duì)象
基本技巧易維護(hù)的代碼具有如下特性:
可讀的
風(fēng)格一致
可預(yù)測(cè)的
看起來(lái)像一個(gè)人寫(xiě)的
有文檔
盡量少用全局變量 全局變量的問(wèn)題隱式創(chuàng)建全部變量有兩種情況:
未經(jīng)聲明的變量就為全局對(duì)象所有。
function sum(x, y){ result = x + y; // result 是全局變量; }
帶有 var 聲明的鏈?zhǔn)劫x值
function foo(){ var a = b = 0; // b 是全局變量 }
由于 = 的運(yùn)算順序是從右到左。即 var a = b = 0; 等價(jià)于 var a = (b = 0)。
因此,對(duì)于鏈?zhǔn)劫x值建議做法如下:
function foo(){ var a, b; a = b = 0; }
隱式創(chuàng)建的全局變量與明確定義的全局變量的不同之處在于:是否能被 delete 操作符刪除。
使用 var 創(chuàng)建的全局變量(在函數(shù)外創(chuàng)建)不能刪除
不使用 var 創(chuàng)建的隱式全局變量(就算在函數(shù)內(nèi)創(chuàng)建)可以刪除
var a = 1; b = 2; (function(){ c = 3; })(); delete a; // false; delete b; // true; delete c; // true typeof a; // number typeof b; // undefined typeof c; // undefined
在ES5 strict模式下,為未聲明的變量賦值會(huì)拋出錯(cuò)誤。
單一var模式在函數(shù)頂部對(duì)所有變量通過(guò)一個(gè) var 進(jìn)行聲明。好處如下:
可以在同一個(gè)位置找到函數(shù)所需的所有變量
避免在變量聲明之前使用這個(gè)變量時(shí)產(chǎn)生的邏輯錯(cuò)誤
提醒你不要忘記聲明變量,順便減少潛在的全局變量
代碼量更少
例子:
function func(){ var a = 1, b = 2, sum = a + b, myobject = {}, i, j; console.log(sum) // 函數(shù)體 } func()
使用逗號(hào)操作符可以在一條語(yǔ)句中執(zhí)行多個(gè)操作。多用于聲明多個(gè)變量,但還可以用于賦值,總會(huì)返回表達(dá)式的最后一項(xiàng)。
(1) 逗號(hào)表達(dá)式的運(yùn)算過(guò)程為:從左往右逐個(gè)計(jì)算表達(dá)式。
(2) 逗號(hào)表達(dá)式作為一個(gè)整體,它的值為最后一個(gè)表達(dá)式的值。var num = (5,4,1,0); // num 為 0。
(3) 逗號(hào)運(yùn)算符的優(yōu)先級(jí)別在所有運(yùn)算符中最低。
使用 === 或 !== 進(jìn)行比較。增強(qiáng)代碼可閱讀性,避免猜測(cè)。
另外,switch 語(yǔ)句的 case 進(jìn)行比較時(shí),使用的是 ===。
new Functin() 與 eval()的不同:
第一點(diǎn):
new Function()中的代碼將在局部函數(shù)空間運(yùn)行,因此代碼中任何采用var定義的變量不會(huì)自動(dòng)成為全局變量(即在函數(shù)內(nèi))。
eval()則會(huì)自動(dòng)成為全局變量,但可通過(guò)立即調(diào)用函數(shù)對(duì)其進(jìn)行封裝。
console.log(typeof un);// "undefined" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" var jsstring = "var un = 1; console.log(un);"; eval(jsstring); // 打印出 "1" jsstring = "var deux = 2; console.log(deux);"; new Function(jsstring)(); // 打印出 "2" jsstring = "var trois = 3; console.log(trois);"; (function () { eval(jsstring); }()); // 打印出 "3" console.log(typeof un); // "number" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined"
第二點(diǎn):
eval()會(huì)影響到作用域鏈,而Function則像一個(gè)沙盒,無(wú)論在哪里執(zhí)行Function,它都僅能看到全局作用域鏈。因此對(duì)局部變量的影響比較小。
(function () { var local = 1; eval("local = 3; console.log(local)"); // 打印出 3 console.log(local); // 打印出 3 }()); (function () { var local = 1; Function("console.log(typeof local);")(); // 打印出 undefined }());使用parseInt()進(jìn)行數(shù)字轉(zhuǎn)換
ECMAScript3中以0為前綴的字符串會(huì)被當(dāng)作八進(jìn)制數(shù)處理,這一點(diǎn)在ES5中已經(jīng)有了改變。為了避免轉(zhuǎn)換類(lèi)型不一致而導(dǎo)致的意外結(jié)果,應(yīng)當(dāng)總是指定第二個(gè)參數(shù):
var month = "06", year = "09"; month = parseInt(month, 10); year = parseInt(year, 10);
字符串轉(zhuǎn)換為數(shù)字還有兩種方法:
+"08" // 結(jié)果為8,隱式調(diào)用Number() Number("08") // 結(jié)果為8
這兩種方法要比parseInt()更快一些,因?yàn)轭櫭剂xparseInt()是一種“解析”而不是簡(jiǎn)單的“轉(zhuǎn)換”。但當(dāng)你期望將“08 hello”這類(lèi)字符串轉(zhuǎn)換為數(shù)字,則必須使用parseInt(),其他方法都會(huì)返回NaN。
命名約定構(gòu)造函數(shù)首字母大寫(xiě)
函數(shù)用小駝峰式(getFirstName),變量用“所有單詞小寫(xiě),并用下劃線分隔各個(gè)單詞”(first_name)。這樣就能區(qū)分函數(shù)和變量了。
常量和全局變量的所有字符大寫(xiě)
私有成員函數(shù)用下劃線(_)前綴命名
當(dāng)然,還要正確編寫(xiě)注釋和更新注釋。最好能編寫(xiě) API 文檔。
字面量與構(gòu)造函數(shù)// 一種方法,使用字面量 var car = {goes: "far"}; // 另一種方法,使用內(nèi)置構(gòu)造函數(shù) // 注意:這是一種反模式 var car = new Object(); car.goes = "far";
字面量寫(xiě)法的一個(gè)明顯優(yōu)勢(shì)是,它的代碼更少。“創(chuàng)建對(duì)象的最佳模式是使用字面量”還有一個(gè)原因,它可以強(qiáng)調(diào)對(duì)象就是一個(gè)簡(jiǎn)單的可變的散列表,而不必一定派生自某個(gè)類(lèi)。
另外一個(gè)使用字面量而不是Object()構(gòu)造函數(shù)創(chuàng)建實(shí)例對(duì)象的原因是,對(duì)象字面量不需要“作用域解析”(scope resolution)。因?yàn)榭赡艽嬖谝粋€(gè)同名的構(gòu)造函數(shù)Object(),當(dāng)你調(diào)用Object()的時(shí)候,解析器需要順著作用域鏈從當(dāng)前作用域開(kāi)始查找,直到找到全局Object()構(gòu)造函數(shù)為止。
Object()構(gòu)造函數(shù)僅接受一個(gè)參數(shù),且依賴于傳遞的值,該Object()根據(jù)值委派另一個(gè)內(nèi)置構(gòu)造函數(shù)來(lái)創(chuàng)建對(duì)象,并返回另外一個(gè)對(duì)象實(shí)例,而這往往不是你想要的。
// 空對(duì)象 var o = new Object(); console.log(o.constructor === Object); // true // 數(shù)值對(duì)象 var o = new Object(1); console.log(o.constructor === Number); // true console.log(o.toFixed(2)); // "1.00" // 字符串對(duì)象 var o = new Object("I am a string"); console.log(o.constructor === String); // true // 普通對(duì)象沒(méi)有substring()方法,但字符串對(duì)象有 console.log(typeof o.substring); // "function" // 布爾值對(duì)象 var o = new Object(true); console.log(o.constructor === Boolean); // true強(qiáng)制使用new的模式
對(duì)于構(gòu)造函數(shù),若忘記使用 new 操作符,會(huì)導(dǎo)致構(gòu)造函數(shù)中的this指向全局對(duì)象(嚴(yán)格模式下,指向undeinfed)。
為了防止忘記 new,我們使用下面的方法:在構(gòu)造函數(shù)中首先檢查this是否是構(gòu)造函數(shù)的實(shí)例,如果不是,則通過(guò)new再次調(diào)用自己:
function Waffle() { // Waffle 可換成 arguments.callee(指向當(dāng)前執(zhí)行的函數(shù)) if (!(this instanceof Waffle)) { return new Waffle(); } this.tastes = "yummy"; } Waffle.prototype.wantAnother = true; // 測(cè)試 var first = new Waffle(), second = Waffle(); console.log(first.tastes); // "yummy" console.log(second.tastes); // "yummy" console.log(first.wantAnother); // true console.log(second.wantAnother); // true函數(shù) 重定義函數(shù)
函數(shù)可以被動(dòng)態(tài)定義,也可以被賦值給變量。如果將你定義的函數(shù)賦值給已經(jīng)存在的函數(shù)變量的話,則新函數(shù)會(huì)覆蓋舊函數(shù)。這樣做的結(jié)果是,舊函數(shù)的引用被丟棄掉,變量中所存儲(chǔ)的引用值替換成了新的函數(shù)。這樣看起來(lái)這個(gè)變量指代的函數(shù)邏輯就發(fā)生了變化,或者說(shuō)函數(shù)進(jìn)行了“重新定義”或“重寫(xiě)”。
var scareMe = function () { alert("Boo!"); scareMe = function () { alert("Double boo!"); }; }; // 使用重定義函數(shù) scareMe(); // Boo! scareMe(); // Double boo!
當(dāng)函數(shù)中包含一些初始化操作,并希望這些初始化操作只執(zhí)行一次,那么這種模式是非常合適的,因?yàn)槲覀円苊庵貜?fù)執(zhí)行不需要的代碼。在這個(gè)場(chǎng)景中,函數(shù)執(zhí)行一次后就被重寫(xiě)為另外一個(gè)函數(shù)了。
使用這種模式可以幫助提高應(yīng)用的執(zhí)行效率,因?yàn)橹匦露x的函數(shù)執(zhí)行的代碼量更少。
這種模式的另外一個(gè)名字是“函數(shù)的懶惰定義”,因?yàn)橹钡胶瘮?shù)執(zhí)行一次后才重新定義,可以說(shuō)它是“某個(gè)時(shí)間點(diǎn)之后才存在”,簡(jiǎn)稱“懶惰定義”。
這種模式有一個(gè)明顯的缺陷,就是之前給原函數(shù)添加的功能在重定義之后都丟失了。同時(shí),如果這個(gè)函數(shù)被重定義為不同的名字,被賦值給不同的變量,或者是作為對(duì)象的方法使用,那么重定義的部分并不會(huì)生效,原來(lái)的函數(shù)依然會(huì)被執(zhí)行。
條件初始化條件初始化(也叫條件加載)是一種優(yōu)化模式。當(dāng)你知道某種條件在整個(gè)程序生命周期中都不會(huì)變化的時(shí)候,那么對(duì)這個(gè)條件的探測(cè)只做一次就很有意義。瀏覽器探測(cè)(或者特征檢測(cè))是一個(gè)典型的例子。
// 接口 var utils = { addListener: null, removeListener: null }; // 實(shí)現(xiàn) if (typeof window.addEventListener === "function") { utils.addListener = function (el, type, fn) { el.addEventListener(type, fn, false); }; utils.removeListener = function (el, type, fn) { el.removeEventListener(type, fn, false); }; } else if (typeof document.attachEvent === "function") { // IE utils.addListener = function (el, type, fn) { el.attachEvent("on" + type, fn); }; utils.removeListener = function (el, type, fn) { el.detachEvent("on" + type, fn); }; } else { // older browsers utils.addListener = function (el, type, fn) { el["on" + type] = fn; }; utils.removeListener = function (el, type, fn) { el["on" + type] = null; }; }
當(dāng)然,重定義函數(shù)也能實(shí)現(xiàn)這種效果。
函數(shù)屬性——記憶模式(Memoization)任何時(shí)候都可以給函數(shù)添加自定義屬性。添加自定義屬性的一個(gè)有用場(chǎng)景是緩存函數(shù)的執(zhí)行結(jié)果(返回值),這樣下次同樣的函數(shù)被調(diào)用的時(shí)候就不需要再做一次那些可能很復(fù)雜的計(jì)算。緩存一個(gè)函數(shù)的運(yùn)行結(jié)果也就是為大家所熟知的記憶模式。
var myFunc = function (param) { if (!myFunc.cache[param]) { var result = {}; // ……復(fù)雜的計(jì)算…… myFunc.cache[param] = result; } return myFunc.cache[param]; }; // 緩存 myFunc.cache = {};
上面的代碼假設(shè)函數(shù)只接受一個(gè)參數(shù)param,并且這個(gè)參數(shù)是原始類(lèi)型(比如字符串)。如果你有更多更復(fù)雜的參數(shù),則通常需要對(duì)它們進(jìn)行序列化。比如,你需要將arguments對(duì)象序列化為JSON字符串,然后使用JSON字符串作為cache對(duì)象的key:
var myFunc = function () { var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)), result; if (!myFunc.cache[cachekey]) { result = {}; // ……復(fù)雜的計(jì)算…… myFunc.cache[cachekey] = result; } return myFunc.cache[cachekey]; }; // 緩存 myFunc.cache = {};
前面代碼中的函數(shù)名還可以使用arguments.callee來(lái)替代,這樣就不用將函數(shù)名硬編碼。不過(guò)盡管現(xiàn)階段這個(gè)辦法可行,但是仍然需要注意,arguments.callee在ECMAScript5的嚴(yán)格模式中是不被允許的。
柯里化是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
下面是通用的柯里化函數(shù):
function cury(fn){ var slice = Array.prototype.slice; var stored_args = slice.call(arguments, 1); return function(){ var new_args = slice.call(arguments), args = stored_args.concat(new_args); return fn.apply(null, args); } } // 測(cè)試 function sum(){ var result = 0; for(var i = 0, len = arguments.length; i < len; i++){ result += arguments[i]; } return result; } var newSum = cury(sum, 1,2,3,4,5,6); console.log(newSum(2,3,5,4)); // 35什么時(shí)候使用柯里化
當(dāng)你發(fā)現(xiàn)自己在調(diào)用同樣的函數(shù)并且傳入的參數(shù)大部分都相同的時(shí)候,就是考慮柯里化的理想場(chǎng)景了。你可以通過(guò)傳入一部分的參數(shù)動(dòng)態(tài)地創(chuàng)建一個(gè)新的函數(shù)。這個(gè)新函數(shù)會(huì)存儲(chǔ)那些重復(fù)的參數(shù)(所以你不需要再每次都傳入),然后再在調(diào)用原始函數(shù)的時(shí)候?qū)⒄麄€(gè)參數(shù)列表補(bǔ)全。
小結(jié)apply()接受兩個(gè)參數(shù):第一個(gè)是在函數(shù)內(nèi)部綁定到this上的對(duì)象,第二個(gè)是一個(gè)參數(shù)數(shù)組,參數(shù)數(shù)組會(huì)在函數(shù)內(nèi)部變成一個(gè)類(lèi)似數(shù)組的arguments對(duì)象。如果第一個(gè)參數(shù)為 null,那么this將指向全局對(duì)象,這正是當(dāng)你調(diào)用一個(gè)函數(shù)(且這個(gè)函數(shù)不是某個(gè)對(duì)象的方法)時(shí)發(fā)生的事情。
在介紹完背景和函數(shù)的語(yǔ)法后,介紹了一些有用的模式,按分類(lèi)列出:
API模式,它們幫助我們?yōu)楹瘮?shù)給出更干凈的接口,包括:
回調(diào)模式
傳入一個(gè)函數(shù)作為參數(shù)
配置對(duì)象
幫助保持函數(shù)的參數(shù)數(shù)量可控
返回函數(shù)
函數(shù)的返回值是另一個(gè)函數(shù)
柯里化
新函數(shù)在已有函數(shù)的基礎(chǔ)上再加上一部分參數(shù)構(gòu)成
初始化模式,這些模式幫助我們用一種干凈的、結(jié)構(gòu)化的方法來(lái)做一些初始化工作(在web頁(yè)面和應(yīng)用中非常常見(jiàn)),通過(guò)一些臨時(shí)變量來(lái)保證不污染全局命名空間。這些模式包括:
即時(shí)函數(shù)
當(dāng)它們被定義后立即執(zhí)行
對(duì)象即時(shí)初始化
初始化工作被放入一個(gè)匿名對(duì)象,這個(gè)對(duì)象提供一個(gè)可以立即被執(zhí)行的方法
條件初始化
使分支代碼只在初始化的時(shí)候執(zhí)行一次,而不是在整個(gè)程序生命周期中反復(fù)執(zhí)行
性能模式,這些模式幫助提高代碼的執(zhí)行速度,包括:
記憶模式
利用函數(shù)的屬性,使已經(jīng)計(jì)算過(guò)的值不用再次計(jì)算
自定義函數(shù)
重寫(xiě)自身的函數(shù)體,使第二次及后續(xù)的調(diào)用做更少的工作對(duì)象創(chuàng)建模式
JavaScript語(yǔ)言本身很簡(jiǎn)單、直觀,也沒(méi)有其他語(yǔ)言的一些語(yǔ)言特性:命名空間、模塊、包、私有屬性以及靜態(tài)成員。本章將介紹一些常用的模式,以此實(shí)現(xiàn)這些語(yǔ)言特性。
我們將對(duì)命名空間、依賴聲明、模塊模式以及沙箱模式進(jìn)行初探——它們可以幫助我們更好地組織應(yīng)用程序的代碼,有效地減少全局污染的問(wèn)題。除此之外,還會(huì)討論私有和特權(quán)成員、靜態(tài)和私有靜態(tài)成員、對(duì)象常量、鏈?zhǔn)秸{(diào)用以及一種像類(lèi)式語(yǔ)言一樣定義構(gòu)造函數(shù)的方法等話題。
命名空間模式使用命名空間可以減少全局變量的數(shù)量,與此同時(shí),還能有效地避免命名沖突和前綴的濫用。
本章后續(xù)要介紹的沙箱模式則可以避免這些缺點(diǎn)。
// 重構(gòu)前:5個(gè)全局變量 // 注意:反模式 // 構(gòu)造函數(shù) function Parent() {} function Child() {} // 一個(gè)變量 var some_var = 1; // 一些對(duì)象 var module1 = {}; module1.data = {a: 1, b: 2}; var module2 = {};
可以通過(guò)創(chuàng)建一個(gè)全局對(duì)象(通常代表應(yīng)用名)比如MYAPP來(lái)重構(gòu)上述這類(lèi)代碼,然后將上述例子中的函數(shù)和變量都變?yōu)樵撊謱?duì)象的屬性:
// 重構(gòu)后:一個(gè)全局變量 // 全局對(duì)象 var MYAPP = {}; // 構(gòu)造函數(shù) MYAPP.Parent = function () {}; MYAPP.Child = function () {}; // 一個(gè)變量 MYAPP.some_var = 1; // 一個(gè)對(duì)象容器 MYAPP.modules = {}; // 嵌套的對(duì)象 MYAPP.modules.module1 = {}; MYAPP.modules.module1.data = {a: 1, b: 2}; MYAPP.modules.module2 = {};
這種模式在大多數(shù)情況下非常適用,但也有它的缺點(diǎn):
代碼量稍有增加;在每個(gè)函數(shù)和變量前加上這個(gè)命名空間對(duì)象的前綴,會(huì)增加代碼量,增大文件大小
該全局實(shí)例可以被隨時(shí)修改
命名的深度嵌套會(huì)減慢屬性值的查詢
本章后續(xù)要介紹的沙箱模式則可以避免這些缺點(diǎn)。
通用命名空間函數(shù)隨著程序復(fù)雜度的提高,代碼會(huì)被分拆在不同的文件中以按照頁(yè)面需要來(lái)加載,這樣一來(lái),就不能保證你的代碼一定是第一個(gè)定義命名空間或者某個(gè)屬性的,甚至?xí)l(fā)生屬性覆蓋的問(wèn)題。所以,在創(chuàng)建命名空間或者添加屬性的時(shí)候,最好先檢查下是否存在,如下所示:
// 不安全的做法 var MYAPP = {}; // 更好的做法 if (typeof MYAPP === "undefined") { var MYAPP = {}; } // 簡(jiǎn)寫(xiě) var MYAPP = MYAPP || {};
如上所示,如果每次做類(lèi)似操作都要這樣檢查一下就會(huì)有很多重復(fù)的代碼。例如,要聲明MYAPP.modules.module2,就要重復(fù)三次這樣的檢查。所以,我們需要一個(gè)可復(fù)用的namespace()函數(shù)來(lái)專門(mén)處理這些檢查工作,然后用它來(lái)創(chuàng)建命名空間,如下所示:
// 使用命名空間函數(shù) MYAPP.namespace("MYAPP.modules.module2"); // 等價(jià)于: // var MYAPP = { // modules: { // module2: {} // } // };
下面是上述namespace函數(shù)的實(shí)現(xiàn)示例。這種實(shí)現(xiàn)是非破壞性的,意味著如果要?jiǎng)?chuàng)建的命名空間已經(jīng)存在,則不會(huì)再重復(fù)創(chuàng)建:
var MYAPP = MYAPP || {}; MYAPP.namespace = function(ns_string){ var parts = ns_string.split("."), parent = MYAPP; if(parts[0] === "MYAPP"){ parts.shift(); } for(var i = 0, len = parts.length; i < len; i++){ if(parent[parts[i]] === undefined){ parent[parts[i]] = {} } parent = parent[parts[i]] } return parent; } var module2 = MYAPP.namespace("MYAPP.modules.module2"); console.log(module2 === MYAPP.modules.module2); // true var modules = MYAPP.namespace("modules"); console.log(modules === MYAPP.modules); // true依賴聲明
JavaScript庫(kù)往往是模塊化而且有用到命名空間的,這使得你可以只使用你需要的模塊。比如在YUI2中,全局變量YAHOO就是一個(gè)命名空間,各個(gè)模塊都是全局變量的屬性,比如YAHOO.util.Dom(DOM模塊)、YAHOO.util.Event(事件模塊)。
將你的代碼依賴在函數(shù)或者模塊的頂部進(jìn)行聲明是一個(gè)好主意。聲明就是創(chuàng)建一個(gè)本地變量,指向你需要用到的模塊:
var myFunction = function () { // 依賴 var event = YAHOO.util.Event, dom = YAHOO.util.Dom; // 在函數(shù)后面的代碼中使用event和dom…… };
這是一個(gè)相當(dāng)簡(jiǎn)單的模式,但是有很多的好處:
明確的依賴聲明是告知使用你代碼的開(kāi)發(fā)者,需要保證指定的腳本文件被包含在頁(yè)面中。
將聲明放在函數(shù)頂部使得依賴很容易被查找和解析。
本地變量(如dom)永遠(yuǎn)會(huì)比全局變量(如YAHOO)要快,甚至比全局變量的屬性(如YAHOO.util.Dom)還要快,這樣會(huì)有更好的性能。使用了依賴聲明模式之后,全局變量的解析在函數(shù)中只會(huì)進(jìn)行一次,在此之后將會(huì)使用更快的本地變量(備注:本地變量直接指向最后一級(jí)對(duì)象,event)。
一些高級(jí)的代碼壓縮工具比如YUI Compressor和Google Closure compiler會(huì)重命名本地變量(比如event可能會(huì)被壓縮成一個(gè)字母,如A),這會(huì)使代碼更精簡(jiǎn),但這個(gè)操作不會(huì)對(duì)全局變量進(jìn)行,因?yàn)檫@樣做不安全。
私有屬性和方法 私有成員通過(guò)閉包實(shí)現(xiàn):
function Gadget() { // 私有成員 var name = "iPod"; // 公有函數(shù) this.getName = function () { return name; }; } var toy = new Gadget(); // name是是私有的 console.log(toy.name); // undefined // 公有方法可以訪問(wèn)到name console.log(toy.getName()); // "iPod"特權(quán)方法
特權(quán)方法的概念不涉及到任何語(yǔ)法,它只是一個(gè)給可以訪問(wèn)到私有成員的公有方法的名字(就好像它們有更多權(quán)限一樣)。
在前面的例子中,getName()就是一個(gè)特權(quán)方法,因?yàn)樗性L問(wèn)name屬性的特殊權(quán)限。
當(dāng)你直接通過(guò)特權(quán)方法返回一個(gè)私有變量,而這個(gè)私有變量恰好是一個(gè)對(duì)象或者數(shù)組時(shí),外部的代碼可以修改這個(gè)私有變量,因?yàn)樗前匆脗鬟f的。
function Gadget() { // 私有成員 var specs = { screen_width: 320, screen_height: 480, color: "white" }; // 公有函數(shù) this.getSpecs = function () { return specs; // 直接返回對(duì)象(數(shù)組也是對(duì)象),會(huì)導(dǎo)致私有對(duì)象能在外面被修改 }; }
解決方法:返回精簡(jiǎn)后新對(duì)象(返回需要用到的部分屬性),或?qū)λ接袑?duì)象進(jìn)行復(fù)制(返回副本)。
原型和私有成員眾所周知的“最低授權(quán)原則”(Principle of Least Authority,簡(jiǎn)稱POLA),指永遠(yuǎn)不要給出比真實(shí)需要更多的東西。
使用構(gòu)造函數(shù)創(chuàng)建私有成員的一個(gè)弊端是,每一次調(diào)用構(gòu)造函數(shù)創(chuàng)建對(duì)象時(shí)這些私有成員都會(huì)被創(chuàng)建一次。
function Gadget() { // 私有成員 var name = "iPod"; // 公有函數(shù) this.getName = function () { return name; }; } Gadget.prototype = (function () { // 私有成員 var browser = "Mobile Webkit"; // 公有函數(shù) return { getBrowser: function () { return browser; } }; }()); var toy = new Gadget(); console.log(toy.getName()); // 自有的特權(quán)方法 console.log(toy.getBrowser()); // 來(lái)自原型的特權(quán)方法將私有函數(shù)暴露為公有方法
“暴露模式”是指將已經(jīng)有的私有函數(shù)暴露為公有方法。
我們來(lái)看一個(gè)例子,它建立在對(duì)象字面量的私有成員模式之上:
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 }; }());模塊模式
模塊模式使用得很廣泛,因?yàn)樗梢詾榇a提供特定的結(jié)構(gòu),幫助組織日益增長(zhǎng)的代碼。不像其它語(yǔ)言,JavaScript沒(méi)有專門(mén)的“包”(package)的語(yǔ)法,但模塊模式提供了用于創(chuàng)建獨(dú)立解耦的代碼片段的工具,這些代碼可以被當(dāng)成黑盒,當(dāng)你正在寫(xiě)的軟件需求發(fā)生變化時(shí),這些代碼可以被添加、替換、移除。
模塊模式是我們目前討論過(guò)的好幾種模式的組合,即:
命名空間模式
即時(shí)函數(shù)模式
私有和特權(quán)成員模式
依賴聲明模式
第一步是初始化一個(gè)命名空間。我們使用本章前面部分的namespace()函數(shù),創(chuàng)建一個(gè)提供數(shù)組相關(guān)方法的套件模塊:
MYAPP.namespace("MYAPP.utilities.array");
下一步是定義模塊。使用一個(gè)即時(shí)函數(shù)來(lái)提供私有作用域供私有成員使用。即時(shí)函數(shù)返回一個(gè)對(duì)象,也就是帶有公有接口的真正的模塊,可以供其它代碼使用:
MYAPP.utilities.array = (function () { return { // todo... }; }());
下一步,給公有接口添加一些方法:
MYAPP.utilities.array = (function () { return { inArray: function (needle, haystack) { // ... }, isArray: function (a) { // ... } }; }());
如果需要的話,你可以在即時(shí)函數(shù)提供的閉包中聲明私有屬性和私有方法。同樣,依賴聲明放置在函數(shù)頂部,在變量聲明的下方可以選擇性地放置輔助初始化模塊的一次性代碼。函數(shù)最終返回的是一個(gè)包含模塊公共API的對(duì)象:
MYAPP.namespace("MYAPP.utilities.array"); MYAPP.utilities.array = (function () { // 依賴聲明 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, // 私有屬性 array_string = "[object Array]", ops = Object.prototype.toString; // 私有方法 // …… // 結(jié)束變量聲明 // 選擇性放置一次性初始化的代碼 // …… // 公有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; } // ……更多的方法和屬性 }; }());
模塊模式被廣泛使用,是一種值得強(qiáng)烈推薦的模式,它可以幫助我們組織代碼,尤其是代碼量在不斷增長(zhǎng)的時(shí)候。
暴露模塊模式我們?cè)诒菊轮杏懻撍接谐蓡T模式時(shí)已經(jīng)討論過(guò)暴露模式。模塊模式也可以用類(lèi)似的方法來(lái)組織,將所有的方法保持私有,只在最后暴露需要使用的方法來(lái)初始化API。
上面的例子可以變成這樣:
MYAPP.utilities.array = (function () { // 私有屬性 var array_string = "[object Array]", ops = Object.prototype.toString, // 私有方法 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; }; // 結(jié)束變量定義 // 暴露公有API return { isArray: isArray, indexOf: inArray }; }());在模塊中引入全局上下文
作為這種模式的一個(gè)常見(jiàn)的變種,你可以給包裹模塊的即時(shí)函數(shù)傳遞參數(shù)。你可以傳遞任何值,但通常情況下會(huì)傳遞全局變量甚至是全局對(duì)象本身。引入全局上下文可以加快函數(shù)內(nèi)部的全局變量的解析,因?yàn)橐胫髸?huì)作為函數(shù)的本地變量:
MYAPP.utilities.module = (function (app, global) { // 全局對(duì)象和全局命名空間都作為本地變量存在 }(MYAPP, this));代碼復(fù)用模式
在做代碼復(fù)用的工作的時(shí)候,謹(jǐn)記Gang of Four在書(shū)中給出的關(guān)于對(duì)象創(chuàng)建的建議:“優(yōu)先使用對(duì)象創(chuàng)建而不是類(lèi)繼承”。
類(lèi)式(傳統(tǒng))繼承(classical inheritance) vs 現(xiàn)代繼承模式
類(lèi)式繼承:按照類(lèi)的方式考慮JavaScript,并產(chǎn)生了一些假定在類(lèi)的基礎(chǔ)上的開(kāi)發(fā)思路和繼承模式。
現(xiàn)代繼承模式:其他任何不需要以類(lèi)的方式考慮的模式。
當(dāng)需要給項(xiàng)目選擇一個(gè)繼承模式時(shí),有不少的備選方案。你應(yīng)該盡量選擇那些現(xiàn)代繼承模式,除非團(tuán)隊(duì)已經(jīng)覺(jué)得“無(wú)類(lèi)不歡”。
跳過(guò)類(lèi)繼承..
通過(guò)復(fù)制屬性實(shí)現(xiàn)繼承在這種模式中,一個(gè)對(duì)象通過(guò)簡(jiǎn)單地復(fù)制另一個(gè)對(duì)象來(lái)獲得功能。下面是一個(gè)簡(jiǎn)單的實(shí)現(xiàn)這種功能的extend()函數(shù):
function extend(parent, child) { var i; child = child || {}; for (i in parent) { if (parent.hasOwnProperty(i)) { child[i] = parent[i]; } } return child; }
上面給出的實(shí)現(xiàn)叫作對(duì)象的“淺拷貝”(shallow copy),與之相對(duì),“深拷貝”是指檢查準(zhǔn)備復(fù)制的屬性本身是否是對(duì)象或者數(shù)組,如果是,也遍歷它們的屬性并復(fù)制。如果使用淺拷貝的話(因?yàn)樵贘avaScript中對(duì)象是按引用傳遞),如果你改變子對(duì)象的一個(gè)屬性,而這個(gè)屬性恰好是一個(gè)對(duì)象,那么你也會(huì)改變父對(duì)象。實(shí)際上這對(duì)方法來(lái)說(shuō)可能很好(因?yàn)楹瘮?shù)也是對(duì)象,也是按引用傳遞),但是當(dāng)遇到其它的對(duì)象和數(shù)組的時(shí)候可能會(huì)有些意外情況。
現(xiàn)在讓我們來(lái)修改一下extend()函數(shù)以便實(shí)現(xiàn)深拷貝。你需要做的事情只是檢查一個(gè)屬性的類(lèi)型是否是對(duì)象,如果是,則遞歸遍歷它的屬性。另外一個(gè)需要做的檢查是這個(gè)對(duì)象是真的對(duì)象還是數(shù)組,可以使用第三章討論過(guò)的數(shù)組檢查方式。最終深拷貝版的extend()是這樣的:
function extendDeep(parent, child) { var i, toStr = Object.prototype.toString, astr = "[object Array]"; child = child || {}; for (i in parent) { if (parent.hasOwnProperty(i)) { if (typeof parent[i] === "object") { child[i] = (toStr.call(parent[i]) === astr) ? [] : {}; extendDeep(parent[i], child[i]); } else { child[i] = parent[i]; } } } return child; }
這種模式并不高深,因?yàn)楦緵](méi)有原型牽涉進(jìn)來(lái),而只跟對(duì)象和它們的屬性有關(guān)。
混元(Mix-ins)“混元”模式,從任意多數(shù)量的對(duì)象中復(fù)制屬性,然后將它們混在一起組成一個(gè)新對(duì)象。
function mix() { var arg, prop, child = {}; for (arg = 0; arg < arguments.length; arg += 1) { for (prop in arguments[arg]) { if (arguments[arg].hasOwnProperty(prop)) { child[prop] = arguments[arg][prop]; } } } return child; }
這里我們只是簡(jiǎn)單地遍歷、復(fù)制自有屬性,并沒(méi)有與父對(duì)象有任何鏈接。
借用方法apply、call、bind(ES5)。
apply是接受數(shù)組,而call是接受一個(gè)一個(gè)的參數(shù)。
在低于ES5的環(huán)境中時(shí)如何實(shí)現(xiàn)Function.prototype.bind():
if (typeof Function.prototype.bind === "undefined") { Function.prototype.bind = function (thisArg) { var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function () { return fn.apply(thisArg, args.concat(slice.call(arguments))); }; }; }小結(jié)
在JavaScript中,繼承有很多種方案可以選擇,在本章中你看到了很多類(lèi)式繼承和現(xiàn)代繼承的方案。學(xué)習(xí)和理解不同的模式是有好處的,因?yàn)檫@可以增強(qiáng)你對(duì)這門(mén)語(yǔ)言的掌握能力。
但是,也許在開(kāi)發(fā)過(guò)程中繼承并不是你經(jīng)常面對(duì)的一個(gè)問(wèn)題。一部分是因?yàn)檫@個(gè)問(wèn)題已經(jīng)被使用某種方式或者某個(gè)你使用的類(lèi)庫(kù)解決了,另一部分是因?yàn)槟悴恍枰贘avaScript中建立很長(zhǎng)很復(fù)雜的繼承鏈。在靜態(tài)強(qiáng)類(lèi)型語(yǔ)言中,繼承可能是唯一可以復(fù)用代碼的方法,但在JavaScript中有更多更簡(jiǎn)單更優(yōu)化的方法,包括借用方法、綁定、復(fù)制屬性、混元等。
記住,代碼復(fù)用才是目標(biāo),繼承只是達(dá)成這個(gè)目標(biāo)的一種手段。
DOM與瀏覽器模式 延遲加載所謂的延遲加載是指在頁(yè)面的load事件之后再加載外部文件。通常,將一個(gè)大的合并后的文件分成兩部分是有好處的:
一部分是頁(yè)面初始化和綁定UI元素的事件處理函數(shù)必須的
第二部分是只在用戶交互或者其它條件下才會(huì)用到的
分成兩部分的目標(biāo)就是逐步加載頁(yè)面,讓用戶盡快可以進(jìn)行一些操作。剩余的部分在用戶可以看到頁(yè)面的時(shí)候再在后臺(tái)加載。
加載第二部分JavaScript的方法也是使用動(dòng)態(tài)script元素,將它加在head或者body中:
……頁(yè)面主體部分……