ToC
惰性函数是什么?
在了解惰性函数是什么之前,我们先看一个例子:
1var timeStamp = null2
3function getTimeStamp(){4 if (timeStamp) return timeStamp5
6 timeStamp = new Date().getTime()7 return timeStamp8}9
10console.log(getTimeStamp())4 collapsed lines
11console.log(getTimeStamp())12console.log(getTimeStamp())13console.log(getTimeStamp())14// 以上示例中四次输出的结果是一致的
以上代码有什么问题吗?有没有优化空间?有,而且有两个。
- 全局污染(副作用)
- 每次都需要判断 timeStamp 是否有值
这就是而这就是惰性函数需要解决的问题。
它的好处是什么?
惰性函数的优点如上,它是为了解决全局污染和多次重复判断的问题而诞生的。查看以下示例,并思考它有什么问题:
1var getTimeStamp = (function() {2 var timeStamp = new Date().getTime()3
4 return function() {5 return timeStamp6 }7})()8
9console.log(getTimeStamp())10console.log(getTimeStamp())2 collapsed lines
11console.log(getTimeStamp())12console.log(getTimeStamp())
以上代码存在的问题是获取的时间是声明时获取到的时间,而不是第一次执行 getTimeStamp
时获取到的时间。那么我们再对其进行改良一下:
1var getTimeStamp = function() {2 var timeStamp = new Date().getTime()3
4 getTimeStamp = function () {5 return timeStamp6 }7
8 return getTimeStamp()9}
以上代码在执行的时候在函数内部覆写了自身,使得在运行第一次后,下一次的执行都是修改后的结果,这样就可以绕过每次都需要判断的问题了,同时因为每次获取到的值都是已经确定的,往后的执行速度都会比第一次快,如果这个函数本身需要处理的逻辑很多的话,这个优势会更加明显。不过这样做对静态类型分析不太友好,比如 TypeScript,使用时需要进行一定的取舍。
实际应用
因为在一些版本比较久的浏览器中对 dom 元素的事件处理的方式是不统一的,如果想要兼容旧版本的浏览器时,我们就需要对其进行处理:
1// 在以往我们可能会这么封装这个函数2var addEvent = (function () {3 if (window.addEventListener) {4 return function (el, type, fn, capture) {5 el.addEventListener(type, fn, capture)6 }7 } else if (window.attachEvent) {8 return function (el, type, fn) {9 el.attachEvent('on' + type, fn.bind(el))10 }25 collapsed lines
11 } else {12 return function (el, type, fn) {13 el['on' + type] = fn14 }15 }16})();17
18// 但我们学习了惰性函数以后,我们可以这么改写它19var addEvent = function (el, type, fn, capture) {20 if (window.addEventListener) {21 addEvent = function (el, type, fn, capture) {22 el.addEventListener(type, fn, capture)23 }24 } else if (window.attachEvent) {25 addEvent = function (el, type, fn) {26 el.attachEvent('on' + type, fn.bind(el))27 }28 } else {29 addEvent = function (el, type, fn) {30 el['on' + type] = fn31 }32 }33
34 addEvent(el, type, fn, capture) // 在内部执行第一次,这样可以使修改立即生效,同时立即绑定事件35};
至于到底是使用立即执行函数还是惰性函数来优化程序,这取决于你。通常这适合于一些只取值一遍的方法与场景。
缓存函数(也叫函数记忆)(memorize)
在了解缓存之前,我们先看一个阶乘的例子:
1// n! = n * (n - 1)!2// 6! = 5 * 4 * 3 * 2 * 13// 0! = 14// n! = n * (n - 1) * ...... * 2 * 15
6var count7function factorial(n) {8 count++9 if (n === 0 || n === 1) {10 return 16 collapsed lines
11 }12
13 return n * factorial(n - 1)14}15
16console.log(factorial(6), count) // 720, 12
在正常流程中,我们每次调用 factorial(6)
的时候都需要重新去计算一次 6 的阶乘的结果,这使得每次执行的时候会多出很多多余的消耗,而这部分的消耗是不必要的,因为结果已经出来了,就不需要再去重复进行求值取值了。同时,因为递归是反复调用自身的特性,这也会让函数本身的调用次数过多,导致程序的调用栈过深,而消耗大量性能资源。这时候我们可以使用缓存来优化这段程序。
1var count = 0, cache = []2
3function factorial(n) {4 count++5 if (cache[n]) return cache[n]6
7 if (n === 0 || n === 1) {8 cache[0] = 19 cache[1] = 110 return 16 collapsed lines
11 }12
13 return (cache[n] = n * factorial(n - 1))14}15
16console.log(factorial(6), count) // 720, 6
从结果来看,执行次数减少了一半,这是因为如果已经计算过值的话,则不会再去递归调用函数来取值,所以达到了优化的目的。
缓存函数的封装
以上代码也不是一点问题没有,至少它在污染全局函数的方面上做了很大的 “贡献”。那我们可以对其进行封装。
1function memorize(fn) {2 var cache = {}3
4 return function (...args) {5 var k = args.join(',') // 将传入的参数以 , 分割保存6
7 return cache[k] = cache[k] || fn.apply(this, args)8 }9}10
19 collapsed lines
11const mf = memorize(factorial)12mf(6) // 72013
14// 使用斐波那契数列来测试一下:15function fab(n) {16 return n <= 2 ? 1 : fab(n - 1) + fab(n - 2)17}18
19const mf = memorize(fab)20mf(20) // 676521
22// 通过 console.time / console.timeEnd 来记录运行的时间23console.time('no memory')24console.log(fab(20))25console.timeEnd('no memory') // no memory: 2.212158203125 ms26
27console.time('memory')28console.log(mf(20))29console.timeEnd('memory') // memory: 0.248291015625 ms
可以看出差距算是比较明显的了。
以上。