函数式编程、异步编程、promise

函数式编程、异步编程、promise

  • 函数式编程(functional programming,FP)
    • 编程范式
    • 函数是一等公民
    • 高阶函数(Higher-order function)
      • 什么是高阶函数?
      • 使用高阶函数的意义
      • 常用的高阶函数
    • 闭包(closure)
    • 纯函数
      • 纯函数的概念
      • 纯函数的好处
    • 副作用
    • lodash
      • lodash中的fp模块
      • lodash和fp的map方法的区别
    • 函数柯里化(curry——高阶函数)
      • lodash中的柯里化方法:_curry(function)
      • 模拟lodash中的curry方法
        • 形参和实参的区别
        • 剩余参数
        • 剩余参数和 arguments对象的区别
      • 函数柯里化的好处
    • 函数组合
      • lodash中的组合函数
      • 模拟lodash中的compose
      • 函数组合要满足结合律
      • 函数组合调试
    • Functor(函子)
      • point free 模式(函数组合)
      • Pointed函子
      • MayBe 函子
      • Either函子:if-else
      • IO函子——输入-输出
      • Monad 函子-单子
      • folktale库
  • JavaScript异步编程
    • JavaScript是单线程语言
    • 同步模式和异步模式
    • 调用栈、消息队列、EventLoop(事件循环)
    • 可以实现JavaScript异步编程的方法
  • promise
    • promise对象有以下特点
    • promise各个api的作用
      • catch和then第二个参数的优先级
    • Pomise.all的使用
    • Promise.race的使用
    • promise的执行时序
      • 宏任务和微任务
      • 执行时序
  • Generator 生成器函数
    • next 方法和yield 语句的作用
    • Generator捕获异常
    • Async/Await

函数式编程(functional programming,FP)

编程范式

  1. 函数式编程的思维方式:把现实中的事物和事物之间的***联系***抽象到程序世界对运算过程进行抽象
    • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即数据(函数)之间的映射关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程大量的使用闭包,所以降低了性能(内存占用)
  2. 面向对象编程的思维方式:把现实中的事物抽象成程序世界中的类和对象,通过封装、集成和多态来演示事物事件的联系
  3. 面向过程编程的思维方式:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了

代码示例:

//面向过程编程
var a=2;
var b=4;
var sum=a+b;
console.log(sum);

//函数式编程
function add(a,b){
	return a+b;
}
console.log(add(4,5));

函数是一等公民

JavaScript中函数就是一个普通的对象(可以通过new Function()创建),所以函数有以下三个特性:

  • 函数可以存储在变量中
  • 函数作为参数——高阶函数
  • 函数作为返回值——高阶函数
  1. 函数可以存储在变量中代码示例:
    //把函数赋值给变量
    var fn=function(){
    	console.log('函数表达式');
    }
    fn();
    
    function get_name(name){
    	console.log(`我是${name}`);
    }
    
    //name包裹了get_name且name和get_name的参数和返回值都一样,所以可以看做name和get_name是一样的函数
    var obj={
    	name(name){return get_name(name)},
    }
    console.log(obj.name)
    
    //优化
    var obj1={
    	name:get_name//把函数get_name赋值给函数name
    }
    console.log(obj1.name)
    

高阶函数(Higher-order function)

