函数式编程范式
1.1函数式编程概念
- 函数式编程就是对运算过程进行抽象
注意:
- 函数式编程的函数指不是程序中的函数(方法),而是数学中的函数即映射关系
- 相同的输入始终要有相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射
// 非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
这段代码是非函数式的,我们是通过步骤一步一步实现的,所以它是面向过程的编程方式。
如果使用函数式的思想实现这个功能:首先我们要对运算过程进行抽象,我们要计算两个数的和,我们会抽象一个add的函数,且相同的输入要有相同的输出。
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(2, 3)
console.log(sum)
函数式编程的好处是我们后续可以无数次地重用,而且在函数式编程过程中,我们抽象出来的函数都是细粒度的函数,我们将来可以把这些函数组合成功能更强大的函数。
函数是一等公民
- 函数是一等公民(First-class Function)
- 函数可以存储在变量中
- 函数作为参数
- 函数作为返回值
在 JavaScript 中函数就是一个普通的对象 ,我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function(‘alert(1)’) 来构造一个新的函数。
- 把函数赋值给变量
// 把函数赋值给变量
let fn = function () {
console.log('Hello First-class Function')
}
fn()
//一个实例
const BlogController = {
index(posts) {
return Views.index(posts)}
}
上面的案例中我们可以看出:index方法和内部调用的方法有相同的形式(它的参数和返回值是一样的),我们就可以把Views.index赋值给index。
注意:我们是要把这个函数的方法赋值给另外一个方法,不是把函数的调用赋值给另一个方法,所以要把方法后的调用去掉。
//可以优化为
const BlogController = {
index : Views.index
}
- 函数是一等公民是我们后面要学习的高阶函数、柯里化等的基础。
高阶函数
-
高阶函数 (Higher-order function)
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
-
函数作为参数
//通过仿写forEach和filter来理解 函数作为参数
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
// 测试
let arr = [1, 3, 4, 7, 8]
// forEach(arr, function (item) {
// console.log(item)
// })
function filter (array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
// 测试
let arr = [1, 3, 4, 7, 8]
let r = filter(arr, function (item) {
return item % 2 === 0
})
console.log(r)
我们上面写了两个函数,如果函数作为参数,它可以让我们变得更灵活,而且我们在调用的时候不用考虑内部是怎么实现的,这个函数把内部实现的细节帮我们屏蔽了,而且我们函数的名字是有实际意义的,比如forEach,通过名字就知道它是要去遍历;而filter,通过它的名字就知道是要去过滤数据。
函数作为返回值我们会在下节闭包中详细讲解。
使用高阶函数的意义:
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
- 高阶函数是用来抽象通用的问题
//不需要考虑函数内部实现的细节
let array = [1, 2, 3, 4]
// 面向过程的方式
for (let i = 0; i < array.length; i++) {
console.log(array[i])
}
// 高阶函数
forEach(array, item => {
console.log(item)
})
闭包
前一小节中我们通过仿写forEach和filter来简单理解了一下函数作为参数下来我们通过几个函数作为返回值的例子来理解下闭包:
下面的例子:如果我们在一个函数里面又返回了一个函数,并且在我们访问的这个函数的内部又访问了外部函数中的成员,其实这就是闭包。
// 函数作为返回值
function makeFn () {
let msg = 'Hello function'
return function () {
//匿名函数
console.log(msg)
}
}
// const fn = makeFn()
// fn()
makeFn()()
- 闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
- 可以在另一个作用域中调用一个函数的内部函数
- 当我们调用内部函数时可以访问到该函数的作用域中的成员
根据上面的描述,其实闭包的核心作用就是把makeFn的作用范围延长了,正常情况下,makeFn执行完成之后,msg会被释放掉,但是如果makeFn中返回了一个成员,并且外部对这个成员有引用,那此时,makeFn内部再执行完成过后就不会被释放,因为外部对内部有引用。
接下来我们来写一个once函数,once我们在jQuery中可能见过,Jquery中的once函数是给一个DOM元素去注册事件,这个事件只会执行一次。在lodash其实也有一个once函数,lodash中的once是对一个函数只执行一次,我们来模拟一下once函数:
//我们通过闭包来实现一个只能执行一次的函数
function once (fn) {
// 假设这是一个支付的函数
let done = false
return function () {
// 在这里我们不确定传参个数
if (!done) {
done = true // 已经被执行
//arguments是具有数组某些特性的'类数组'(伪数组)
//每个函数都有一个Arguments对象实例arguments
//它引用着函数的实参,可以用数组下标的方式'[]'引用arguments的元素
return fn.apply(this, arguments)//通过arguments来取实参
}
}
}
let pay = once(function (money) {
console.log(`支付:${
money} RMB`)
})
// 只会支付一次
pay(5)
pay(5)
支付:5 RMB
以上是我们once函数的实现,通过once函数返回了一个函数。
- 闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
- 闭包案例
接下来通过案例来体会闭包。假设在一个项目中,经常会求数字的幂,我们可以通过Math.pow去实现,但假设我们经常去求一个数字的二次方、三次方,我们就经常要去重复写,我们可以简化下:
// 生成计算数字的多少次幂的函数
function makePower (power) {
//power:幂
return function (x) {
return Math.pow(x, power)
}
}
// 求平方
let power2 = makePower(2)
// 求三次方
let power3 = makePower(3)
1.2纯函数
概念
- 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
- 例如:数组方法slice splice
- slice 不会改变原数组 就是一个纯函数
- splice 会改变原数组 就是一个不纯的函数
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
let array = [1, 2, 3, 4, 5]
// 纯函数
console.log(array.slice(0, 3))
console.log(array)
console.log(array.slice(0, 3))
// 不纯的函数
console.log(array.splice(0, 3))
console.log(array)
console.log(array.splice(0, 3))
console.log(array)
[ 1, 2, 3 ]
[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 4, 5 ]
[ 4, 5 ]
[]
// 自己写一个求和的纯函数
function getSum (n1, n2) {
return n1 + n2
}
console.log(getSum(1, 2)