This article explains a functional-programming design pattern, which I call Lazy Function Definition. I've found this pattern useful in JavaScript more than once, especially when writing cross-browser, efficient libraries.
Warm-up questions
Write a function foo, which returns a Date object. This object saves the time when foo is first called.
Method 1: Technology from ancient times
This simplest solution uses the global variable t to save the Date object. The first time foo is called, the time will be saved in t. Subsequent calls to foo will only return the value stored in t.
var t; function foo() { if (t) { return t; } t = new Date(); return t; }
But there are two problems with this code. First, the variable t is a redundant global variable and may be changed between calls to foo. Second, the code is not optimized for efficiency at call time because the condition must be evaluated every time foo is called. Although evaluating the condition does not appear to be inefficient in this example, real-world practical examples often have extremely expensive condition evaluation, such as in if-else-else-... structures.
Method 2: Module Pattern
We can make up for the shortcomings of the first method through the module pattern that is attributed to Cornford and Crockford. Use a closure to hide the global variable t so that only code within foo can access it.
var foo = (function() { var t; return function() { if (t) { return t; } t = new Date(); return t; } })();
But this still does not optimize the efficiency of the call, because each call to foo still requires an evaluation condition.
Although the module pattern is a powerful tool, I firmly believe that it is used in the wrong place in this situation.
Method 3: Functions as objects
Since JavaScript functions are also objects, they can have attributes. Based on this, we can implement a solution that is similar in quality to the module pattern.
function foo() { if (foo.t) { return foo.t; } foo.t = new Date(); return foo.t; }
In some cases, function objects with properties can produce cleaner solutions. I think this approach is conceptually simpler than the pattern module approach.
This solution avoids the global variable t in the first method, but it still cannot solve the conditional evaluation caused by each call to foo.
Method 4: Lazy function definition
Now, this is the reason why you are reading this article:
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文档的
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社区来说,还需要很长时间来积累良好的函数式模式。
更新:
这个模式虽然有趣,但由于大量使用闭包,可能会由于内存管理的不善而导致性能问题。来自 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; };