资讯专栏INFORMATION COLUMN

前端进击的巨人(五):学会函数柯里化(curry)

chengtao1633 / 1253人阅读

摘要:函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数,而反柯里化就是把参数全部释放出来。但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。

柯里化(Curring, 以逻辑学家Haskell Curry命名)
写在开头

柯里化理解的基础来源于我们前几篇文章构建的知识,如果还未能掌握闭包,建议回阅前文。

代码例子会用到 apply/call ,一般用来实现对象冒充,例如字符串冒充数组对象,让字符串拥有数组的方法。待对象讲解篇会细分解析。在此先了解,两者功能相同,区别在于参数传递方式的不同, apply 参数以数组方式传递,call 多个参数则是逗号隔开。

apply(context, [arguments]);
call(context, arg1, arg2, arg3, ....);

代码例子中使用到了ES6语法,对ES6还不熟悉的话,可学习社区这篇文章:《30分钟掌握ES6/ES2015核心内容(上)》

函数柯里化

函数柯里化在JavaScript中其实是高阶函数的一种应用,上篇文章我们简略介绍了高阶函数(可以作为参数传递,或作为返回值)。

理论知识太枯燥,来个生活小例子,"存款买房"(富二代绕道)。假设买房是我们存钱的终极目标。那么在买房前,存在卡里的钱(老婆本)就不能动。等到够钱买房了,钱从银行卡取出来,开始买买买。。。

函数柯里化就像我们往卡里存钱,存够了,才能执行买房操作,存不够,接着存。

函数柯里化公式

先上几个公式(左边是普通函数,右边就是转化后柯里化函数支持的调用方式):

// 公式类型一
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a, b)(c)(d);
fn(a,b,c,d) => fn(a)(b,c,d);

// 公式类型二
fn(a,b,c,d) => fn(a)(b)(c)(d)();
fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();

两种公式类型的区别 —— 函数触发执行的机制不同:

公式一当传入参数等于函数参数数量时开始执行

公式二当没有参数传入时(且参数数量满足)开始执行

通过公式,我们先来理解这行代码 fn(a)(b)(c)(d), 执行 fn(a) 时返回的是一个函数,并且支持传参。何时返回目标函数结果值而不是函数的触发机制,控制权在我们手里,我们可以为函数制定不同的触发机制。

普通的函数调用,一次性传入参数就执行。而通过柯里化,它可以帮我们实现函数部分参数传入执行(并未立即执行原始函数,钱没存够接着存),这就是函数柯里化的特点:"延迟执行和部分求值"

"函数柯里化:指封装一个函数,接收原始函数作为参数传入,并返回一个能够接收并处理剩余参数的函数"

函数柯里化的例子
// 等待我们柯里化实现的方法add
function add(a, b, c, d) {
    return a + b + c + d;
};
// 最简单地实现函数add的柯里化
// 有点low,有助于理解
function add(a, b, c, d) {
    return function(a) {
        return function(b) {
            return function(c) {
                return a + b + c + d;
            }
        }
    }
}

分析代码知识点:

函数作为返回值返回,闭包形成,外部环境可访问函数内部作用域

子函数可访问父函数的作用域,作用域由内而外的作用域链查找规则,作用域嵌套形成

在函数参数数量不满足时,返回一个函数(该函数可接收并处理剩余参数)

当函数数量满足我们的触发机制(可自由制定),触发原始函数执行

前几篇文章的知识点此时刚好。可见基础知识的重要性,高阶的东西始终要靠小砖头堆砌出来。

弄清原理后,接下来就是将代码写得更通用些(高大上些)。

// 公式类型一: 参数数量满足函数参数要求,触发执行
// fn(a,b,c,d) => fn(a)(b)(c)(d);

