00 - 序
本系列/专栏为拉勾教育-大前端高薪训练营学习笔记,内容为本系列课程的讲授内容、亮点题目分析、重点难点的总结、以及个人的体会。个人感觉拉勾教育比体验过的其他教育平台要更好一点。老师讲授的内容比较全面,相对于自学可以节省很多不必要的走弯路的时间,可以更快的使自己在技术上系统的有所提高。同时随堂测的题目也很用代表性,老师跟进解答很快,推荐和我一样在自学路上遇到瓶颈或者找不到进一步学习方向的同学尝试一下。
01 - 说说函数式编程
之前阅读一些经验丰富的开发者的源码,或者使用某些库的时候。我经常发现,有的代码可以一直“点下去”。比如:
// maybe
function badCode(para){
let temp = sth.doSth(para)
temp = sth.doAnother(temp)
return sth
}
// but ?
function elegentCode(para){
return sth(para).doSth().doAnother()
}
或者,当我需要抽象某一个经常会使用到的功能函数,但我不希望把每一个我需要用到的属性全部当作参数传入。然而我又希望这个函数可以用在更多的地方,有更好的可复用性,所以我可能会这样做:
function translateAFile({
safeNoFile:true,
default:'English',
defaultTarget:'Chinese',
path
})
但是这样依然非常的“不优雅”,我想到可能可以拆分成更小粒度的函数进行整合,我可能会:
function readFile(){
//do Sth.
}
function requestTranslate(){
// do Sth.
}
// more function
但是这样依然很麻烦,我不仅要记住所有的函数名,函数的参数和返回值,而中间任何一步的错误没有正确捕获,或美誉正确进行转换就会引发错误,大大增加了测试和开发的成本,反而不方便了。在这种情况下,“一直点下去”的方法显然就会优雅很多。但是…这其中是如何实现的呢?这可能要说到函数式编程。
02 - 函数式编程的位置
面对函数式编程,在考虑如何实现之前,可能要先研究一下函数式编程到底式什么层次的概念。函数式编程是一种编程范式,可以理解为与面向过程编程和面向对象编程同级。
如果说面向过程专注与逻辑的先后次序,逐步执行;面向对象编程专注与抽象对象和对象的属性和方法;函数式编程的关注点可能是“联系”,从这一点上,它就很“函数”,数学的那种“函数”。
03 - 函数式编程依赖的特点和前提
因为在JS中函数是一等公民,我们可以“随意”的使用函数,函数就是一个普通的对象(可以通过new Function()
来创建)。函数可以赋值给变量,可以使用new Function()
构造,甚至可以以作为另一个函数的返回值,而这是函数式编程的前提,是高阶函数,柯里化得以实现和应用的必须特性。
高阶函数
JS中,函数可以作为参数传递给另一函数,同时也可以作为一个函数的返回值。对于函数作为参数,我想每一个人都不陌生,每天都会使用。当我们需要为一个异步操作提供回调函数的时候,如:
function doSomeThing(foo,callback){
// doSth
var result = bar.doSth(foo)
callback()
}
此时,一个回调函作为函数的参数被传入,并且在函数中被调用。同时,函数也可作为一个函数的返回值,之前有提到过,函数可以赋值给变量。那么我们可以推断,当一个函数返回一个函数的时候,如果使用一个变量接收这个函数,我们就可以使用这个变量名+括号的形式来调用他,就像我们在把函数作为参数的情况一样。比如:
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn()
我们可以看到,由于闭包的存在,声明的msg
变量的作用域虽然局限在makeFn
内部,但是我们在调用的时候,msg
依然存在,没有被GC。在这里,它相当于被作为一种状态保存下来了。我们运行Fn()
依然能顺利的在控制台打印Hello function
。
这就隐隐的和之前的案例产生关联:“当我需要一些参数,但我不希望每次调用都传入,又想使耦合度降低,提高复用性”,高阶函数似乎提供了一种方案。他可以通过闭包返回一个包含一个我们需要的状态的函数,然后我们调用“生成的函数”就好了。这就是高阶函数的优点,概括起来说就是:
-
抽象可以帮我们屏蔽细节,只需要关注与我们的目标
-
高阶函数是用来抽象通用的问题
以上特点的存在,使得高阶函数成为我们函数式编程中一个不可或缺的部分。
纯函数
在讨论纯函数之前,我们先看几个案例:
let numbers = [1, 2, 3, 4, 5]
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.splice(0, 3)
// => [1, 2, 3]
numbers.splice(0, 3)
// => [4, 5]
numbers.splice(0, 3)
// => []
我们可以看到,对于slice()
和splice()
,在第一次执行的时候,都会返回数组的前三项。但是当继续执行的时候,splice()
的执行结果就变的与预期不同了。假设我们抽象了这一逻辑,那么在多次,多处,或者链式调用的时候,情况就会变的复杂且难以控制。我们把这种情况叫做“函数具有副作用”。副作用的产生通常是函数内部改变了外部状态,或函数依赖于某一外部状态。这种“依赖”就会使得函数变的“不纯”。副作用的来源通常可能是:
-
配置文件
-
数据库
-
用户输入
-
文件系统
-
…
与此同时我们也可以得出纯函数的特点:
-
无论多少次执行,相同的输入总会得到相同的输出
-
纯函数不会保留中间计算的结果,变量是不可变的
因为纯函数的输入和输出我们都可以预期,所以我们很清楚函数的执行结果。这会给我们带来很多好处,比如:
-
可缓存——因为相同的输入总会得到相同的输出。所以如果对于一个耗时的操作,我们可以将结果缓存,并且重复执行的时候返回缓存的结果以节省时间和资源。
-
易测试——因为纯函数的执行不依赖外部状态,只依赖明确的输入,所以可以不受限制的单独进行测试。
-
并行安全——因为纯函数不需要访问共享的内存数据,也不会产生副作用,每一次执行都是相对独立的,所以在并行环境下可以十分看全的运行纯函数。
那么如何把一个不纯的函数变成纯函数呢?有两种方法,增加参数和硬编码。如:
//一个不纯的函数
let mini = 18
function checkAge (age) {
return age >= mini
}
// 第一种方法
function checkAgeNew(age, target){
return age >= target
}
// 第二种方法
function checkAgeHardcode(age){
let mini = 18
return age >= mini
}
但是这两种方法都会带来另外的问题,第一种方法增加了参数,传递变的不方便。而且一但需要修改基准年龄,需要更改所有的调用,一但漏掉就会引发bug。第二种缺点变的更加的明显,众所周知在编程中我们要尽力的避免硬编码。以降低程序的耦合度。难道为了函数式编程我们就不得不向这些缺点妥协么?
04 - 柯里化
如果说让我用一句话来概括“柯里化”,就是利用闭包先预先“储存”一个参数,再通过高阶函数返回一个已经预先填好一个参数的新函数。简言之——柯里化过后的函数就是一个函数生成器。感觉还是有点绕,回到刚才的问题上。我们需要一个检查年龄是否合规的函数。
function checkAge (age) {
let min = 18
return age >= min
}
// 普通纯函数
function checkAge (min, age) {
return age >= min
}
checkAge(18, 24)
checkAge(18, 20)
checkAge(20, 30)
// 柯里化
function checkAge (min) {
return function (age) {
return age >= min
}
}
// ES6 写法
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(24)
checkAge18(20)
通过上面的代码,我们可以进一步概括一个可能不那么准确但是非常好理解的概念——“柯里化就是利用闭包,返回一个已经动态的把其中一部分参数硬编码的函数”。这样一方面拥有硬编码只需要传递一部分参数的优点,一方面减小了调用的难度(如果需要修改只需要修改柯里化的部分就可以了)
尝试实现柯里化
柯里化很方便,但是我们手动把每一个函数都柯里化会非常费神。那么我们如何编写一个函数,帮助我们把多个参数的纯函数柯里化呢?我们可以简单整理一下需求,比如先把这个函数命名为“curry
”:
-
curry函数需要把我们需要柯里化的函数作为输入
-
curry函数需要返回一个函数
-
柯里化过的函数,需要在没有提供足够多的参数的时候“暂存”已经输入的参数,并继续接收输入
-
当输入了完整的参数时,被柯里化的函数就会执行
接下来我们来尝试实现一下这一函数:
function curry (func) {
return function curriedFn (...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
// 实参和形参个数相同,调用 func,返回结果
return func(...args)
}
}
为了实现这一需求,我们首先使用了剩余参数语法,即...args
。这个语法允许我们将一个不定数量的参数表示为一个数组,如:
function sum(...args){
console.log(args)
}
sum(1,2,3,4) // [1, 2, 3, 4]
同时我们使用了另一特性——一个函数的length属性为他的参数个数,如:
function sum(a, b){
return a + b
}
console.log(sum.length) // 2
解释了这些之后这一函数就很好理解了,接下来尝试一下它的应用:
function sum(a, b, c){
return a + b +c
}
var curriedSum = curry(sum)
curriedSum(1)(2)(2) // 5
curriedSum(1, 2)(2) // 5
curriedSum(1)(2,2) // 5
不难看出,函数成功的被柯里化了,我们可以分多次,每次传入任意个数的参数即可。函数都将正常地被执行。
本篇主要讲述了函数式编程的思想,核心功能特点,以及柯里化。在下一篇中我们将进一步的讲述函数式编程以及柯里化的实际应用;以及进一步解决柯里化可能带来的问题。
https://t2.lagounews.com/fR8DRXRXcu024 前端福音!这次彻底搞懂 Webpack 原理与实践,做合格前端“配置”工程师!点击链接7天搞定webpack原理和实践,仅需19元√强烈推荐!