函数式编程是一种编程范式,和面向对象编程呈并列关系。
- 面向对象编程:对现实世界中事物的抽象,抽象出对象以及对象和对象之间的关系;
- 函数式编程:把现实世界事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)。
推荐书籍
- 你不知道Javascript
- Javascript忍者秘籍
- Javascript20years
函数是一等公民
- 函数可以存储在变量中;
- 函数作为参数;
- 函数作为返回值。
原理:在js中函数的数据类型为对象。
高阶函数
可以把函数作为参数/返回值。
意义:屏蔽细节,便于抽象。
常用的高阶函数:
- forEach
- map
- filter
- every:检测数组中的所有元素是否符合条件,有一个不符合返回false
- some:检测数组中是否有函数符合条件,有一个符合返回true
- find/findIndex:返回数组中满足条件的第一个元素的值/索引,没有则返回undefined、-1
- reduce:接收一个函数作为累加器,数组中的值从左到右,上一个输出作为下一次迭代的输入,最后返回一个值。可以作为一个高阶函数用于函数组合
- sort
this指向
改变函数this指向对方法:bind(不调用)、call、apply
- 模拟bind实现
// 模拟bind实现
Function.prototype.myBind = function (context, ...args) {
return (...rest) => this.call(context, ...args, ...rest)
}
执行上下文
全局执行上下文
-
函数级执行上下文
函数的执行阶段可以分为:
1.函数建立阶段:当调用函数时,还没有执行函数内部的代码- variableObject(VO):收集函数中的arguments、参数、内部成员;
- scopeChains:词法环境,作用域链,记录当前函数所在父级作用域中的活动对象;[[Scopes]]作用域链,函数在创建时就会生成该属性,js引擎才可以访问,这个属性中存储的是所有父级中的变量对象。
- this:当前函数内部的this指向,this的指向是动态确定的,当函数在调用时才能确定。
2.函数执行阶段
- activationObject(AO):用AO指向VO
eval执行上下文
闭包
能够读取其他函数内部变量的函数。
本质:函数在执行时会被放入执行栈,当函数执行完毕会从执行栈上移除,但堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
作用:
1.可以读取函数内部的变量;
2.让这些变量的值始终保持在内存中。
使用注意点:
1.由于闭包会使函数中的变量都被保存在内存中,因此滥用闭包会造成性能问题。解决方法:在退出函数之前,删除所有无用的变量。
2.不要随意修改父函数中变量的值。
// once
function once(fn) {
let done = false
return function () {
if (!done) {
done = true
// arguments:类数组对象,存储传入函数的所有参数。具有数组的部分属性,比如.length、索引
// apply(obj, args):
// 劫持另一个对象的方法,继承另一个对象的属性,obj代替function中的this对象,args作为参数传递给函数args-->arguments
return fn.apply(this, arguments)
}
}
}
纯函数(⚠️重点掌握)
相同的输入永远会得到相同的输出,类似于数学中的函数关系,没有任何可观察的副作用(副作用不可能完全禁止)。
纯函数的好处
- 可缓存
// 模拟memorize
function memorize(fn) {
let cache = {}
return function () {
let arg_str = JSON.stringify(arguments)
cache[arg_str] = cache[arg_str] || fn.apply(fn, arguments)
return cache[arg_str]
}
}
- 可测试
- 并行处理
柯里化(⚠️重点掌握)
当一个函数有多个参数时先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个参数接收剩余的参数,返回结果。
对函数进行降维,为函数组合作准备。
lodash中的柯里化函数
// 模拟_.curry()的实现
function curry(fn) {
// ...args-剩余参数:将不定量的参数表示为一个数组
// 剩余参数和arguments的区别:
// 1.剩余参数只包含那些没有对应形参的实参,而arguments包含传给函数的所有实参;
// 2.arguments对象不是一个真正的数组,而剩余参数是一个真正的Array实例;
// 3.arguments还有一些剩余的属性。
return function curriedFn(...args) {
// 判断实参和形参的个数
// function.length -> 函数形参的个数
if (args.length < fn.length) {
return curriedFn(...args.concat(Array.from(arguments)))
}
return fn(...args) // args展开
}
}
函数组合(⚠️重点掌握)
如果一个函数要经过多个函数处理才能得到最终值,可以把中间过程的函数组合为一个函数。顺序默认从右向左执行。
// 模拟lodash中的flowRight
function compose(...fns) {
return function (value) {
return fns.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value) // value为初始值
}
}
// 使用钩子函数
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)
如何调试函数组合
可以编写一个接收“当前位置标记”和value并返回value的柯里化函数,插入到函数组合中,用于追踪哪一步出错。
// 'NEVER SAY DIE' --> 'never-say-die'
// 调试
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, arr) => _.join(arr, sep))
const map = _.curry((fn, arr) => _.map(arr, fn))
// 错误
const f = _.flowRight(join('-'), trace('map之后'), _.toLower, trace('map之前'), split(' '))
// 正确
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('map之前'), split(' '))
console.log('NEVER SAY DIE');
lodash/fp
提供了对函数式编程友好的方法,提供了不可变自动柯里化、函数优先、数据滞后的方法。
Functor(函子)
函子就是一个装有一个变量的容器,通过map方法维护这个变量。
函子在开发中的实际应用场景:作用是空值副作用(IO)、异常处理(Either)、异步任务(Task)。
// Functor 函子
// 函数式的编程不直接操作值,而是由函子完成
class Container{
// of静态方法,可以省略new关键字创建对象
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
// map方法,传入处理value的函数,返回一个包含新值的函子
map(fn) {
return Container.of(fn(this._value))
}
}
MayBe函子
// MayBe 函子
// 处理空置异常
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return MayBe.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing() {
return this._value === null || this._value === undefined
}
}
Either函子
// Either函子
// 传入空值调用的函子
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))
}
}
// Either用来处理异常
function parseJSON(json) {
try {
return Right.of(JSON.parse(json))
} catch (e) {
return Left.of({ error: e.message })
}
}
IO函子
IO函子中的_value是一个函数,可以把不纯的动作存储到_value中,延迟这个不纯的操作。
const fp = require('lodash/fp')
// IO函子
class IO {
static of(value) {
// 给当前传入的value包裹一层函数,让不纯的操作滞后发生,保证当前函数相同输入得到相同输出。
// 将不纯的操作交给调用者处理。
return new IO(function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
Task异步执行
// folktale中的task函子,处理异步执行
function readFile(filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
// 调用run执行
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
Pointed函子
是实现了of静态方法的函子,更深层的意义是将值放到上下文Context(把值放到容器中,使用map处理值)。
Monad(单子)
是可以变扁的Pointed函子,具有join和of两个方法,能够解决IO函子中出现的多层嵌套问题。
const fp = require('lodash/fp')
// IO Monad
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()
}
// 拍平map
flatMap(fn) {
return this.map(fn).join()
}
}