Libon

Lazy Function / Cache Function

惰性函数与缓存函数

惰性函数是什么?

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

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())
// 以上示例中四次输出的结果是一致的

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

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

它的好处是什么?

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

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

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

以上。

cd ../