柯里化函数的认识与应用

高阶函数之柯里化函数

维基百科中的定义

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

解释有点抽象,可以代码实现下效果

    //普通函数
    function add(a,b){
        return a + b;
    }
    add(1,2); // 结果 => 3
    
    //柯里化后
    function curryingAdd(a){
        return function(b){
            return add(a,b);
        }
    }
    let sum = curryingAdd(1)
        sum(2) //结果 => 3
    
    //这里也可以利用到 bind() 返回一个新函数
    let sum1 = add.bind(null,1)
        sum1(2) //结果 => 3

这里你可能会有点疑惑,可能暂时看不出柯里化函数的用处,体验不到柯里化的精髓,别急,下面是关于柯里化函数的使用场景

柯里化函数的使用场景

参数复用

我们知道Object.prototype.toString()可以获取每个对象的类型,并且不同的对象的toString()的结果也是不一样的

普通版

function typeTest(dataType,obj){
    return new RegExp(dataType,"gi")
            .test(
            Object
                .prototype
                .toString.call(obj)
            )        
}
typeTest("array",[1,2,3]) //true

typeTest("array","Hello") //false

typeTest("boolean",true) //true 

Currying 化

function typeTest(dataType,obj){
    return new RegExp(dataType,"gi")
            .test(
            Object
                .prototype
                .toString.call(obj)
            )        
}
//第一种
var isNum = (function(type){
    return function(data){
        return typeTest(type,data)
    }
})("number")

isNum(1) //true
isNum([]) //false

//第二种
var isArr = typeTest.bind(null,"array")

isArr(1) //false
isArr([]) //true

如果使用场景一样,参数的形式有一些是没有变动的情况下,我们可以优先设置,然后后续直接传频繁变动的参数即可,从而达到参数复用,提高开发效率,调用起来更加方便

延迟计算

可以看下方的场景,很好地体现出延迟计算的优点

function add(){
    if(!arguments.length){
        return 0;
    }
    var args = [].slice.call(arguments);
    return args.reduce(function(a,b){
        return a + b;
    })
}

//currying 延迟计算
function currying(){
    var nums = [];
    return function fn(){
        if(arguments.length){
            nums.push(...arguments)
            return fn;
        }
        return add.apply(null,nums);
    }
}

var sum = currying();

    sum(1,2)              //无结果 未真正求值
    sum(10,5)(10)(666,233)//无结果 未真正求值
    sum()                 //返回总计 927

上面的代码理解起来很容易, 就是「用闭包把传入参数保存起来, 当传入参数的数量足够执行函数时, 就开始执行函数。上面的 currying 函数是一种简化写法, 判断传入的参数长度是否为 0, 若为 0 则立刻执行函数并返回结果, 否则收集参数。以此实现延迟计算

动态创建函数

动态创建函数?什么意思呢?

其实就和我们写兼容一样的道理,例如我们的事件监听,除了IE低版本内核使用的是attachEvent()detachEvent,主流浏览器基本使用addEventListener()removeEventListener()注册和注销事件监听,所以我们在使用时,需要确保当前方法是否可以调用,如果没有该方法则调用另外一个方法或者自定义方法,以此确保我们程序的正常运行.

这样的话, 我们在后续继续调用该方法的时候,都会进行判断兼容处理流程, 但其实这是没有必要的, 因为第一次的判断选取的兼容方案就已经明确下来接下来我们要选取那一种兼容方案, 而不是每次调用都会去判断, 这种情况下就非常适合柯里化方案来处理了。即第一次判断之后,动态创建一个新的函数用于后续参数的传入,并返回这个新函数

拿事件监听的兼容场景来作案例,在DOM2级事件监听时,需要兼容主流浏览器和IE低版本浏览器(IE < 9),方法就是对浏览器的环境进行判断,看当前浏览器是否支持

