JavaScript 函数式编程

0x01 函数式编程

(1)概述

  • 函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算机运算视为数学上的函数计算,强调将计算过程看作是函数之间的转换而不是状态的改变

    • ❗ “函数” 的意思是指映射关系
    • 其他常见编程范式包括面向过程编程、面向对象编程等
  • 核心思想:将函数视为一等公民

    “函数是一等公民”(First-class Function):

    • 函数可以像变量一样传递和使用

      const success = (response) => console.log(response);
      const failure = (error) => console.error(error);
      
      $.ajax({
        type: "GET",
        url: "url",
        success: success,
        error: failure,
      });
      
    • 函数可以作为参数传递给其他函数(高阶函数)

    • 函数可以作为返回值返回(高阶函数)

  • 优势:

    • 可以脱离 this
    • 更好地利用 tree-shaking 过滤无用代码
    • 便于测试、并行处理等
  • 举例:

    • 非函数式

      let a = 1;
      let b = 2;
      let sum = a + b;
      console.log(sum);
      
    • 函数式

      function add(a, b) {
        return a + b;
      }
      
      let sum = add(1, 2);
      console.log(sum);
      

(2)高阶函数

  • 高阶函数(Higher-order Function)指可以把函数作为参数传递给其他函数或作为函数返回值返回

  • 函数作为参数:

    Array.prototype.myForEach = function (callback) {
      for (let i = 0; i < this.length; i++) callback(this[i], i, this);
    };
    
    const arr = [1, 2, 3];
    arr.myForEach((item, index, arr) => {
      console.log(item, index, arr);
    });
    
  • 函数作为返回值:

    function once(fn) {
      let called = false;
      return function () {
        if (!called) {
          called = true;
          return fn.apply(this, arguments);
        }
      };
    }
    
    const logOnce = once(console.log);
    logOnce("hello world");	// 正常输出
    logOnce("hello world");	// 未输出
    
  • 意义:通过抽象通用的问题,屏蔽函数的细节,实现以目标为导向

(3)闭包

  • 闭包(Closure)指函数与其词法环境的引用打包成一个整体

  • 特点:可以在某个作用域中调用一个函数的内部函数并访问其作用域中的成员

  • 实现方法:将函数作为返回值返回

    function add(a) {
      return function (b) {
        return a + b;
      };
    }
    
    const add5 = add(5);
    console.log(add5(3));	// 8
    
  • 本质:函数在执行的时候会放到一个执行栈上当函数执行完毕后会从栈上移除,而堆上的作用域成员因为被外部引用而不能释放,从而使得内部函数可以访问外部函数的成员

  • 包含关系:

    graph TB subgraph 函数式编程 subgraph 高阶函数 subgraph 闭包 a[ ] end end end

(4)纯函数

  • 纯函数(Pure Function)指相同的输入会永远得到相同的输出,即一一映射

  • 举例:Array.slice 是纯函数(不会修改原数组),而 Array.splice 是非纯函数

  • 优势:

    • 可缓存

      const memoize = (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];
        };
      };
      
      const calcArea = (radius) => {
        return Math.PI * Math.pow(radius, 2);
      };
      
      const memoizedCalcArea = memoize(calcArea);
      console.log(memoizedCalcArea(5));
      
    • 可测试

    • 并行处理

(5)副作用

  • 含义:当让一个函数变为非纯函数时,会带来的副作用
  • 来源:配置文件、数据库、输入的内容
  • 隐患:
    • 降低扩展性、重用性
    • 增加不确定性

(6)柯里化

  • 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数函数,并且返回接受余下的参数且返回结果的新函数的技术

  • 举例:

    const compareTo = (target) => {
      return function (current) {
        return current >= target;
      };
    };
    
    const compareTo5 = compareTo(5);
    console.log(compareTo5(4));		// flase
    console.log(compareTo5(6));		// true
    
    // ES6 柯里化
    const compareTo = (target) => (current) => current >= target;
    
  • 优势:

    • 传递较少的参数得到一个已缓存某些固定参数的新函数
    • 降低函数的粒度
    • 将多元函数转换为一元函数

(7)函数组合

  • 函数组合(Compose)是指将细粒度的函数组合为一个新函数

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

  • 举例:

    const compose = (f, g) => {
      return function (value) {
        return f(g(value));
      };
    };
    const reverse = (array) => array.reverse();
    const first = (array) => array[0];
    const last = compose(first, reverse);
    
    console.log(last([1, 2, 3, 4]));	// 4
    
  • 结合律:compose(compose(f, g), h) === compose(f, compose(g, h))