什么是高阶函数?

  • 可以把函数作为参数传递给另一个函数
  • 可以把函数作为另一个函数的返回值
  1. 函数作为参数代码示例:
    • 模拟数组的foreach方法:用于调用数组的每个元素,并将元素传递给回调函数
      const arr=[1,2,3,4,5];
      /**
       * @description: 数组的foreach方法使用
       */
      arr.forEach(function(item,index){
          console.log(`第${index}项是${item}`);
      })
      
      
      /**
       * @description: 模拟数组的foreach方法
       * @param {*} array 
       * @param {*} fn 
       */
      function forEach(array, fn) {
          for (var i = 0; i < array.length; i++) {
              fn(array[i]);
          }
      }
      
      
      /**
       * 测试forEach
       */
      forEach(arr,function(item){
      	console.log(item)
      })
      
    • 模拟数组的filter方法:创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,不会改变原始数组
      /**
       * @description:数组的filter方法使用
       */
      let even_number=arr.filter(function(item,index){
          return item%2==0;
      })
      console.log(even_number)
      
      
      /**
       * @name:filter
       * @description: 模拟数组的filter方法
       * @param {*} array 
       * @param {*} fn 
       */
      function filter(array,fn){
          var result=[];
          for (var i = 0; i < array.length; i++) {
              if(fn(array[i],i)){
                  result.push(array[i]);
              }
          }
          return result;
      }
      
      
      /**
       * 测试filter
       */
      let res=filter(arr,function(item,index){
      	return item%2===0&&index!=0;
      })
      console.log(res)
      
  2. 函数作为返回值代码示例:模拟lodash中的once函数
    /**
     * @name:once
     * @description: 模拟lodash中的once函数 
     * @param {*} fn 
     */
     function once (fn) {
        var done = false;
        return function (){
            if(!done){
                done = true;
                // 下面这两种写法都可以,就是将arguments展开传递给fn
                // fn(...arguments);
                return fn.apply(this,arguments);
            }
        }
    }
    
    /**
     * 测试once
     */
    var pay = once (function (money){
        console.log(`支付了:${money}元`);
    });
    pay(2);
    pay(3);
    pay(4);
    // 以上的执行只输出了:支付了:2元
    
    
    console.log(pay)//是once函数的返回值(匿名函数)
    // 也可以使用下面的方式执行
    once (function (money){
        console.log(`支付了:${money}元`);
    })(2)
    once (function (money){
        console.log(`支付了:${money}元`);
    })(5)
    
    // 以上的执行只输出了:支付了:2元、支付了:5元
    
    补充:call、bing、apply
    call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象。
    
    第二个参数:
    call的第二个参数是用逗号分隔,直接放到后面,例如:obj.myFun.call(this,'参数一', ... ,'参数n' )。
    
    apply的第二个参数是以数组的形式传入,例如:obj.myFun.apply(this,['参数一', ... ,'参数n'])。
    
    bind除了返回的是函数以外,它的参数传递方式和 call 一样。
    

使用高阶函数的意义

  • 抽象运算过程可以帮我们屏蔽细节,在调用函数的时候只需要关注我们的目标
  • 高阶函数就是用来抽象通用的问题
  • 使用高阶函数可以让函数更加灵活

常用的高阶函数

forEach、map、filter、 every、some、find/findIndex、reduce、sort等
  • 模拟map:返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,不会改变原始数组。
    const arr = [1, 2, 3, 4, 5];
    
    /**
     * @description: 数组的map方法
     */
    let map_res = arr.map(function (item, index) {
        return item + index;
    })
    // console.log(map_res)
    
    /**
     * @name:map
     * @description: 模拟数组的map方法
     * @param {*} array 
     * @param {*} fn 
     */
    
    function map(array, fn) {
        let res = [];
        for (var i = 0; i < array.length; i++) {
            res.push(fn(array[i]));
        }
        return res;
    }
    
    
    /**
     * 测试map
     */
    console.log(map(arr, function (item) {
        return item + 1;
    }))
    
  • 模拟every:用于检测数组所有元素是否都符合指定条件(通过函数提供),不会改变原始数组
    • 如果数组中检测到有一个元素不满足,则整个表达式返回 false,且剩余的元素不会再进行检测。
    • 如果所有元素都满足条件,则返回 true
    const arr = [1, 2, 3, 4, 5];
    /**
     * @description: 数组every方法
     */
    let every_res = arr.every(function (item, index) {
        return item % 2 === 0;
    })
    // console.log(every_res)
    
    /**
     * @name:every
     * @description: 模拟数组的every方法
     * @param {*} array 
     * @param {*} fn 
     */
    function every(array, fn) {
        let res = true;
        for (var i = 0; i < array.length; i++) {
            res = fn(array[i]);
            if (!res) {//只要有一个为false则不再循环,直接返回false
                break;
            }
        }
        return res;
    }
    
    /**
     * 测试every
     */
    console.log(every(arr, function (item) {
        return item >= 3;
    }))
    
  • 模拟some:用于检测数组中的元素是否满足指定条件(通过函数提供),不会改变原始数组。
    • 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
    • 如果没有满足条件的元素,则返回false
    /**
     * @description: 数组some方法
     */
    let some_res = arr.some(function (item, index) {
        return item % 2 === 0;
    })
    console.log(some_res)
    
    /**
     * @name:some
     * @description: 模拟数组的some方法
     * @param {*} array 
     * @param {*} fn 
     */
    function some(array, fn) {
        let res = false;
        for (var i = 0; i < array.length; i++) {
            res = fn(array[i]);
            if (res) {//只要有一个为false则不再循环,直接返回false
                break;
            }
        }
        return res;
    }
    
    /**
     * 测试some
     */
    console.log(some(arr, function (item) {
        return item >= 3;
    }))
    
    
  • 模拟find/findIndexfind返回满足条件的第一个元素值、findIndex返回满足条件的第一个索引值,不会改变原始数组
    • 当数组中的元素在测试条件返回 true 时, find()返回符合条件的元素,之后的值不会再调用执行函数。
    • 如果没有符合条件的元素返回 undefined
    /**
     * @description: 数组find方法
     */
    let find_res = arr.find(function (item, index) {
        return item >= 7;
    })
    // console.log(find_res)////true、undefined
    
    /**
     * @name:find
     * @description: 模拟数组的find方法
     * @param {*} array 
     * @param {*} fn 
     */
    function find(array, fn) {
        let res = undefined;
        let falg=false;
        for (var i = 0; i < array.length; i++) {
            falg = fn(array[i]);
            if (falg) {//只要有一个为true则不再循环,直接返回当前的元素值
                res=array[i];
                break;
            }
        }
        return res;
    }
    
    /**
     * 测试find
     */
    // console.log(find(arr, function (item) {
    //     return item >= 7;
    // }))//true、undefined
    
    console.log(find(arr, function (item) {
        return item >= 3;
    }))//true、3
    

