前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • JavaScript Part 1

    • Ajax总结篇
    • Canvas 绘制八大行星
    • DOM编程之API学习总结篇
    • JavaScript事件机制
    • JavaScript代码片段100个
    • JavaScript作用域分析总结
    • JavaScript原型链回顾
    • JavaScript及jQuery中的各种宽高属性图解
    • JavaScript工程项目的一系列最佳实践
    • JavaScript常用API合集
    • JavaScript数组、字符串、对象常用方法
    • JavaScript数组方法总结篇
    • JavaScript深浅拷贝
    • JavaScript运动框架之速度时间版本
    • JavaScript运行机制Event Loop
    • JavaScript防抖节流原理
    • Javascript中的复制粘贴功能
    • Javascript数组详解
    • OOP之原型与原型链
    • Object.defineProperty详解
    • V8源码浅析JS数组常见方法
    • await 在 forEach 中不生效解决方案
    • iframe+表单跨域提交POST请求
    • javascript笔记总结篇
    • 业务中处理数据结构常用的JS方法
    • 作用域
    • 你真的掌握变量和类型了吗
    • 前后端分离之数据Mock
    • 原型与原型链
    • 原生JS补给(上)
    • 如何写出一个惊艳面试官的深拷贝
    • 带你填一些JS容易出错的坑
    • 彻底弄懂 JavaScript 执行机制
    • 执行上下文 执行栈
    • 正则回顾总结
    • 正则基础知识
    • 正则完整篇
    • 正则表达式
    • 浅析Promise原理
    • 浅谈JavaScript中的异步处理
    • 深拷贝 vs 浅拷贝
    • 聊一聊typeof instanceof 实现原理.
    • 聊一聊闭包
    • 高阶函数map reduce filter
  • JavaScript Part 2

  • CSS

  • HTML

  • Jquery

  • ES6

  • 小程序

  • Vue

  • React

  • 深入React

  • React Native

  • NodeJS

  • Angular

  • TypeScript

  • Webpack

  • 浏览器

  • 移动端

  • 前端工程化

  • Electron

  • HTTP

  • Nginx

  • Linux

  • 数据结构与算法

  • LeetCode算法题

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

偏函数和柯里化|博客系列

# 前言

在第一章我们主要介绍了函数的一些基本功能和结构,以及介绍了一些实用的小技巧。这些都是为了后面一步一步入门打下好的基础。因为函数式编程并不是一个看看文档就能很好掌握的东西,它需要你集合实际例子然后理解每一步为什么要这样,如果你只是想粗略的看看,不去思考🤔,相信我,后面的案例你会感觉特别跳,特别绕(开始学习时我就是这样😼)。

在这一章中,我会针对函数式编程的另一个重点:函数的输入来做讲解和案例分析,个人建议:打开你的vscode,关上文档,把案例敲上一遍,需要的时候把每一步做个对比,确保自己是真的理解它们。

# 偏函数

先来看一个大家都很熟悉的函数:

  1. 一个ajax函数,第一个参数为请求的API地址,第二个为请求的参数,第三个是请求成功之后的回调函数。
function ajax (url, data, callback) {
	// ...
}
@前端进阶之旅: 代码已经复制到剪贴板
  1. 现在如果你已经很确定一个API地址,此外只是需要等待另外两个参数的时候,比如获取用户信息和获取订单详情的请求:
function getUser (data, cb) {
	ajax('/api/user', data, cb)
}
function getOrder (data, cb) {
	ajax('api/order', data, cb)
}
@前端进阶之旅: 代码已经复制到剪贴板
  1. 现在如果你已经很确定一个API地址,同时已经很确定请求的参数(比如用户的id),此外只需要等待另一个参数的时候:
function getCurrentUser (cb) {
	getUser({ userId: 1 }, cb)
}
function getCurrentOrder (cb) {
	getUser({ orderId: 1 }, cb)
}
@前端进阶之旅: 代码已经复制到剪贴板

不知道大家发现了没,从第一步到第三步,每过一步,函数的参数就少一个,直到最后只需要传递一个cb。

用一句话来说明发生的事情:getUser(data, cb)是ajax(url, data, cb)的偏函数(partially-applied functions)。

(注意⚠️:前方高能!)

关于该模式更正式的说法是:偏函数严格来讲是一个减少函数参数个数(arity)的过程;这里的参数个数指的是希望传入的形参的数量。我们通过 getUser(..) 把原函数 ajax(..) 的参数个数从 3 个减少到了 2 个。

# partial函数

