ホームページ >ウェブフロントエンド >jsチュートリアル >遅延関数定義モードの使用法_JavaScript のヒント
この記事では、私が Lazy Function Definition と呼んでいる関数プログラミング設計パターンについて説明します。私は、このパターンが JavaScript で、特にクロスブラウザーの効率的なライブラリを作成する場合に役立つことに何度か気づきました。
ウォームアップの質問
Date オブジェクトを返す関数 foo を作成します。このオブジェクトにより、foo が最初に呼び出される時間を節約できます。
方法 1: 古代からのテクノロジー
この最も単純な解決策は、グローバル変数 t を使用して Date オブジェクトを保存します。初めて foo が呼び出されるとき、時間は t に保存されます。後続の foo 呼び出しでは、t に格納されている値のみが返されます。
var t; function foo() { if (t) { return t; } t = new Date(); return t; }
しかし、このコードには 2 つの問題があります。まず、変数 t は冗長グローバル変数であり、foo の呼び出しの間に変更される可能性があります。第 2 に、foo が呼び出されるたびに条件を評価する必要があるため、コードは呼び出し時の効率が最適化されていません。この例では条件の評価が非効率であるようには見えませんが、実際の実際の例では、if-else-else-... 構造などの条件評価に非常に負荷がかかることがよくあります。
方法 2: モジュール パターン
最初の方法の欠点は、Cornford と Crockford に起因するモジュール パターンによって補うことができます。クロージャを使用してグローバル変数 t を非表示にし、foo 内のコードのみがそれにアクセスできるようにします。
var foo = (function() { var t; return function() { if (t) { return t; } t = new Date(); return t; } })();
しかし、foo への各呼び出しには依然として評価条件が必要であるため、これでも呼び出しの効率は最適化されません。
モジュール パターンは強力なツールですが、この状況では間違った場所で使用されていると私は強く信じています。
方法 3: オブジェクトとしての関数
JavaScript 関数もオブジェクトであるため、これに基づいて、モジュール パターンと同様の品質のソリューションを実装できます。
function foo() { if (foo.t) { return foo.t; } foo.t = new Date(); return foo.t; }
場合によっては、プロパティを持つ関数オブジェクトにより、よりクリーンなソリューションを生成できることがあります。このアプローチは、概念的にはパターン モジュール アプローチよりも単純だと思います。
この解決策は、最初のメソッドのグローバル変数 t を回避しますが、foo の各呼び出しによって引き起こされる条件付き評価を解決することはできません。
方法 4: 遅延関数定義
さて、これがこの記事を読んでいる理由です:
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文档的93f0f5c25f18dab9d176bd4f6de5d30e加载,与此同时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社区来说,还需要很长时间来积累良好的函数式模式。
更新:
这个模式虽然有趣,但由于大量使用闭包,可能会由于内存管理的不善而导致性能问题。来自 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; };