一、函数式编程是什么?
函数式编程(Funtional Programming),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
面向对象编程的思维方式:把现实世界中的事物抽象中程序世界中的类和对象,通过封装、继承、多态来演示事物事件的联系。
函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)。
// 非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
// 函数式
function add(n1, n2) {
return n1 + n2
}
let sum = add(2, 3)
console.log(sum)
二、头等函数
当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在这门语言中,函数可以被当作参数、返回值、变量。
为什么 JavaScript 是头等函数 ?
在 JavaScript 中函数就是一个普通的对象(可以通过 new Function( ) ),我们可以把函数储存在变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的过程中通过 new Function( ) 来构造一个新的函数。
三、高阶函数
当一个函数接收另一个函数作为参数,这种函数就称之为高阶函数。
高阶函数的特点:
- 可以把函数作为参数返回给另一个函数
- 可以把函数作为另一个函数的返回结果
函数作为参数:
// forEach
// 定义一个遍历数组的并对每一项做处理的函数,第一个函数是一个数组,第二个参数是一个函数。
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
// test
let arr = [1, 2, 3]
forEach(arr, item => {
item = item * 2
console.log(item) // 2 4 6
})
// filter
// 遍历数组,并把满足条件的元素存储成数组,再进行返回
function filter(array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
//如果满足条件
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
// test
let arr = [1, 3, 4, 7, 8]
let result = filter(arr, item => item % 2 === 0)
console.log(result) // [4, 8]
函数作为返回值:
// 一个函数返回另一个函数
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
// test
// 第一种调用方式
const fn = makeFn()
fn() //Hello function
// 第二种调用方式
makeFn()()///Hello function
// once
// 让函数只执行一次
function once(fn) {
let done = false
return function() {
// 判断值有没有被执行,如果是false表示没有执行,如果是true表示已经执行过了,不必再执行
if(!done) {
done = true
// 调用fn,当前this直接传递过来,第二个参数是把fn的参数传递给return的函数
return fn.apply(this, arguments)
}
}
}
// test
let pay = once(function (money) {
console.log(`支付:${
money} RMB`)
})
pay(5) //支付:5 RMB
pay(5)
pay(5)
pay(5)
pay(5)
四、闭包函数
函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
通俗的讲:可以在另一个作用域中调用一个函数的内部函数并访问到该函数作用域中的成员
当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用而不能释放,因此内部函数依然可以访问外部函数的成员。
局部变量会常驻在内存中,可以避免使用全局变量,防止全局变量污染。如果被长期占用,而不被释放,会造成内存泄漏。
function makeFn () {
let msg = 'Hello function'
}
// 正常情况下,执行完makeFn,里面的变量msg会释放掉
// 但是下面的情况
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
// 在上面函数中,返回了一个函数,而且在函数中还访问了原来函数内部的成员,就可以称为闭包
const fn = makeFn()
fn()
// fn为外部函数,当外部函数对内部成员有引用的时候,那么内部的成员msg就不能被释放。当我们调用fn的时候,我们就会访问到msg。
//注意的点:
//1、我们可以在另一个作用域调用makeFn的内部函数
//2、当我们调用内部函数的时候我们可以访问到内部成员
案例
// 计算一个数平方和立方的运算
ath.pow(4, 2)
Math.pow(5, 2)
// 后面的二次方三次方很多次重复,下面要写一个二次方三次方的函数
function makePower (power) {
return function (number) {
return Math.pow(number, power)
}
}
// 求平方
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4)) // 16
console.log(power2(5)) // 25
console.log(power3(4)) // 64
// 计算不同级别的员工工资
// 假设计算员工工资的函数第一个函数传基本工资,第二个参数传绩效工资
// getSalary(12000, 2000)
// getSalary(15000, 3000)
// getSalary(15000, 4000)
// 不同级别的员工基本工资是一样的,所以我们将基本工资提取出来,之后只需要加上绩效工资
function makeSalary (base) {
return function (performance) {
return base + performance
}
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)
console.log(salaryLevel1(2000)) //14000
console.log(salaryLevel2(3000)) //18000
console.log(salaryLevel2(4000)) //19000
五、函数的柯里化
- 使用柯里化Curry解决硬编码问题
- 函数多个参数时进行改造,调用时只传递部分参数,并且让函数返回新的函数,新的函数接收剩余参数,并返回相应的结果
- lodash _curry(func)使用:
- 接收一个或多个func的参数,如果func所需的参数都被提供则执行func并返回执行结果,否则继续返回该函数等待接收剩余参数
- 参数:需要柯里化的函数,返回值:柯里化后的函数
- 类似于对于参数的’缓存’,将多参数函数分割成颗粒度更小的纯函数,便于重用,同时组合产生强大的功能
//lodash _.curry()使用
const _ = require("lodash")
//三元函数
function getSum(a,b,c) {
return a+b+c
}
const curried = _.curry(getSum)
console.log(curried(1,2,3))
console.log(curried(1)(2,3))
console.log(curried(1,2)(3))
console.log(curried(1)(2)(3));
函数珂里化的原理实现
//柯里化原理模拟
//通过判断返回的函数参数是否传全产生两种情况,
//传全那么直接调用传入的fn方法,为传全返回一个新函数,未传全则返回一个新函数,新函数的参数为上次参数和当前的arguments的和
/**
* 解释01:此时的arguments为继续调用的参数的伪数组 例如:fn(1)(2,3) arguments指(2,3)
* 伪数组转化通过Array.from(),
* args是个闭包内的变量被保存下来,指的是上一次调用curriedFn时的args
* */
function curry(fn:Function) {
return function curriedFn(...args) {
if(args.length < fn.length) //传入参数的个数 or 获取fn函数形参个数
{
return function () {
return curriedFn(...args.concat(Array.from(arguments))) //解释01
}
}
return fn.apply(null,args) //fn(...args)
}
}
function addSum(a,b,c) {
return a+b+c
}
const _addSum = curry(addSum)
console.log(_addSum(1,2,3));
console.log(_addSum(1)(2,3));
函数组合
- 纯函数和柯里化很容易写出洋葱代码 h(g(f())) .toUpper(.first(_.reverse(array)))
- 函数组合实现以上相同功能,但是通过组合的方式
//函数组合演示
function compose(f,g) {
return function (value) {
return f(g(value)) //封装个洋葱函数
}
}
//案例:取数组最后一个值
const _reverse = function (value) {
//辅助函数
return value.reverse()
}
const _first = function (value) {
//辅助函数
return value[0]
}
const _compose = compose(_first,_reverse)
console.log(_compose([1,2,3]))
注意,组合函数执行顺序从右到左,a -> fn3 -> fn2 -> fn1 -> b
lodash _.flow(fn3,fn2,fn1) _.flowRight(fn1,fn2,fn3)
const arr_ = ["one","two","three"]
const f1 = _.flowRight(_.toUpper,_.first,_.reverse)
console.log(f1(arr_));
flowRight的原理
//通过fn循环调用
function flowRight(...args) {
return function (val) {
letfn:Function;
for(let i = args.length-1; i>=0;i--){
fn = !fn?args[i](val):args[i](fn)
}
return fn
}
}
//通过reduce实现循环调用
function compose(...args) {
return function (val) {
return args.reverse().reduce((data,currentdata)=>{
//data累积器 currentdata当前值,每次用当前值调
return currentdata(data)
},val)//累积器的初始值
}
}
//优化成ES6
const compose1 = (...args) => val => args.reverse().reduce((data,currentdata)=> currentdata(data),val)
//案例:取出数组的最后一个元素并转化为大写
const _t = value => value.toUpperCase()
const _f = value => value[0]
const _r = value => value.reverse()
const f = flowRight(_t,_f,_r)
console.log(f(["one","two","three"]));
const f1 = compose(_t,_f,_r)
console.log(f1(["one","two","three"]));