【 js 基础 】【 源码学习 】柯里化和箭头函数
最近在看 redux 的源码,代码结构很简单,主要就是6个文件,其中 index.js 负责将剩余5个文件中定义的方法 export 出来,其他5个文件各自负责一个方法的实现。
大部分代码比较简单,很容易看懂,但是在 applyMiddleware.js 中 有一个地方很有意思,用到了柯里化和箭头函数的组合。在增强 store,丰富 dispath 方法的时候,可能会用到多个 中间件,所以这个柯里化的嵌套可能会很深,导致对 箭头函数和柯里化 不是很熟悉的童鞋,一看源码就会有些理不清调用思路。
一、柯里化
是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
--来自 wiki
举个例子:
柯里化前:
- function add(a, b) {
- return a + b;
- } // 执行 add 函数,一次传入两个参数即可
- add(10, 2) //
柯里化后:
- var add = function(a) {
- return function(b) {
- return a + b;
- };
- };
- var addTen = add(10);
- addTen(2); //
那么我们为什么要使用柯里化呢?
首先柯里化可以使得参数复用并且延迟计算,也就是说像上面的例子,比如我们的 a 值一直都是 10,只是 b 值在变化,那么这个时候用到柯里化,就可以减少一些重复性的传参。
比如 b 是 2,3,4
那么在柯里化前的调用就是:
- add(10,2)
- add(10,3)
- add(10,4)
而在柯里化之后,就像上面的例子中写的,我们通过定义
- var addTen = add(10);
将第一个参数 a 预置了 10,
之后我们只需要调用
- addTen(2)
- addTen(3)
- addTen(4)
就可以了。
而在这个过程中,如果使用柯里化前的代码,或当即就把结果计算出来,而在柯里化之后,我们可以现传入一个10,然后在想得到真正结果的时候再传入另一个参数。
同时柯里化函数还可以 提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况不使用柯里化可能会这样写
- var addEvent = function(el, type, fn, capture) {
- if (window.addEventListener) {
- el.addEventListener(type, function(e) {
- fn.call(el, e);
- }, capture);
- } else if (window.attachEvent) {
- el.attachEvent("on" + type, function(e) {
- fn.call(el, e);
- });
- }
- };
这个时候我们没调用一次 addEvent,就会进行一次 if else 的判断,而其实具体用哪个方法进行方法的绑定的判断执行一次就已经知道了,所以我们可以使用柯里化来解决这个问题:
- var addEvent = (function(){
- if (window.addEventListener) {
- return function(el, sType, fn, capture) {
- el.addEventListener(sType, function(e) {
- fn.call(el, e);
- }, (capture));
- };
- } else if (window.attachEvent) {
- return function(el, sType, fn, capture) {
- el.attachEvent("on" + sType, function(e) {
- fn.call(el, e);
- });
- };
- }
- })();
一开始的自执行函数,完成了对 addEvent 具体使用 哪个方法的判断,之后在调用传参的时候都是直接给了已经判断好的返回方法,所以使用了柯里化 减少了我们每次的判断,提前返回了我们需要的具体方法。
这里还要多提一点,我们也经常会听到高阶函数,那么高阶函数和柯里化是什么关系?
所谓高阶函数,即至少满足下列的一个条件的函数:
a、接受一个或多个函数作为输入
b、输出一个函数
这样你就明白了,柯里化其实是高阶函数的一种实现。
二、箭头函数
- (参数1, 参数2, …, 参数N) => {函数声明}
- (参数1, 参数2, …, 参数N) => 表达式(单一)
- //相当于:(参数1, 参数2, …, 参数N) =>{ return 表达式 }
- // 当只有一个参数时,圆括号是可选的:
- (单一参数) => {函数声明}
- 单一参数 => {函数声明}
- // 没有参数的函数应该写成一对圆括号。
- () => {函数声明}
举个例子:
- var func = x => x * x;
- // 简写函数 省略return
- var func = (x, y) => { return x + y; };
- //常规编写 明确的返回值
这个转换成 es5 的格式就是
- function (x) {
- return x * x;
- }
- function (x, y) {
- return x + y;
- }
那么我们为什么要使用箭头函数?
主要的好处有两点:
1、更简短的函数
2、不绑定this
第一个优点是很容易理解的,你观察上面的例子,虽然很简单,但明显箭头函数的写法剩下了不少的代码,对照 es5 来看写的更加简洁了。
而第二个优点:
在箭头函数之前,每个新定义的函数都有它自己的 this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中为 undefined),这个知识点,大家可以我的另一篇文章(http://www.cnblogs.com/lijiayi/p/this.html)
举个例子:
- function Person() {
- // Person() 构造函数定义 `this`作为它自己的实例.
- this.age = 0;
- setInterval(function growUp() {
- // 在非严格模式, growUp()函数定义 `this`作为全局对象,
- // 与在 Person()构造函数中定义的 `this`并不相同.
- this.age++;
- }, 1000);
- }
- var p = new Person();
而我们通常会这样解决 setInterval 中 this 的指向问题,将 this 赋值给变量 self
- function Person() {
- var self = this;
- this.age = 0;
- setInterval(function growUp() {
- // 回调引用的是`that`变量, 其值是预期的对象.
- self.age++;
- }, 1000);
- }
而在箭头函数中,这个问题是不会出现的。因为箭头函数不会创建自己的this,它使用封闭执行上下文的this值。因此,在下面的写法中,传递给 setInterval 的函数内的 this 与封闭函数中的 this 值相同:
- function Person(){
- this.age = 0;
- setInterval(() => {
- this.age++; // |this| 正确地指向person 对象
- }, 1000);
- }
- var p = new Person();
那么如果使用 call 和 apply 调用,会怎么样呢
- var adder = {
- base : 1,
- add : function(a) {
- var f = v => v + this.base;
- return f(a);
- },
- addThruCall: function(a) {
- var f = v => v + this.base;
- var b = {
- base : 2
- };
- return f.call(b, a);
- }
- };
- console.log(adder.add(1)); // 输出 2
- console.log(adder.addThruCall(1)); // 仍然输出 2
也就是说通过 call() 或 apply() 方法调用一个函数时,只是传入了参数而已,对 this 并没有什么影响。
关于 箭头函数和柯里化,就说这么多,具体深入的学习大家可以去搜资料,因为有太多太多,写的很好的资料。
三、柯里化和箭头函数的结合
先来一个简单的例子,咱们就利用上面的 add 方法:
es5写法:
- 1 var add = function(a) {
- 2 return function(b) {
- 3 return a + b;
- 4 };
- 5 };
而当你加上了箭头
- let add = a => b => a + b
就只有这样简单的一句。
目前比较起来,还是比较容易理解的,但是当没有给你 es5 的写法并且箭头也更加多,柯里化函数每层的调用、参数预置分散开来的时候,理解起来还是会有一些蒙的。
咱们来举个例子,理清一下如何去理解这样组合的思路。
- //[p]roperty, [v]alue, [o]bject:
- const is = p => v => o => o.hasOwnProperty(p) && o[p] == v;
- // outer: p => [inner1 function, uses p]
- // inner1: v => [inner2 function, uses p and v]
- // inner2: o => o.hasOwnProperty(p) && o[p] = v;
这个函数看最后一个箭头后面的表达式很容易理解,就是对传进来的属性和值,在传进来的对象上的匹配情况,如果对象上有这个属性,并且对应的值也和传进来的值相等,就返回 true。
而在我们对这个函数的调用过程,可以灵活的分为三步。
第一步 传入 property 即 p 参数 ,这会返回一个需要 v 和 o 作为参数的函数:
- v => o => o.hasOwnProperty(p) && o[p] == v;
第二步 传入 value 即 v 参数,这同样会返回一个需要 o 作为参数的函数:
- o => o.hasOwnProperty(p) && o[p] == v;
在这个时候,咱们的表达式里只有 o 是未知的,所以当我们再调用一次,就会得到一个 boolean 结果。
也就是说如果有 n 个箭头,那么我们在 n-1 次调用之前,都只是在像 表达式里传参,而伴随这第 n 次的调用 的传参,就会直接返回结果。
这个的调用过程就像是拨洋葱,一层层的调用函数,到最后一层返回结果。
那么这样写有什么好处呢?
1、减少代码重复
2、提高代码重用性
举个例子来比较一下:
- result = users
- .filter(x => x.hasOwnProperty('pets'))
- .filter(x => x.hasOwnProperty('title'))
和
- const has = p => o => o.hasOwnProperty(p);
- result = users
- .filter(has('pets'))
- .filter(has('title'))
- ...
优点还是很明显的。
最后咱们再来说一下 redux 源码中对以上知识的应用:
- import compose from './compose'
- export default function applyMiddleware(...middlewares) {
- return createStore => (...args) => {
- const store = createStore(...args)
- let dispatch = () => {
- throw new Error(
- `Dispatching while constructing your middleware is not allowed. ` +
- `Other middleware would not be applied to this dispatch.`
- )
- }
- let chain = []
- const middlewareAPI = {
- getState: store.getState,
- dispatch: (...args) => dispatch(...args)
- }
- chain = middlewares.map(middleware => middleware(middlewareAPI))
- dispatch = compose(...chain)(store.dispatch)
- return {
- ...store,
- dispatch
- }
- }
这个是 redux 的 源码,这里重点要讲的是 第 17、18 行,就两句话,实现了将传入的多个中间件套在一起,层层返回结果,最终丰富了 dispatch 。
首先背景知识:
我问我们用来丰富 dispatch 的中间件都是按照一定规律编写的,有固定传参顺序,
首先 17 行,使用 map 遍历 传进来的 多个 middlewares ,给每个中间件都传入参数 middlewareAPI,也就是最后的 chain 是这样的:
- chain = [
- function middlewareCreator1(next) {
- // with getState, dispath
- },
- function middlewareCreator2(next) {
- // with getState, dispath
- },
- ...
- ]
middlewareAPI 是一个对象,这个对象是中间件所需要的第一个参数 ({dispatch, getState}[简化的store]) 。第17行的目的就是将每个中间件所需要的第一个参数预置了进去,这个时候每个中间件就会返回一个 需要 next 作为参数的函数,chain 就是由这些返回的函数而组成的一个数组。
之后执行了
- dispatch = compose(...chain)(store.dispatch)
compose 是在第一行 引入的,它是用来组合函数,也就是将传入的多个中间件套在一起。
咱们来看一下 compose 的源码,看看它是如何组合的:
- export default function compose(...funcs) {
- // 如果什么都没有传,则直接返回 参数
- // return arg => arg 即
- // return function (arg) {
- // return arg;
- // };
- if (funcs.length === 0) {
- return arg => arg
- }
- // 如果funcs中只有一个中间件,那么就直接返回这个 中间件
- if (funcs.length === 1) {
- return funcs[0]
- }
- // reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。
- return funcs.reduce((a, b) => (...args) => a(b(...args)))
- }
重点在第16行。
可以看到 redux 源码中在调用 compose 方法的时候 后面跟了两个括号,也就是调用了两次
第一次调用:
- compose([a,b])
这个会返回
- (...args) => a(b(...args))
把 a、b 换成刚才得到的 chain,即返回了一个需要 ...args 作为参数的函数:
- (...args) => middlewareCreator1(middlewareCreator2(middlewareCreator3(...args)))
然后 第二次调用,
- dispatch = compose(...chain)(store.dispatch)
将所需要的 ...args 传入了进去,即未丰富之前的 dispatch,最终得到:
- middlewareCreator1(middlewareCreator2(middlewareCreator3(store.dispatch)))
也就是在 17 、18 行之后,咱们的 dispatch
- dispatch = middlewareCreator1(middlewareCreator2(middlewareCreator3(store.dispatch)))
你还记得咱们刚才讲的中间件的固定格式中,在上面 对 middlewares 遍历之后,将每个中间件第一个参数预置进去,还需要调用两次,传入两个参数才会得到的真正的结果,而在 compose 之后,就只剩下一个参数了,你反应过来了吗?
chain 之后每个中间件需要一个 (next[上一个中间件的dispatch方法]) 作为第二个参数,而在执行完 compose 之后,多个中间件套在了一起,
当想要执行 middlewareCreator1 的时候,由于 middlewareCreator1 的执行依赖于内部参数的求值,所以会对内部参数进行调用,也就是执行 middlewareCreator2,而 middlewareCreator2 的执行依赖于内部 middlewareCreator3 的执行,所以最终将会先执行 middlewareCreator3 ,middlewareCreator3 传入了参数 store.dispatch 作为 (next[上一个中间件的dispatch方法]) 会返回一个需要 (action[实际派发的action对象]) 的函数,也就是丰富过 middlewareCreator3 的新的 dispatch 给 middlewareCreator2,这将作为 middlewareCreator2 的第二个参数 (next[上一个中间件的dispatch方法]),然后有了参数的 middlewareCreator2 得以顺利调用,这回返回一个需要 (action[实际派发的action对象]) 的函数,也就是丰富过 middlewareCreator3 和 middlewareCreator2 的新 dispatch 给 middlewareCreator1,这将作为 middlewareCreator1 的第二个参数 (next[上一个中间件的dispatch方法]),然后有了参数的 middlewareCreator1 得以顺利调用,最后就会返回一个 一个需要 (action[实际派发的action对象]) 的函数,也就是丰富过 middlewareCreator3 、 middlewareCreator2 和 middlewareCreator1 的新 dispatch ,这也就是最终的 dispatch,最后在我们使用的时候,常规调用 dispatch (action)的时候,将第三个参数传入了进来,进行了第三次调用,返回函数结果。
以上就是整个调用过程。
- action => {
- // .....中间件真正逻辑....
- }
而在Middleware2 未得到 Middleware3 的返回结果即未被调用之前 Middleware2 是这样的
- next => action => {
- // ...一些 Middleware2 代码...
- next(action);
- //...一些 Middleware2 代码...
- }
然后在 Middleware3 调用之后, Middleware2 则接受了来自 Middleware3 返回的接受 action 为参数的函数 action => {} 作为 next 参数,然后进行调用,又会返回一个函数传给在下一个 等待 接受action为参数的函数 action => {} 作为下一个中间件的 next 参数,就这样层层组装,丰富了我们的 dispatch。
这个过程再想一下,其实一开始 通过 compose 的第一次调用依次嵌套起来,然后又通过第二次调用,将嵌套的函数从内到外反向调用,生成一个新的 dispatch,而这里面就用到了两次 柯里化和箭头函数的结合,第一次在 生成chain和调用 compose 方法,第二次在中间件本身(const reduxMiddleware = ({dispatch, getState}[简化的store]) => (next[上一个中间件的dispatch方法]) => (action[实际派发的action对象]) => {})。
而实现这样的逻辑的核心代码,就两行。
学习并感谢:
JS中的柯里化(currying):http://www.zhangxinxu.com/wordpress/2013/02/js-currying/
箭头函数MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions (很详细,值得看)
高阶箭头函数:https://cnodejs.org/topic/56a1d827cd415452622eed07
【 js 基础 】【 源码学习 】柯里化和箭头函数的更多相关文章
- JavaScript函数式编程(纯函数、柯里化以及组合函数)
JavaScript函数式编程(纯函数.柯里化以及组合函数) 前言 函数式编程(Functional Programming),又称为泛函编程,是一种编程范式.早在很久以前就提出了函数式编程这个概念了 ...
- Scala基础:闭包、柯里化、隐式转换和隐式参数
闭包,和js中的闭包一样,返回值依赖于声明在函数外部的一个或多个变量,那么这个函数就是闭包函数. val i: Int = 20 //函数func的方法体中使用了在func外部定义的变量 那func就 ...
- scala学习----柯里化
1.鸭子类型,走起来像鸭子,叫起来像鸭子,就是鸭子.函数中使用{ def close(): Unit }作为参数类型,因此任何含有此函数的类都可以作为参数传递.好处是不必使用继承特性. def wit ...
- javascript柯里化及组合函数~
大家是不是看我上篇博文有点蒙.用的的curry和compose是什么鬼,怎么那么神奇.上篇博文也是主要用到了这两个函数.那今天我们来聊一下curry和compose,这两个东西是函数式编程很重要的东西 ...
- 【js】vue 2.5.1 源码学习 (十) $mount 挂载函数的实现
大体思路(九) 本节内容: 1. $mount 挂载函数的实现. // 将Vue.prototype.$mount 缓存下来 ==>mountComponet(this,el) { // 组建挂 ...
- 函数柯里化实现sum函数
需求 实现sum函数,使其可以传入不定长参数,以及不定次数调用 //示例 console.log(sum(1,2)(3)()) //6 console.log(sum(2,3,4,5)(1,2)(3) ...
- JS中的柯里化(currying)
何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名). 柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参 ...
- JS中的柯里化(currying) 转载自张鑫旭-鑫空间-鑫生活[http://www.zhangxinxu.com]
JS中的柯里化(currying) by zhangxinxu from http://www.zhangxinxu.com 本文地址:http://www.zhangxinxu.com/wordpr ...
- Vue.js 源码学习笔记
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...
随机推荐
- 10_python_函数进阶
一.函数参数-动态参数 形参:位置参数.默认值参数.动态参数 动态参数分为两种:动态接收位置参数 *args .动态接收关键字参数 *kwargs 1. *args def chi(*foo ...
- 文件上传下载、socketserver(并发)、解读socketserver源码
1.文件上传/下载 学习了socket套接字,我们现在可以写一个文件上传/下载的程序,如下示例: 分析上边代码,我们发现,client发送上传文件相关信息的字典序列化之后,server又给client ...
- Shell - 简明Shell入门02 - 变量(Variable)
示例脚本及注释 #!/bin/bash v1=test-variable_123 # 全局变量 v2=12345 v3='This is a test!' # 赋值语句使用单引号或双引号可以包含空格 ...
- Python网络练习题
练习题 什么是C/S架构? C/S架构客户端.服务端架构,C/S端软件主要有网络游戏,QQ等 互联网协议是什么?分别介绍五层协议中每一层的功能? 互联网协议:计算机之间的通信标准 物理层:主要是基于电 ...
- 【xsy1130】tree 树形dp+期望dp
题目写得不清不楚的... 题目大意:给你一棵$n$个节点的树,你会随机选择其中一个点作为根,随后随机每个点深度遍历其孩子的顺序. 下面给你一个点集$S$,问你遍历完$S$中所有点的期望时间,点集S中的 ...
- python2 里边自定义线程池
#!/usr/bin/env python # -*- coding:utf-8 -*- import Queue import threading class ThreadPool(object): ...
- Linux下删除某些非法字符文件名的文件
1.首先利用 ls -i 查找ID 2.find ./ -inum 20718697 -exec rm '{}' \;
- Vue.js项目引入less文件报错解决
解决方案: 需要局部安装vue-style-loader,less-loader,css-loader,vue-loader和less包(需注意就算全局安装以上包仍需局部安装) 即:npm i vue ...
- Linux环境下Java中文乱码解决方案
相信很多朋友遇到过Java的乱码问题,最近我也在解决一个“使用文本生成图片过程中中文以及特殊字符乱码”的问题:花了我大量时间,Debug了sun.font.sun.awt下面的各种源码,终于搞懂了其机 ...
- Attr类中进行类型推断
涉及到重要的类的继承关系如下图所示. 关于抛出的异常继承体系: