模式是解決或者避免一些問題的方案。
在JavaScript中,會用到一些常用的編碼模式。下面就列出了一些常用的JavaScript編碼模式,有的模式是為了解決特定的問題,有的則是幫助我們避免一些JavaScript中容易出現的錯誤。
單一var模式
所謂“單一var模式”(Single var pattern)就是指在函數頂部,只使用一個var進行變量聲明的模式。例如:
function func() {
var a = 1,
b = 2,
sum = a + b,
myObject = {},
i,
j;
// other code
}
使用這個模式的好處:
- 在函數頂部展示了所有函數中使用的局部變量
- 防止變量提升引起的問題
變量提升
JavaScript允許在函數的任意地方聲明變量,但是效果都等同於在函數頂部進行聲明,這個是所謂的變量提升(Hoisting)。
看一個例子:
var num = 10;
function func() {
alert(num); // undefined
var num = 1;
alert(num); // 1
}
func();
從這個例子可以看到,第一次alert
的值並不是10
,而是undefined
。所以,應該盡量使用“單一var模式”來避免類似的問題。
關於變量提升的細節,請參考我前面一篇JavaScript的執行上下文。
for-in循環
在JavaScript中,for-in循環主要用來枚舉對象的屬性。
但是,由於JavaScript中原型鏈的存在,一般都會結合hasOwnProperty()來使用for-in循環,從而過濾原型鏈上的非該對象的屬性。
var wilber = {
name: "Wilber",
age: 28,
gender: "male"
};
Object.prototype.printPersonalInfo = function() {
console.log(this.name, "is", this.age, "years old");
};
for(var prop in wilber) {
if(wilber.hasOwnProperty(prop)) {
console.log(prop, ":", wilber[prop]);
}
}
開放的大括號位置
根據開發人員的習慣,開放大括號的位置會有不同的選擇,可以和語句放在同一行,也可以放在新的一行:
var total = 10;
if(tatal > 5) {
console.log("bigger than 5");
}
if(tatal > 5)
{
console.log("bigger than 5");
}
兩種形式的代碼都能實現同樣的邏輯,但是,JavaScript允許開發人員省略分號,JavaScript的分號插入機制(semicolon insertion mechanism)會負責加上省略的分號,這時開放大括號的位置不同就可能產生不同的結果。
看一個例子:
function func() {
return
{
name: "Wilber"
};
}
alert(func());
// undefined
之所以得到的結果是undefined
就是因為JavaScript的分號插入機制,在return
語句之后自動添加了分號。
調整一下開放的大括號的位置就可以避免這個問題:
function func() {
return {
name: "Wilber"
};
}
alert(func());
// [object]
所以,關於開放的大括號位置,建議將開放的大括號放置在前面語句的同一行。
強制new模式
JavaScript中,通過new
關鍵字,可以用構造函數來創建對象,例如:
function Person(name, city) {
this.name = name;
this.city = city;
this.getInfo = function() {
console.log(this.name, "lives at", this.city);
}
}
var will = new Person("Will", "Shanghai");
will.getInfo();
// Will lives at Shanghai
但是,如果開發人員忘記了new
關鍵字,那么構造函數中的this
將代表全局對象(瀏覽器中就是window
對象),所有的屬性將會變成全局對象的屬性。
function Person(name, city) {
this.name = name;
this.city = city;
this.getInfo = function() {
console.log(this.name, "lives at", this.city);
}
}
var will = Person("Will", "Shanghai");
console.log(will.name);
// Uncaught TypeError: Cannot read property 'name' of undefined
console.log(window.name);
// Will
console.log(window.city);
// Shanghai
window.getInfo();
// Will lives at Shanghai
所以,為了避免這類問題的方式,首先是從代碼規范上下手。建議對於所有的JavaScript構造函數的命名方式都遵循,構造函數使用首字母大寫的命名方式。
這樣當我們看到首字母大寫的函數,就要考慮是不是漏掉了new
關鍵字。
自調用構造函數
當然除了規范之外,還可以通過代碼的方式來避免上面的問題。
具體的做法就是,在構造函數中檢查this是否為構造函數的一個實例,如果不是,構造函數可以通過new
關鍵字進行自調用。
下面就是使用自調用構造函數對上面的例子進行改進:
function Person(name, city) {
if(!(this instanceof Person)) {
return new Person(name, city);
}
this.name = name;
this.city = city;
this.getInfo = function() {
console.log(this.name, "lives at", this.city);
}
}
var will = Person("Will", "Shanghai");
console.log(will.name);
// Will
console.log(will.city);
// Shanghai
will.getInfo();
// Will lives at Shanghai
window.getInfo();
// Uncaught TypeError: window.getInfo is not a function
結合構造函數的命名約定和自調用的構造函數,這下就不用擔心漏掉new
關鍵字的情況了。
數組性質檢查
當在JavaScript中判斷一個對象是不是數組的時候,不能直接使用typeof
,因為我們會得到object
。
在ECMA5中提出了Array.isArray()這個函數,我們可以直接使用來判斷一個對象是不是數組類型。
對於不支持ECMA5的環境,我們可以通過下面的方式自己實現Array.isArray()這個函數。
if(typeof Array.isArray === "undefined") {
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === "[object Array]";
};
}
var arr = [];
console.log(Array.isArray(arr));
// true
立即執行函數
立即執行函數是JavaScript中非常常用的一種模式,形式如下:
(function() {
// other code
}());
通過這個模式可以提供一個局部的作用域,所以函數代碼都會在局部作用域中執行,不會污染其他作用域。
現在的很多JavaScript庫都直接使用了這種模式,例如JQuery、underscore等等。
立即執行函數的參數
關於立即執行函數另外一點需要注意的地方就是立即執行函數的參數。
我們可以像正常的函數調用一樣進行參數傳遞:
(function(name, city) {
console.log(name, "lives at", city);
}("Wilber", "Shanghai"));
// Wilber lives at Shanghai
在立即執行函數中,是可以訪問外部作用域的(當然包括全局對象),例如:
var name = "Wilber";
var city = "Shanghai";
(function() {
console.log(name, "lives at", city);
}());
// Wilber lives at Shanghai
但是,如果立即執行函數需要訪問全局對象,常用的模式就是將全局對象以參數的方式傳遞給立即執行函數。
var name = "Wilber";
var city = "Shanghai";
(function(global) {
console.log(global.name, "lives at", global.city);
}(this));
// Wilber lives at Shanghai
這樣做的好處就是,在立即執行函數中訪問全局變量的屬性的時候就不用進行作用域鏈查找了,關於更多JavaScript作用域鏈的內容,可以參考理解JavaScript的作用域鏈。
初始化時分支
初始化時分支(Init-time Branching)是一種常用的優化模式,就是說當某個條件在整個程序聲明周期內都不會發生改變的時候,不用每次都對條件進行判斷,僅僅一次判斷就足夠了。
這里最常見的例子就是對瀏覽器的檢測,在下面的例子中,每次使用utils.addListener1
屬性的時候都要進行瀏覽器判斷,效率比較低下:
var utils = {
addListener: function(el, type, fn) {
if (typeof window.addEventListener === 'function') {
el.addEventListener(type, fn, false);
} else if (typeof document.attachEvent === 'function') { // IE
el.attachEvent('on' + type, fn);
} else { // older browsers
el['on' + type] = fn;
}
},
removeListener: function(el, type, fn) {
// pretty much the same...
}
};
所以,根據初始化時分支模式,可以在腳本初始化的時候進行一次瀏覽器檢測,這樣在以后使用utils
的時候就不必進行瀏覽器檢測了:
// the interface
var utils = {
addListener: null,
removeListener: null
};
// the implementation
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;
};
}
命名空間模式
JavaScript代碼中,過多的全局變量經常會引發一些問題,比如命名沖突。
結合命名空間模式就可以一定程度上減少代碼中全局變量的個數。
下面就看一個通用命名空間函數的實現:
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');
聲明依賴關系
JavaScirpt庫通常是通過命名空間來進行模塊化,當我們在代碼中使用第三方的庫的時候,可以只引入我們代碼依賴的模塊。
所謂聲明依賴關系,就是指在函數或者模塊的頂部是聲明代碼需要依賴哪些模塊,這個聲明包括創建一個局部變量,並將它們指向你需要的模塊:
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.Do)更快,可以得到更好的性能,全局符號只會在函數中出現一次,然后就可以使用局部變量,后者速度更快。
- 壓縮工具比如YUICompressor 和 Google Closure compiler會重命名局部變量,產生更小的體積的代碼,但從來不會重命名全局變量,因為那樣是不安全的
代碼復用模式
下面就看看JavaScript中的代碼復用模式。一般來說,通常使用下面的方式來實現代碼的復用:
- 繼承
- 借用方法
繼承
在JavaScript中可以很方便的通過原型來實現繼承。
關於原型式繼承,ECMA5通過新增Object.create()
方法規范化了原型式繼承。這個方法接收兩個參數:
- 一個用作新對象原型的對象
- 一個為新對象定義額外屬性的對象(可選的)
看一個使用Object.create()
的例子:
utilsLibC = Object.create(utilsLibA, {
sub: {
value: function(){
console.log("sub method from utilsLibC");
}
},
mult: {
value: function(){
console.log("mult method from utilsLibC");
}
},
})
utilsLibC.add();
// add method from utilsLibA
utilsLibC.sub();
// sub method from utilsLibC
utilsLibC.mult();
// mult method from utilsLibC
console.log(utilsLibC.__proto__);
// Object {add: (), sub: (), __proto__: Object}
console.log(utilsLibC.__proto__.constructor);
// function Object() { [native code] }
關於JavaScript繼承的更多信息,可以參考關於JavaScript繼承的那些事。
借用方法
有時候可能只需要一個已經存在的對象的一個或兩個方法,但是又不想通過繼承,來建立額外的父子(parent-child)關系。
這時就可以考慮使用借用方法模式完成一些函數的復用。借用方法模式得益於function的方法call()和apply()。
這種模式一個常見用法就是借用數組方法。
數組擁有有用的方法,那些類數組對象(array-like objects)比如arguments類數組對象(array-like objects)比如arguments沒有的方法。所以arguments可以借用數組的方法,比如slice()方法,看一個例子:
function f() {
var args = [].slice.call(arguments, 1, 3);
return args;
}
// example
f(1, 2, 3, 4, 5, 6); // returns [2,3]
總結
本文主要介紹了JavaScript中常用的編碼模式,通過這些模式可以使代碼健壯、可讀。
主要參考《JavaScript patterns》。