闭包(closure)

闭包:函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包

  • 可以在另一个作用域中调用一个函数的内部函数,并访问到该函数的作用域中的成员
    //函数作为返回值
    function makeFn(){
        let msg='msg';
        return function(){
            console.log(msg);//内部函数访问了外部函数的变量形成了闭包
        }
    }
    
    const fn=makeFn();
    fn();
    
    在上述代码中形成了闭包:
    • 在另一个作用域中可以访问makeFn的内部匿名函数
    • 当调用内部匿名函数的时候可以访问到makeFn作用域中的变量msg
  • 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕后会从执行栈上移除。但是堆上的作用域成员因为被外部引用不能被释放,因此内部函数依然可以访问外部函数的成员。
  • 闭包的核心作用:延长了外部函数内部变量的作用范围

纯函数

纯函数的概念

  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
    • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系)
  • lodash是一个纯函数的功能库、提供了对数组、数字、字符串、函数等操作的一些方法
  • 数组的slicesplice分别是:纯函数和不纯函数
    • slice返回数组中的指定部分,不会改变原数组
    • splice对数组进行操作返回该数组,会改变原数组
      let arr = [1, 2, 3, 4];
      
      // 纯函数
      /**
       * @name:array.slice
       * @description: 用于从已有的数组中返回选定的元素,返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素
       * @param {*} start 
       * start:可选。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推
       * @param {*} end 
       * end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
       */
      console.log(arr.slice(0, 3))//[1,2,3]
      console.log(arr.slice(0, 3))//[1,2,3]
      console.log(arr.slice(0, 3))//[1,2,3]
      console.log(arr.slice(-1, 3))//[]
      console.log(arr.slice(0, -3))//[1]
      
      
      /**
       * @name:getSum
       * @description:
       * @param {*} a 
       * @param {*} b 
       */
      function getSum(a,b){
          return a+b;
      }
      console.log(getSum(1,2))//3
      console.log(getSum(1,2))//3
      console.log(getSum(1,2))//3
      
      // 不纯函数
      /**
       * @name:array.splice
       * @description: 用于添加或删除数组中的元素,会改变原始数组,如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。
       * @param {*} start 
       * start:必需。规定从何处添加/删除元素。该参数是开始插入和(或)删除的数组元素的下标,必须是数字
       * @param {*} howmany 
       * howmany:可选。规定应该删除多少元素。必须是数字,但可以是 "0"。如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素。
       * @param {*} item1,.....,itemX 
       * item1,.....,itemX:可选。要添加到数组的新元素
       */
      console.log(arr.splice(0, 3))//[1,2,3]
      console.log(arr.splice(0, 3))//[4]
      console.log(arr.splice(0, 3))//[]
      
  • 函数式编程不会保留计算的中间结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去执行

纯函数的好处

  1. 可缓存
    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
  2. 可测试
    • 纯函数让测试更加方便
  3. 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(web worker)

副作用

副作用让一个函数变的不纯(如下面代码所示),纯函数的根据是相同的输入始终得到相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

代码示例:

// 不纯的
let mini=18;
function checkAge(age){
    return age>=mini;
}

// 纯的(有硬编码,可以通过柯里化解决)
function checkAge(age){
    let mini=18;
    return age>=mini;
}

副作用来源:

  • 配置文件
  • 数据库
  • 获取数据输入

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

lodash

lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。

lodash中的fp模块

lodash中的fp模块提供的方法是已经被柯里化的、如果一个方法的 参数是函数的话,要求函数优先,数据之后

const _=require('lodash');
const fp=require('lodash/fp');

//数据优先,函数之后
console.log(_.map(['a','b','c'],_.toUpper))//[ 'A', 'B', 'C' ]

console.log(_.split('h_j','_'))

// 函数优先,数据之后
fp.map(fp.toUpper,['a','b','c'])//[ 'A', 'B', 'C' ]

