为什么要学习函数式编程
-
Vue3和React都开始拥抱函数式编程
-
方便测试、方便并行处理
-
打包过程中可以更好的利用 tree shaking 过滤无用代码
-
有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
-
…
含义
-
函数式编程是一种编程范式,是一种编程思想,类比于面向过程编程和面向对象编程
-
函数式编程关注的是函数,而不是过程,强调通过函数的拆分与组合去解决问题。
-
函数式编程的思维方式:把具体的事物之间的联系抽象到程序中,对运算的过程进行抽象
-
进一步地:
-
程序的本质是根据输入通过某种元素获得相应的输出
- 函数式编程中的函数不是程序中的函数或者方法,而是数学层面上的
函数映射关系
。比如 y=f(x),x对应我们的输入,y对应输出,f就是对应关系。
- 函数式编程中的函数不是程序中的函数或者方法,而是数学层面上的
-
相同的输入始终要得到相同的输出(即纯函数:一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,后面会详细介绍)
- 函数式编程用来描述数据(函数)之间的映射
-
案例:函数式编程和非函数式的对比:
// 非函数式 聚焦计算的过程 let num1 = 2 let nmu2 = 3 let sum = num1 + num2 console.log(sum) // 函数式 聚焦函数的设计,对过程的抽象 function add(nmu1, nmu2){ return num1+num2 } let sum = add(2,3) console.log(sum)
-
优点:
- 函数重用
- 细粒度函数可以进行二次组合,形成功能更加丰富的函数。
个人理解:函数式编程是用简单地函数组合成复杂的函数来实现预想的功能。
函数式编程相关概念
1. 函数是一等公民:
首先"一等公民"这个概念源于书籍《Programming Language Pragmatics》,书中指出在编程语言中一等公民是指:
- 可以赋值给变量
let fn = function () {
console.log('Hello First-class Function')
}
fn()
// 将一个匿名函数赋值给变量fn
-
可以作为函数的参数
-
可以作为函数的返回值
(上述2、3性质将在高阶函数部分进行具体举例)
显然,在JavaScript中,函数就是普通对象,它具有上述所有性质,故函数也是“一等公民”。
2. 高阶函数
含义
1. 可以把函数作为参数
传递给另一个函数
-
案例一:实现数组的filter方法:
(filter方法:将被操作数组中的元素进行遍历,根据传入的
筛选条件函数
来筛选出符合条件的数组元素组成新的数组)
// 实现:
function filter (arr,fn){
let newArry = []
for(let value of arr){
if(fn(value)) newArry.push(value)
}
return newArry
}
// 测试:
let arr = [1,2,3,4,5,6]
let fn1 = function (x){
return x%2 ==0
}
let result = filter(arr,fn1)
console.log(result) // [2,4,6]
// 这里的函数fn1作为参数是从外部传入,在filter内部进行调用的,并充当了filter函数的筛选条件的角色
-
案例二:实现数组的map方法:
(map方法:将被操作数组中的元素进行遍历,并根据传入的处理函数进行处理,返回处理后的数组)
// 实现: function map (arr,fn){ let newArry = [] for(let value of arr){ newArry.push(fn(value)) } return newArry } // 测试: let arr = [1,2,3,4,5,6] let fn1 = function (x){ return x+1 } let result = map(arr,fn1) console.log(result) // [2,3,4,5,6,7] // 这里的函数fn1作为参数是从外部传入,在map内部进行调用的,并充当了map函数的处理函数的角色
2. 可以把函数作为另一个函数的返回结果
-
案例一:实现lodash中的once函数
(lodash中的once函数:函数只执行一次。实际意义: 比如用户的付款行为只执行一次,使用once函数可以防止付款操作多次触发)
function once (fn){
let done = false
return function (){
if(!done){
done = true
fn.apply(this,arguments)
}
}
}
let fn1 = function (x){
console.log(`支付了${x}元`);
}
let result = once(fn1)
result(6) // 只输出一次 【支付了6元】
result(6)
result(6)
// 这里的once执行时拿到了一个once内部的新的函数,并用pay来接收,这样就使得pay引用了内部新的函数,形成了闭包,所以once内部变量得以保留,pay执行一次后,done的值就恒为true了,实现了once函数。
意义:
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标 ,比如我们常用的其他高阶函数:forEach、every、 some、、reduce 等等
- 高阶函数是用来抽象通用的问题
(后面的科里化和函数组合都用到了这些,都使用了函数作为参数,使用函数作为返回值)
3. 闭包
-
函数和周围的状态(词法环境)的引用捆绑在一起形成闭包。
-
可以在一个作用域中调用一个函数的内部函数,并访问到该函数的作用域中的成员
-
闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是 堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。也就是说外部函数会从调用栈中移出掉,但是
和闭包相关的变量会被缓存下来
(比如我们上文实现的once函数就用到了闭包)
-
闭包案例: 生成求n次幂的功能函数:
function fn (n){
return function(num){
return Math.pow(num,n) // Math.pow(x,y) 计算x的y次幂
}
}
let result = fn(2)
console.log(result(3));
// 这里的fn函数的参数将会作为持久性变量被闭包给圈住不被释放,所以后续return出去的内部的函数可以使用这些数据。
-
进一步地
首先每当函数执行,都会形成一个私有的执行上下文,这个私有的执行上下文里面包含着变量对象(和作用域链还有this,不过这里用不上就先不谈),这个变量对象里面包含着当前执行上下文中的变量、函数和函数参数声明,一般情况下当函数执行完,当前私有上下文及里面的内容就会被释放掉。但是在闭包条件下,函数执行完后在栈中弹出,但是当前私有执行上下文中的某些内容**被当前上下文以外的内容所引用。**我们反观上面的那个例子,fn函数内部资源就不会被释放掉,因为他内部返回的匿名函数被全局中的变量引用着。那么当前上下文就不能被释放,当前上下文中保存的变量对象中的内容也就不能被销毁,也就被保留起来了,自己维护一个自己的私有变量,且不会被GC机制给回收。