函数式编程(Functional Programming, FP)是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。 就是对运算过程进行抽象,函数式编程中函数指的不是程序中的函数(方法),而是数学中的函数即映射关系。
// 非函数式编程
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(1, 2)
console.log(sum)
函数是“第一等公民(first class)”
"第一等公民"指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
const fn = () => {
console.log('我可以存储在变量中')
}
fn()
// 示例
const obj = {
index (posts) { return Views.index(posts) }
}
// 优化
const obj = {
index: Views.index
}
高阶函数(Higher-order function)
什么是高阶函数
// forEach
const forEach = (array, fn) => {
for (let i = 0; i < array.length; i ++) {
fn(array[i])
}
}
let arr = [1, 2, 3, 4, 5]
forEach(arr, i => { console.log(i) })
const once = fn => {
let flag = false
// 注意:这里返回的函数如果写成箭头函数的话,会找不到 this
return function () {
if (!flag) {
flag = true
return fn.apply(this, arguments)
}
}
}
const pay = once(money => { console.log(`支付了${money}元`) })
pay(5)
pay(5)
pay(5)
使用高阶函数的意义
闭包(Closure)
函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
// 案例
function makePower (power) {
return function (number) {
return Math.pow(number, power)
}
}
// 优化写法
const makePower = power => num => Math.pow(num, power)
let power2 = makePower(2) // 求平方
let power3 = makePower(3) // 求立方
console.log(power2(2))
console.log(power2(3))
console.log(power3(2))
纯函数
let arr = [1, 2, 3, 4, 5]
// 纯函数
console.log(arr.slice(0, 3)) // [1, 2, 3]
console.log(arr.slice(0, 3)) // [1, 2, 3]
// 不纯函数
console.log(arr.splice(0, 3)) // [1, 2, 3]
console.log(arr.splice(0, 3)) // [4, 5]
console.log(arr.splice(0, 3)) // []
惰性计算
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。
不修改状态
函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。
没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
因为 FP 语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。
lodash(中文官网、英文官网)是一个一致性、模块化、高性能的 JavaScript 实用工具库
npm install lodash
const _ = require('lodash')
const array = ['red', 'green', 'yellow', 'pink']
console.log(_.first(array)) // red
console.log(_.last(array)) // pink
console.log(_.toUpper(_.first(array))) // RED
console.log(_.reverse(array) // ['pink', 'yellow', 'green', 'red']
_.each(array, (v, i) => {
console.log(v, i) // red 0 green 1 yellow 2 pink 3
})
// 记忆函数
const _ = require('lodash')
function getArea (r) {
console.log(r, '我只会执行一次,因为我被缓存起来了')
return Math.PI * r * r
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 模拟实现
function myMemoize(fn) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
Web Worker
)在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
// 简单的举例
const add = (a, b) => a + b
// Currying 后
const curryingAdd = a => b => a + b
add(1, 2) // 3
curryingAdd(1)(2) // 3
由此可以看出,柯里化一个函数,就是把原可以接收多个参数的函数变换成只接收单一参数,并用返回的函数接收余下的参数且返回结果的技术。
// 例:判断年龄基准值
function checkAge (min, age) {
return age >= min
}
// Currying 后
function checkAge (min) {
return function (age) {
return age >= min
}
}
// ES6 写法
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(16) // false
checkAge18(20) // true
checkAge20(18) // false
checkAge20(22) // true
// 模拟实现 lodash 中的 curry 方法 (传入一个函数,把该函数转换为柯里化函数)
const curry = fn => {
return function curriedFn(...args) {
if (args.length < fn.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
const getSum = (a, b, c) => (a + b + c)
const curried = curry(getSum)
curried(1, 2, 3) // 6
curried(1, 2)(3) // 6
curried(1)(2, 3) // 6
curried(1)(2)(3) // 6
总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
这是一种对函数参数的‘缓存’
让函数变得更灵活,让函数的粒度更小
可以把多元函数转换成一元函数,可以组合使用,产生强大的功能
面试题:
// 实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) = 6;
// add(1, 2, 3)(4) = 10;
// add(1)(2)(3)(4)(5) = 15;
const add = (...rest) => {
const fn = (...args) => {
rest.push(...args)
return fn
}
fn.toString = () => rest.reduce((total, num) => total + num, 0)
return fn
}
console.log(+add(1)(2)(3)) // 6
console.log(+add(1, 2, 3)(4)) // 10
console.log(+add(1)(2)(3)(4)(5)) // 15
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间的过程函数合并成一个函数
函数组合默认是从右到左执行
// 实现一个组合函数
function compose (...args) {
return function (value) {
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
// ES6改造一下
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
// 测试
const fn1 = arr => arr.reverse()
const fn2 = arr => arr[0]
const fn3 = str => str.toUpperCase()
const f = compose(fn3, fn2, fn1)
// const f = compose(compose(fn3, fn2), fn1)
// const f = compose(fn3, compose(fn2, fn1))
console.log(f(['one', 'two', 'three'])) // THREE
我们既可以把 a
和 b
组合,还可以把 b
和 c
组合,结果都是一样的
// 结合律
let f = compose(a, b, c)
compose(compose(a, b), c) == compose(a, compose(b, c))
// true
const _ = require('lodash')
// 需要写一个辅助函数做调试
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// 将此辅助函数插入到组合函数中即可
compose(a, trace('我在b之后'), b, trace('我在c之后'), c)
Point Free模式
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
案例演示
// Hello World => hello_world
// 非 Point Free 模式
function f (str) {
return str.toLowerCase().replace(/\s+/g, '_')
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World')) // hello_world
什么是Functor
?
map
方法,map
方法可以i运行一个函数对值进行处理(变形关系)总结
map
契约的对象map
方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理map
方法返回一个包含新值的盒子(函子)class Container {
constructor (value) {
this._value = value
}
map (fn) {
return new Container(fn(this._value))
}
}
new Container(5)
.map(v => v + 1) // Container { _value: 6 }
.map(v => v * v) // Container { _value: 36 }
// ============改============造============>
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
Container.of(5)
.map(v => v + 1) // Container { _value: 6 }
.map(v => v * v) // Container { _value: 36 }
处理空值情况
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(this._value) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
MayBe.of(null)
.map(v => v * v) // MayBe { _value: null }
class Left {
static of (value) {
return new Left(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))
}
}
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
parseJSON('{ name: xxx }') // Left { _value: {error: 'Unexpected token n in JSON at position 2'} }
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))
}
}
let r = IO.of(process).map(p => p.execPath) // IO { _value: [Function] }
r._value() // C:\xxx\xxx\xxx\xxx
异步执行
folktale
中的Task
来演示folktale
一个标准的函数式编程库
lodash
、ramda
不同的是,没有提供很多功能函数const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
let f = curry(2, (x, y) => {
return x + y
})
f(1, 2) // 3
f(1)(2) // 3
let fn = compose(toUpper, first)
fn(['one', 'two']) // ONE
Task处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('xxx.xx') // 查找某一文件
.map(fn) // 可以对文件进行某些处理
.map(fn) // 可以对文件进行某些处理(可以进行多次处理)
.run() // 执行
.listen({ // 监听
onRejected: err => console.log(err), // 失败
onResolved: value => console.log(value) // 成功
})
class Container {
static of (value) {
return new Container(value)
}
...
}
Container.of(6).map(v => v * v)
const fs = require('fs')
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))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO(function() {
console.log(x)
return x
})
}
readFile('xxxx.xx').faltMap(print).join()