function addEvent(type,el,fn,capture = false){
    if(window.addEventListener){
        el.addEventListener(type,fn,capture);
    }else if(window.addachEvent){
        el.addachEvent("on" + type,fn);
    }
}

这种写法虽然也可以达到兼容效果,但就是每次调用都会去判断,这是沐浴必要的,那么有没有什么方法让她只判断就行呢?可以采用闭包和函数自执行(IIFE)来处理

const addEvent = (function(){
    if(window.addEventListener){
        return function(type,el,fn,capture = false){
            el.addEventListener(type,fn,capture);   
        }
    }else if(window.addachEvent){
        return function(type,el,fn){
            el.addachEvent("on" + type,fn);  
        }
    }
})()

上面这种实现方案就是一种典型的柯里化应用,在第一次的 if...else if... 判断之后完成部分计算,动态创建新的函数用于处理后续传入的参数,这样做的好处就是之后调用就不需要再次计算了。

封装 currying 函数

说这么多,有没有通用的柯里化函数封装方法呢?
答案是: 当然有!

    function currying(fn,len){
        //捕获函数需要传入 len 个函数后才可执行
        //第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
        var len = len || fn.length;
        return function(){
            //判断新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
            if( arguments.length >= len ){
            //参数一致则执行函数
                return fn.apply(this,arguments)
            }else{
            //不满足参数个数则递归 currying 函数传入 fn.bind() => 传入部分参数的新函数,并且重新定义新函数所需的参数个数 
                return currying(fn.bind(this,...arguments),len - arguments.length)
            }
        }
    }
    //测试
    var test = currying(function(a,b,c){
        return a + b + c;
    })
    
    test(1)()(2,3) // 6
    test()(1,2)(3) // 6
    text(1)(2)(3)  // 6

Function.prototype.length

函数的length属性属于ES6语法的,表示函数参数的个数,但是,有个需要注意的就是length不一定就是函数的参数个数

来自MDN的说法

描述

length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。 形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。与之对比的是,arguments.length 是函数被调用时实际传参的个数。

Function 构造器的属性

Function 构造器本身也是个Function。他的 length 属性值为 1

Function.length 属性的属性特性:

属性名 | 默认值
-|-|-
writable | false |
enumerable | false |
configurable | true |

Function.prototype 对象的属性

Function.prototype 对象的 length 属性值为 0

console.log(Function.length); /* 1 */

console.log((function()        {}).length); /* 0 */
console.log((function(a)       {}).length); /* 1 */
console.log((function(a, b)    {}).length); /* 2 etc. */

console.log((function(...args) {}).length); 
// 0, rest parameter is not counted

console.log((function(a, b = 1, c) {}).length);
// 1, only parameters before the first one with 
// a default value is counted

所以如果要封装柯里化时,得注意这个坑,不然也可以手动传入参数个数

小结

通过定义,我们认识了什么是柯里化函数,柯里化函数主要应用于哪一些场景[参数复用,延迟计算,动态创建函数]

定义

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术


应用场景
  • 参数复用
    • 利用Object.prototype.toString判断对象的类型
  • 延迟计算
    • 部分求和
  • 动态创建函数
    • 添加事件绑定

currying 函数的封装

用闭包将传入的参数保存起来,当传入参数的数量符合要求时则执行函数,否则则通过递归传入fn.bind()返回的新函数接收剩余的参数


函数参数的length

length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。 形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。


最后来道劲爆的题目

封装一个函数,需求如下

function sum(...){
    //code block
}
sum(1,2)(3)        //6
sum(1,2)(3)(4)     //10
sum(1)(2)(3)(4)(5) //15

请思考下,不要立刻看答案,别人就不好玩咯 (_) (

w_w ... ... 嘘!别吵偶,让偶思考一下!

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

(ˇˍˇ) 嗯~,思考中

^^v 成功了,高兴地笑,在用胜利的手势

(ノ・ω・)ノヾ(・ω・ヾ) 你挑战成功了吗

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

推荐阅读更多精彩内容