const createCurry = (fn, ...args) => {
    let _args = args || [];
    let length = fn.length; // fn.length代码函数参数数量

    return (...rest) => {
        let _allArgs = _args.slice(0);  
        // 深拷贝闭包共用对象_args,避免后续操作影响(引用类型)
        _allArgs.push(...rest);
        if (_allArgs.length < length) {
            // 参数数量不满足原始函数数量,返回curry函数
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 参数数量满足原始函数数量,触发执行
            return fn.apply(this, _allArgs);
        }
    }
}

const curryAdd = createCurry(add, 2);
let sum = curryAdd(3)(4)(5);    // 14

// ES5写法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (_allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}
// 公式类型二: 无参数传入时并且参数数量已经满足函数要求
// fn(a, b, c, d) => fn(a)(b)(c)(d)();
// fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn();

const createCurry = (fn, ...args) => {
    let all = args || [];
    let length = fn.length;

    return (...rest) => {
        let _allArgs = all.slice(0);
        _allArgs.push(...rest);
        if (rest.length > 0 || _allArgs.length < length) {
            // 调用时参数不为空或存储的参数不满足原始函数参数数量时,返回curry函数
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 调用参数为空(),且参数数量满足时,触发执行
            return fn.apply(this, _allArgs);
        }
    }
}
const curryAdd = createCurry(add, 2);
let sum = curryAdd(3)(4)(5)();  // 14

// ES5写法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (arguments.length > 0 || _allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}

为实现公式中不同的两种调用公式,两个createCurry方法制定了两种不同的触发机制。记住一个点,函数触发机制可根据需求自行制定。

偏函数与柯里化的区别

先上个公式看对比:

// 函数柯里化:参数数量完整
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a,b)(c)(d);

// 偏函数:只执行了部分参数
fn(a,b,c,d) => fn(a);
fn(a,b,c,d) => fn(a, b);

"函数柯里化中,当你传入部分参数时,返回的并不是原始函数的执行结果,而是一个可以继续支持后续参数的函数。而偏函数的调用方式更像是普通函数的调用方式,只调用一次,它通过原始函数内部来实现不定参数的支持。"

如果已经看懂上述柯里化的代码例子,那么改写支持偏函数的代码,并不难。

// 公式:
// fn(a, b, c, d) => fn(a);
// fn(a, b, c, d) => fn(a,b,c);

const partialAdd = (a = 0, b = 0, c = 0, d = 0) => {
    return a + b + c +d;
}

partialAdd(6);      // 6
partialAdd(2, 3);   // 5

使用ES6函数参数默认值,为没有传入参数,指定默认值为0,支持无参数或不定参数传入。

柯里化的特点:

参数复用(固定易变因素)

延迟执行

提前返回

柯里化的缺点

柯里化是牺牲了部分性能来实现的,可能带来的性能损耗:

存取 arguments 对象要比存取命名参数要慢一些

老版本浏览器在 arguments.lengths 的实现相当慢(新版本浏览器忽略)

fn.apply()fn.call() 要比直接调用 fn()

大量嵌套的作用域和闭包会带来开销,影响内存占用和作用域链查找速度

柯里化的应用

利用柯里化制定约束条件,管控触发机制

处理浏览器兼容(参数复用实现一次性判断)

函数节流防抖(延迟执行)

ES5前bind方法的实现

一个应用例子:浏览器事件绑定的兼容处理
// 普通事件绑定函数
var addEvent = function(ele, type, fn, isCapture) {
    if(window.addEventListener) {
        ele.addEventListener(type, fn, isCapture)
    } else if(window.attachEvent) {

        ele.attachEvent("on" + type, fn)
    }
}
// 弊端:每次调用addEvent都会进行判断

// 柯里化事件绑定函数
var addEvent = (function() {
    if(window.addEventListener) {
        return function(ele, type, fn, isCapture) {
            ele.addEventListener(type, fn, isCapture)
        }
    } else if(window.attachEvent) {
        return function(ele, type, fn) {
             ele.attachEvent("on" + type, fn)
        }
    }
})()
// 优势:判断只执行一次,通过闭包保留了父级作用域的判断结果
秒懂反柯里化

