0

0

JavaScript函数柯里化

小云云

小云云

发布时间:2017-12-06 16:11:27

|

1477人浏览过

|

来源于php中文网

原创

什么是柯里化?

在计算机科学中,柯里化(英语:currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是moses schönfinkel和戈特洛布·弗雷格发明的。

在直觉上,柯里化声称如果你固定某些参数,你将得到接受余下参数的一个函数。
在理论计算机科学中,柯里化提供了在简单的理论模型中,比如:只接受一个单一参数的lambda演算中,研究带有多个参数的函数的方式。
函数柯里化的对偶是Uncurrying,一种使用匿名单参数函数来实现多参数函数的方法。

方便的理解

Currying概念其实很简单,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

如果我们需要实现一个求三个数之和的函数:

<span style="font-size: 16px;">function add(x, y, z) {<br/>  return x + y + z;<br/>}<br/>console.log(add(1, 2, 3)); // 6<br/></span>
<span style="font-size: 16px;">var add = function(x) {<br/>  return function(y) {<br/>    return function(z) {<br/>      return x + y + z;<br/>    }<br/>  }<br/>}<br/><br/>var addOne = add(1);<br/>var addOneAndTwo = addOne(2);<br/>var addOneAndTwoAndThree = addOneAndTwo(3);<br/><br/>console.log(addOneAndTwoAndThree);<br/></span>

这里我们定义了一个add函数,它接受一个参数并返回一个新的函数。调用add之后,返回的函数就通过闭包的方式记住了add的第一个参数。一次性地调用它实在是有点繁琐,好在我们可以使用一个特殊的curry帮助函数(helper function)使这类函数的定义和调用更加容易。

用ES6的箭头函数,我们可以将上面的add实现成这样:

立即学习Java免费学习笔记(深入)”;

<span style="font-size: 16px;">const add = x => y => z => x + y + z;<br/></span>

好像使用箭头函数更清晰了许多。

偏函数?

来看这个函数:

<span style="font-size: 16px;">function ajax(url, data, callback) {<br/>  // ..<br/>}<br/></span>

有这样的一个场景:我们需要对多个不同的接口发起HTTP请求,有下列两种做法:

  • 在调用ajax()函数时,传入全局URL常量。

  • 创建一个已经预设URL实参的函数引用。

下面我们创建一个新函数,其内部仍然发起ajax()请求,此外在等待接收另外两个实参的同时,我们手动将ajax()第一个实参设置成你关心的API地址。

对于第一种做法,我们可能产生如下调用方式:

<span style="font-size: 16px;">function ajaxTest1(data, callback) {<br/>  ajax('http://www.test.com/test1', data, callback);<br/>}<br/><br/>function ajaxTest2(data, callback) {<br/>  ajax('http://www.test.com/test2', data, callback);<br/>}<br/></span>

对于这两个类似的函数,我们还可以提取出如下的模式:

<span style="font-size: 16px;">function beginTest(callback) {<br/>  ajaxTest1({<br/>    data: GLOBAL_TEST_1,<br/>  }, callback);<br/>}<br/></span>

相信您已经看到了这样的模式:我们在函数调用现场(function call-site),将实参应用(apply) 于形参。如你所见,我们一开始仅应用了部分实参 —— 具体是将实参应用到URL形参 —— 剩下的实参稍后再应用。

上述概念即为偏函数的定义,偏函数一个减少函数参数个数的过程;这里的参数个数指的是希望传入的形参的数量。我们通过ajaxTest1()把原函数ajax()的参数个数从3个减少到了2个。

我们这样定义一个partial()函数:

<span style="font-size: 16px;">function partial(fn, ...presetArgs) {<br/>  return function partiallyApplied(...laterArgs) {<br/>    return fn(...presetArgs, ...laterArgs);<br/>  }<br/>}<br/></span>

partial()函数接收fn参数,来表示被我们偏应用实参(partially apply)的函数。接着,fn形参之后,presetArgs数组收集了后面传入的实参,保存起来稍后使用。

我们创建并return了一个新的内部函数(为了清晰明了,我们把它命名为partiallyApplied(..)),该函数中,laterArgs数组收集了全部实参。

使用箭头函数,则更为简洁:

<span style="font-size: 16px;">var partial =<br/>  (fn, ...presetArgs) =><br/>    (...laterArgs) =><br/>      fn(...presetArgs, ...laterArgs);<br/></span>

使用偏函数的这种模式,我们重构之前的代码:

<span style="font-size: 16px;">function ajax(url, data, callback) {<br/>  // ..<br/>}<br/><br/>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br/>var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');<br/></span>

再次思考beginTest()函数,我们使用partial()来重构它应该怎么做呢?

<span style="font-size: 16px;">function ajax(url, data, callback) {<br/>  // ..<br/>}<br/><br/>// 版本1<br/>var beginTest = partial(ajax, 'http://www.test.com/test1', {<br/>  data: GLOBAL_TEST_1,<br/>});<br/><br/>// 版本2<br/>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br/>var beginTest = partial(ajaxTest1, {<br/>  data: GLOBAL_TEST_1,<br/>});<br/></span>

一次传一个

相信你已经在上述例子中看到了版本2比起版本1的优势所在了,没错,柯里化就是:将一个带有多个参数的函数转换为一次一个的函数的过程。每次调用函数时,它只接受一个参数,并返回一个函数,直到传递所有参数为止。

The process of converting a function that takes multiple arguments into a function that takes them one at a time.

Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

假设我们已经创建了一个柯里化版本的ajax()函数curriedAjax():

<span style="font-size: 16px;">curriedAjax('http://www.test.com/test1')<br/>  ({<br/>    data: GLOBAL_TEST_1,<br/>  })<br/>  (function callback(data) {<br/>    // dosomething<br/>  });<br/></span>

我们将三次调用分别拆解开来,这也许有助于我们理解整个过程:

<span style="font-size: 16px;">var ajaxTest1 = curriedAjax('http://www.test.com/test1');<br/><br/>var beginTest = ajaxTest1({<br/>  data: GLOBAL_TEST_1,<br/>});<br/><br/>var ajaxCallback = beginTest(function callback(data) {<br/>  // dosomething<br/>});<br/></span>

实现柯里化

那么,我们如何来实现一个自动的柯里化的函数呢?

<span style="font-size: 16px;">var currying = function(fn) {<br/>  var args = [];<br/><br/>  return function() {<br/>    if (arguments.length === 0) {<br/>      return fn.apply(this, args); // 没传参数时,调用这个函数<br/>    } else {<br/>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br/>      return arguments.callee; // 返回这个函数的引用<br/>    }<br/>  }<br/>}<br/></span>

调用上述currying()函数:

PHP的使用技巧集
PHP的使用技巧集

PHP 独特的语法混合了 C、Java、Perl 以及 PHP 自创新的语法。它可以比 CGI或者Perl更快速的执行动态网页。用PHP做出的动态页面与其他的编程语言相比,PHP是将程序嵌入到HTML文档中去执行,执行效率比完全生成HTML标记的CGI要高许多。下面介绍了十个PHP高级应用技巧。 1, 使用 ip2long() 和 long2ip() 函数来把 IP 地址转化成整型存储到数据库里

下载
<span style="font-size: 16px;">var cost = (function() {<br/>  var money = 0;<br/>  return function() {<br/>    for (var i = 0; i < arguments.length; i++) {<br/>      money += arguments[i];<br/>    }<br/>    return money;<br/>  }<br/>})();<br/><br/>var cost = currying(cost);<br/><br/>cost(100); // 传入了参数,不真正求值<br/>cost(200); // 传入了参数,不真正求值<br/>cost(300); // 传入了参数,不真正求值<br/><br/>console.log(cost()); // 求值并且输出600<br/></span>

上述函数是我之前的JavaScript设计模式与开发实践读书笔记之闭包与高阶函数所写的currying版本,现在仔细思考后发现仍旧有一些问题。

我们在使用柯里化时,要注意同时为函数预传的参数的情况。

因此把上述柯里化函数更改如下:

<span style="font-size: 16px;">var currying = function(fn) {<br/>  var args = Array.prototype.slice.call(arguments, 1);<br/><br/>  return function() {<br/>    if (arguments.length === 0) {<br/>      return fn.apply(this, args); // 没传参数时,调用这个函数<br/>    } else {<br/>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br/>      return arguments.callee; // 返回这个函数的引用<br/>    }<br/>  }<br/>}<br/></span>

使用实例:

<span style="font-size: 16px;">var cost = (function() {<br/>  var money = 0;<br/>  return function() {<br/>    for (var i = 0; i < arguments.length; i++) {<br/>      money += arguments[i];<br/>    }<br/>    return money;<br/>  }<br/>})();<br/><br/>var cost = currying(cost, 100);<br/>cost(200); // 传入了参数,不真正求值<br/>cost(300); // 传入了参数,不真正求值<br/><br/>console.log(cost()); // 求值并且输出600<br/></span>

你可能会觉得每次都要在最后调用一下不带参数的cost()函数比较麻烦,并且在cost()函数都要使用arguments参数不符合你的预期。我们知道函数都有一个length属性,表明函数期望接受的参数个数。因此我们可以充分利用预传参数的这个特点。

借鉴自mqyqingfeng:

<span style="font-size: 16px;">function sub_curry(fn) {<br/>  var args = [].slice.call(arguments, 1);<br/>  return function() {<br/>    return fn.apply(this, args.concat([].slice.call(arguments)));<br/>  };<br/>}<br/><br/>function curry(fn, length) {<br/><br/>  length = length || fn.length;<br/><br/>  var slice = Array.prototype.slice;<br/><br/>  return function() {<br/>    if (arguments.length < length) {<br/>      var combined = [fn].concat(slice.call(arguments));<br/>      return curry(sub_curry.apply(this, combined), length - arguments.length);<br/>    } else {<br/>      return fn.apply(this, arguments);<br/>    }<br/>  };<br/>}<br/></span>

在上述函数中,我们在currying的返回函数中,每次把arguments.length和fn.length作比较,一旦arguments.length达到了fn.length的数量,我们就去调用fn(return fn.apply(this, arguments);)

验证:

<span style="font-size: 16px;">var fn = curry(function(a, b, c) {<br/>  return [a, b, c];<br/>});<br/><br/>fn("a", "b", "c") // ["a", "b", "c"]<br/>fn("a", "b")("c") // ["a", "b", "c"]<br/>fn("a")("b")("c") // ["a", "b", "c"]<br/>fn("a")("b", "c") // ["a", "b", "c"]<br/></span>

bind方法的实现

使用柯里化,能够很方便地借用call()或者apply()实现bind()方法的polyfill。

<span style="font-size: 16px;">Function.prototype.bind = Function.prototype.bind || function(context) {<br/>  var me = this;<br/>  var args = Array.prototype.slice.call(arguments, 1);<br/>  return function() {<br/>    var innerArgs = Array.prototype.slice.call(arguments);<br/>    var finalArgs = args.concat(innerArgs);<br/>    return me.apply(contenxt, finalArgs);<br/>  }<br/>}<br/></span>

上述函数有的问题在于不能兼容构造函数。我们通过判断this指向的对象的原型属性,来判断这个函数是否通过new作为构造函数调用,来使得上述bind方法兼容构造函数。

Function.prototype.bind() by MDN如下说到:

绑定函数适用于用new操作符 new 去构造一个由目标函数创建的新的实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。然而, 原先提供的那些参数仍然会被前置到构造函数调用的前面。

这是基于MVC的JavaScript Web富应用开发的bind()方法实现:

<span style="font-size: 16px;">Function.prototype.bind = function(oThis) {<br/>  if (typeof this !== "function") {<br/>    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");<br/>  }<br/><br/>  var aArgs = Array.prototype.slice.call(arguments, 1),<br/>    fToBind = this,<br/>    fNOP = function() {},<br/>    fBound = function() {<br/>      return fToBind.apply(<br/>        this instanceof fNOP && oThis ? this : oThis || window,<br/>        aArgs.concat(Array.prototype.slice.call(arguments))<br/>      );<br/>    };<br/><br/>  fNOP.prototype = this.prototype;<br/>  fBound.prototype = new fNOP();<br/><br/>  return fBound;<br/>};<br/></span>

反柯里化(uncurrying)

可能遇到这种情况:拿到一个柯里化后的函数,却想要它柯里化之前的版本,这本质上就是想将类似f(1)(2)(3)的函数变回类似g(1,2,3)的函数。

下面是简单的uncurrying的实现方式:

<span style="font-size: 16px;">function uncurrying(fn) {<br/>  return function(...args) {<br/>    var ret = fn;<br/><br/>    for (let i = 0; i < args.length; i++) {<br/>      ret = ret(args[i]); // 反复调用currying版本的函数<br/>    }<br/><br/>    return ret; // 返回结果<br/>  };<br/>}<br/></span>

注意,不要以为uncurrying后的函数和currying之前的函数一模一样,它们只是行为类似!

<span style="font-size: 16px;">var currying = function(fn) {<br/>  var args = Array.prototype.slice.call(arguments, 1);<br/><br/>  return function() {<br/>    if (arguments.length === 0) {<br/>      return fn.apply(this, args); // 没传参数时,调用这个函数<br/>    } else {<br/>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br/>      return arguments.callee; // 返回这个函数的引用<br/>    }<br/>  }<br/>}<br/><br/>function uncurrying(fn) {<br/>  return function(...args) {<br/>    var ret = fn;<br/><br/>    for (let i = 0; i < args.length; i++) {<br/>      ret = ret(args[i]); // 反复调用currying版本的函数<br/>    }<br/><br/>    return ret; // 返回结果<br/>  };<br/>}<br/><br/>var cost = (function() {<br/>  var money = 0;<br/>  return function() {<br/>    for (var i = 0; i < arguments.length; i++) {<br/>      money += arguments[i];<br/>    }<br/>    return money;<br/>  }<br/>})();<br/><br/>var curryingCost = currying(cost);<br/>var uncurryingCost = uncurrying(curryingCost);<br/>console.log(uncurryingCost(100, 200, 300)()); // 600<br/></span>

柯里化或偏函数有什么用?

无论是柯里化还是偏应用,我们都能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。

另一个最能体现柯里化应用的的是,当函数只有一个形参时,我们能够比较容易地组合它们(单一职责原则(Single responsibility principle))。因此,如果一个函数最终需要三个实参,那么它被柯里化以后会变成需要三次调用,每次调用需要一个实参的函数。当我们组合函数时,这种单元函数的形式会让我们处理起来更简单。

归纳下来,主要为以下常见的三个用途:

  • 延迟计算

  • 参数复用

  • 动态生成函数

以上内容就是JavaScript函数柯里化讲解,希望对大家有帮助。

相关推荐:

js柯里化的实例详解

JS中的反柯里化

JavaScript函数柯里化详解

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

16

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

23

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

75

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

95

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

218

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

420

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

168

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

222

2026.03.03

C++高性能网络编程与Reactor模型实践
C++高性能网络编程与Reactor模型实践

本专题围绕 C++ 在高性能网络服务开发中的应用展开,深入讲解 Socket 编程、多路复用机制、Reactor 模型设计原理以及线程池协作策略。内容涵盖 epoll 实现机制、内存管理优化、连接管理策略与高并发场景下的性能调优方法。通过构建高并发网络服务器实战案例,帮助开发者掌握 C++ 在底层系统与网络通信领域的核心技术。

33

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号