V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Windlike
V2EX  ›  程序员

开发自己的前端工具库(二):函数式编程

  •  
  •   Windlike · 2018-08-03 16:04:17 +08:00 · 1093 次点击
    这是一个创建于 2110 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    本系列文章将通过自己的一个开发工具库的实战经验(踩过的坑)教大家如何开发属于自己的一个工具库,在这里你可以学到 Git 的使用规范,基础项目的搭建,代码编写的规范,函数式编程思想,TypeScript 实战,单元测试,编写文档和发布 NPM 包等等知识。

    阅读文章你可能需要以下基础知识:

    项目源码

    Windlike-Utils

    系列目录

    1. 开发自己的工具库(一):项目搭建

    为什么要用函数式编程

    因为函数式编程不会改变外部的变量,且对固定输入有唯一输出,这样我们可以不管函数内部的具体实现去使用它,而且可以很方便地通过组合多个函数而成我们想要的那个函数,更接近自然语言的表达。

    比如我们要实现一个y=f(x)=2*x+1的函数,通常我们会这么写:

    function f(x) {
        return 2*x + 1;
    }
    
    f(1);  // 3
    

    而函数式编程则是将他们拆分为几个小函数,再组装起来使用:

    function double(x) {
        return 2*x;
    }
    
    function plusOne(x) {
        return x + 1;
    }
    
    plusOne(double(1));  // 3
    
    // 或者还有更好一点的写法,这里暂未实现,
    // 这里只是写下他们的调用方法,具体下面的文会讲到
    const doubleThenPlusOne = compose(plusOne, double);
    doubleThenPlusOne(1);
    

    纯函数

    • 不可变性(immutable) 即对输入的实参及外部变量不能进行改变,没有副作用,以保证函数是“干净”的。
    • 唯一性 对每个固定输入的参数,都有唯一对应的输出结果,有点类似于数学里的y=f(x),当输入的x不变,输出的y也不会改变

    这是一个栗子:

    const array = [1, 9, 9, 6];
    
    // slice 是纯函数,因为它不会改变原数组,且对固定的输入有唯一的输出
    array.slice(1, 2);  // [9, 9]
    array.slice(1, 2);  // [9, 9]
    
    // splice 不是纯函数,它即改变原数组,且对固定输入,输出的结果也不同
    array.splice(0, 1);  // [9 ,9 ,6]
    array.splice(0, 1);  // [9 ,6]
    

    柯里化(Currying)

    柯里化就是传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 我们上面实现了一个加一的函数,但当我们又需要一个加二的函数,又重新去写代码实现它的话效率是很低的,所以我们就需要柯里化,我们设想一下可不可以这样呢:

    const plusOne = add(1);
    const plusTwo = add(2);
    
    plusOne(1);  // 2
    plusTwo(2);  // 4
    

    这样我们就可以很容易地得到想要的函数,下面是add函数的实现:

    function add(a) {
        return function(b) {
            return a + b;
        }
    }
    

    虽然基本满足我们现在的需求,但感觉还是不太方便,如果我们要实现三个或多个数的相加我们可能得这样写:

    function add(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }
    

    于是我们再设想一种更方便的方法:

    function add(a, b, c) {
        return a + b + c;
    }
    
    const curryAdd = curry(add);
    const plusOne = curryAdd(1);
    const plusOneAndTwo - curryAdd(1, 2);
    
    plusOne(2, 3);  // 6
    plusOneAndTwo(3);  // 6
    curryAdd(1)(2, 3);  // 6
    curryAdd(1)(2)(3);  // 6
    

    这样我们就可以自由产生需要参数不同的函数啦,下面是curry的实现方法(有兴趣的同学可以先思考下再看):

      function curry<Return>(fn: Function): CurryFunction<Return> {
        // 记录传进来的函数总共需要多少个参数
        let paramsLength: number = fn.length;
    
        function closure(params: any[]): CurryFunction<Return> {
    
          let wrapper: CurryFunction<Return> = function (...newParams: any[]) {
            // 将所有的参数取出
            let allParams = [...params, ...newParams];
    
            if (allParams.length < paramsLength) {
              // 如果参数数量还不够则返回新的函数
              return closure(allParams);
            } else {
              // 否则返回结果
              return fn.apply(null, allParams);
            }
          };
    
          return wrapper;
        }
    
        return closure([]);
      }
    

    可能有些不太好理解,一时看不懂的同学可以先跳过这里看下面~

    这里是源码,及头文件定义

    另外也可以用原生的bind函数来实现柯里化:

    const plusOne = add.bind(null, 1);
    
    plusOne(2, 3);
    

    函数组合(Compose)

    函数组合就是把多个不同的函数组合成一个新的函数。

    比如这样:

    // 将函数从右往左组合
    const doubleThenPlusOne = compose(plusOne, double);
    
    // 1*2 + 1
    doubleThenPlusOne(1);  // 3
    
      function compose<Return>(...fn: any[]): (...params: any[]) => Return {
        return (...params: any[]): Return => {
          let i = fn.length - 1;
          let result = fn[i].apply(null, params);
    
          while (--i >= 0) {
            result = fn[i](result);
          }
    
          return result;
        };
      }
    

    这里是源码,及头文件定义

    延迟输出

    有时候这个世界并不是那么美好的,并不是所有的代码都是那么“干净”的,比如 I/O 操作和 DOM 操作这些等待,因为这些操作都对外部有依赖,会对外部有影响。这时候就需要用延迟输出来保证我们的函数是“干净”的,例如下面实现的这个random函数:

      function random(min: number = 0, max: number, float: boolean): () => number {
        return (): number => {
          if (min > max) {
            [min, max] = [max, min];
          }
          if (float || min % 1 || max % 1) {
            return min + Math.random() * (max - min);
          }
    
          return min + Math.floor(Math.random() * (max - min + 1));
        };
      }
    

    对于固定的输入,它总返回的是产生符合条件的随机数的函数,这样我们就通过“拖延症”来让我们的代码保持“干净”啦,是不是很机智呢!这样做的好处还有它通过闭包机制把参数都记住,缓存起来,下次可以不用重复传同样的参数:

    const createRandomNumber = random(1, 100, false);
    
    createRandomNumber();
    createRandomNumber();  // 可以多次重复调用产生 1 到 100 随机数
    

    总结

    本章节讲了函数式编程的一些主要概念,以及为何用它来开发一个工具库是很好的,因为纯函数都是“干净”的,不依赖外部也不会对外部有影响,不用担心会影响到原有的代码。

    下章节我们来讲下如何为自己的项目编写测试用例。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5701 人在线   最高记录 6547   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 40ms · UTC 09:00 · PVG 17:00 · LAX 02:00 · JFK 05:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.