在上面的例子中,getCurrentUser(cb)和getCurrentOrder(cb)的模式其实很想,我们可以来定一个partial()实用函数:

function partial (fn, ...prestArgs) {
	return function partiallyApplied (...laterArgs) {
		return fn(...prestArgs, ...laterArgs)
	}
}
@前端进阶之旅: 代码已经复制到剪贴板

partial函数接受一个fn函数,和若干个参数…prestArgs。

它返回的是另一个函数partiallyApplied()函数,这个函数也接受若干个参数…laterArgs,并返回partial函数传递进来fn函数。

返回的fn函数会将partial和partiallyApplied中的参数都接收过去。

(这个实用函数我至少敲了3遍…)

好吧,我们还是来看看我参考资料的原版本是怎么描述这个实用函数的吧,感觉它说的也比较清晰:

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

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

你注意到在内部函数中的 fn 和 presetArgs 引用了吗?他们是怎么如何工作的?在函数 partial(..) 结束运行后,内部函数为何还能访问 fn 和 presetArgs 引用?你答对了,就是因为闭包!内部函数 partiallyApplied(..) 封闭(closes over)了 fn 和 presetArgs 变量,所以无论该函数在哪里运行,在 partial(..) 函数运行后我们仍然可以访问这些变量。所以理解闭包是多么的重要!

当 partiallyApplied(..) 函数稍后在某处执行时,该函数使用被闭包作用(closed over)的 fn 引用来执行原函数,首先传入(被闭包作用的)presetArgs 数组中所有的偏应用(partial application)实参,然后再进一步传入 laterArgs 数组中的实参。

当然你也可以用更便捷的箭头函数语法来重写上面的函数:

var partial = (fn, ...presetArgs) => 
														(...laterArgs) => 
																fn(...prestArgs, ...laterArgs);
@前端进阶之旅: 代码已经复制到剪贴板

优点:更加简洁,甚至代码稀少。

缺点:函数会变成匿名函数,可读性上失去益处,此外,由于作用域边界变得模糊,我们会更加难以辩认闭包。

不过是否采用箭头函数都是你的个人喜好。

# ajax案例

  1. 介绍完上面的函数,我们现在可以用partial实用函数来制造这些之前提及的偏函数:
// example1
function partial (fn, ...prestArgs) {
	return function partiallyApplied (...laterArgs) {
		return fn(...prestArgs, ...laterArgs)
	}
}

var getUser = partial(ajax, '/api/user')

var getOrder = partial(ajax, '/api/order')
@前端进阶之旅: 代码已经复制到剪贴板

不知道大家脑中是否有getUser 函数的外形和内在,它其实就相当于这样:

var getUser = partial(ajax, '/api/user')
// 相当于=>
var getUser = function partailApplication (...laterArgs) {
  return ajax('/api/user', ...laterArgs)
}
@前端进阶之旅: 代码已经复制到剪贴板
  1. 我相信大家已经知道怎样用partial来写getUser函数了

那么再进一层,getCurrentuser函数可以怎么写呢?

// example2
var getCurrentUser = partial(ajax, '/api/user', { userId: 1 })
@前端进阶之旅: 代码已经复制到剪贴板

哈哈😄,看到这里你是否想到了还能用案例1中的getUser和partial配合:

// example3
var getCurrentUser = partial(getUser, { userId: 1 })
@前端进阶之旅: 代码已经复制到剪贴板

过程是这样的:

function ajax (url, data, callback) {
	// ...
}

function partial (fn, ...prestArgs) {
	return function partiallyApplied (...laterArgs) {
		return fn(...prestArgs, ...laterArgs)
	}
}

var getUser = partial(ajax, '/api/user')

var getCurrentUser = partial(getUser, { userId: 1 })
@前端进阶之旅: 代码已经复制到剪贴板

我们可以像案例2一样通过指定url和data两个实参来定义getCurrentUser(...)函数。

也可以像案例3将getCurrentUser(…)函数定义成getUser(…)的偏应用,该偏应用仅指定一个附加的 data 实参。

案例3的函数包含了一个额外的函数包装层。这看起来有些奇怪而且多余,但对于你真正要适应的函数式编程来说,这仅仅是它的冰山一角。随着本文的继续深入,我们将会把许多函数互相包装起来。记住,这就是函数式编程!

# add案例

理解了上面的一个案例之后,我们再来看下面的案例应该就会变得非常简单了:

这是一个计算返回两数之和的函数:

function add (x, y) {
	return x + y
}
@前端进阶之旅: 代码已经复制到剪贴板

