是什么
函数缓存,就是将函数运算过的结果进行缓存
定义:将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。
本质上就是用空间(缓存存储)换时间(计算过程)
常用于缓存数据计算结果和缓存对象
const add = (a,b) => a+b;
const calc = memoize(add); // 函数缓存
calc(10,20);// 30
calc(10,20);// 30 缓存
缓存只是一个临时的数据存储,它保存数据,以便将来对该数据的请求能够更快地得到处理
我们来实现一个缓存函数:
let memoize = function (func, content) {
let cache = Object.create(null)
content = content || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(content, key)
}
return cache[key]
}
}
过程分析:
- 在当前函数作用域定义了一个空对象,用于缓存运行结果
- 运用柯里化返回一个函数,返回的函数因为作用域链的原因,可以访问到cache
- 然后判断输入参数是不是在cache的中。如果已经存在,直接返回cache的内容,如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache中。
Vue中也有所体现
/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
/**
* Capitalize a string.
*/
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});
...
capitalize(camelizedId)
适用场景:
- 需要大量重复计算
- 大量计算并且依赖之前的结果
curry与偏函数
curry
function currying 把接受多个参数的函数转换成接受一个单一参数的函数
// 非函数柯里化
var add = function (x,y) {
return x+y;
}
add(3,4) //7
// 函数柯里化
var add2 = function (x) {
//**返回函数**
return function (y) {
return x+y;
}
}
add2(3)(4) //7
在上面的例子中,我们将多维参数的函数拆分,先接受第一个函数,然后返回一个新函数,用于接收后续参数。
就此,我们得出一个初步的结论:柯里化后的函数,如果形参个数等于实参个数,返回函数执行结果,否者,返回一个柯里化函数。
通过柯里化可实现代码复用,使用函数式编程。
实现柯里化函数
从上面例子中,我们定义了有两个形参的函数,为了实现柯里化,函数传入第一个形参后返回一个函数用来接收第二个形参。那么如果我们的定义的形参有三个,那么也就需要嵌套2层,分别处理后两个参数,如
var add3 = function (x) {
return function (y) {
return function (z) {
return x + y + z;
}
}
}
add3(1)(3)(5)
如果形参有5个,7个呢?这里我们使用递归,进行简化。不知有没有看到规律,形参的个数决定了函数的嵌套层数。 即 有n个参数就得嵌套n-1个函数 ,那我们来改造一番。
// 通用型柯里化
function currying (fn) {
// 未柯里化函数所需的参数个数 https://www.cnblogs.com/go4it/p/9678028.html
var limit = fn.length
var params = [] // 存储递归过程的所有参数,用于递归出口计算值
return function _curry (...args) {
params = params.concat(...args) // 收集递归参数
if (limit <= params.length) {
let tempParams=params.slice(0,limit)
if(limit===params.length){ //参数个数满足时清除已缓存的参数
params=[]
}
// 返回函数执行结果
return fn.apply(null, tempParams)
} else {
// 返回一个柯里化函数
return _curry
}
}
}
function add(x,y,z){
return x + y+z;
}
// 函数柯里化
var addCurried=currying(add);
console.log(`addCurried(1)(2)(3)`,addCurried(1)(2)(3))//6
console.log(`addCurried(3,3,3)`,addCurried(3,3,3))//9
console.log(`addCurried(1,2)(3)`,addCurried(1,2)(3))//6
console.log(`addCurried(3)(4,5)`,addCurried(3)(4,5))//12
我们看看addCurried(1)(2)(3)中发生了什么:
- 首先调用`addCurried(1),将1保存在词法环境中,然后递归调用_curry继续收集后续参数
- addCurried(1)(2),参数2与第一次的参数1,合并调用,因未达到形参个数要求,继续递归返回_curry
- 调用addCurried(1)(2)(3),参数为3,在接下去的调用中,与1,2进行合并,传入原函数add中
注意点
- 柯里化基于闭包实现,可能会导致内存泄露
- 使用递归,执行会降低性能,递归多时会发生栈溢出,需要进行递归优化,参考
- arguments是类数组,使用Array.prototype.slice.call转换为数组时,效率低。
偏函数
简单描述,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单。
// 乘法
let multi = (x,y) => x * y;
// 构造一个对数值乘以2的函数
let double = multi.bind(null,2);
console.log(double(3));//6
console.log(double(5));//10
在这个例子中,我们使用bind 固定了 乘数,返回一个函数。该函数接受一个参数作为 被乘数。--将部分参数固定,只对剩余参数进行计算。
基于以上推导,我们来实现一个无绑定上下文的偏函数:
/**
* 偏函数实现
* @param func 应用函数
* @param argsBound 固定参数
* @return {function(...[*]): *}
*/
let partial = (func, ...argsBound) => {
if (typeof func !== 'function') throw new TypeError(
`${typeof func} is not a function`)
return function (...args) { // (*)
if(func.length-argsBound.length>args.length) throw new Error(`miss arguments`)
return func.call(this, ...argsBound.concat(...args))
}
}
let partialMulti= partial(multi,2)
console.log(partialMulti());//Error: miss arguments
console.log(partialMulti(3));//6
partial(func[, arg1, arg2...]) 调用的结果是一个基于 func 的封装函数,以及:
- 和它传入的函数一致的 this
- 然后传入 ...argsBound —— 来自偏函数调用传入的参数
- 然后传入 ...args —— 传入封装函数的参数
区别
偏函数与柯里化很相似,下面我们做个对比:
柯里化:将一个对参数函数转换成多个单参数的函数,也就是将一个n元函数转换为n个一元函数。
偏函数:固定一个函数的一个或多个参数,也就是将一个n元函数转换成一个n-x元函数。
如何实现
实现函数缓存主要依靠闭包、柯里化、高阶函数,这里再简单复习下:
闭包
闭包可以理解成,函数 + 函数体内可访问的变量总和
(function() {
var a = 1;
function add() {
const b = 2
let sum = b + a
console.log(sum); // 3
}
add()
})()
add函数本身,以及其内部可访问的变量,即 a = 1,这两个组合在?起就形成了闭包
柯里化
把接受多个参数的函数转换成接受一个单一参数的函数
// 非函数柯里化
var add = function (x,y) {
return x+y;
}
add(3,4) //7
// 函数柯里化
var add2 = function (x) {
//**返回函数**
return function (y) {
return x+y;
}
}
add2(3)(4) //7
将一个二元函数拆分成两个一元函数
高阶函数
通过接收其他函数作为参数或返回其他函数的函数
function foo(){
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2
函数 foo 如何返回另一个函数 bar,baz 现在持有对 foo 中定义的bar 函数的引用。由于闭包特性,a的值能够得到
下面再看看如何实现函数缓存,实现原理也很简单,把参数和对应的结果数据存在一个对象中,调用时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果
如下所示
const memoize = function (func, content) {
let cache = Object.create(null)
content = content || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(content, key)
}
return cache[key]
}
}
调用方式也很简单
const calc = memoize(add);
const num1 = calc(100,200)
const num2 = calc(100,200) // 缓存得到的结果
过程分析:
- 在当前函数作用域定义了一个空对象,用于缓存运行结果
- 运用柯里化返回一个函数,返回的函数由于闭包特性,可以访问到cache
- 然后判断输入参数是不是在cache的中。如果已经存在,直接返回cache的内容,如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache中
应用场景
虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存
以下几种情况下,适合使用缓存:
- 对于昂贵的函数调用,执行复杂计算的函数
- 对于具有有限且高度重复输入范围的函数
- 对于具有重复输入值的递归函数
- 对于纯函数,即每次使用特定输入调用时返回相同输出的函数
给大家分享我收集整理的各种学习资料,前端小白交学习流程,入门教程等回答-下面是学习资料参考。