Peter[1] 提出一種用於函數式編程的設計模式稱為"Lazy Function Definition"。首天他提出一個問題:
"撰寫一個 foo 函數,並回傳第一次呼叫 foo 的時間。"
針對這個問題,他提出下面四種不同的解法:
解法#1: 傳統解法-全域變數
var t; // 紀錄第一次呼叫 foo 的時間
function foo() {
if (t) { // 假如已經有存放時間,則直接回傳
return t;
}
t = new Date(); // 紀錄第一次呼叫時間
return t;
}
上述解法有兩個問題:
1. 需要一個額外的全域變數 t,該變數有可能在呼叫 foo 時被修改
2. 程式碼在執行階段無法達到最佳效率。因為每次呼叫都需要進行條件評估。雖然上述範例的條件式損耗很廉價,但是真實情況的條件式可能是多層次的 if-else-else-... 結構。
解法 #2: Module Pattern
利用 Cornford and Crockford 所提出的 Module Pattern[2] 可以解決解法#1 的第一個問題,透過 closure 技法將全域變數封裝成只有 foo 內的程式可存取,如下:
var foo = (function() {
var t; // 封裝在 foo 內的變數
return function() {
if (t) {
// 假如已經有存放時間,則直接回傳
return t;
}
t = new Date();
// 紀錄第一次呼叫時間
return t;
}
})();
這種解法仍有解法#1的第2個問題存在,仍需重複進行條件式評估。
解法 #3: 函數即物件-使用物件屬性保存狀態
function foo() {
if (foo.t) {
// 假如已經有存放時間,則直接回傳
return foo.t;
}
foo.t = new Date();
// 紀錄第一次呼叫時間
return foo.t;
}
這個解法比 Module Pattern 更為簡潔,直接透過物件屬性存放資料,而不需要在產生一個額外的 Function Object。所以也能避免全域變數的問題。但這種解法仍有解法#1的第2個問題存在,仍需重複進行條件式評估。
解法 #4: Lazy Function Definition
var foo = function() {
var t = new Date();
foo = function() { // 改寫原先的函數定義
return t; // Closure t
};
return foo(); // 套用新的 foo 函數回傳值
};
當 foo 第一次被呼叫時,會先實體化一個 Date
物件並重新指定將 Date 物件 Closure 的 foo 函數,透過重新的指定可以改寫原來的函數定義,因為 Date 物件已經被 Closure,所以會一直存在。透過此一解法可以避免解法#1的兩個問題。
因此上述原來的 foo 函數定義可以想像成初始化內部 foo。簡單的說,第一次呼叫 foo 的函數定義如上程式,但之後的程式碼定義則是如下:
function foo() {
return t; // Closure t
};
上述可透過下面的程式碼進行測試:
var foo = function() {
var t = new Date();
alert(1); foo = function() {
return t;
};
alert(2) return foo();
};
alert( foo() ); // 出現 1, 2, 日期
alert( foo() ); // 出現 日期
應用:判斷不同瀏覽器的頁面捲動
var
getScrollY = function() {
if (typeof window.pageYOffset == 'number') {
getScrollY = function() {
return window.pageYOffset;
};
} else if ((typeof document.compatMode == 'string') &&
(document.compatMode.indexOf('CSS') >= 0) &&
(document.documentElement) &&
(typeof document.documentElement.scrollTop == 'number')) {
getScrollY = function() {
return document.documentElement.scrollTop;
};
} else if ((document.body) &&
(typeof document.body.scrollTop == 'number')) {
getScrollY = function() {
return document.body.scrollTop;
}
} else {
getScrollY = function() {
return NaN;
};
}
return getScrollY();
}
透過 "Lazy Function Definition" 只需要在第一次花費條件式評估,對於重複呼叫的函數特別能夠改善效率。
補充 Firefox/Safari/Opera 的 getter methods[3] 能夠在屬性上模擬 "lazy definition" 如下:
this.__defineGetter__("foo", function() {
var t = new Date();
this.__defineGetter__("foo", function() {
return t;
});
return t;
});
// To the user, foo appears as a plain old
// non-function valued property of the global object.
console.log(this.foo);
setTimeout(function(){console.log(this.foo);}, 3000);