文章输出主要来源:拉勾大前端高新训练营(链接)。
啊啊啊,没错,我又加入了拉勾的前端大课,这毕业一年,天天买课,钱也攒不下,那就都花了叭......以后再赚回来
1. 对前端来说函数式的优势
- React16.8+ 与 vue3 两大框架拥抱函数式编程,为我们展示了函数式在实际工程方面的一种应用形式,使得函数式在前端领域形成一股潮流势力
- 函数式编程使得代码单元相比面向对象来说更加独立,在tree shaking过程中更便于打包工具对未使用的代码进行过滤,对项目体积有一定的优化。
- 函数式编程以函数为代码单元,相比于面向对象的方式减少了对
this的控制,对于js这种this指向可能会发生改变的语言,一定程度上减轻了开发人员对于this指向问题的困扰。 - 函数式有利于单元测试。
- JavaScript天生对函数式友好,社区很多库可以帮助我们进行函数式开发:
lodash,underscore,ramda
函数式与面向对象的讨论
函数式与面向对象作为两种不同的开发风格,虽然奉行的理念有所差异,但两者并非非黑即白、有你无我的存在,在适合的场景可以使用更为适合的技术,两者也可以共同使用,相互取长补短。
2. 函数式编程(Functional Programming, FP)概念
函数式编程与面向对象、面向过程等方式都是使用代码解决现实问题的形式。
面向对象对现实中的类和事物进行抽象,通过封装、继承、多态表示事物之间的联系。在面向对象的世界中,一切皆对象,js作为一种支持面向对象的语言,当然也可以将一切作为对象,但在实际开发中,面向对象有时候并不是全能的,在tree shaking过程中,无用的方法不能被有效判断出来,从而无法达到最佳的无用代码优化效果。
函数式编程的思维方式:
- 函数式编程是对运算过程的抽象。
- 函数式编程中的函数并非程序中的函数或方法,而是数学中的函数映射关系。
- 纯函数:相同的输入始终会得到相同的输出。
3. 函数是一等公民
JavaScript中的函数是一等公民,它作为跟其他数据同等的地位出现在任何地方.
- 函数可以向数字一样存储在变量中
- 函数可以被存储在数组的插槽中
- 函数可以被赋值给对象的字段
- 函数可以根据需要来进行创建
- 函数可以被传递到其他函数中
- 函数可以作为另外其他函数的返回值返回
4. 高阶函数(Higher-order function)
定义:
- 函数作为参数被传递到另外一个函数中
- 函数可以作为另一个函数的返回值被返回
**意义:**抽象通用问题,只需关注实现目标
常用场景:
- 作为参数:钩子函数
- 作为返回值:对函数执行进行预处理
##5. 闭包(Closure)
个人理解
闭包是函数作用域的产物,说起闭包便要先讨论函数作用域,js拥有函数作用域,内层函数可以使用外层作用域中的变量。
外层函数中定义变量在内层函数中进行使用,当调用完外层函数后,函数生命周期结束,但是其作用域定义的变量由于被秦勇,生命周期仍未结束,会对之前的值进行保存,调用内层函数时就可以访问之前外层作用域中定义的变量的值。
闭包的本质
函数执行完毕后会从执行栈中移除,其内部定义的变量如果此时还被外部所引用,则堆上的作用域成员由于外部引用不能被释放,因此依然存在内存中,内部函数也因此可以访问外部函数定义的成员。
闭包示例
// 1. 明确定义
function example1() {
const hello = 'hello'; // 外层函数作用域变量
return function(arg) {
return `${hello} ${arg}`;
}
}
const helloSombody = exapmle1(); // 此时hello变量未被销毁,即闭包
// 2. 通过参数隐式定义外层函数作用域参数
function example2(firstWord) { // firstWord为外层函数作用域变量
return function(secondWord) {
return `${firstWord} ${secondWord}`
}
}
const hiSomething = example('hi'); // firstWord值为hi,被保存在内存中未销毁,即闭包
6. 纯函数
概念:
- 介绍过只能从它的参数的值来计算
- 不能依赖于被外部操作改变的数据
- 不能改变外部状态
特性:相同的输入永远会得到相同的输出,且没有可观察的副作用。 <===> 对应数学中的函数 y = f(x)
例:
array.slice:不改变数组,相同输入有相同输出 ---> 纯函数array.splice: 改变数组,相同输入输出不同 ---> 非纯函数
函数式 编程不会保留计算的中间结果,变量是不可变的。
可以将一个函数执行结果交给另外一个函数处理。
纯函数优势:
- 可缓存: 相同函数有相同输出,可将函数结果缓存,在参数未发生更改时可以直接读取缓存,提高访问速度(空间换时间)。
- 方便进行单元测试
- 并能处理能力:纯函数不需要访问共享内存数据,在并行环境可任意运行纯函数。
7. 副作用
副作用会让一个纯函数变得不纯,副作用函数会依赖一些可变的变量等,无法保证相同的输入永远得到相同的输出。
副作用的来源:
- 配置文件
- 数据库
- 用户输入
- 其他外部状态...
副作用会使得方法通用性下降,不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
8. 柯里化
个人理解
利用闭包保存变量,返回函数的方式,将本需要传入的多个参数分为多次进行传递。返回的函数可以接收剩余指定的参数(看分为多少层,每层规定了多少个参数)
柯里化(currying):
当函数又多个参数时候,先传递一部分参数调用它,这部分参数之后不变。然后返回一个新的函数接收剩余参数。
简单的通用柯里化可以如下定义
function curry(fn) {
let length = fn.length;
return function curriedFn(...args) {
if (args.length < length) {
return function(...laterArgs) {
return curriedFn(...args.concat(laterArgs))
}
}
return fn(...args);
}
}
补充:偏函数
柯里化是将一个多元函数转换为多个m元函数,用于延迟计算。
偏函数是柯里化的一种特殊应用,偏函数是固定x个函数,生成另一个函数,调用时传入其他需要的参数。
偏函数可以如下定义:
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
let allArgs = presetArgs.concat(laterArgs);
return fn(...allArgs);
}
}
柯里化与偏函数的主要用途:
- 动态生成函数
- 减少参数
- 延迟计算
9. 函数组合
函数组合解决的问题:
纯函数与柯里化导致了各种小函数在使用的时候可能需要嵌套调用,如获取数组的最后一个元素再转换为大写字母,代码可能为_.toUpper(_.first(_.reverse(array))),这样的嵌套结构类似洋葱结构,写法复杂,函数组合就是解决这种嵌套调用的写法问题。
compose
使用: const resFn = compose(fn1, fn2); 组合完后,函数从右向左一次调用,前面的代码会先执行fn2,再执行fn1
lodash中对应的方法有_.flowRight()从右向左执行, _.flow()会从左向右执行
函数组合要满足结合律
如compose(compose(fn1, fn2), fn3) == compose(fn1, compose(fn2, fn3))
10. Point Free
Point Free: 将数据的处理过程定义为与数据无关的合成运算,不需要用到代表数据的那个参数
,只要把简单的运算步骤合成到一起,在使用这种模式前需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
11. 函子(Functor)
Functor:
- 容器: 包含值和值的变形关系(即函数)
- 函子: 是个特殊容器,通过普通对象实现,该对象具有map方法与一个值(永远不对外公布),map方法接收一个函数,可以对容器中的值进行操作,然后返回一个新的函子
例:
class Container {
constructor(value) {
this._value = value;
}
map(fn) {
return new Container(fn(this._value))
}
}
const res = new Container(5)
.map(x => x + 1)
.map(x => x * x)
console.log(res); // Container { _value: 36 }
总结:
- 函数式编程运算不直接操作值,而由函子完成
- 函子是一个实现了map契约的对象
- 可以将函子想象为一个盒子,里面封装了一个值
- 想要处理值,只能给盒子中的map传递一个处理值的函数(纯函数),由这个函数对值进行处理
- map方法返回一个包含新值的函子
11.1 MayBe函子:处理传入值为null或undefined的情况
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;
}
}
11.2 Either函子:用来做异常处理。
Either两者中的一个,类似if...else逻辑
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 (exp) {
return Left.of({ error: exp.message })
}
}
11.3 IO函子
- IO函子的_value是一个函数
- IO函子可以将不纯的动作存储到
_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));
}
}
let res = IO.of(process).map(p => p.execPath)
let res1 = IO.of(process).map(p => p.execPath).map(path => 'path is ' + path);
console.log(res)
console.log(res._value())
console.log(res1._value())
11.4 Task函子(folktale库提供的处理异步任务的函子)
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);
})
})
}
readFile('./package.json')
.map(split('\n'))
.map(find(item => item.includes('version')))
.run()
.listen({
onRejected: (err) => {
console.error(err)
},
onResolved: data => {
console.log(data);
}
})
11.5 Pointed函子:实现了of静态方法的函子
of方法可以避免使用new创建对象,深层含义为of方法可以用来将值放到上下文Context中,便于map方法处理
11.6 Monad(单子)函子:是可以变扁的Pointed函子
一盒函子如果具有join和of两个方法并遵守一些定律就是一个Monad
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();
}
}
const readFile = function(filename) {
return new IO(() => {
return fs.readFileSync(filename, 'utf-8');
})
}
const print = function(x) {
return new IO(() => {
console.log(x);
return x;
})
}
readFile('./package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
总结:
- 具有静态of方法,且具有join方法的函子
- 当一个函数返回一个函子,就需要想到使用Monad
- Monad解决了函子嵌套的问题
- 当需要合并一个函数,且函数返回一个值,需要使用map方法
- 当需要合并一个函数,且函数返回一个函子,需要使用flatMap方法