惰性函数模式

本文翻译自http://peter.michaux.ca/articles/lazy-function-definition-pattern

本文审查了一种被我称为惰性函数定义的函数式编程设计模式。我已经多次发现这种模式在javascript中非常有用,尤其在开发跨浏览器库的时候可以提高运行时的效率。

热身问题

定义一个foo函数,当它第一次被调用的时候返回一个Date对象将此刻的时间保存下来。

解决方案#1 过时技术

这个最简单的解决方案使用全局变量t保存Date对象。当foo函数第一次运行的时候将Date对象保存在变量t中。在随后的执行中,foo直接返回存储在t中的值。

var t;
function foo() {
    if (t) {
        return t;
    }
    t = new Date();
    return t;
}

上面的代码会产生两个问题。首先,变量t是一个额外的全局变量所以它的值随时都有可能被改变。第二,在运行时代码并不能达到最佳效率,因为每一次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;
    }
})();

这样效率还是不高因为每一次调用的时候还是需要计算条件语句的结果。

模块模式是一个很强大的模式但是在这个场景中还是不适用。

解决方案#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。这样当之前的判断逻辑非常复杂而且判断条件很多的情况下,这样的方案效率非常高。

另外一个对这个模式的思考角度是,一开始赋值给foo变量的外层函数是一个承诺。它会承诺第一次运行它后会将foo变量重写为一个更加有用的函数。术语“promise”来自scheme语言的惰性赋值机制。任何javascript程序员真的都应该去学习Scheme这种函数式编程语言。

确定页面滚动

当跨浏览器写javascript的时候,频繁出现的几个不同的指定浏览器的算法会被包裹在一个单独的js函数中。通过隐藏浏览器的差异之处来使浏览器API正规化,这让构建和维护复杂页面中的js代码变得更加简单。当被包裹的函数调用时,通过判断与浏览器对应的逻辑就会执行。

在一个拖拽库中通常凭借鼠标事件来提供光标位置信息是非常重要的。鼠标事件会提供光标的坐标,这个坐标位置是相对于window而不是页面。通过页面已经滑动到鼠标的window坐标来累积操作可以提供鼠标相对于页面的坐标位置。因此我们需要一个页面滚动的时候提供信息的函数。为了演示,这个例子定义了一个叫做getScrollY的方法。当拖拽库在拖拽过程中连续运行的时候,我们的getScrollY方法必须尽可能的提高效率。

很不幸的是有三种不同的浏览器滚动位置算法。Richard Cornford曾写过这四种算法在他的文章中 feature detection article。最大的收获就是四种页面滚动位置算法其中有一种使用了document.body。js库通常在html文档中的head标签中加载,但是当它加载的时候document.body属性还不存在。所以我们不能使用特性检测来决定四种算法哪一种是合适的。

对于这些问题很多js库会做下面其中之一。第一种选项是嗅探浏览器的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();
}

只作为一个边注,上面的代码看起来是一个为了获取页面滚动位置的出乎意料的尝试。很多人和代码库更愿意使用下面的方式来获取页面滚动位置。甚至在本文的评论中也有被提及。

var getScrollY = function() {
  return window.pageYOffset ||
         (document.documentElement && document.documentElement.scrollTop) ||
         (document.body && document.body.scrollTop);
}

对于上面的代码,如果页面还没有被滚动并且前面两个选项都不是合适的并且document.body.scrollTop是undefined,那么函数就会返回undefined而不是0。这样的getScrollY方法不足以检查浏览器是否能获取滚动信息。

总结

惰性函数定义模式使得我写一些健壮且效率高的代码。每一次当我遇到这个模式我都花一些时间来欣赏js的函数式编程能力。

javascript同时支持函数式编程和面向对象编程。关于面向对象的书籍很多,但是函数式编程设计模式的书却很少。js社区需要时间来聚集更多好的函数式编程模式。

posted @ 2018-11-16 10:19  hahazexia  阅读(873)  评论(0)    收藏  举报