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