这篇文章阐述的是一种函数式编程(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;
};
请看具体的评论 。
相关推荐
惰性函数定义模式是一种函数式编程的设计模式,其核心思想是推迟函数的实际计算或执行,直到它的结果真正被需要时才会进行。这种模式在JavaScript中特别有用,尤其是在编写高性能、跨浏览器的库时,因为它可以帮助...
现代C++函数编程模式是C++11及后续版本引入的一种增强编程范式的特性,它使得C++在传统的面向对象编程之外,增加了函数式编程的能力。这种编程风格强调使用不可变数据、高阶函数和惰性计算,尤其适用于处理多线程...
这种行为称为惰性定义模式,它在初次调用时执行一些初始化操作,之后函数被重定义为期望的执行体,提高了代码的效率。 这种自我定义和重定义的技术常用于函数检测(Feature Detection)和初始化时间分支,如检查...
函数定义/声明 函数运行机制 递归 函数注意事项和细节 过程 惰性函数和异常 面向对象编程初级 类和对象 包 包对象 面向对象编程中级 封装 继承 多态 面向对象编程高级 伴生对象 特质 嵌套类 类型...
惰性模式是JavaScript设计模式中的一种,其主要目的是优化代码执行效率,减少不必要的重复判断,尤其是在处理跨浏览器兼容性问题时。在JavaScript中,惰性模式通常用于将一些昂贵的操作延迟到真正需要它们的时候才...
这种方式可以使代码更加模块化,每个函数只关注一个特定任务,而整个流程由组合函数定义。 五、柯里化 柯里化是将接受多个参数的函数转换为一系列接受单个参数的函数的过程。这样可以提前部分应用参数,创建具名的...
书中还可能涵盖了其他函数式编程概念,如柯里化(Currying)、高阶函数(Higher-Order Functions)、尾递归(Tail Recursion)优化、闭包(Closures)的使用,以及如何通过函数式编程风格来实现常见的设计模式。...
《函数式Swift》是一本深入探讨Swift编程语言中函数式...《函数式Swift》这本书不仅涵盖了上述知识点,还可能涉及函数式编程设计模式、状态管理、错误处理等主题,对于深入理解Swift编程和提升开发技能具有很高的价值。
在JavaScript中,代理模式可用于控制对象访问、惰性初始化、访问日志记录等。 JavaScript中的代理模式通常是通过ES6中的Proxy对象实现的。Proxy对象允许你定义一个自定义的行为,当访问某个对象的属性或者方法时,...
Haskell和OCaml原生支持柯里化,Python可以通过函数定义或库(如functools模块)实现。 4. 函数组合:函数组合是将两个或更多函数串联在一起,形成一个新的函数。在Haskell中,函数组合非常直观,只需用`.`操作符...
Rust 的 trait 系统允许定义函数接口,使得函数式编程风格的代码更加模块化和可重用。另外,Rust 的所有权系统确保了内存安全,避免了数据竞争和其他并发问题,这对于大规模并行处理至关重要。 Haskell 函数编程: ...
在Scala中,匿名函数就是闭包的一个例子,它可以访问定义它的作用域内的变量。 函数式数据结构 Scala提供了许多不可变的数据结构,如List、Set、Map等,它们遵循函数式编程的原则,即在创建后不能被修改。这些数据...
- **Lambda表达式与闭包**:Lambda表达式允许我们定义匿名函数,而闭包则是一种特殊类型的函数,它可以访问在其外部定义的变量。这两者都是函数式编程中非常强大的特性。 - **高阶函数**:这是一种接受一个或多个...
4. **规则(Rule)**:定义变量或表达式的替换模式。 5. **模式(Pattern)**:用于匹配特定结构的数据。 6. **控制结构(Control Structures)**:如循环、条件判断等。 #### 三、常用函数详解 1. **数值操作** - **...
Coconut增加了诸如惰性求值、高阶函数和模式匹配等功能,让Python代码变得更加简洁和优雅。它的设计初衷是让Python开发者能够更容易地实现函数式编程技巧,而无需完全转向其他更函数式的语言。 在Pythonic函数式...
5. **惰性求值**:虽然JavaScript本身并不支持惰性求值,但在某些库如Ramda中,可以通过函数来模拟这种行为,只在真正需要结果时才计算表达式。 6. **尾调用优化**:JavaScript ES6引入了尾调用优化,允许在函数的...
- **惰性初始化模式**:延迟对象的创建,直到第一次使用它。 - **代理模式**:为另一个对象提供一个替身或占位符以控制对这个对象的访问。 #### 结语 《学用JavaScript设计模式》不仅是一本技术手册,更是一本启发...
除此之外,Swift还引入了惰性计算和可选链,这些都是函数式编程中常见的模式。惰性计算允许我们延迟计算直到实际需要,这对于处理大数据集或昂贵的计算非常有用。可选链则允许我们安全地访问可能为nil的属性和方法,...