Libon

Lazy Function / Cache Function

6Mins #JavaScript
惰性函数与缓存函数

ToC

惰性函数是什么?

在了解惰性函数是什么之前,我们先看一个例子:

1
var timeStamp = null
2
3
function getTimeStamp(){
4
if (timeStamp) return timeStamp
5
6
timeStamp = new Date().getTime()
7
return timeStamp
8
}
9
10
console.log(getTimeStamp())
4 collapsed lines
11
console.log(getTimeStamp())
12
console.log(getTimeStamp())
13
console.log(getTimeStamp())
14
// 以上示例中四次输出的结果是一致的

以上代码有什么问题吗?有没有优化空间?有,而且有两个。

这就是而这就是惰性函数需要解决的问题。

它的好处是什么?

惰性函数的优点如上,它是为了解决全局污染和多次重复判断的问题而诞生的。查看以下示例,并思考它有什么问题:

1
var getTimeStamp = (function() {
2
var timeStamp = new Date().getTime()
3
4
return function() {
5
return timeStamp
6
}
7
})()
8
9
console.log(getTimeStamp())
10
console.log(getTimeStamp())
2 collapsed lines
11
console.log(getTimeStamp())
12
console.log(getTimeStamp())

以上代码存在的问题是获取的时间是声明时获取到的时间,而不是第一次执行 getTimeStamp 时获取到的时间。那么我们再对其进行改良一下:

1
var getTimeStamp = function() {
2
var timeStamp = new Date().getTime()
3
4
getTimeStamp = function () {
5
return timeStamp
6
}
7
8
return getTimeStamp()
9
}

以上代码在执行的时候在函数内部覆写了自身,使得在运行第一次后,下一次的执行都是修改后的结果,这样就可以绕过每次都需要判断的问题了,同时因为每次获取到的值都是已经确定的,往后的执行速度都会比第一次快,如果这个函数本身需要处理的逻辑很多的话,这个优势会更加明显。不过这样做对静态类型分析不太友好,比如 TypeScript,使用时需要进行一定的取舍。

实际应用

因为在一些版本比较久的浏览器中对 dom 元素的事件处理的方式是不统一的,如果想要兼容旧版本的浏览器时,我们就需要对其进行处理:

1
// 在以往我们可能会这么封装这个函数
2
var 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] = fn
14
}
15
}
16
})();
17
18
// 但我们学习了惰性函数以后,我们可以这么改写它
19
var 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] = fn
31
}
32
}
33
34
addEvent(el, type, fn, capture) // 在内部执行第一次,这样可以使修改立即生效,同时立即绑定事件
35
};

至于到底是使用立即执行函数还是惰性函数来优化程序,这取决于你。通常这适合于一些只取值一遍的方法与场景。

缓存函数(也叫函数记忆)(memorize)

在了解缓存之前,我们先看一个阶乘的例子:

1
// n! = n * (n - 1)!
2
// 6! = 5 * 4 * 3 * 2 * 1
3
// 0! = 1
4
// n! = n * (n - 1) * ...... * 2 * 1
5
6
var count
7
function factorial(n) {
8
count++
9
if (n === 0 || n === 1) {
10
return 1
6 collapsed lines
11
}
12
13
return n * factorial(n - 1)
14
}
15
16
console.log(factorial(6), count) // 720, 12

在正常流程中,我们每次调用 factorial(6) 的时候都需要重新去计算一次 6 的阶乘的结果,这使得每次执行的时候会多出很多多余的消耗,而这部分的消耗是不必要的,因为结果已经出来了,就不需要再去重复进行求值取值了。同时,因为递归是反复调用自身的特性,这也会让函数本身的调用次数过多,导致程序的调用栈过深,而消耗大量性能资源。这时候我们可以使用缓存来优化这段程序。

1
var count = 0, cache = []
2
3
function factorial(n) {
4
count++
5
if (cache[n]) return cache[n]
6
7
if (n === 0 || n === 1) {
8
cache[0] = 1
9
cache[1] = 1
10
return 1
6 collapsed lines
11
}
12
13
return (cache[n] = n * factorial(n - 1))
14
}
15
16
console.log(factorial(6), count) // 720, 6

从结果来看,执行次数减少了一半,这是因为如果已经计算过值的话,则不会再去递归调用函数来取值,所以达到了优化的目的。

缓存函数的封装

以上代码也不是一点问题没有,至少它在污染全局函数的方面上做了很大的 “贡献”。那我们可以对其进行封装。

1
function 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
11
const mf = memorize(factorial)
12
mf(6) // 720
13
14
// 使用斐波那契数列来测试一下:
15
function fab(n) {
16
return n <= 2 ? 1 : fab(n - 1) + fab(n - 2)
17
}
18
19
const mf = memorize(fab)
20
mf(20) // 6765
21
22
// 通过 console.time / console.timeEnd 来记录运行的时间
23
console.time('no memory')
24
console.log(fab(20))
25
console.timeEnd('no memory') // no memory: 2.212158203125 ms
26
27
console.time('memory')
28
console.log(mf(20))
29
console.timeEnd('memory') // memory: 0.248291015625 ms

可以看出差距算是比较明显的了。

以上。


CD ..
接下来阅读
CSS Conditional Rules