JavaScript 专题之函数柯里化

JavaScript 专题系列第十三篇,讲解函数柯里化以及如何实现一个 curry 函数

定义

维基百科中对柯里化 (Currying) 的定义为:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

翻译成中文:

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

举个例子:

function add(a, b) {return a + b;
}// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3复制代码

用途

我们会讲到如何写出这个 curry 函数,并且会将这个 curry 函数写的很强大,但是在编写之前,我们需要知道柯里化到底有什么用?

举个例子:

// 示意而已
function ajax(type, url, data) {var xhr = new XMLHttpRequest();xhr.open(type, url, true);xhr.send(data);
}// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")// 利用 curry
var ajaxCurry = curry(ajax);// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");复制代码

想想 jQuery 虽然有 $.ajax 这样通用的方法,但是也有 $.get 和 $.post 的语法糖。(当然 jQuery 底层是否是这样做的,我就没有研究了)。

curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。

可是即便如此,是不是依然感觉没什么用呢?

如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 map 呢?

举个例子:

比如我们有这样一段数据:

var person = [{name: 'kevin'}, {name: 'daisy'}]复制代码

如果我们要获取所有的 name 值,我们可以这样做:

var name = person.map(function (item) {return item.name;
})复制代码

不过如果我们有 curry 函数:

var prop = curry(function (key, obj) {return obj[key]
});var name = person.map(prop('name'))复制代码

我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?

但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了?

person.map(prop('name')) 就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。

是不是感觉有点意思了呢?

第一版

未来我们会接触到更多有关柯里化的应用,不过那是未来的事情了,现在我们该编写这个 curry 函数了。

一个经常会看到的 curry 函数的实现为:

// 第一版
var curry = function (fn) {var args = [].slice.call(arguments, 1);return function() {var newArgs = args.concat([].slice.call(arguments));return fn.apply(this, newArgs);};
};复制代码

我们可以这样使用:

function add(a, b) {return a + b;
}var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3复制代码

已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。

第二版

// 第二版
function sub_curry(fn) {var args = [].slice.call(arguments, 1);return function() {return fn.apply(this, args.concat([].slice.call(arguments)));};
}function curry(fn, length) {length = length || fn.length;var slice = Array.prototype.slice;return function() {if (arguments.length < length) {var combined = [fn].concat(slice.call(arguments));return curry(sub_curry.apply(this, combined), length - arguments.length);} else {return fn.apply(this, arguments);}};
}复制代码

我们验证下这个函数:

var fn = curry(function(a, b, c) {return [a, b, c];
});fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]复制代码

效果已经达到我们的预期,然而这个 curry 函数的实现好难理解呐……

为了让大家更好的理解这个 curry 函数,我给大家写个极简版的代码:

function sub_curry(fn){return function(){return fn()}
}function curry(fn, length){length = length || 4;return function(){if (length > 1) {return curry(sub_curry(fn), --length)}else {return fn()}}
}var fn0 = function(){console.log(1)
}var fn1 = curry(fn0)fn1()()()() // 1复制代码

大家先从理解这个 curry 函数开始。

当执行 fn1() 时,函数返回:

curry(sub_curry(fn0))
// 相当于
curry(function(){return fn0()
})复制代码

当执行 fn1()() 时,函数返回:

curry(sub_curry(function(){return fn0()
}))
// 相当于
curry(function(){return (function(){return fn0()})()
})
// 相当于
curry(function(){return fn0()
})复制代码

当执行 fn1()()() 时,函数返回:

// 跟 fn1()() 的分析过程一样
curry(function(){return fn0()
})复制代码

当执行 fn1()()()() 时,因为此时 length > 2 为 false,所以执行 fn():

fn()
// 相当于
(function(){return fn0()
})()
// 相当于
fn0()
// 执行 fn0 函数,打印 1复制代码

再回到真正的 curry 函数,我们以下面的例子为例:

