函数试编程范式
为什么学习函数式编程
函数式编程是一个非常古老的概念, 早于第一台计算机的诞生
为什么现在还要学习函数式编程?
- 函数式编程是随着React的流行受到越来越多的关注
- vue3 也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的李勇 tree shaking 过滤无用代码
- 方便测试,方便并行处理
- 有很多库可以帮助我们进行函数式开发, lodash、underscore、ramda
函数式编程概念
函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免>使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入和输出。
历史函数式编程中最古老的例子莫过于1958年被创造出来的LISP了。但是要提及函数式编程的例子却不得不从更早的λ演算说起。
- 面向对象的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装继承多态来演示事物事件的关系
- 函数式的思维方式: 把现实世界中的事物和事物之间的联系抽象到程序世界中(对运算过程进行抽象)
- 程序的本质: 根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
- x -> f(联系, 映射) -> y, y = f(x)
- 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如: y=sin(x),x和y的关系
- 相同的输入始终要得到相同的输出
- 函数式编程来描述数据(函数)之间的映射
例子
计算两个数的和
在我们初学编程的时候会先定义两个数,然后把这两个数相加,用一个变量保存,这种方式是非函数式的,是通过步骤一步一步做出来的,这是面向过程的编程方式
// 非函数式
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)
如果我们使用函数式编程,需要把运算过程进行抽象,首先我们需要抽象一个 add 的函数,这个函数需要接收两个参数,当这个函数执行完的时候需要把两个值的计算结果返回,所以函数式编程中的函数一定要有输入,一定要有输出,并且相同的输入要有相同的输出
, add 函数定义完成之后,我们就可以调用add函数传对饮的参数并把结果保存起来
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(1, 2)
console.log(sum)
当使用函数式编程的时候一定会有一些函数,这些函数可以无数次的重用,所以函数式编程的好处就是可以让代码进行重用,这些函数可以组合成功能更强大的函数
函数是一等公民
函数是一等公民(英文:First-class citizen) JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为 第一等公民。
- 函数可以存储在变量中
// 赋值给变量
let fn = function () {
console.log('First-class citizen')
}
let Dialog = {
show () {
return View.show()
},
hide () {
return View.hide()
}
}
// 与上面等价
let Dialog = {
show: View.show,
hide: View.hide,
}
- 函数作为参数
function alert () {
console.log('alert')
}
function trigger (callback) {
typeof callback === 'function' && callback()
}
// 函数作为参数
trigger(alert)
- 函数作为返回值
function add (n1, n2) {
let sum = n1 + n2
return function (n3) {
return sum + n3
}
}
let fn = add(1, 2)
let num = fn(3)
例子
用函数的特性模拟数组方法
实现forEach
forEach() 方法对数组的每个元素执行一次提供的函数。
- forEach 回调函数的的参数
- 第一个参数是遍历的数组内容,
- 第二个参数是对应的数组索引,
- 第三个参数是数组本身
function forEach (array, fn) {
for (let i=0; i<array.length; i++) {
fn(array[i], i, array)
}
}
// 测试
let arr = [1, 2, 3]
forEach(arr, (item) => {
console.log(item)
})
实现filter
filter用于对数组进行过滤。它创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
- filter 回调函数的参数
- 第一个参数是遍历的数组内容,
- 第二个参数是对应的数组索引,
- 第三个参数是数组本身
function filter (array, fn) {
let results = []
for (let i=0; i<array.length; i++) {
if (fn(array[i], i, arr)) {
results.push(array[i])
}
}
return results
}
// 测试
let arr = [1, 2, 3, 4, 5]
let newArray = filter(arr, (item) => {
return item > 2
})
实现once函数
在实际的工作中我们可能经常遇到某些内容只执行一次,不再需要执行,我们可以把这些内容封装成函数,作为once函数的参数,达到我们的需求
function once (fn) {
let done = false
return function () {
if (!done) {
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function (money) {
console.log(`支付: ${money} 元`)
})
// 多次执行只会执行一次
pay(2)
pay(2)
pay(2)
pay(2)
pay(2)
实现map方法
方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成
- map 回调函数的参数
- 第一个参数是遍历的数组内容,
- 第二个参数是对应的数组索引,
- 第三个参数是数组本身
function map (array, fn) {
let results = []
for (let i=0; i<array.length; i++) {
results.push(fn(array[i]))
}
return results
}
// 测试
let arr = [1, 2, 3, 4]
let newArray = map(arr, (item) => {
return item * 2
})
实现every方法
方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值
- map 回调函数的参数
- 第一个参数是遍历的数组内容,
- 第二个参数是对应的数组索引,
- 第三个参数是数组本身
function every (array, fn) {
let result = true
for (let i = 0; i < array.length; i++) {
if(!fn(array[i], i, array)){
result = false
break;
}
}
return result
}
// 测试
let arr = [4, 5, 6]
console.log(every(arr, (item) => {
return item > 3
}))
实现some方法
方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。
- map 回调函数的参数
- 第一个参数是遍历的数组内容,
- 第二个参数是对应的数组索引,
- 第三个参数是数组本身
function some (array, fn) {
let result = false
for (let i = 0; i < array.length; i++) {
if(fn(array[i], i, array)){
result = true
break;
}
}
return result
}
// 测试
let arr = [4, 5, 6]
console.log(some(arr, (item) => {
return item > 3
}))
使用高阶函数的意义
- 抽象可以帮助我们屏蔽细节,只需要关注与我们的目标
- 高阶函数是用来抽象通用的问题