這
篇文章闡述的是一種函數式編程(functional-programming)設計模式,我稱之為惰性函數定義(Lazy Function
Definition)。我不止一次發現這種模式在JavaScript中大有用處,尤其是編寫跨瀏覽器的、高效運行的庫之時。
熱身問題
編寫一個函數foo
,它返回的是Date
對象,這個對象保存的是foo
首次調用的時間。
方法一:上古時代的技術
這個最簡陋的解決方案使用了全局變量t
來保存Date
對象。foo
首次調用時會把時間保存到t
中。接下來的再次調用,foo
只會返回保存在t
中的值。
var t;
function foo() {
if (t) {
return t;
}
t = new Date();
return t;
}
但是這樣的代碼有兩個問題。第一,變量t
是一個多余的全局變量,并且在 foo
調用的間隔期間有可能被更改。第二,在調用時這些代碼的效率并沒有得到優化因為每次調用 foo
都必須去求值條件。雖然在這個例子中,求值條件并不顯得低效,但在現實世界的實踐例子中常常會有極為昂貴的條件求值,比如在if-else-else-…的結構中。
方法二:模塊模式
我們可以通過被認為歸功于Cornford 和 Crockford 的模塊模式來彌補第一種方法的缺陷。使用閉包可以隱藏全局變量t
,只有在 foo
內的代碼才可以訪問它。
var foo = (function() {
var t;
return function() {
if (t) {
return t;
}
t = new Date();
return t;
}
})();
但這仍然沒有優化調用時的效率,因為每次調用foo
依然需要求值條件。
雖然模塊模式是一個強大的工具,但我堅信在這種情形下它用錯了地方。
方法三:函數作為對象
由于JavaScript的函數也是對象,所以它可以帶有屬性,我們可以據此實現一種跟模塊模式質量差不多的解決方案。
function foo() {
if (foo.t) {
return foo.t;
}
foo.t = new Date();
return foo.t;
}
在一些情形中,帶有屬性的函數對象可以產生比較清晰的解決方案。我認為,這個方法在理念上要比模式模塊方法更為簡單。
這個解決方案避免了第一種方法中的全局變量t
,但仍然解決不了foo
每次調用所帶來的條件求值。
方法四:惰性函數定義
現在,這是你閱讀這篇文章的理由:
var foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
};
當foo
首次調用,我們實例化一個新的Date
對象并重置 foo
到一個新的函數上,它在其閉包內包含Date
對象。在首次調用結束之前,foo
的新函數值也已調用并提供返回值。
接下來的foo
調用都只會簡單地返回t
保留在其閉包內的值。這是非常快的查找,尤其是,如果之前那些例子的條件非常多和復雜的話,就會顯得很高效。
弄清這種模式的另一種途徑是,外圍(outer)函數對foo
的首次調用是一個保證(promise)。它保證了首次調用會重定義foo
為一個非常有用的函數。籠統地說,術語“保證” 來自于Scheme的惰性求值機制(lazy evaluation mechanism)。每一位JavaScript程序員真的都應該學習Scheme,因為它有很多函數式編程相關的東西,而這些東西會出現在JavaScript中。
確定頁面滾動距離
編寫跨瀏覽器的JavaScript,
經常會把不同的瀏覽器特定的算法包裹在一個獨立的JavaScript函數中。這就可以通過隱藏瀏覽器差異來標準化瀏覽器API,并讓構建和維護復雜的頁
面特性的JavaScript更容易。當包裹函數被調用,就會執行恰當的瀏覽器特定的算法。
在拖放庫中,經常需要使用由鼠標事件提供的光標位置信息。鼠標事件給予的光標坐標相對于瀏覽器窗口而不是頁面。加上頁面滾動距離鼠標的窗口坐標的距離即可得到鼠標相對于頁面的坐標。所以我們需要一個反饋頁面滾動的函數。演示起見,這個例子定義了一個函數getScrollY
。因為拖放庫在拖拽期間會持續運行,我們的getScrollY
必須盡可能高效。
不過卻有四種不同的瀏覽器特定的頁面滾動反饋算法。Richard Cornford在他的feature detection article文章中提到這些算法。最大的陷阱在于這四種頁面滾動反饋算法其中之一使用了 document.body
. JavaScript庫通常會在HTML文檔的<head>
加載,與此同時docment.body
并不存在。所以在庫載入的時候,我們并不能使用特性檢查(feature detection)來確定使用哪種算法。
考慮到這些問題,大部分JavaScript庫會選擇以下兩種方法中的一種。第一個選擇是使用瀏覽器嗅探navigator.userAgent
,為該瀏覽器創建高效、簡潔的getScrollY
. 第二個更好些的選擇是getScrollY
在每一次調用時都使用特性檢查來決定合適的算法。但是第二個選擇并不高效。
好消息是拖放庫中的getScrollY
只會在用戶與頁面的元素交互時才會用到。如果元素業已出現在頁面中,那么document.body
也會同時存在。getScrollY
的首次調用,我們可以使用惰性函數定義模式結合特性檢查來創建高效的getScrollY
.
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();
}
總結
惰性函數定義模式讓我可以編寫一些緊湊、健壯、高效的代碼。用到這個模式的每一次,我都會抽空贊嘆JavaScript的函數式編程能力。
JavaScript同時支持函數式和面向對象便程。市面上有很多重點著墨于面向對象設計模式的書都可以應用到JavaScript編程中。不過卻沒有多少書涉及函數式設計模式的例子。對于JavaScript社區來說,還需要很長時間來積累良好的函數式模式。
原文:Lazy Function Definition Pattern. 轉載沒有我的信息沒有關系,但你一定得寫上原文信息,謝謝。
更新:
這個模式雖然有趣,但由于大量使用閉包,可能會由于內存管理的不善而導致性能問題。來自FCKeditor的FredCK改進了getScrollY
,既使用了這種模式,也避免了閉包:
var getScrollY = function() {
if (typeof window.pageYOffset == 'number')
return (getScrollY = getScrollY.case1)();
var compatMode = document.compatMode;
var documentElement = document.documentElement;
if ((typeof compatMode == 'string') &&
(compatMode.indexOf('CSS') >= 0) &&
(documentElement) &&
(typeof documentElement.scrollTop == 'number'))
return (getScrollY = getScrollY.case2)();
var body = document.body ;
if ((body) &&
(typeof body.scrollTop == 'number'))
return (getScrollY = getScrollY.case3)();
return (getScrollY = getScrollY.case4)();
};
getScrollY.case1 = function() {
return window.pageYOffset;
};
getScrollY.case2 = function() {
return documentElement.scrollTop;
};
getScrollY.case3 = function() {
return body.scrollTop;
};
getScrollY.case4 = function() {
return NaN;
};
請看具體的評論。
This entry was posted
on Thursday, August 16th, 2007 at 10:42 and is filed under JS / Dom.
You can follow any responses to this entry through the RSS 2.0 feed.
You can leave a response, or trackback from your own site.