0x02 第三方库

(1)Lodash

a. 概述

  • 官网:https://lodash.com/

  • Lodash 是一个现代的 JavaScript 实用工具库,提供模块化、性能和附加功能

    • 纯函数的功能库
  • 安装:

    1. 使用命令 npm init -y 初始化 NodeJS 环境

    2. 使用命令 npm i --save lodash 安装 Lodash

    3. 在 js 文件中导入并使用 Lodash

      const _ = require('lodash');
      
      const arr = [1, 2, 3]
      console.log(_.first(arr));
      console.log(_.last(arr));
      

b. 柯里化

  • Lodash 中的柯里化:curry(func)

  • 功能:

    flowchart TB a[创建一个可以接收一个或多个 func 参数的函数]-->b{func 所需的参数都被提供} b--是-->c[执行 func 并返回执行的结果] b--否-->d[继续返回该函数并等待接收剩余的参数]
  • 举例:

    const _ = require('lodash');
    
    const add = (a, b, c) => a + b + c;
    const curried = _.curry(add);
    
    console.log(curried(1)(2)(3));	// 6
    console.log(curried(1, 2)(3));	// 6
    console.log(curried(1, 2, 3));	// 6
    
  • 复现:

    /**
     * 函数柯里化
     * @param {Function} func 需要柯里化的函数
     * @returns {Function} 柯里化后的函数
     */
    const curry = (func) => {
      /**
       * 柯里化函数
       * 递归地处理参数,直到达到原始函数所需的参数数量
       * @param {...any} args 当前函数调用时传入的参数
       * @returns {*} 如果参数足够则调用原始函数返回结果,否则返回下一个接收更多参数的函数
       */
      return function curriedFn(...args) {
        // 判断当前传入的参数数量是否足够调用原始函数
        if (args.length >= func.length) {
          // 参数足够,调用原始函数并返回结果
          return func.apply(this, args);
        } else {
          // 参数不足,返回一个新的函数,合并当前参数和新传入的参数
          return function (...args2) {
            // 递归调用柯里化函数,合并参数
            return curriedFn.apply(this, args.concat(args2));
          };
        }
      };
    };
    

c. 函数组合

  • Lodash 中有两种函数组合方法

    1. flow():从左到右执行
    2. flowRight():从右到左执行
  • 举例:

    const _ = require("lodash");
    
    const reverse = (array) => array.reverse();
    const first = (array) => array[0];
    const fn = _.flow(reverse, first);
    
    console.log(fn([1, 2, 3, 4]));吧
    
  • 复现:

    const flow =
      (...args) =>
      (value) =>
        args.reduce((acc, fn) => fn(acc), value);
    
    const flowRight =
      (...args) =>
      (value) =>
        args.reduceRight((acc, fn) => fn(acc), value);
    
  • 调试:使用柯里化函数

    const _ = require("lodash");
    
    const trace = _.curry((tag, v) => {
      console.log(tag, v);	// 调试 [ 4, 3, 2, 1 ]
      return v;
    });
    
    const reverse = _.curry((array) => array.reverse());
    const first = _.curry((array) => array[0]);
    const last = _.flow(reverse, trace("调试"), first);
    
    console.log(last([1, 2, 3, 4]));	// 4
    

d. FP 模块

  • Lodash 的 FP 模块提供函数式编程的方法

  • 提供了不可变的 auto-curriediteratee-firstdata-last 方法

  • 举例:

    const fp = require("lodash/fp");
    
    console.log(fp.map(fp.toUpper, ["a", "b", "c"]));	// [ 'A', 'B', 'C' ]
    console.log(fp.filter((x) => x % 2 === 0, [1, 2, 3, 4]));	// [ 2, 4 ]
    console.log(fp.split(" ", "Hello world"));	// [ 'Hello', 'world
    

e. Point Free

  • Point Free 是一种编程风格,把数据处理的过程定义成与数据无关的合成运算

  • 举例:

    const fp = require("lodash/fp");
    
    const last = fp.flow(fp.reverse, fp.first);
    console.log(last([1, 2, 3, 4]));
    