//已经被柯里化了
console.log(fp.split('_','h_j'))
console.log(fp.split('_')('h_j'))

lodash和fp的map方法的区别

parseInt() 函数可解析一个字符串,并返回一个整数。

  • 语法:parseInt(string, radix)
  • 参数:
    • string:必需。要被解析的字符串。
    • radix:可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。
const _=require('lodash');
const fp=require('lodash/fp');

//lodash的map方法中的函数接收的参数是三个(item,index,array)
console.log(_.map(['123','123b','1'],parseInt))//[ 123, NaN, 1 ]
//fp中的map方法的函数接收的参数只有一个(item)
console.log(fp.map(parseInt,['123','123b','1']))//[ 123, 123, 1 ]

函数柯里化(curry——高阶函数)

柯里化指的是当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,并且返回函数的执行结果。

代码示例:

// 不纯的
let mini=18;
function checkAge(age){
    return age>=mini;
}

// 普通的纯函数
function checkAge(mini,age){
    return age>=mini;
}
console.log(checkAge(18,20));
console.log(checkAge(18,17))

// 柯里化
function checkAge(mini){
    return function(age){
        return age>=mini;
    }
}
const checkAge18=checkAge(18);
const checkAge20=checkAge(20);
console.log(checkAge18(20))
console.log(checkAge20(17));

lodash中的柯里化方法:_curry(function)

  • 功能:创建一个函数,该函数接收一个或多个function参数,如果function的参数都被提供,则执行function并返回执行的结果。否则继续返回该函数并等待接收剩余的参数
  • 参数:需要柯里化的函数
  • 返回值:柯里化后的函数
    代码示例:
const _=require('lodash');

function getSum(a,b,c){
    return a+b+c;
}

/**
 * 柯里化以后的函数
 */
const curried=_.curry(getSum);

console.log(curried(1,2,3));
console.log(curried(1,2)(3));
console.log(curried(1)(2,3));

模拟lodash中的curry方法

形参和实参的区别

参数有形参(parameter)和实参(argument)的区别,形参相当于函数中定义的变量,实参是在运行时的函数调用时传入的参数。

说明白就是,形参就是函数声明时的变量,实参是我们调用该函数时传入的具体参数。

声明函数add时,ab就是形参。调用函数add(1,2)12就是实参。

代码示例:

function add(a,b) {
    return a + b
};
add(1,2);

剩余参数

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

如果一个命名参数为(...theArgs),则它将成为一个由剩余参数组成的真数组,其中从0(包括)到theArgs.length(排除)的元素由传递给函数的实际参数提供。

剩余参数和 arguments对象的区别

剩余参数和 arguments对象之间的区别主要有三个:

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
  • arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sortmapforEachpop
  • arguments对象还有一些附加的属性 (如callee属性)。

模拟lodash中的curry函数代码示例:

/**
 * @name:curry
 * @description: 模拟lodash中的curry函数
 * @param {*} fn 
 */
function curry(fn){
    // console.log(fn.length)//返回fn函数的实际参数个数
    return function curriedFn(...args){//这里行成了闭包,每次传入的参数被保存在args中
        // console.log(args)
        // 判断形参和实参的个数是否相同
        if(args.length<fn.length){//如果形参小于实参则需要返回一个函数接收剩余的参数
            return function(){
                // 第一次传入的参数保存在 args中
                // arguments是一个伪数组,使用Array.from可以将伪数组转换成数组
                return curriedFn(args.concat(Array.from(arguments)));
            }
        }
        return fn(...args);//如果形参大于等于实参则执行传入的fn函数
    }
}

function getSum(a,b,c){
    return a+b+c;
}
// 柯里化以后的函数
let curried=curry(getSum);
console.log(curried(1,2,3))
console.log(curried(1,2)(3))

函数柯里化的好处

  • 函数柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些参数的新函数
  • 这是一种对函数参数的“缓存”
  • 让函数的灵活度更高、粒度更小
  • 可以把多元函数变成一元函数,可以组合多个一元函数产生强大的功能

函数组合

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

  • 函数组合默认从右到左开始执行
  • 函数组合可以让我们把细粒度的函数重新组合成一个新的函数。

lodash中的组合函数

  • flow()是从左到右执行
  • flowRight() 是从右到左执行

模拟lodash中的compose

/**
 * @name:compose
 * @Description: 模拟组合函数,接收多个一元函数返回一个新的函数
 * @param  {...any} args function
 */

