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

写在开头

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

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

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

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


函数柯里化

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

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

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

函数柯里化公式

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

  1. // 公式类型一
  2. fn(a,b,c,d) => fn(a)(b)(c)(d);
  3. fn(a,b,c,d) => fn(a, b)(c)(d);
  4. fn(a,b,c,d) => fn(a)(b,c,d);
  5. // 公式类型二
  6. fn(a,b,c,d) => fn(a)(b)(c)(d)();
  7. fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();

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

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

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

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

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

函数柯里化的例子

  1. // 等待我们柯里化实现的方法add
  2. function add(a, b, c, d) {
  3. return a + b + c + d;
  4. };
  1. // 最简单地实现函数add的柯里化
  2. // 有点low,有助于理解
  3. function add(a, b, c, d) {
  4. return function(a) {
  5. return function(b) {
  6. return function(c) {
  7. return a + b + c + d;
  8. }
  9. }
  10. }
  11. }

分析代码知识点:

  1. 函数作为返回值返回,闭包形成,外部环境可访问函数内部作用域
  2. 子函数可访问父函数的作用域,作用域由内而外的作用域链查找规则,作用域嵌套形成
  3. 在函数参数数量不满足时,返回一个函数(该函数可接收并处理剩余参数)
  4. 当函数数量满足我们的触发机制(可自由制定),触发原始函数执行

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

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

  1. // 公式类型一: 参数数量满足函数参数要求,触发执行
  2. // fn(a,b,c,d) => fn(a)(b)(c)(d);
  3. const createCurry = (fn, ...args) => {
  4. let _args = args || [];
  5. let length = fn.length; // fn.length代码函数参数数量
  6. return (...rest) => {
  7. let _allArgs = _args.slice(0);
  8. // 深拷贝闭包共用对象_args,避免后续操作影响(引用类型)
  9. _allArgs.push(...rest);
  10. if (_allArgs.length < length) {
  11. // 参数数量不满足原始函数数量,返回curry函数
  12. return createCurry.call(this, fn, ..._allArgs);
  13. } else {
  14. // 参数数量满足原始函数数量,触发执行
  15. return fn.apply(this, _allArgs);
  16. }
  17. }
  18. }
  19. const curryAdd = createCurry(add, 2);
  20. let sum = curryAdd(3)(4)(5); // 14
  21. // ES5写法
  22. function createCurry() {
  23. var fn = arguments[0];
  24. var _args = [].slice.call(arguments, 1);
  25. var length = fn.length;
  26. return function() {
  27. var _allArgs = _args.slice(0);
  28. _allArgs = _allArgs.concat([].slice.call(arguments));
  29. if (_allArgs.length < length) {
  30. _allArgs.unshift(fn);
  31. return createCurry.apply(this, _allArgs);
  32. } else {
  33. return fn.apply(this, _allArgs);
  34. }
  35. }
  36. }
  1. // 公式类型二: 无参数传入时并且参数数量已经满足函数要求
  2. // fn(a, b, c, d) => fn(a)(b)(c)(d)();
  3. // fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn();
  4. const createCurry = (fn, ...args) => {
  5. let all = args || [];
  6. let length = fn.length;
  7. return (...rest) => {
  8. let _allArgs = all.slice(0);
  9. _allArgs.push(...rest);
  10. if (rest.length > 0 || _allArgs.length < length) {
  11. // 调用时参数不为空或存储的参数不满足原始函数参数数量时,返回curry函数
  12. return createCurry.call(this, fn, ..._allArgs);
  13. } else {
  14. // 调用参数为空(),且参数数量满足时,触发执行
  15. return fn.apply(this, _allArgs);
  16. }
  17. }
  18. }
  19. const curryAdd = createCurry(2);
  20. let sum = curryAdd(3)(4)(5)(); // 14
  21. // ES5写法
  22. function createCurry() {
  23. var fn = arguments[0];
  24. var _args = [].slice.call(arguments, 1);
  25. var length = fn.length;
  26. return function() {
  27. var _allArgs = _args.slice(0);
  28. _allArgs = _allArgs.concat([].slice.call(arguments));
  29. if (arguments.length > 0 || _allArgs.length < length) {
  30. _allArgs.unshift(fn);
  31. return createCurry.apply(this, _allArgs);
  32. } else {
  33. return fn.apply(this, _allArgs);
  34. }
  35. }
  36. }

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

