如果一个编程语言的编程范式(编程方法,编程规范)是函数是一等公民,那么就可以认为这个编程语言使用的是函数式编程
在函数式编程中,函数可以作为参数或者返回值使用,在另一个函数中进行传递
纯函数
函数式编程中有一个非常重要的概念叫纯函数(pure function),JavaScript符合函数式编程的范式,所以也有纯函数的概念
纯函数必须满足如下两个条件:
- 相同的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
- 函数的结果只取决于传入的函数参数和内部逻辑
- 函数的执行和I/O设备的输入输出无关
- 狭义上,函数中如果存在
console.log,console.log是副作用,但是广义上,console.log不是副作用
副作用(side effect)表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改了引用类型的参数或者改变了外部的存储
如果一个函数产生了副作用,那么就意味着修改了函数之外的值,此时就可以导致其它成员在使用对应值的时候,容易产生bug
当然JavaScript中的函数并不是全部都要写成纯函数,很多时候,我们的确需要书写非纯函数,但是能使用纯函数的时候,还是推荐使用纯函数
示例
JS对数组的截取有2个截取数组的方法slice和splice
-
slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组 ---> 纯函数
-
splice截取数组, 会返回一个新的数组, 也会对原数组进行修改 ---> 非纯函数
slice
const arr = [1, 2, 3, 4, 5]
// slice的结果只取决于start和end这两个传入的参数
// 所以slice是一个纯函数
const res = arr.slice(2)
console.log(res) // => [3, 4, 5]
console.log(arr) // => [1, 2, 3, 4, 5]
splice
const arr = [1, 2, 3, 4, 5]
// splice会修改原数组,而这就是splice函数产生的副作用
// 因此splice函数不是一个纯函数
const res = arr.splice(2)
console.log(res) // => [3, 4, 5]
console.log(arr) // => [1, 2]
示例
// foo是一个纯函数
function foo(info) {
return {
...info,
age: 23
}
}
console.log(foo({
name: 'Klaus',
age: 18
}))
const num = 12
// foo函数依赖了全局的变量num
// 也就是说当num的值发生了改变的时候,函数的相同参数的返回值会不一样
// 所以一个函数如果依赖了外部的变量来进行逻辑运算,那么这个函数就不是一个纯函数
// 因此foo函数并不是一个纯函数
function foo(v) {
return v + num
}
console.log(foo(3))
// Cpn是一个纯函数
// 在react等框架中就要求我们尽可能的像纯函数一样
// 来保证我们的props不会被修改,这其实和vue中的单向数据流的概念是很像的
function Cpn(props) {
return {...props}
}
函数柯里化
- 将一个有多个参数的函数转变为接收一部分参数的函数
- 接收一部分参数的函数又返回一个函数去处理剩余的参数
- 这个转换的过程就被称之为函数柯里化(Currying)
function sum(num1, num2, num3, num4) {
return num1 + num2 + num3 + num4
}
console.log(sum(10, 20, 30, 40))
// 转换方式1
const sum = num1 => num2 => num3 => num4 => num1 + num2 + num3 + num4
console.log(sum(10)(20)(30)(40))
// 转换方式2
const sum = (num1, num2) => (num3, num4) => num1 + num2 + num3 + num4
console.log(sum(10, 20)(30, 40))
只要多个参数的函数转换为多次函数调用的过程 就被称之为函数柯里化
柯里化的作用
- 单一职责原则: 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个 函数来处理
- 函数公共逻辑复用
// 不使用柯里化
const printLog = (date, type, message) => `[${date.getHours()}]:[${date.getMinutes()}]:[${type}]:[${message}]`
// 在下边的代码中,短时间内打印的日期都是一样的,而且日志类型都是一样的
// 所以其实这部分代码是可以复用的
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
const printLog = date => type => message => `[${date.getHours()}]:[${date.getMinutes()}]:[${type}]:[${message}]`
printNowLog = printLog(new Date())
printErrorLog = printNowLog('ERROR')
printWarningLog = printNowLog('WARNING')
console.log(printErrorLog('something error'))
console.log(printErrorLog('something error'))
console.log(printErrorLog('something error'))
console.log(printWarningLog('something warning'))
console.log(printWarningLog('something warning'))
console.log(printWarningLog('something warning'))
- 函数功能定制
// 以下是一些伪代码
const vue = () => {
function render() {
// 这里实际将vnode渲染为dom的渲染器
}
return {
render,
createApp: createAppAPI(render)
}
}
function createAppAPI(render) {
// 这里是将App组件对象转换为VNode的代码
return function(App) {
// 这里就类似于createAppAPI(render)(App)
// 这样一方面保证了各个函数内部的职责单一
// 另一方面确保了vue的render是独立的
// 我们可以使用的是渲染成DOM的render函数
// 也可以调用的是渲染成APP控件的render函数
render(App)
}
}
// 实际调用
import { createApp } from 'vue'
createApp(App).mount('#app')
模拟柯里化转换函数
function foo(num1, num2) { ... }
console.log(foo.length) // => 2
console.log(foo.name) // => 'foo'
const currying = fn => {
// 这里需要返回一个具名函数,因为后续需要递归调用
return function curried(...args) {
// 如果收集到了足够多的参数的时候,直接调用函数
if (args.length >= fn.length) {
// 使用apply或call调用是为了避免外部调用的时候修改了实际调用的this指向
return fn.apply(this, args)
} else {
// 如果没有接收到足够多的参数的时候,返回一个新的函数,继续接收参数
return function(...params) {
// 参数合并后递归调用
// 使用apply的目的是为了避免调用函数的时候,调用者显示绑定了this的值
// 所以在高阶函数封装的时候,内部函数调用的this一般需要显示指定
return curried.apply(this, [...args, ...params])
}
}
}
}
const sum = (num1, num2, num3) => num1 + num2 + num3
const currySum = currying(sum)
console.log(currySum(10, 20, 30)) // => 60
console.log(currySum(10, 20)(30)) // => 60
console.log(currySum(10)(20)(30)) // => 60
const tmp = currySum(10)
console.log(tmp.call({}, 20, 30))
组合函数
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧
比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的
那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复
那么是否可以将这两个函数组合起来,自动依次调用呢?
这个过程就是对函数的组合,我们称之为 组合函数(Compose Function)
function compose(...fns) {
if (!fns.length) {
throw new TypeError('Function compose need arguments')
}
for (let fn of fns) {
if (typeof fn !== 'function') {
throw new TypeError('Exception arguments are functions')
}
}
// 组合函数返回一个新的函数
// 在这个新的函数中依次调用我们需要调用的函数
return function(...args) {
let index = 0
// 第一个函数可以接收任意个数的参数
let res = fns[index].call(this, ...args)
while(++index < fns.length) {
// 从第二个参数起,接收的参数为上一个参数的返回值
res = fns[index].call(this, res)
}
return res
}
}
const double = num => num * 2
const square = num => num ** 2
const fn = compose(double, square)
console.log(fn(3)) // => 36