function compose(...args){//接收多个参数
    return function(value){//返回一个函数,接收的参数是最后调用的时候传递的参数
        //组合函数默认从右到左开始执行,所以需要将args先进行反转args.reverse()
        //反转完成后一次开始执行,每一次执行完的结果传递给下一个函数作为参数,使用reduce
        return args.reverse().reduce(function(total,fn){//fn是args中的每一项函数
            return fn(total);
        },value)
    }
}

const compose= (...args) => (value=> args.reverse().reduce((total,fn)=>fn(total),value));

function reverse(array){
    return array.reverse();
}

function first(array){
    return array[0];
}
function toUpper(str){
    return str.toUpperCase();
}
//反转
//获取第一个元素
//转成大写字母
const res=compose(toUpper,first,reverse);

console.log(res(['one','two','three']))

函数组合要满足结合律

结合律: 按照函数执行的顺序不管把那几个函数结合在一起,其返回的结果都是一样的。

代码示例:

let fn=compose(f,g,h);
let bool=compose(compose(f,g),h)==compose(f,compose(g,h));
console.log(bool)//true

函数组合调试

const _=require('lodash');

// log
const log=(res)=>{
    console.log(res);
    return res;
}
// 追踪
const trace=_.curry((tag,res)=>{//从右到左执行
    console.log(`${tag}的结果是:${res}`);
    return res;
})

// _.split()
// const split=_.curry(function(seq,str){
//     return _.split(str,seq);
// })
const split=_.curry((seq,str)=>_.split(str,seq));
// join
const join=_.curry((seq,str)=>_.join(str,seq));
// map
const map=_.curry((fn,array)=>_.map(array,fn));

// const fn=_.flowRight(join('_'),_.toLower,split(' '));

// console.log(fn("HELLO WORLD"))//h_e_l_l_o_,_w_o_r_l_d_,

const fn=_.flowRight(join('_'),map(_.toLower),trace('split'),split(' '));

console.log(fn("HELLO WORLD"))//hello_world

Functor(函子)

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

代码示例1:使用new关键字创建函子

/**
 * 使用new关键字创建函子
 */
class Container {
    constructor(value) {
        this._value = value//_value私有成员
    }
    //接收一个处理值的纯函数
    map(fn) {
        return new Container(fn(this._value))//返回新的函子
    }
}

const functor1 = new Container(5).map(value => value + 1).map(value => value * 2);
console.log(functor1)//Container { _value: 12 }

代码示例2:使用含子的静态方法创建函子

/**
 * 定义静态方法of(通过类名调用),封装new关键字,返回函子对象,避免使用new创建函子,
 * 更深层的含义是of方法用来把值放到上下文中(把值放到容器中,使用map来处理值)
 */
class Container {
    static of(value){//
        return new Container(value);
    }
    constructor(value) {
        this._value = value//_value私有成员
    }
    //接收一个处理值的纯函数
    map(fn) {
        return Container.of(fn(this._value))//返回新的函子
    }
}

const functor1 = new Container(5).map(value => {
    console.log(`value改变之前${value}`);
    value=value + 1;
    console.log(`value改变之后${value}`);
    return value ;

}).map(value => value * 2);
console.log(functor1)//Container { _value: 12 }
  1. 函子的作用
    • 控制空值的副作用——MayBe 函子
    • 处理异常——Either函子
    • 进行异步操作
  2. 函子总结
    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子就是一个实现了map契约的对象
    • 我们可以把函子当做一个盒子,这个盒子里面封装了一个值
    • 想要处理盒子中的值,我们需要给函子的map方法传递一个处理值的函数(纯函数),由这个函数对盒子中的值进行处理
    • 最终map方法返回一个包含新值的盒子(函子)

point free 模式(函数组合)

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些基本的辅助运算函数

代码示例:

const fp=require('lodash/fp');

//Hello World转成 hello_world
const fn=fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower);
console.log(fn('Hello World'))

//world wild web  转成 W. W. W
//使用空格分割成数据
//遍历数据,把数组的每一项都转换成大写,拿到数组每一项的第一个字符
// 把数据使用'. '合并成字符串
const fn=fp.flowRight(fp.join('. '),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));
console.log(fn('world wild web'))//W. W. W

Pointed函子

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

MayBe 函子

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

注意:MayBe 函子虽然可以处理空值的问题,但是如果多次调用map方法的时候我们无法确定是那一次出现了空值

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));
    }
    // 判断value是否为空
    isNothing() {
        return this._value === null || this._value === undefined;
    }
}

/**
 * 验证输入null或者undefined
 */

// let res = MayBe.of('Hello World').map(value => value.toUpperCase());//MayBe { _value: 'HELLO WORLD' }
// let res = MayBe.of(null).map(value => value.toUpperCase());//MayBe { _value: null }
// let res = MayBe.of().map(value => value.toUpperCase());//MayBe { _value: null }
// console.log(res)


