高阶函数
JavaScript 的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。高阶函数可以用来抽象通用的问题,让我们可以只关注于我们需要实现的目标。
函数作为参数
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
function filter (array, fn) {
const res = []
for (let i = 0; i < array.length; i++) {
if(fn(array[i])) {
res.push(array[i])
}
}
return res
}
const arr = [1, 2, 4, 5, 2]
forEach(arr, console.log)
console.log(filter(arr, function (item) {
return item % 2 === 0
}))
函数作为返回值
function makeFn () {
let msg = 'hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn() // hello function
makeFn()() // hello function
应用
once函数,即只执行一次的函数,比如说支付情况,无论用户点多少次,这个函数都只执行一次。
function once(fn) {
let done = false
return function () {
if(!done) {
done = true
fn.apply(this, arguments)
}
}
}
let pay = once(function (money) {
console.log(`支付了${money}元`)
})
pay(1) // 支付了1元
pay(2)
pay(3)
常用的高阶函数
let arr = [1, 5, 3, 7, 6, 8];
// forEach()
function forEach(arr, fn) {
for(let i = 0; i < arr.length; i++) {
fn(arr[i])
}
}
// test
forEach(arr, (item) => {
// do something for each item
console.log(item)
})
// filter()
function filter(arr, fn) {
let results = []
for(let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
result.push(arr[i])
}
}
return results
}
// test
filter(arr, item => {
return item % 2 === 0;
})
// map()
const map = (arr, fn) => {
let results = []
for (let value of arr) {
result.push(fn(value))
}
return results
}
// test
arr = map(arr, v => v * v)
// every()
const every = (arr, fn) => {
let result = true
for (let value of result) {
result = fn(value)
if(!result) {
break
}
}
return result
}
// test
let res = every(arr, v => v > 10)
// some()
const some = (arr, fn) => {
let result = false
for (let value of result) {
result = fn(value)
if(result) {
break
}
}
return result
}
// test
let res = some(arr, v => v > 10)
闭包
概念
一个函数内包含有另一个函数,而被包含内部函数调用了其外部(父级)函数中的参数,此时,即使函数执行结束,被调用到的参数也无法释放,也即形成了闭包。
本质
函数在执行的时候,会放到一个执行栈上,当函数执行完毕之后便会从执行栈上移除。但是,如果作用域中的成员参数被外部引用,则不能释放,因此内部函数依然可以访问外部函数的成员。
案例
function makePower(power) {
return function(number) {
return Math.power(number, power)
}
}
//test
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4))
console.log(power3(4))
console.log(power2(5))
纯函数
定义
纯函数对于相同的输入,永远会得到相同的输出。
优势
- 可测试
- 并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
lodash(纯函数库)
记忆函数(memoize)
const _ = require('lodash')
function getArea(r) {
return Math.PI * r * r
}
// test
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
记忆函数模拟实现
function memoize(arg) {
let cache = {}
return function() {
let key = JSON.stringfy(arguments)
cache[key] = cache[key] ||
arg.apply(arg, arguments)
return cache[key]
}
}
柯里化
概念
- 当一个函数需要传入多个参数时,先传递一部分参数并调用(这部分的参数以后永远不变)
- 然后,返回一个新的函数接收剩余的参数,返回结果
案例
// 原函数
function checkAge (age, min) {
return age > min
}
// 柯里化
function checkAge (min) {
return function (age) {
return age > min
}
}
// ES6 实现
function checkAge (min) => (age => age > min)
// test
let checkAge18 = checkAge(18)
checkAge18(20)
实现函数的柯里化
function curry(func) {
return function curriedFn(...args) {
// 对比实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
总结
- 柯里化可以使一个函数可以在只传递了部分参数的情况下获得一个已存储有某些参数的新函数
- 这是一种对函数参数的“缓存”
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数,产生强大的功能
函数组合
概念
- 将多个函数组合起来,实现特定的功能
- 函数可类比为管道,函数组合,即将多个管道连接起来
案例
// 实现
function compose(...func) {
return function (value) {
return func.reverse().reduce(function(acc, fn) {
return fn(acc)
}, value)
}
}
// ES6 箭头函数实现方案
const compose = (...func) =>
value => func.reverse().reduce((acc, fn) =>fn(acc), value)
函子(Functor)
概念
- 容器:包含值和值的变形关系(函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
用途
- 把函数式编程中的副作用控制在可控的方位内
- 异常处理
- 异步操作
Maybe 函子
- 可以对外部的空值情况做处理(控制副作用在允许范围)
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
// test
let r = MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r)
Either 函子
- 类似于 if...else... 的处理
- 异常会让函数变得不纯,Either 函子可以用来处理异常
class Error {
static of(value) {
return new Error(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return this
}
}
class Right {
static of(value) {
return new Right(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Right.of(fn(this._value))
}
}
// e.g.
function parseJSON(str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Error.of({error: e.message})
}
}
// test
let res1 = parseJSON('{name: Azer}')
let res2 = parseJSON('{"name": "Azer"}')
console.log(res1)
console.log(res2)
IO 函子
- 函子中的 _value 是一个函数,这里是把函数作为值来处理
- 可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class IO {
static of(value) {
return new IO(function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
// test
let res = IO.of(process).map(p => p.execPath)
console.log(res._value())
Task 函子
const fs = require('fs')
const {task} = require('folktale/concurrency/task')
const {split, find} = require('lodash/fp')
function readFile(fileName) {
return task(resolver => {
fs.readFile(fileName,
'utf-8',
(err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
// test
readFile('package.json') // 返回一个 task 函子
.map(split('\n'))
.map(find(x => x.includes('version')))
.run() // 执行 task 函子
.listen({ // 监听执行状态
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
Monad 函子
- Monad 函子是可以变扁的 Pointed 函子
- 一个函子如果具有 join 和 of 两个方法并遵循一些定律,就是一个 Monad
- 可以用于解决函子嵌套的问题
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of(value) {
return new IO(function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
join() {
return this._value()
}
flatMap(fn) {
return this.map(fn).join()
}
}
// test
let readFile = (fileName) => {
return new IO(() => {
return fs.readFileSync((fileName, 'utf-8'))
})
}
let print = (x) => {
return new IO(() => {
console.log(x)
return x
})
}
let res = readFile('package.json')
.flatMap(print)
.join()
console.log(res)
总结:
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 可以把函子想象成一个盒子,盒子中封装了一个值
- 想要处理盒子中的值,需要给盒子提供的 map 方法传递一个用于处理这个值得函数(纯函数),有这个函数来对值进行处理
- 最终, map 方法返回一个包含新值的盒子(函子)
folktate
- 一个标准的函数式编程库
- 提供了一些函数式处理的操作,如 compose、curry等
const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')
let test = curry(2, (x, y) => {
return x + y
})
console.log(test(1, 2))
console.log(test(1)(2))
let test2 = compose(toUpper, first)
console.log(test2(['one', 'two']))