函数式编程

思维导图

函数式编程的优点

  • React和Vue3使用函数式编程

  • 函数式编程可以抛弃this

  • 打包过程中可以更好的利用tree shaking过滤无用代码

  • 方便测试、方便并行处理

  • 可以对函数进行最大程度的被重用

  • 有函数式开发的库:lodash、underscore、ramda

函数的执行上下文

执行上下文(Execution Context)

  • 全局执行上下文

  • 函数级执行上下文

  • eval执行上下文

函数执行的阶段可以分文两个:函数建立阶段、函数执行阶段

函数建立阶段:当调用函数时,还没有执行函数内部的代码

创建执行上下文对象

fn.ExecutionContext = {
     variableObject: //函数arguments、参数、局部成员
     scopeChains://当前函数所在的父级作用域中的活动对象
     this:{}//当前函数内部this的指向        不同调用对象this指向不同
}

函数执行阶段

//变量对象转换为活动对象
fn.ExecutionContext = {
     activationObject: //函数arguments、参数、局部成员
     scopeChains://当前函数所在的父级作用域中的活动对象
     this:{}//当前函数内部this的指向        不同调用对象this指向不同
}

[[Scopes]]作用域链,函数在创建时就会生成该属性, js引擎才可以访问。这个属性中存储的是所有父级中的变量对象。[[]]不能访问,表示内部管理的

function fn (a, b) {
       function inner () {
            console.log(a, b);
       }
       console.dir(inner);
  }
console.dir(fn);
const f = fn(1, 2);

  • FunctionLocation 是函数源码定义的位置
  • Scopes作用域

闭包(Closure)

定义:
  • 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

作用:
  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

  • 延长了外部函数成员的作用范围和生命周期

本质:
  • 函数在执行的时候会到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员变量

发生闭包的两个必要条件

  1. 外部对一个函数 makeFn内部有引用

  2. 在另一个作用域能够访问到makeFn 作用域内部的局部成员

使用闭包可以突破变量作用域的限制,原来只能从一个作用域访问外部作用域的成员有了闭包之后,可以在外部作用域访问一个内部作用域的成员

可以缓存参数

根据不同参数生成不同功能的函数

纯函数

概念:
  • 相同的输入永远会得到相同的愉出,而且没有任何可观察的副作用

  • y=f(x):纯函数就类似数学中的函数(用来描述输入和输出之间的关系)

  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)

  • 可以把一个函数的执行结果交给另一个函数去处理

数组的 slice和splice分别是:纯函数和不纯的函数

  • slice返回数组中的指定部分,不会改变原数组

  • splice对数组进行操作返回该数组,会改变原数组

纯函数的好处

  • 可缓存:纯函数对相同的输入始终有相同的结果,可以把纯函数的结果缓存起来

  • 可测试:纯函数让测试更方便

  • 并行处理:一般在多线程环境下并行操作共享的内存数据很可能会出现意外情况,但纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)

副作用:
  • 影响:让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

  • 来源:配置文件 数据库 获取用户输入

所有的外部交互都有可能代理副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化 (Haskell Brooks Curry)

  • 函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),返回一个新的函数接收剩余的参数,返回结果

lodash中的柯里化函数

-_.curry(func) func参数:需要柯里化的函数 返回值:柯里化后的函数

功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。

  1. 先创建纯函数

  2. 传入_.curry传入函数

总结

  • 函数参数的缓存:柯里化可以让一个函数传递较少的参数返回一个已经记住了某些固定参数的新函数

  • 使函数变的更灵活,让函数的粒度更小

  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合(compose)

  • 定义:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

  • 作用:纯函数和柯里化很容易写出洋葱代码,一个函数套一个函数,函数组合可以避免这种情况,可以把细粒度的函数重新组合生成另一个函数

  • 要求:函数进行组合时要满足柯里化的形式才可以进行组合

当函数要实现比较复杂的功能时,可以实现一个管道函数fn,把复杂函数拆成f1、f2、f3三个小模块,将模块传入fn管道函数,实现一个组合f1、f2、f3的复杂功能函数

fn = compose(f1,f2,f3)
b = f(a)

函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果

lodash中组合多个函数

函数组合默认是从右到左执行

  • lodash中的组合函数

  • lodash中组合函数flov()或者flowRight(),他们都可以组合多个函数- flow()是从左到右运行

  • flowRight()是从右到左运行,使用的更多一些

函数的组合要满足结合律(associativity):既可以把g和h组合,还可以把f和g组合,结果都是一样的

//结合律(associativity)
let f = compose(f, g,h)
let associative = compose(compose(f, g), h) == compose(f,compose(g.h)) //true

组合函数的调试 可以通过写一个辅助函数,来查看每个函数处理的结果

function log(v){
     console.log(v);
     return v;
}
const splitC = _.curry((sep, str) => _.split(str,sep));
const joinC = _.curry((sep, arr) => _.join(arr, sep));
const mapC = _.curry((fn, arr) => _.map(arr,fn));
​
const strUpperToLower = _.flowRight(joinC('-'),log,mapC(_.toLower),splitC(' '));
const arrUpperTolower = _.flowRight(joinC('-'),log,mapC(_.toLower));
console.log(strUpperToLower('HELLO WORLD THIS NEW YEAR')); //hello-world-this-new-year
console.log(arrUpperTolower(arrUpperStr));//hello-world-,-test-domostrate

lodash/fp

  • lodash 的模块提供了实用的对函数式编程友好的方法

  • 提供了不可变auto-curried iteratee-first data-last的方法(已柯里化)

  • 函数优先,数据之后

Point Free编码风格:

定义:把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据

  • 只需要合成运算过程

  • 需要定义—些辅助的基本运算函数

