1 首先来看一个简单的计算函数,这样写很常见,也没有什么问题。但是,如果传入的值一样, 就会出现重复计算,每调用一次就会计算一次。我们首先来考虑,怎样避免这些重复计算?
let count = 0
function add(a, b) {
count ++
return a + b
}
add(7, 8)
add(7, 8)
add(7, 8)
add(7, 8)
console.log('计算的次数', count) // 计算的次数 4
下面代码实现了一个闭包。里面存储了计算的结果,相同的参数避免了重复的计算。
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
const cacheAdd = cache(add)
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log('计算的次数', count) // 计算的次数 1
缓存函数写好了,下面我们就来测试一下这两个函数的性能。理论上讲,缓存函数应该比简单的add函数快很多,事实是这样吗?
let count = 0
function add(a, b) {
count ++
return a + b
}
console.time('add')
for(let i=0; i<10000; i++) {
add(1, 1)
}
console.timeEnd('add') // 执行次数 10000
console.log('执行次数', count)
// add: 0.77294921875ms 不太稳定
let count = 0
function add(a, b) {
count ++
return a + b
}
const cacheAdd = cache(add)
console.time('cacheAdd')
for(let i=0; i<10000; i++) {
cacheAdd(1, 1)
}
console.timeEnd('cacheAdd') // 执行次数 1
console.log('执行次数', count)
// cacheAdd: 17.5810546875ms
对比一下时间发现,缓存函数并没有比非缓存函数快,相反,还会比非缓存函数慢很多。这是为什么呢?
加入我们把函数修改一下,改为 a*b, 传入大数字值,结果会怎样?
let count = 0
function add(a, b) {
count ++
return a * b
}
const cacheAdd = cache(add)
console.time('cacheAdd')
for(let i=0; i<10000; i++) {
cacheAdd(645, 893)
}
console.timeEnd('cacheAdd') // 执行次数 1
console.log('执行次数', count)
// cacheAdd: 14.232177734375ms
let count = 0
function add(a, b) {
count ++
return a * b
}
// const cacheAdd = cache(add)
console.time('add')
for(let i=0; i<10000; i++) {
// cacheAdd(645, 893)
add(645, 893)
}
console.timeEnd('add') // 执行次数 10000
console.log('执行次数', count)
// add: 0.810791015625ms
然而并没有出现我们想要的结果,那是不是说缓存函数没有呢?看下面的例子
/*
这是一个生成斐波纳契数列的函数,传入的参数,就是数列在第几位上的值
在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=2, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
*/
let count = 0
function Fibonacci(n) {
count++
return n <= 2 ? 1 : Fibonacci(n-1) + Fibonacci(n-2)
}
console.time('Fibonacci')
Fibonacci(30)
console.timeEnd('Fibonacci') // Fibonacci: 10.988037109375ms
console.log('执行次数', count) // 执行次数 1664079
console.log(Fibonacci(30)) // 832040
我传递的是30,大家可以试试如果传递50会怎样?浏览器会卡死,一直转圈圈。
let count = 0
function Fibonacci(n) {
count++
return n <= 2 ? 1 : cacheFibonacci(n-1) + cacheFibonacci(n-2)
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
let cacheFibonacci = cache(Fibonacci)
console.time('cacheFibonacci')
cacheFibonacci(30)
console.timeEnd('cacheFibonacci') // cacheFibonacci: 0.118896484375ms
console.log('执行次数', count) // 执行次数 30
console.log(cacheFibonacci(30)) // 832040
区别已经很明显,运行快了很多。计算次数也小了很多,这样我们算出第100位斐波那契数列值也不是问题。
2 上面我们亲自验证了缓存函数的好处,那我们写的缓存函数有没有什么问题呢?是不是百分之百都回返回正确的值呢?看下面的例子
function add(obj) {
return obj.a
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
let cacheAdd = cache(add)
console.log(cacheAdd({a:1})) // 1
console.log(cacheAdd({a:2})) // 1
console.log(cacheAdd({a:3})) // 1
参数传入的是对象,每次返回的值都是1 这是为什么?我门来看下cacheObj是什么样的?
console.log(cacheObj)
/* {[object Object]: 1}
[object Object]: 1
__proto__: Object
*/
可以看到cacheObj中只有一个属性,[object,object],因为参数会作为key存储,key在对象中以字符串的形式存在的,这里有一个隐士转换,生成key的过程中,也把调用了对象的toString方法,参数都会转变成[object,object]。每次执行的时候就会去取第一次存下来来的值,所以一直就只能拿到一个结果。我们可以在 Array.prototype.slice.apply(arguments)改一下代码,JSON.stringfy(Array.prototype.slice.apply(arguments))就可以了。
let count = 0
function add(obj) {
count++
return obj.a
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
let argumentString = JSON.stringify(Array.prototype.slice.apply(arguments))
console.log(cacheObj)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argumentString]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argumentString] = fn.apply(this, argument)
}
}
let cacheAdd = cache(add)
console.log(cacheAdd({a:1, b:3, c:4})) // 1
console.log(cacheAdd({a:1, c:4, b:3})) // 1
console.log(cacheAdd({c:1, b:3, a:3})) // 1
console.log('执行次数', count) // 执行次数 3
看下上面的例子,我传递的对象是一样的,就是换了顺序,函数就执行了3次,而我们希望的值执行一次。打印一下cacheObj会发现{[{"a":1,"b":3,"c":4}]: 1, [{"a":1,"c":4,"b":3}]: 1}结果是这样的。所以在使用缓存函数的时候,要注意传递参数问题。