偏函数与柯里化的区别

先上个公式看对比:

  1. // 函数柯里化:参数数量完整
  2. fn(a,b,c,d) => fn(a)(b)(c)(d);
  3. fn(a,b,c,d) => fn(a,b)(c)(d);
  4. // 偏函数:只执行了部分参数
  5. fn(a,b,c,d) => fn(a);
  6. fn(a,b,c,d) => fn(a, b);

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

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

  1. // 公式:
  2. // fn(a, b, c, d) => fn(a);
  3. // fn(a, b, c, d) => fn(a,b,c);
  4. const partialAdd = (a = 0, b = 0, c = 0, d = 0) => {
  5. return a + b + c +d;
  6. }
  7. partialAdd(6); // 6
  8. partialAdd(2, 3); // 5

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

柯里化的特点:

  1. 参数复用(固定易变因素)
  2. 延迟执行
  3. 提前返回

柯里化的缺点

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

  1. 存取 arguments 对象要比存取命名参数要慢一些
  2. 老版本浏览器在 arguments.lengths 的实现相当慢(新版本浏览器忽略)
  3. fn.apply()fn.call() 要比直接调用 fn()
  4. 大量嵌套的作用域和闭包会带来开销,影响内存占用和作用域链查找速度

柯里化的应用

  • 利用柯里化制定约束条件,管控触发机制
  • 处理浏览器兼容(参数复用实现一次性判断)
  • 函数节流防抖(延迟执行)
  • ES5前bind方法的实现

一个应用例子:浏览器事件绑定的兼容处理

  1. // 普通事件绑定函数
  2. var addEvent = function(ele, type, fn, isCapture) {
  3. if(window.addEventListener) {
  4. ele.addEventListener(type, fn, isCapture)
  5. } else if(window.attachEvent) {
  6. ele.attachEvent("on" + type, fn)
  7. }
  8. }
  9. // 弊端:每次调用addEvent都会进行判断
  10. // 柯里化事件绑定函数
  11. var addEvent = (function() {
  12. if(window.addEventListener) {
  13. return function(ele, type, fn, isCapture) {
  14. ele.addEventListener(type, fn, isCapture)
  15. }
  16. } else if(window.attachEvent) {
  17. return function(ele, type, fn) {
  18. ele.attachEvent("on" + type, fn)
  19. }
  20. }
  21. })()
  22. // 优势:判断只执行一次,通过闭包保留了父级作用域的判断结果

秒懂反柯里化

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

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

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

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

  1. // 反柯里化:最简单的反柯里化(普通函数)
  2. function add(a, b, c, d) {
  3. return a + b + c + d;
  4. }

反思:为何要使用柯里化

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

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

实际开发中,如果你的逻辑处理相对复杂,不妨换个思维,用函数柯里化来实现,技能包不嫌多。

说到底,程序员就是解决问题的那群人。


写在结尾

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


参考文档:

本文首发Github,期待Star!

https://github.com/ZengLingYong/blog

作者:以乐之名

本文原创,有不当的地方欢迎指出。转载请指明出处。