const strUpperToLowerFP = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));

函子

作用:把副作用控制在可控的范围内、异常处理、异步操作等。先把副作用延迟到最后调用的时候,不进行调用时没有副作用,有副作用时,调用函数需要先进行处理副作用。

纯函数和柯里化的缺点:编程的过程中可能会遇到很多错误,没有对错误做相应的处理

Functor函子

容器:包含值和值的变形关系(这个变形关系就是函数)

函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

步骤

1、静态new类的方法

2、构造器,初始值

3、处理值的闭包

总结

  • 函数式编程的运算不直接操作值,而是由函子完成

  • 函子就是一个实现了 map契约的对象,我们可以把函子想象成一个盒子,这个盒子里封装了一个值

  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理

  • 最终map方法返回一个包含新值的盒子(函子)【链式基础】

MayBe函子

  • MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)

  • 优点:解决纯函子的空值问题,对空值进行处理

  • 缺点:在进行值处理时,不能判断在哪个处理过程出现null值

Either函子

  • Either 两者中的任何一个,类似于if.. .else..的处理

  • 异常会让函数变的不纯,Either 函子可以用来做异常处理

  • 优点: 解决MayBe函子不能在哪个处理阶段出现异常的问题

IO函子

  • IO函子中的value是一个函数,这里是把函数作为值来处理

  • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯

  • 把不纯的操作交给调用者来处理

Task异步执行

异步任务的实现过于复杂,我们使用folkiale中的Task来演示

. folkale 一个标准的函数式编程库

.和lodash. ramda 不同的是,他没有提供很多功能函数

.只提供了一些函数式处理的操作,例如: compose. curry等, -些函子Task. Either. MayBe 等

. folktal(2.3.2) 2.x中的Task和1.0中的Task区别很大,1.0 中的用法更接近我们现在演示的函子

Pointed函子

Pointed函子是实现了of静态方法的函子

.of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context (把值放到容器中, 使用map来处理值)

Monad函子

Monad 函子是可以变扁的Pointed函子IO(IO(x))

一个函子如果具有join和of两个方法并遵守这些定律就是一个Monad

Monad可以解决函子嵌套的问题

 

事件循环(Event loops)

每个代理都是由事件循环驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。

在浏览器执行事件的时候,事件循环会依次轮询三个任务:

  • 调用顺序:由上往下依次调用

  • 任务执行顺序:Callstack -> Micro Queue(微任务) -> Macro Queue(宏任务)

伪代码
//事件循环,主线程
while (macroQueue.waitForMessage()){
     // 1.执行完调用栈上当前的宏任务(同步任务)
     // call stack
     // 2.遍历微任务队列,把微任务队里上的所有任务都执行完毕(清空微任务队列)
     //微任务又可以往微任务队列中添加微任务
     for (let i = 0; i < microQueue. length; i++){
          //获取并执行下一个微任务(先进先出)
          microQueue[i].processNextMessage();
     }
     //3.渲染(渲染线程)
     //4.从宏任务队列中取一个任务,进入下一个消息循环
     macroQueue.processNextvessage();
}

任务vs微任务

一个任务就是指计划由标准机制来执行的任何JavaScript,如程序的初始化、事件触发的回调等。除了使用事件,你还可以使用setTimeout()或者setInterval()来添加任务。

任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.

  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行—即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

产生宏任务的方式

  • script 中的代码块

  • setTimeout()

  • setInterval()

  • setlmmediate()(非标准,IE和Node.js 中支持)

  • 注册事件

产生微任务的方式

  • Promise 处理异步回调时才使用,基于微任务实现的

  • MutationObserver

  • queueMicrotask()

何时使用微任务

微任务的执行时机,晚于当前本轮事件循环的Call Stack(调用栈)中的代码(宏任务),早于事件处理函数和定时器的回调函数。

使用微任务的最主要原因简单归纳为:

  • 减少操作中用户可感知到的延迟

  • 确保任务顺序的一致性,即便当结果或数据是同步可用的

  • 批量操作的优化

确保任务顺序的一致性

customElement.prototype.getData = url => {
    //判断缓存有无数据
    if (this.cache[url]) {
        this.data = this.cache[ur1];
        this.dispatchEvent(new Event("load"));
    } else { //如果没有,发动异步请求获取事件
        fetch(ur1)
            .then(result => result.arrayBuffer())
            .then(data => {
                    this.cache[ur1] = data;
                    this.data = data;
                    this.dispatchEvent(new Event("load"));
            })
    };
};
element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data. ..");
element.getData();
console.log("Data fetched");
上面代码数据未缓存和已缓存会出现任务执行顺序不一致的问题

加入微任务可确保任务的执行顺序一致
customElement.prototype.getData = url => {
     if (this.cache[ur1]){
          queueMicrotask(()=>{
          this.data = this.cache[url];
          this.dispatchEvent( new Event("load"));
});
}else {
     fetch(url).then(result => result.arrayBuffer()).then(data => {
          this.cache[url] = data;
          this.data = data;
          this.dispatchEvent( new Event( "load"));
     };
}
};
批量操作
let messageQueue = []
let sendMessage = message => {
    messageQueue.push(message)
    if (messageQueue.length === 1) {
        queueMicrotask(() => {
            const json = JSON.stringify(messageQueue);
            messageQueue.length = 0;
            // fetch( "ur1-of-receiver", json);
            console.log(json)
        });
    }
};
queueMicrotask(() => {
    console.log("queueMicrotask ")
})
sendMessage('刘备')
sendMessage('关羽')
sendMessage("曹操")


posted @ 2022-07-02 21:42  airspace  阅读(72)  评论(0编辑  收藏  举报