先上公式,从来没有这么喜欢写公式,简明易懂。

// 反柯里化公式:
curryFn(a)(b)(c)(d) = fn(a, b, c, d);
curryFn(a) = fn(a);

看完公式,是不是似曾相识,这不就是我们日常敲码的普通函数么?没错的,函数柯里化就是把普通函数变成成一个复杂的函数,而反柯里化其就是柯里化的逆反,把复杂变得简单。

函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数:fn(a,b,c,d) => fn(a)(b)(c)(d),而反柯里化就是把参数全部释放出来:fn(a)(b)(c)(d) => fn(a,b,c,d)

// 反柯里化:最简单的反柯里化(普通函数)
function add(a, b, c, d) {
    return a + b + c + d;
}
反思:为何要使用柯里化

函数柯里化是函数编程中的一个重要的基础,它为我们提供了一种编程的思维方式。显然,它让我们的函数处理变得复杂,代码调用方式并不直观,还加入了闭包,多层作用域嵌套,会有一些性能上的影响。

但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。

实际开发中,如果你的逻辑处理相对复杂,不妨换个思维,用函数柯里化来实现,技能包不嫌多。
说到底,程序员就是解决问题的那群人。

写在结尾

本篇函数柯里化知识点的理解确实存在难度,暂时跳过这章也无妨,可以先了解再深入。耐得主寂寞的小伙伴回头多啃几遍,没准春季面试就遇到了。

参考文档:

js高阶函数应用—函数柯里化和反柯里化

前端基础进阶(八):深入详解函数的柯里化

系列更文请关注专栏:《前端进击的巨人》,不断更新中。。。

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101329.html

相关文章

  • 前端进击巨人(六):知否知否,须知this

    摘要:有关函数柯里化的详解,请回阅前端进击的巨人五学会函数柯里化。构造函数中的通过操作符可以实现对函数的构造调用。在了解构造函数中的前,有必要先了解下实例化对象的过程。 showImg(https://segmentfault.com/img/bVburMp?w=800&h=600); 常见this的误解 指向函数自身(源于this英文意思的误解) 指向函数的词法作用域(部分情况) th...

    Andrman 评论0 收藏0
  • JavaScript函数式编程(一)

    摘要:所以下面介绍一些函数式编程的知识和概念。函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。我会在下一篇文章中介绍函数式编程的更加高阶一些的知识,例如等等概念。 一、引言 说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东...

    Shihira 评论0 收藏0
  • 前端面试题系列6」理解函数柯里

    摘要:原题如下写一个方法,当使用下面的语法调用时,能正常工作这道题要考察的,就是对函数柯里化的理解。当参数只有一个的时候,进行柯里化的处理。这其实就是函数柯里化的简单应用。 showImg(https://segmentfault.com/img/bVbopGm?w=620&h=350); 前言 这是前端面试题系列的第 6 篇,你可能错过了前面的篇章,可以在这里找到: ES6 中箭头函数的...

    liaorio 评论0 收藏0
  • JS中柯里

    摘要:作为函数式编程语言,带来了很多语言上的有趣特性,比如柯里化和反柯里化。个人理解不知道对不对延迟执行柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。 作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化和反柯里化。 这里可以对照另外一篇介绍 JS 反柯里化 的文章一起看~ 1. 简介 柯里化(Currying),又称部分求值(Partial Evalu...

    Hancock_Xu 评论0 收藏0
  • JavaScript 函数式编程技巧 - 柯里

    摘要:作为函数式编程语言,带来了很多语言上的有趣特性,比如柯里化和反柯里化。在一些函数式编程语言中,会定义一个特殊的占位变量。个人理解不知道对不对延迟执行柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化和反柯里化。 这里可以对照另外一篇介绍 JS 反柯里化 的文章一起看~ 1. 简介 柯里化(Currying)...

    edgardeng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<