Javascript缓存函数&柯里化&偏函数

缓存函数

memorizition

定义:将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

let add = (a,b) => a+b;
let 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)中发生了什么:

  1. 首先调用``addCurried(1),将1保存在词法环境中,然后递归调用_curry`继续收集后续参数
  2. addCurried(1)(2),参数2与第一次的参数1,合并调用,因未达到形参个数要求,继续递归返回_curry
  3. 调用addCurried(1)(2)(3),参数为3,在接下去的调用中,与1,2进行合并,传入原函数add

注意点

  1. 柯里化基于闭包实现,可能会导致内存泄露

  2. 使用递归,执行会降低性能,递归多时会发生栈溢出,需要进行递归优化,参考

  3. 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元函数。

个人理解:偏函数是柯里化的一种特定的应用场景

使用场景

  • 动态生成函数
  • 减少参数
  • 延迟计算
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容