函数式 js 接口

之前在 youtube 上看到一个技术视频,讲“underscore.js的接口为什么不好用”,以及什么样的接口更好用。演讲者是 lodash.js 的作者,他提出了一种“全面函数式”的 js 接口设计模式。大概类似这样:

// 传统接口
_.map([1, 2, 3], function (el) {return el * 2}); // return [2, 4, 6] // 函数式接口
var fn = _.map([1, 2, 3]); // return a function
fn(function (el) {return el * 2}); // return [2, 4, 6]; // 或者
_.map([1, 2, 3])(function (el) {return el * 2}); // return [2, 4, 6];

找到一点感觉没有?其实就是函数式编程语言中广泛存在的“科里化”函数。当实参填满形参表的时候,执行结算返回结果,否则返回一个临时函数,继续接受实参。

看到这个写法眼前一亮,感觉有大规模简化代码的潜力。当时实际试了一下发下很多地方用不了,因为之前写的代码受 jQuery 影响,有很多这样的接口:

foobar.attribute(name); // 读属性
foobar.attribute(name, newValue); // 写属性

这样的接口是按照上述方法 curry 化会使得读属性变得不可能,根本原因是参数数量不同时 attribute 函数的语义根本不一样。使用 jQuery 的时候感觉这种写法非常爽,后来就跟着这么写,但是目前看来这样的接口设计是有问题的。

言归正传,今天聊聊这样的接口如何实现,以及 lodash 中的 fp 模块。

实现原理

说到底就是个 currying 的问题,currying 在很多语言中是内置功能,但是 js 没有,所以我们要实现一个 currying 工具函数。首先贴一个最简易的 currying 实现,它的功能非常简单,输入一个函数 fn1 和部分实参,返回一个保存部分实参,继续接收实参的函数 fn2,调用fn2,它会合并实参数组,并调用 fn1。

/**
* 函数柯里化
* @param fn 输入函数
* @return 柯里化后的函数
*/
var curry = function (fn) {
if (!isFunction(fn)) {
return;
} var args = slice(arguments, 1); return function () {
return fn.apply(this, args.concat(slice(arguments, 0)));
}
}

isFunction 和 slice 大家都知道我就不贴了。看一下如何调用:

function add(a, b) {
return a + b;
} addOne = curry(add, 1); addOne(2); // return 3

  

有时候我们需要输入的部分实参是数组列表形式,所以我们包装一下刚才的 curry 函数:

/**
* 函数柯里化
* @param fn 输入函数
* @param arr 参数列表
* @return 柯里化后的函数
*/
var curryApply = function (fn, arr) {
if (!isFunction(fn)) {
return;
} var args = arr.slice(0);
args.unshift(fn);
return curry.apply(this, args);
}

上面的 curry 函数有个问题,就是连续多次补充实参,我们还需要封装一个支持连续调用的版本:

/**
* 自动柯里化
* @param fn 输入函数
* @param n 输入函数参数个数
* @return 柯里化后的函数
*/
var autoCurry = function (fn, n) {
if (!isFunction(fn)) {
return;
} function retFn() {
var len = arguments.length;
var args = slice(arguments, 0);
var nextn = n - len; if (nextn > 0) {
return autoCurry(curryApply(retFn, args), nextn);
} return fn.apply(this, args);
} return retFn;
}

  

autoCurry 使用的递归的方法,输出函数可以可以通过简单调用的方式连续补充实参,当实参和预设的参数数量相等时,执行输入函数。使用方法如下:

function compute(a, b, c) {
return (a + b) * c;
} var curryedCompute = autoCurry(compute, 3); compute(1, 2, 3); // return 9
curryedCompute(1)(2)(3); // return 9

大家如果使用 node.js 的话,可能知道 npm 中有个 curry 模块,实现的功能是一样的,不同的是当你不输入参数个数 n 时,curry 模块 会使用 Function 对象的 length 属性作为预设的 n 值。

lodash/fp

到这里实现原理就讲清楚了。本着不造轮子的原则,如果大家想尝试一下函数式风格的基础 js 库的话,建议使用 lodash/fp 这个模块。大家都知道 lodash 是 underscore 的 better implemention,而 lodash/fp 就是科里化的 lodash。与简单的 currying 不同的是,为了方便使用,lodash/fp 的设计者调换了一些接口的参数顺序,比如开头提到的 _.map 接口,如果简单 currying 的话第一个参数应该是数组[1, 2, 3],但是大多数时候,我们想要持有的是一个算法,用这个算法处理不同的数据。所以我们希望暂存的实际上是第二个参数 fn,所以 lodash/fp 的接口是这样的:

// The `lodash/map` iteratee receives three arguments:
// (value, index|key, collection)
_.map(['6', '8', '10'], parseInt);
// → [6, NaN, 2] // The `lodash/fp/map` iteratee is capped at one argument:
// (value)
fp.map(parseInt)(['6', '8', '10']);
// → [6, 8, 10]

关于 lodash/fp 更详细的说明,请看:https://github.com/lodash/lodash/wiki/FP...