现在我们有一个数组,要给数组中的每一项都固定加上一个数3,也许你想到了可以用JS中的map来写:

var arr = [1, 2, 3, 4]
var arr2 = arr.map(function adder (val) => {
	return add(3, val)
})
@前端进阶之旅: 代码已经复制到剪贴板

map中执行的事情其实也是返回一个函数add的计算结果,那么我们就可以用partial函数来写它:

// example4
var arr2 = arr.map(partial(add, 3))
@前端进阶之旅: 代码已经复制到剪贴板

注意: 如果你没见过 map(..) ,别担心,我会在后面的部分详细介绍它。目前你只需要知道它用来循环遍历(loop over)一个数组,在遍历过程中调用函数产出新值并存到新的数组中。

# 柯里化

我们来看一个跟偏应用类似的技术,该技术将一个期望接收多个实参的函数拆解成连续的链式函数(chained functions),每个链式函数接收单一实参(实参个数:1)并返回另一个接收下一个实参的函数。

这就是柯里化(currying)技术。

还记得前面的ajax函数吗?

function ajax (url, data, callback) {
	// ...
}
@前端进阶之旅: 代码已经复制到剪贴板

现在想象一下我们已经创建了一个ajax(…)的柯里化版本:

curriedAjax('/api/user')
						({ userId: 1 })
							( function foundUser(user) { ... } )	
@前端进阶之旅: 代码已经复制到剪贴板

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

var userFetcher = curriedAjax('/api/user')
var getCurrentUser = userFetcher({ userId: 1 })
getCurrentUser( function foundUser(user){ /* .. */ } )
@前端进阶之旅: 代码已经复制到剪贴板

可以看到curriedAjax函数在每次调用的时候只接收一个实参,而不是一次性接收所有实参(像 ajax(..) 那样),也不是先传部分实参再传剩余部分实参(借助 partial(..) 函数)。

柯里化和偏应用进行对比:

相同点:

  • 每个类似偏应用的连续柯里化调用都把另一个实参应用到原函数,一直到所有实参传递完毕。

不同点:

  • 柯里化会明确地返回一个期望只接收下一个实参 data 的函数,而偏应用是能接收所有的剩余参数。

# curry函数

下面我们来看看如何定义一个用来柯里化的实用函数:

← 进阶柯里化组合函数 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • JavaScript Part 1

    • Ajax总结篇
    • Canvas 绘制八大行星
    • DOM编程之API学习总结篇
    • JavaScript事件机制
    • JavaScript代码片段100个
    • JavaScript作用域分析总结
    • JavaScript原型链回顾
    • JavaScript及jQuery中的各种宽高属性图解
    • JavaScript工程项目的一系列最佳实践
    • JavaScript常用API合集
    • JavaScript数组、字符串、对象常用方法
    • JavaScript数组方法总结篇
    • JavaScript深浅拷贝
    • JavaScript运动框架之速度时间版本
    • JavaScript运行机制Event Loop
    • JavaScript防抖节流原理
    • Javascript中的复制粘贴功能
    • Javascript数组详解
    • OOP之原型与原型链
    • Object.defineProperty详解
    • V8源码浅析JS数组常见方法
    • await 在 forEach 中不生效解决方案
    • iframe+表单跨域提交POST请求
    • javascript笔记总结篇
    • 业务中处理数据结构常用的JS方法
    • 作用域
    • 你真的掌握变量和类型了吗
    • 前后端分离之数据Mock
    • 原型与原型链
    • 原生JS补给(上)
    • 如何写出一个惊艳面试官的深拷贝
    • 带你填一些JS容易出错的坑
    • 彻底弄懂 JavaScript 执行机制
    • 执行上下文 执行栈
    • 正则回顾总结
    • 正则基础知识
    • 正则完整篇
    • 正则表达式
    • 浅析Promise原理
    • 浅谈JavaScript中的异步处理
    • 深拷贝 vs 浅拷贝
    • 聊一聊typeof instanceof 实现原理.
    • 聊一聊闭包
    • 高阶函数map reduce filter
  • JavaScript Part 2

  • CSS

  • HTML

  • Jquery

  • ES6

  • 小程序

  • Vue

  • React

  • 深入React

  • React Native

  • NodeJS

  • Angular

  • TypeScript

  • Webpack

  • 浏览器

  • 移动端

  • 前端工程化

  • Electron

  • HTTP

  • Nginx

  • Linux

  • 数据结构与算法

  • LeetCode算法题

  • 综合