专栏首页小码农学习笔记一文看懂 JavaScript 函数柯里化 - 什么是柯里化
原创

一文看懂 JavaScript 函数柯里化 - 什么是柯里化

函数柯里化

柯里化的定义

红宝书(第3版):用于创建已经设置好了一个或多个参数的函数。基本方法是使用一个闭包返回一个函数。(P604)

维基百科:柯里化(英语:Currying),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。(原链接

官方解释看得有点懵,大白话概括一下:

柯里化技术,主要体现在函数里面返回函数。就是将多变量函数拆解为单变量(或部分变量)的多个函数并依次调用。

再直白一点:利用闭包,可以形成一个不销毁的私有作用域,把预先处理的内容都存在这个不销毁的作用域里面,并且返回一个函数,以后要执行的就是这个函数。

PS:如果还是不理解也没关系,跟闭包一样不用死扣定义,继续往下面看应用就行了。

柯里化的应用

柯里化有 3 个常见应用:

  • 参数复用 – 当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选
  • 提前返回 – 多次调用多次内部判断,可以直接把第一次判断的结果返回外部接收
  • 延迟计算/运行 – 避免重复的去执行程序,等真正需要结果的时候再执行

应用一:参数复用

如下名为 uri 的函数,接收 3 个参数,函数的作用是返回三个参数拼接的字符串。

function uri(protocol, hostname, pathname) {
  return `${protocol}${hostname}${pathname}`;
}

// 测试一下
const uri1 = url('https://', 'www.fedbook.cn', '/function-curring/')
console.log(uri1)

上面这种写法的弊端是:当我们有很多网址时,会导致非常多重复的参数(比如 https:// 就是重复的参数,我们在浏览器里面输入网址也不需要输入 http 或者 https)。

利用柯里化实现参数复用的思路:

  • 原函数(称为函数 A)只设置一个参数(接收协议这个参数);
  • 在函数内部再创建一个函数(称为函数 B),函数 B 把刚才剩余的两个参数给补上,并返回字符串形式的 url;
  • 函数 A 返回函数 B。
function uri_curring(protocol) {
  return function(hostname, pathname) {
    return `${protocol}${hostname}${pathname}`; 
  }
}

// 测试一下
const uri_https = uri_curring('https://');

const uri1 = uri_https('www.fedbook.cn', '/frontend-languages/javascript/function-currying/');
const uri2 = uri_https('www.fedbook.cn', '/handwritten/javascript/10-实现bind方法/');
const uri3 = uri_https('www.wenyuanblog.com', '/');

console.log(uri1);
console.log(uri2);
console.log(uri3);

应用二:兼容性检测

::: warning

以下代码为了编写方便,会使用 ES6 的语法。实际生产环境中如果要做兼容性检测功能,需要转换成 ES5 语法。

:::

因为浏览器的发展和各种原因,有些函数和方法是不被部分浏览器支持的,此时需要提前进行判断,从而确定用户的浏览器是否支持相应的方法。

以事件监听为例,IE(IE9 之前) 支持的是 attachEvent 方法,其它主流浏览器支持的是 addEventListener 方法,我们需要创建一个新的函数来进行两者的判断。

const addEvent  = function(element, type, listener, useCapture) {
  if(window.addEventListener) {
    console.log('判断为其它浏览器')
    // 和原生 addEventListener 一样的函数
    // element: 需要添加事件监听的元素
    // type: 为元素添加什么类型的事件
    // listener: 执行的回调函数
    // useCapture: 要进行事件冒泡或者事件捕获的选择
    element.addEventListener(type, function(e) {
      // 为了规避 this 指向问题,用 call 进行 this 的绑定
      listener.call(element, e);
    }, useCapture);
  } else if(window.attachEvent) {
    console.log('判断为 IE9 以下浏览器')
    // 原生的 attachEvent 函数
    // 不需要第四个参数,因为 IE 支持的是事件冒泡
    // 多拼接一个 on,这样就可以使用统一书写形式的事件类型了
    element.attachEvent('on' + type, function(e) {
      listener.call(element, e);
    });
  }
}

// 测试一下
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {console.log('点击了 div');}, true);
addEvent(p, 'click', (e) => {console.log('点击了 p');}, true);
addEvent(span, 'click', (e) => {console.log('点击了 span');}, true);

上面这种封装的弊端是:每次写监听事件的时候调用 addEvent 函数,都会进行 if...else... 的兼容性判断。事实上在代码中只需要执行一次兼容性判断就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。

那么怎么用函数柯里化优化这个封装函数?

// 使用立即执行函数,当我们把这个函数放在文件的头部,就可以先进行执行判断
const addEvent  = (function() {
  if(window.addEventListener) {
    console.log('判断为其它浏览器')
    return function(element, type, listener, useCapture) {
      element.addEventListener(type, function(e) {
        listener.call(element, e);
      }, useCapture);
    }
  } else if(window.attachEvent) {
    console.log('判断为 IE9 以下浏览器')
    return function(element, type, handler) {
      element.attachEvent('on'+type, function(e) {
        handler.call(element, e);
      });
    }
  }
}) ();

// 测试一下
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {console.log('点击了 div');}, true);
addEvent(p, 'click', (e) => {console.log('点击了 p');}, true);
addEvent(span, 'click', (e) => {console.log('点击了 span');}, true);

上述封装因为立即执行函数的原因,触发多次事件也依旧只会触发一次 if 条件判断。

这里使用了函数柯里化的两个特点:提前返回和延迟执行。

应用三:实现一个 add 函数

这是一道经典面试题,要求我们实现一个 add 函数,可以实现以下计算结果:

add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

通过这道题正好可以解释柯里化的延迟执行,直接上代码:

function add() {
  // 将传入的不定参数转为数组对象
  let args = Array.prototype.slice.call(arguments);

  // 递归:内部函数里面进行自己调用自己
  // 当 add 函数不断调用时,把第 N+1 个括号的参数加入到第 N 个括号的参数里面
  let inner = function() {
    args.push(...arguments);
    return inner;
  }
  
  inner.toString = function() {
    // args 里的值不断累加
    return args.reduce(function(prev, cur) {
      return prev + cur;  
    });
  };

  return inner;
}

// 测试一下
let result = add(1)(2)(3)(4);
console.log(result);

解释几个关键点:

1)不定参数 arguments 需要转为数组对象:

因为 arguments 并不是真正的数组,而是与数组类似对象,Array.prototype.slice.call(arguments) 能将具有 length 属性的对象转成数组。

2)toString 隐形转换的特性:

对于 add(1)(2)(3)(4),执行每个括号的时候都返回 inner 函数,不断自己调用自己,每次内部函数返回的都是内部函数。

如果打印函数执行的最终返回结果,可以发现返回了一个字符串(原本的函数被转换为字符串返回了),这即是发生了隐式转换,而发生隐式转换是因为调用了内部的 toString 方法。

知道了这一点,我们就可以利用这个特性自定义返回的内容:重写 inner 函数的 toString 方法,在里面实现参数相加的执行代码。

值得一提的是,这种处理后能够返回正确的累加结果,但返回的结果是个函数类型(function),这是因为我们在用递归返回函数,内部函数在被隐式转换为字符串之前本来就是一个函数。


文章持续更新,本文 GitHub 前端修炼小册 已经收录,欢迎 Star。如对文章内容有不同见解,欢迎留言交流。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一文讲懂什么是函数柯里化,柯里化的目的及其代码实现

    柯里化(Currying)[1]是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。

    @超人
  • javascript教程:实现函数柯里化与反柯里化

    维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。其由数学家Haske...

    用户4831957
  • JavaScript: 函数式编程 - 柯里化

    curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次...

    西南_张家辉
  • [译]为什么柯里化是有用的

    作者:Hugh FD Jackson 原标题: 《Why Curry Helps》 原文地址:https://hughfdjackson.com/javascr...

    the5fire
  • [译]柯里化有用吗?

    在两年半前我写了这篇文章《为什么柯里化是有用的》 ——一些关于在JavaScript中使用柯里化函数的赞美之言。这篇文章轻松成为阅读量最大的一篇,每个月给我带来...

    the5fire
  • 一段柯里化函数代码阅读

    柯里化的概念大家应该都清楚,就是将一个接受多个参数的函数转化为接受单一参数的函数的技术。

    用户1515472
  • 函数curry化(Haskell Curry)

    当一个函数fn有多个参数时,可以先传入一部分参数,生成一个中继函数nextFn,然后在nextFn当中再传入剩下的参数。(一步curry化)

    elson
  • javascript中bind绑定接收者与函数柯里化

    如果我已经把匿名函数抽象出来,做成了一个公共的方法 (可能其他地方也会用的到) 那么,这个遍历会是这样的;

    liulun
  • JavaScript 中的函数式编程:函数,组合和柯里化

    但是,JavaScript 并没有一直遵循一个规则,而是正好处于这两个规则的中间,它提供了普通OOP语言的一些方面,比如类、对象、继承等等。但与此同时,它还为你...

    前端小智@大迁世界
  • JavaScript 中的函数式编程:函数,组合和柯里化

    作者:Fernando Doglio 译者:前端小智 来源:medium 移动端阅读:点这里

    用户7886150
  • JavaScript 柯里化

    柯里化即 Currying,是一门编译原理层面的技术,用途是实现多参函数,其为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受第一个参...

    Leophen
  • JavaScript 设计模式学习第三篇- 闭包与高阶函数

    JavaScript 的函数也是对象,可以有属性,可以赋值给一个变量,可以放在数组里作为元素,可以作为其他对象的属性,什么都可以做,别的对象能做的它能做,别的对...

    越陌度阡
  • 【JS】222-JS 函数的 6 个基本术语

    让我们谈谈什么是:lambdas(匿名函数)、 first-class functions(头等函数)、higher-order functions(高阶函数)...

    pingan8787
  • JavaScript函数式编程,真香之组合(二)

    组合是一种为软件的行为,进行清晰建模的一种简单、优雅而富于表现力的方式。通过组合小的、确定性的函数,来创建更大的软件组件和功能的过程,会生成更容易组织、理解、调...

    桃翁
  • 译 | JavaScript函数的6个基本术语

    让我们谈谈什么是:lambdas(匿名函数)、 first-class functions(头等函数)、higher-order functions(高阶函数)...

    苏南
  • 柯里化与反柯里化

    柯里化与反柯里化 最近在看一本书《JavaScript函数式编程》 里边提到了一个名词,柯里化(currying),阅读后发现在日常...

    贾顺名
  • 柯里化与反柯里化

    一个柯里化函数的简单应用,我们有一个进行三个参数求和的函数。 我们可以调用currying传入sum获得sum1,一个固定了第一个参数为10的求和函数 然后我们...

    贾顺名
  • 一文带你了解JavaScript 函数式编程

    函数式编程在前端已经成为了一个非常热门的话题。在最近几年里,我们看到非常多的应用程序代码库里大量使用着函数式编程思想。

    心莱科技雪雁
  • 彻底搞懂闭包,柯里化,手写代码,金九银十不再丢分!

    这段时间我试着通过思维导图来总结知识点,主要关注的是一些相对重要或理解难度较高的内容。下面是同系列文章:

    Tusi

扫码关注云+社区

领取腾讯云代金券