函数式 js 接口实现原理,以及 lodash/fp 模块的更多相关文章

  1. Sea.Js的运行原理(转)

    1.CMD(Common Module Definition)规范 Sea.js采用了和Node相似的CMD规范,使用require.exports和module来组织模块.但Sea.js比起Node ...

  2. IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理

    1.前言 一个安全的信息系统,合法身份检查是必须环节.尤其IM这种以“人”为中心的社交体系,身份认证更是必不可少. 一些PC时代小型IM系统中,身份认证可能直接做到长连接中(也就是整个IM系统都是以长 ...

  3. 函数式JS: 原来promise是这样的monad

    转载请注明出处: http://hai.li/2017/03/27/prom... 背景 上篇文章 函数式JS: 一种continuation monad推导 得到了一个类似promise的链式调用, ...

  4. 如何编写高质量的js代码--底层原理

    转自: 如何编写高质量的 JS 函数(1) -- 敲山震虎篇   本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/7lCK9cHmunvYlbm ...

  5. 微信调用照相拍照等 js 接口的权限配置 和 照片上传和下载实现

    直接上代码: 1. 前端调试代码: <html> <head> <meta http-equiv="Content-Type" content=&qu ...

  6. 报表开发工具Finereport移动端app js接口列表【全】

    应用报表工具Finereport的开发人员会发现其移动端app 同样也推出了很多js接口,那这些接口到底有多少,其移动端又有哪些地方支持调用js,这些接口具体又该如何调用呢.根据我平时的开发经验,给大 ...

  7. 微信JS接口

      微信JS接口 分享到朋友圈 分享给朋友 分享到QQ 拍照或从手机相册中选图 识别音频并返回识别结果 使用微信内置地图查看位置来源:http://www.cnblogs.com/txw1958/p/ ...

  8. 报表开发工具中开放的部分图表js接口列表

    1.. 描述 报表开发工具FineReport 8.0版本中开放了部分图表js接口,在具体应用的过程中很多人都不知道这些接口到底有什么作用,该怎么应用,所以根据我自己的应用在下面归纳了这些新开放js接 ...

  9. ajax请求node.js接口时出现 No 'Access-Control-Allow-Origin' header is present on the requested resource错误

    ajax请求node.js接口出现了如下的错误: XMLHttpRequest cannot load http://xxx.xxx.xx.xx:8888/getTem?cityId=110105&a ...

随机推荐

  1. JMeter常见问题集合

    前言 本文内容仅仅是针对Jmeter的部分功能名词的介绍和解释,以及初学者不易理解的问题的整理.部分内容来自别人做的整理,为了更好地整理自己的思路,所以可耻的整理一下发到博客上. 标题[1-6]和[参 ...

  2. [转载]fullPage.js中文api 配置参数~

    fullPage.js中文api 配置参数 选项 类型 默认值 说明 verticalCentered 字符串 true 内容是否垂直居中 resize 布尔值 false 字体是否随着窗口缩放而缩放 ...

  3. Mybatis

    Mybatis MyBatis本是apache的一个开源项目iBatis,2010年这个项目有Apache software foundation 迁移到了Google code,并改名MyBatis ...

  4. Unity中关于作用力方式ForceMode的功能注解

    功能注解:ForceMode为枚举类型,用来控制力的作用方式,有4个枚举成员,在以下举例中均设刚体质量为m=2.0f,力向量为f=(10.0f,0.0f,0.0f). (1)ForceMode.For ...

  5. Go语言常用命令介绍

    go build go build 命令主要是用于测试编译.在包的编译过程中,若有必要,会同时编译与之相关联的包. 如果是普通包,当你执行go build命令后,不会产生任何文件. 如果是main包, ...

  6. mybatis一对多关联

    这里的一对多指的是:当我们查询一个对象的时候,同时将其有关联的多方对象都查询出来. 下面以国家(Country)和部长(Minsiter)做案例 一个国家有多个部长 1.定义实体 定义实体的时候需要注 ...

  7. iOS开发--JS调用原生OC篇

    JS调用原生OC篇 方式一(反正我不用) 第一种方式是用JS发起一个假的URL请求,然后利用UIWebView的代理方法拦截这次请求,然后再做相应的处理. 我写了一个简单的HTML网页和一个btn点击 ...

  8. 【UOJ #35】后缀排序 后缀数组模板

    http://uoj.ac/problem/35 以前做后缀数组的题直接粘模板...现在重新写一下模板 注意用来基数排序的数组一定要开到N. #include<cstdio> #inclu ...

  9. CSS display:inline-block

    CSS display:inline-block 在css布局里,我们经常看到代码 「display:inline-block; *display:inline; zoom:1; 」,大多人会说上面的 ...

  10. 编译CM14.1(sudmod71.1)过程记录

    编译CM14.1内存要求很高,至少8G以上,我的6G也可以搞定,交换空间分配大一点. 1.安装平台 建议安装Deepin 15.3桌面版系统,系统UI好看. 2.配置环境 (1)安装编译依赖库 sud ...