/**
 * 
 */
let res = MayBe.of('Hello World')
        .map(value => value.toUpperCase())
        .map(value=>null)
        .map(value=>value.split(' '))
console.log(res)//MayBe { _value: null }

Either函子:if-else

Either函子的作用:异常可以让函子变得不纯,Either函子可以用来做异常处理,使用Left处理异常,使用Right处理正常的情况。

代码示例:

/**
 * 处理异常的函子
 */
class Left {
    static of(value) {
        return new Left(value);
    }
    constructor(value) {
        this._value = value;
    }
    map(fn) {
        // return Left.of(fn(this._value));
        return this;
    }
}

/**
 * 处理正常情况的函子
 */
class Right {
    static of(value) {
        return new Right(value);
    }
    constructor(value) {
        this._value = value;
    }
    map(fn) {
        return Right.of(fn(this._value));
    }
}


// let rihgt1=Right.of(12)
//             .map(value=>value+2)

// let left1=Left.of(12)
//         .map(value=>value+2)

// console.log(rihgt1)
// console.log(left1)


function parseJSON(str) {
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        return Left.of({ error: e.message })
    }
}

// let res=parseJSON('{name:qian}');
// console.log(res);//Left { _value: { error: 'Unexpected token n in JSON at position 1' } }


let res = parseJSON('{"name":"qian"}').map(value => value.name.toUpperCase());
console.log(res);//Right { _value: 'QIAN' }

IO函子——输入-输出

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){//把fn和this._value组合成新的函数传递给IO函子
         return new IO(fp.flowRight(fn,this._value));
     }
 }


 /**
  * 调用IO函子
  */
//  process:进程对象
// execPath:当前node的执行路径
  let res=IO.of(process).map(value=>value.execPath)
  console.log(res)//IO { _value: [Function] }
  console.log(res._value())//D:\Program Files\nodejs\node.exe

Monad 函子-单子

Monad是可以变扁(解决函子嵌套的问题)的pointed函子,如果一个函子同时具有joinof两个方法并遵守一些定律,那么这个函子就是Monad 函子。

folktale库

folktale:一个标准的函数式编程库,和lodash、ramda的不同之处是没有提供很多的功能函数,只提供了一些函数式处理的操作,例如compose、curry、Task、MayBe 等。

Task 函子异步执行

代码示例:读取文件、解析内容

const {task}=require('folktale/concurrency/task');
const fs=require('fs');
const {find,split}=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()//执行task
.listen({
    onCancelled: () => { console.log('the task was cancelled') },
    onRejected:err=>{
        console.log(err);
    },
    onResolved:values=>{
        console.log(values);//  "version": "1.0.0",
    }
})

JavaScript异步编程

JavaScript是单线程语言

JavaScript作为浏览器脚本语言,主要用途是与用户互动,以及操作DOM。想想看如果JavaScript同时有两个线程,一个在某个DOM节点上添加内容,另一个删除了这个节点,那这个时候浏览器应该以哪个线程为准?所以JavaScript的用途决定了它是单线程的语言。

注意:单线程指的是在js的执行环境中负责执行代码的线程只有一个。

同步模式和异步模式

单线程就意味着所有的任务都需要排队,前一个任务结束,才会执行下一个任务。如果前一个任务耗时很长,那么后面的任务就必须等待着,出现“假死”的情况。为了解决这一问题JavaScript将任务的执行模式分成了两种:

  • 同步模式:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务(排队执行,不是同时执行)
  • 异步模式:不会等待这个任务的结束才去开始执行下一个任务,对于耗时操作是开启之后立即执行下一个任务,耗时任务的后续逻辑问题是通过"回调函数"的方式定义,回调函数执行完成之后进入消息队列,等待调用栈中的所有任务完成之后,EventLoop就会从消息队列中取出第一个"回调函数"任务压入调用栈开启新一轮的任务执行。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

调用栈、消息队列、EventLoop(事件循环)

  • 调用栈:是正在执行的工作表。
  • 消息队列:待办的工作表,是一个先进先出的数据结构,排在前面的事件,优先被调用栈读取。
  • EventLoop:负责监听调用栈和消息队列,一旦调用栈中的所有的任务都结束了,那EventLoop就会从消息队列中取出第一个回调函数任务压入调用栈中开启新一轮的任务执行,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

事件循环图:
函数式编程、异步编程、promise_第1张图片

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"消息队列"中加入各种事件(click,load等)。只要栈中的代码执行完毕,主线程就会去读取"消息队列",依次执行那些事件所对应的回调函数。

可以实现JavaScript异步编程的方法

  • 回调函数
  • 事件监听
  • 发布、订阅(观察者模式)
  • promise对象