(2)Folktale

  • 官网:https://folktale.origamitower.com/

  • Folktale 是一个标准的函数式编程库,仅提供一些函数式处理操作等

  • 使用命令 npm i --save folktale 安装

  • 举例:

    const { curry, compose } = require("folktale/core/lambda");
    const { toUpper, first } = require("lodash/fp");
    
    const f = curry(2, (x, y) => x + y);
    console.log(f(3, 4) === f(3)(4));	// true
    
    const g = compose(toUpper, first);
    console.log(g(["a", "b"]));	// A
    

0x03 函子

(1)概述

  • 函子(Functor)是一个特殊的容器,通过一个普通对象来实现;该对象具有 map 方法,可以执行变形关系

    • 容器包含值的变形关系
  • 作用:将副作用控制在可控范围内

  • 举例:

    class Container {
      static of(value) {
        return new Container(value);
      }
    
      constructor(value) {
        this._value = value;
      }
    
      map(fn) {
        return Container.of(fn(this._value));
      }
    }
    
    const obj = Container.of(1)
      .map((x) => x + 1)
      .map((x) => x * x);
    console.log(obj);	// Container { _value: 4 }
    

(2)MayBe

  • 作用:处理外部空值清空

  • 实现并举例:

    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;
      }
    }
    
    console.log(MayBe.of("Hello world").map((x) => x.toUpperCase()));	// MayBe { _value: 'HELLO WORLD' }
    console.log(MayBe.of(null).map((x) => x.toUpperCase()));	// MayBe { _value: null }
    

(3)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));
      }
    }
    
    const parseJSON = (str) => {
      try {
        return Right.of(JSON.parse(str));
      } catch (e) {
        return Left.of(`Error parsing JSON: ${e.message}`);
      }
    }
    console.log(parseJSON('{ "id": "0" }'));
    console.log(parseJSON({ id: 0 }));
    

(4)IO

  • 作用:将非纯函数作为值,惰性执行该函数

  • 实现并举例:

    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));
      }
    }
    
    console.log(
      IO.of(process)
        .map((p) => p.execPath)
        ._value()
    );	// path\to\node.exe
    

(5)Task

  • Folktale 提供用于执行异步任务的 Task 函子

    Folktale 2.x 与 Folktale 1.x 的 Task 区别较大

    当前 Folktale 版本为 2.3.2

  • 举例:

    const fs = require("fs");
    const { task } = require("folktale/concurrency/task");
    const { split, find } = require("lodash/fp");
    
    const readFile = (filename) => {
      return task((promise) => {
        fs.readFile(filename, "utf-8", (err, data) => {
          if (err) promise.reject(err);
          else promise.resolve(data);
        });
      });
    };
    
    readFile("package.json")
      .map(split("\n"))
      .map(find((x) => x.includes("version")))
      .run()
      .listen({
        onRejected: (err) => console.log(err),
        onResolved: (data) => console.log(data),
      });
    

(6)Pointed

  • 作用:实现 of 静态方法的函子
  • of 用于避免使用 new 创建对象,并将值放入上下文从而使用 map 处理值

(7)Monad

  • 定义:当一个函子具有 ofjoin 方法,并且遵守一些定律,则这个函子是 Monad 函子

    • 可以“变扁”的 Pointed 函子
  • 实现并举例:

    class IO_Monad {
      static of(value) {
        return new IO_Monad(() => value);
      }
    
      constructor(fn) {
        this._value = fn;
      }
    
      // map方法,用于在IO_Monad实例的函数执行结果上应用给定的函数
      map(fn) {
        // 使用flowRight函数组合fn和this._value,确保fn在this._value执行后应用
        return new IO_Monad(require("lodash/fp").flowRight(fn, this._value));
      }
    
      // join方法,用于执行IO_Monad实例内部的函数并返回结果
      join() {
        return this._value();
      }
    
      // flatMap方法,用于先应用map方法,然后执行结果中的函数
      flatMap(fn) {
        // 先应用map方法,然后通过join执行结果中的函数
        return this.map(fn).join();
      }
    }
    
    const readFile = (filename) => {
      return new IO_Monad(() => {
        return require("fs").readFileSync(filename, "utf-8");
      });
    };
    
    const print = (value) => {
      return new IO_Monad(() => {
        console.log(value);
        return value;
      });
    };
    
    readFile("package.json").flatMap(print).join();
    

-End-

posted @ 2024-09-09 11:47  SRIGT  阅读(19)  评论(0编辑  收藏  举报