前端进击的巨人(五):学会函数柯里化(curry)的更多相关文章

  1. Scala 基础(十二):Scala 函数式编程(四)高级(二)参数(类型)推断、闭包(closure)、函数柯里化(curry)、控制抽象

    1  参数(类型)推断 参数推断省去类型信息(在某些情况下[需要有应用场景],参数类型是可以推断出来的,如list=(1,2,3) list.map() map中函数参数类型是可以推断的),同时也可以 ...

  2. Javascript函数柯里化(curry)

    函数柯里化currying,是函数式编程非常重要的一个标志.它的实现需要满足以下条件,首先就是函数可以作为参数进行传递,然后就是函数可以作为返回值return出去.我们依靠这个特性编写很多优雅酷炫的代 ...

  3. 函数柯里化 curry

    一.函数柯里化的特性: (1)参数复用 $.ajax // 示例一 function ajax(type,url,data) { var xhr = new XMLHttpRequest(); xhr ...

  4. 前端开发者进阶之函数柯里化Currying

    穆乙:http://www.cnblogs.com/pigtail/p/3447660.html 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接 ...

  5. JavaScript函数柯里化的一些思考

    1. 高阶函数的坑 在学习柯里化之前,我们首先来看下面一段代码: var f1 = function(x){ return f(x); }; f1(x); 很多同学都能看出来,这些写是非常傻的,因为函 ...

  6. 前端面试手写代码——JS函数柯里化

    目录 1 什么是函数柯里化 2 柯里化的作用和特点 2.1 参数复用 2.2 提前返回 2.3 延迟执行 3 封装通用柯里化工具函数 4 总结和补充 1 什么是函数柯里化 在计算机科学中,柯里化(Cu ...

  7. 【React】学习笔记(一)——React入门、面向组件编程、函数柯里化

    课程原视频:https://www.bilibili.com/video/BV1wy4y1D7JT?p=2&spm_id_from=pageDriver 目录 一.React 概述 1.1.R ...

  8. 【转载】JS中bind方法与函数柯里化

    原生bind方法 不同于jQuery中的bind方法只是简单的绑定事件函数,原生js中bind()方法略复杂,该方法上在ES5中被引入,大概就是IE9+等现代浏览器都支持了(有关ES5各项特性的支持情 ...

  9. [转]js函数式变成之函数柯里化

    本文转自:https://segmentfault.com/a/1190000003733107 函数柯里化是指参数逐渐求值的过程. 我觉得它是:降低通用性,提高专用性. 通常,柯里化是这样的过程,“ ...

随机推荐

  1. Zabbix安装部署(CentOS系统下)

    zabbix(音同 zæbix)是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案. zabbix能监视各种网络参数,保证服务器系统的安全运营:并提供灵活的通知机制以让系统 ...

  2. Python中操作Redis

    一 Rdis基本介绍 redis是一个key-value存储系统.它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted set -- ...

  3. hugepage优势

    hugepage的优势与使用 +2投票   优势 通过使用hugepage分配可以提高性能,因为需要更少的页,因此需要更少Translation Lookaside Buffers (TLB,高速传送 ...

  4. 20172306《Java程序设计与数据结构》第一周总结

    20172306<Java程序设计>第一周学习总结 教材学习内容总结 本周主要学习<Android和Java>书中的第二十三章和第二十六章. 第二十三章:Android简介 A ...

  5. python07 函数式编程

    1.作用域 1.1  pass关键字表示,暂时不写该方法 1.2表示返回值为方法 输出结果333 1.3函数作用域:和函数调用没关系,和声明的位置有关系, 结果为444 2.匿名函数 lanmbda ...

  6. redis主从复制详述

    一.主从复制详述 原理其实很简单,master启动会生成一个run id,首次同步时会发送给slave,slave同步命令会带上run id以及offset,显然,slave启动(初次,重启)内存中没 ...

  7. 别人的Linux私房菜(11)认识与学习BASH

    Linux下使用BASH   Bourne Again Shell        另外一种由用于Unix的伯克利大学的Bill Joy设计的C Shell 系统中合法的shell会写入到/etc/sh ...

  8. Linux 网卡Bond模式

    网卡bond是通过把多张网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡. 有7种模式: mod 0/mod 1/mod 2/mod 3/mod 4/mod 5 mod=0 ,即:(ba ...

  9. overlay fs挂载及操作测试

    overlayfs是目前使用比较广泛的层次文件系统,实现简单,性能较好,可以充分利用不同或则相同overlay文件系统的page cache,具有 上下合并 同名遮盖 写时拷贝 等特点. 一个 ove ...

  10. SPI通信协议(非原创,转载他人,用于学习)

    SPI通信协议:1.SPI主从模式:2.数据信号的相位与极性:3.数据帧的格式. 一.什么是SPI? SPI是串行外设接口(Serial Peripheral Interface)的缩写.是 Moto ...