promise

promise对象有以下特点

  1. Promise就是一个类,在执行的时候需要传递一个执行器,执行器会立即执行
  2. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    • Promise对象的状态改变,只有两种可能:
      • pending变为fulfilled
      • pending变为rejected
    • 只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。

promise各个api的作用

  1. resolvereject函数的作用是:更改状态
    • resolve->fulfilled
    • reject->rejected
  2. then方法的作用是判断状态
    • 如果状态是成功,则调用成功的回调函数
    • 如果状态是失败,则调用失败的回调函数,then方法是被定义在原型对象上的
    • then的成功回调函数有一个参数,表示成功之后的值
    • then失败回调 函数有一个参数,表示失败的原因
    • 同一个promise对象下面的then方法是可以被调用多次的
    • then方法是可以被链式调用的
      • promise对象的then方法会返回一个全新的promise对象
      • 后面的then方法就是在为上一个then返回的promise注册回调
      • 前面then方法中回调函数返回的值会作为then方法回调的参数
      • 如果回调中返回的是promise对象,那后面then方法的回调会等待他的结束
    • then方法的参数可选
  3. promise.resolve() 将给定的值转换成promise对象
  4. finally的特点:
    • 不管回调函数是失败还是成功,finally都会执行一次
    • 可以在finally的后面链式调用then方法可以拿到当前这个promise对象的最终值
  5. catch()的作用:处理当前promise对象最终执行失败的情况,这样在then中就不需要在调用失败回调函数,将会在catch中捕获到。

注意:catch写法是针对于整个链式写法的错误捕获的,而then第二个参数是针对于上一个返回Promise的。

catch和then第二个参数的优先级

就是看谁在链式写法的前面,在前面的先捕获到错误,后面就没有错误可以捕获了,链式前面的优先级大,而且两者都不是break, 可以继续执行后续操作不受影响。

  1. catch在then调用的后面代码示例:
    new Promise((resolve,reject)=>{
    	foo();
    })
    .then(()=>{
    	console.log(1)
    },error=>{
    	console.log('我是then的第二个参数')
    	console.log(error)
    }).catch(error=>{
    	console.log('我是catch')
    	console.log(error)
    })
    
    执行结果:
    catch在链式调用的后面
  2. catch在then调用的前面代码示例:
    new Promise((resolve,reject)=>{
    	foo();
    })
    .catch(error=>{
    	console.log('我是catch')
    	console.log(error)
    })
    .then(()=>{
    	console.log(1)
    },error=>{
    	console.log('我是then的第二个参数')
    	console.log(error)
    })
    
    执行结果:
    catch在链式调用的后面

Pomise.all的使用

Promise.all可以将多个Promise实例包装成一个新的Promise对象,等待所有的任务完成才会结束。

  • 成功的时候返回的是一个结果数组,这个数组包含每一个异步函数的执行结果。
  • 失败的时候则返回最先被reject失败状态的值。
  • Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的

Promise.race的使用

promise.race和第一个结束的任务一起结束,promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

promise的执行时序

宏任务和微任务

“消息队列”(回调队列)中的任务可以称为宏任务,宏任务执行过程中可以临时加上一些额外的需求,那这些额外的需求可以选择作为一个新的宏任务进到队列中排队,也可以作为当前任务的微任务,直接在当前任务结束之后立即执行,而不是到整个队伍的末尾在重新排队。

作为宏任务的有:

  • 定时器
  • 事件绑定
  • ajax
  • 回调函数
  • Nodefs可以进行异步的I/O操作

作为微任务的有:

  • promise的回调函数
  • MutationObserver(创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用。)
  • node中的process.nextTicknode中实现的api,把当前任务放到主栈最后执行,当主栈执行完,先执行nextTick,再到等待队列中找)

执行时序

先同步,再异步(先微任务,再宏任务)

每次执行栈的同步任务执行完毕,就会去“消息队列”中取出完成的异步任务,队列中又分为微任务队列和宏任务队列,等到把微任务队列所有的微任务都执行完毕,才会从宏任务队列中取事件。等到把队列中的事件取出一个,放入执行栈执行完成,就算一次循环结束,之后event loop还会继续循环,它会再去微任务队列执行所有的任务,然后再从宏任务队列里面取一个,如此反复循环。

Generator 生成器函数

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

