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