var fn0 = function(a, b, c, d) {return [a, b, c, d];
}var fn1 = curry(fn0);fn1("a", "b")("c")("d")复制代码

当执行 fn1("a", "b") 时:

fn1("a", "b")
// 相当于
curry(fn0)("a", "b")
// 相当于
curry(sub_curry(fn0, "a", "b"))
// 相当于
// 注意 ... 只是一个示意,表示该函数执行时传入的参数会作为 fn0 后面的参数传入
curry(function(...){return fn0("a", "b", ...)
})复制代码

当执行 fn1("a", "b")("c") 时,函数返回:

curry(sub_curry(function(...){return fn0("a", "b", ...)
}), "c")
// 相当于
curry(function(...){return (function(...) {return fn0("a", "b", ...)})("c")
})
// 相当于
curry(function(...){return fn0("a", "b", "c", ...)
})复制代码

当执行 fn1("a", "b")("c")("d") 时,此时 arguments.length < length 为 false ,执行 fn(arguments),相当于:

(function(...){return fn0("a", "b", "c", ...)
})("d")
// 相当于
fn0("a", "b", "c", "d")复制代码

函数执行结束。

所以,其实整段代码又很好理解:

sub_curry 的作用就是用函数包裹原函数,然后给原函数传入之前的参数,当执行 fn0(...)(...) 的时候,执行包裹函数,返回原函数,然后再调用 sub_curry 再包裹原函数,然后将新的参数混合旧的参数再传入原函数,直到函数参数的数目达到要求为止。

如果要明白 curry 函数的运行原理,大家还是要动手写一遍,尝试着分析执行步骤。

更易懂的实现

当然了,如果你觉得还是无法理解,你可以选择下面这种实现方式,可以实现同样的效果:

function curry(fn, args) {length = fn.length;args = args || [];return function() {var _args = args.slice(0),arg, i;for (i = 0; i < arguments.length; i++) {arg = arguments[i];_args.push(arg);}if (_args.length < length) {return curry.call(this, fn, _args);}else {return fn.apply(this, _args);}}
}var fn = curry(function(a, b, c) {console.log([a, b, c]);
});fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]复制代码

或许大家觉得这种方式更好理解,又能实现一样的效果,为什么不直接就讲这种呢?

因为想给大家介绍各种实现的方法嘛,不能因为难以理解就不给大家介绍呐~

第三版

curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢?

我们可以创建一个占位符,比如这样:

var fn = curry(function(a, b, c) {console.log([a, b, c]);
});fn("a", _, "c")("b") // ["a", "b", "c"]复制代码

我们直接看第三版的代码:

// 第三版
function curry(fn, args, holes) {length = fn.length;args = args || [];holes = holes || [];return function() {var _args = args.slice(0),_holes = holes.slice(0),argsLen = args.length,holesLen = holes.length,arg, i, index = 0;for (i = 0; i < arguments.length; i++) {arg = arguments[i];// 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标if (arg === _ && holesLen) {index++if (index > holesLen) {_args.push(arg);_holes.push(argsLen - 1 + index - holesLen)}}// 处理类似 fn(1)(_) 这种情况else if (arg === _) {_args.push(arg);_holes.push(argsLen + i);}// 处理类似 fn(_, 2)(1) 这种情况else if (holesLen) {// fn(_, 2)(_, 3)if (index >= holesLen) {_args.push(arg);}// fn(_, 2)(1) 用参数 1 替换占位符else {_args.splice(_holes[index], 1, arg);_holes.splice(index, 1)}}else {_args.push(arg);}}if (_holes.length || _args.length < length) {return curry.call(this, fn, _args, _holes);}else {return fn.apply(this, _args);}}
}var _ = {};var fn = curry(function(a, b, c, d, e) {console.log([a, b, c, d, e]);
});// 验证 输出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)复制代码

写在最后

至此,我们已经实现了一个强大的 curry 函数,可是这个 curry 函数符合柯里化的定义吗?柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,可是什么又是偏函数呢?下篇文章会讲到。

专题系列

JavaScript专题系列目录地址:github.com/mqyqingfeng…。

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/455415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习模板

根据心情补充&#xff0c;语言都是Python hash&#xff0c;把所有的文本转化成数字 from sklearn.preprocessing import LabelEncoder for c in train.columns:if train[c].dtype object:lbl LabelEncoder()lbl.fit(list(train[c].values) list(test[c].values))train[c] l…

漂亮特殊字体可复制_12个创意字体免费下载网站

今天为大家介绍12个创意字体的网站&#xff0c;这些网站都有提供免费下载的字体哦&#xff0c;希望对大家在创作上面有所帮助。FontSpace在Fontspace上有超过42000种免费字体。在这里字体被整齐的分门归类&#xff0c;帮助你找到想要的字体。除了典型的“serif” “script”等&…

使用postman测试接口

Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。在java web开发中使用非常多&#xff0c;经常用来测试接口。 使用postman模拟json数据的发送 第一步:在header里边设置发送数据的类型 Paste_Image.png设置发送数据类型为json&#xff0c;也就是key为Content-…

删除github上的commit历史记录

删除github上的commit历史记录 起步 今天小编发现了git克隆下来的远程库特别大: 经过查询之后发现是每次推送之后都会留下记录缓存&#xff0c;这样很多没用的记录就会占用多余的空间&#xff0c;别人克隆的时候也会多耗费时间&#xff0c;今天我查到了一个清除无用记录的方…

DirectShow组件原理分析及应用

1 DirectX简介  DirectX是Microsoft公司为游戏和其他高性能多媒体应用所提供的一套底层应用程序编程接口。这些接口包括对二维和三维图形&#xff0c;声效和音乐&#xff0c;输入设备以及多玩家网络游戏等的支持。目前DirectX的最高版本是DirectX 9.0。  1.1 DirectX的组成…

接口安全

老大发了篇文章&#xff0c;让看如何写出安全的接口。 如何写出安全的API接口&#xff1f;接口参数加密签名设计思路转载于:https://www.cnblogs.com/Tpf386/p/7053795.html

python中xml模块_python学习第十五天-2(XML模块)

也是一种文本转换形式。importxxxxxxxxxxxxxxxxxxx asxx,可以用xx代替xxxxxxxxxxxxxxxxxxx模块xml文件的新增&#xff0c;修改&#xff0c;删除&#xff0c;查询。新增&#xff1a;​import xml.etree.ElementTreeas ET​new_xmlET.Element(nameList)#创建xml的根节点相当于na…

ubuntu系统下Java环境JDK的安装

Debian Linux下安装jdk 下载压缩包 官网下载对应的.gz包 点击下载 解压文件 创建一个目录用于存放解压后的文件&#xff0c;并解压缩到该目录下 sudo mkdir /opt/java8 sudo tar -zxvf jdk-8u221-linux-x64.tar.gz -C /opt/java8修改环境变量 sudo vim ~/.bashrc 进入…

栈溢出笔记1.3 准备Shellcode

经过1.1和1.2节的讲述&#xff0c;我们已经知道了怎样更改EIP的值。程序运行函数之后将跳转到我们设定的位置開始运行&#xff0c;因此&#xff0c;我们须要准备一个自己的程序&#xff0c;接手后面的工作。这是一个什么样的程序&#xff1f;是一个C语言编写的代码&#xff1f;…

DirectShow开发快速入门之慨述

文章来源&#xff1a;http://tech.163.com/school 2005-08-18 10:21:32 来源: 天极网摘要&#xff1a;本篇文档概括性的介绍了DirectShow的主要组成部分&#xff0c;以及一些Directshow的基本概念。熟悉这些基本的知识对于Directshow的应用开发或者过滤器的开发者都会有所帮助…

Android selector中的item的顺序

在selector中&#xff0c;要将默认状态的item放在最后面&#xff0c;因为一旦前面的item满足匹配条件&#xff0c;后面的item就不会去匹配。因此&#xff0c;把默认状态的item放在前面的话&#xff0c;后面的item没有执行的机会转载于:https://www.cnblogs.com/xiaoyuersdch/p/…

权限表使用联合主键吗_天天写 order by,你知道Mysql底层执行流程吗?

前言 在实际的开发中一定会碰到根据某个字段进行排序后来显示结果的需求&#xff0c;但是你真的理解order by在 Mysql 底层是如何执行的吗&#xff1f;假设你要查询城市是苏州的所有人名字&#xff0c;并且按照姓名进行排序返回前 1000 个人的姓名、年龄&#xff0c;这条 sql 语…

nodejs简介

nodejs是啥&#xff1f; Node.js是运行在服务端的JavaScript。 Node.js是一个基于Chrome JavaScript运行时建立的一个平台。 Node.js是一个事件驱动I/O服务端JavaScript环境&#xff0c;基于Google的V8引擎&#xff0c;V8引擎执行Javascript的速度非常快&#xff0c;性能非常…

jumpserver v0.4.0 基于 CenOS7 的安装详解

标签&#xff08;linux&#xff09;&#xff1a; jumpserver 笔者Q:972581034 交流群&#xff1a;605799367。有任何疑问可与笔者或加群交流 首首先使用Jumpserver前要理解清楚这三个用户关系: 1.用户&#xff1a; 是指你在web上创建的用户,会在跳板机上创建这个用户,作用就是用…

Node.js中事件的循环

Node.js 事件循环 Node.js 是单进程单线程应用程序&#xff0c;但是通过事件和回调支持并发&#xff0c;所以性能非常高。 Node.js 的每一个 API 都是异步的&#xff0c;并作为一个独立线程运行&#xff0c;使用异步函数调用&#xff0c;并处理并发。 Node.js 基本上所有的事…

python爬boss网站_python之requests爬虫Boss数据

python之requests爬虫Boss数据需要用到的库&#xff1a;reqeusts、lxml没有的可以用直接下载pip install requestspip install lxm这里以python岗位&#xff0c;地点北京为例爬取的数据就是岗位名称、薪资、地点 首先导入需要用到的模块import requestsfrom lxml import etree岗…

live555源代码简介

文章出自&#xff1a;http://blog.csdn.net/imliujie/archive/2008/01/30/2072657.aspx live555源代码简介liveMedia项目的源代码包括四个基本的库&#xff0c;各种测试代码以及IVE555 Media Server。四个基本的库分别是UsageEnvironment&TaskScheduler&#xff0c;groups…

并发无锁队列学习(单生产者单消费者模型)

1、引言 本文介绍单生产者单消费者模型的队列。依据写入队列的内容是定长还是变长&#xff0c;分为单生产者单消费者定长队列和单生产者单消费者变长队列两种。单生产者单消费者模型的队列操作过程是不须要进行加锁的。生产者通过写索引控制入队操作&#xff0c;消费者通过读索…

ecshop 收货人信息电话必填改为手机必填

首先通过在flow.dwt中&#xff0c;查找flow.php?stepconsignee中的关键字 consignee&#xff08;结算中心&#xff09;查找所在模板/Library/consignee.lbi 大概57行 把必填去掉&#xff0c;其次 在js/shopping_flow.js里边注释掉 if (Utils.isEmpty(frm.elements[‘tel’].v…

流媒体传输协议

1&#xff0e;流媒体( Streaming Media) 1.1流媒体概念 流媒体技术是网络技术和多媒体技术发展到一定阶段的产物。术语流媒体既可以指在网上传输连续时基媒体的流式技术,也可以指使用流式技术的连续时基媒体本身。在网上传输音频、视频等多媒体信息目前主要有两种方式:下载和流…