next 方法和yield 语句的作用

  1. next 方法的作用:是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。
    • value 属性是 yield 语句后面表达式的值,表示当前阶段的值;
    • done 属性是一个布尔值,表示 Generator函数是否执行完毕,即是否还有下一个阶段。
  2. yield 语句的作用:暂停生成器函数的执行,直到外界再一次调用生成器对象的next方法时从yield的位置往下执行
  3. 调用生成器对象的next方法时传一个参数,那这个参数会作为yield的返回值,即在yield的左边可以接收到这个值。
    function* foo(x) {
    	console.log('start');
    	let res = yield 'foo';
    	console.log(res);
    	return res;
    }
    let g = foo(); //调用一个生成器函数并不会立即执行这个函数,而是得到一个生成器对象
    
    //foo函数的函数体开始执行
    console.log(g.next());//{value: "foo", done: false}
    
    //向 Generator 函数体内输入数据
    console.log(g.next('我是next的传值')); //{value: "我是next的传值", done: true}
    

上面的代码中,第一个 next 方法的 value 属性,返回表达式 ‘foo’。第二个 next 方法带有参数我是next的传值,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 res 接收。因此,这一步的 value 属性,返回的就是我是 next的传值(变量res的值)。

Generator捕获异常

function* foo(x) {
	console.log('start');
	try{
		let res = yield 'foo';
		console.log(res);
	}catch(e){
		console.log(e);//Error: 异常
	}
}
let g = foo(); //调用一个生成器函数并不会立即执行这个函数,而是得到一个生成器对象

let result = g.next(); //foo函数的函数体开始执行
console.log(result); //{value: "foo", done: false}

//抛出异常
g.throw(new Error('异常'))

Async/Await

  • async必须声明的是一个function,没有awaitasync是同步函数
  • async函数会隐式地返回一个promise,该promisereosolve值就是函数return的值。
  • await关键字只能用在async定义的函数内,必须是直系(作用域链不能隔代)
  • await的本质是可以提供等同于”同步效果“的等待异步返回结果的语法糖。
  • await声明的Promise异步返回,必须“等待”到有返回值的时候,即reslove函数执行的返回值,代码才继续执行下去
  • await声明非promise直接返回await后面的值而不是等待其执行结果
  • 不管await声明的是Promise异步返回、定时器返回,还是普通值返回,then都是最后执行的
  • await普通值,直接输出
  • Async/Await终止程序和一般的function写法一样,想要中断的时候,直接 return一个值就行,nullfalse等都是可以的
  1. 代码示例1:async函数会隐式地返回一个promise
    let asyncFn=async()=>{
    	return '我是Promise';
    	//等同于 return Promise.resolve('我是Promise');
    	//等同于 return  new Promise((resolve,reject)=>{ resolve('我是Promise') });
    }
    //`async`函数会隐式地返回一个`promise`,
    //所以它具有then方法,也可以在then方法中获取到`async`函数的返回值
    asyncFn().then((value)=>{
    	console.log(value);//我是Promise
    })
    
  2. 代码示例2:await的本质是可以提供等同于”同步效果“的等待异步返回结果的语法糖
    const demo = async () => {
    	let result = await new Promise((resolve, reject) => {
    		setTimeout(() => {
    			resolve('我延迟了一秒')
    		}, 1000)
    	});
    	console.log('await程序还没执行完,我要先“等一会”');
    	return result;
    }
    // demo的返回当做Promise
    demo().then(result => {
    	console.log('输出', result);
    })
    
    执行结果,等待1秒之后输出:
    • ‘await程序还没执行完,我要先“等一会”’
    • 输出 我延迟了一秒
  3. 代码示例3:await遇到定时器没有等待
    const demo = async () => {
    	let result = await setTimeout(() => {
    		console.log('我延迟了一秒');
    	}, 1000)
    	console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    	return result
    }
    demo().then(result => {
    	console.log('输出', result);
    })
    
    执行结果:
    • ‘await程序还没执行完,我要先“等一会”’
    • 输出 Timeout对象
    • 等待1秒后输出:我延迟了一秒
  4. 代码示例4: await用于声明一般情况下的传值
    const demo = async () => {
    	let msg='msg';
    	let result = await msg;
    	console.log(result);
    	console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    	return result
    }
    demo().then(result => {
    	console.log('输出', result);
    })
    
    执行结果:
    • msg
    • 我由于上面的程序还没执行完,先不执行“等待一会”
    • 输出 msg
  5. 代码示例5:Async/Await终止程序
    const demo = async () => {
    	let msg='msg';
    	let result = await msg;
    	console.log(result);
    	return;
    	//以下几种写法也是可以的
    	// return null;
    	// return false;
    	// return Promise.resolve('我退出了下面不进行了');
    	// return Promise.reject(new Error('拒绝'))
    	console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    	return result
    }
    demo().then(result => {
    	console.log('输出', result);
    }).catch(error=>{
    	console.log('catch',error);
    })
    

你可能感兴趣的:(html,css,js)