07_纯函数-柯里化实现-组合函数

理解 JavaScript 纯函数

纯函数的概念

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数时编程的范式,所以也有纯函数的概念;

    • 在 React 开发中纯函数是被多次提及的;
    • 比如 React 中组件就被要求像是一个纯函数,redux 中有一个 reducer 的概念,也是要求必须是一个纯函数;
    • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
  • 纯函数的维基百科定义:

    • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
    • 此函数在相同的输入值时,需产生相同的输出
    • 函数的输出和输入值以外的其它隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关
    • 该函数不能有语义上可观察的函数副作用,诸如"触发事件",使输出设备输出,或更改输出值以外物件的内容等
  • 简单总结以下:

    • 确定的输入,一定会产生确定的输出
    • 函数在执行过程中,不能产生副作用

副作用的理解

  • 副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其它的副作用
  • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还会调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部存储
  • 纯函数在执行的过程中就是不能产生这样的副作用:
    • 副作用往往是产生 bug 的温床

纯函数的案例

我们来看一下对数组操作的两个函数:

  • slice: slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组

  • splice: splice 截取数组,会返回一个新的数组,也会对原数组进行修改

  • slice 就是一个纯函数,不会修改传入的参数

var names = ["abc", "cba", "nba", "dna"];

var newNames = names.slice(0, 2);
console.log(newNames);

var newNames2 = names.splice(0, 2);
console.log(newNames2);
console.log(names);

区分是否为纯函数就从上面总结的两句话出发,不能有副作用也不能有不确定的输出

// baz也不是一个纯函数 因为我们修改了传入的参数
function baz(info) {
  info.age = 100
}
var obj = {
  name: 'beanbag'
  age:22
}
baz(obj)

// 这样做就是一个纯函数 info是没有做修改的,只是拷贝
function test(info) {
  return {
    ...info,
    age: 100
  }
}

严格意义上来说,在控制台打印东西之后,就不是纯函数了,但是如果只是打印,社区中存在争议

纯函数的优势

  • 为什么纯函数在函数式编程中非常重要呢?

    • 因为你可以安心的编写和安心的使用
    • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其它的外部变量是否已经发生了修改
    • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
  • React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改:

    • React 非常灵活,但它也有一个严格的规则;
    • 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改

JavaScript 柯里化

柯里化的概念

  • 柯里化也是属于函数式编程里面非常重要的概念

  • 维基百科的解释:

    • 在计算机科学中,柯里化(Currying),又译为卡瑞化或加里化
    • 是把接收多个参数的函数,编程接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
    • 柯里化声称 '如果你固定某些参数,你将得到接受余下参数的一个函数'
  • 危机百科的解释有点抽象,这里做一个总结

    • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
    • 这个过程就称之为柯里化

柯里化的结构

那么柯里化到底是怎么样的表现呢?

// 未柯里化的函数
function add1(x, y, z) {
  return x + y + z;
}

console.log(add1(10, 20, 30));

// 柯里化处理的函数
function add2(x) {
  return function (y) {
    return function (z) {
      return x + y + z;
    };
  };
}

console.log(add2(10)(20)(30));
var add3 = (x) => (y) => (z) => x + y + z;

console.log(add3(10)(20)(30));

柯里化的好处

  1. 让函数的职责单一
    • 在函数时编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
    • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果
    • 比如上面的案例我们进行一个修改:传入的函数需要分别进行如下处理
function add2(x) {
  x = x + 2;
  return function (y) {
    y = y * 2;
    return function (z) {
      z = z ** 2;
      return x + y + z;
    };
  };
}
  1. 柯里化的复用
    • 另外一个使用柯里化的场景是可以帮助我们可以复用参数逻辑:
    • maskAdder 函数要求我们传入一个 num(并且如果我们需要的话,可以在这里对 num 进行一些修改)
    • 在之后使用返回的函数时,我们不需要再继续传入 num 了
function makeAdder(num) {
  return function (count) {
    return num + count;
  };
}

var add5 = makeAdder(5);
add5(10);
add5(100);

var add10 = makeAdder(10);
add10(10);
add10(100);

案例

这里我们演示一个案例,需求是打印一些日志:

  • 日志包括时间、类型、信息

普通函数的实现方案如下:

function log(date, type, message) {
  console.log(
    `[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`
  );
}
log(new Date(), "DEBUG", "修复问题");
log(new Date(), "FEATURE", "新功能");

柯里化之后实现方案:

// function log(date) {
//   return function (type) {
//     return function (message) {
//       console.log(
//         `[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`
//       );
//     };
//   };
// }
var log = (date) => (type) => (message) =>
  console.log(
    `[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`
  );

var logBug = log(new Date())("DEBUG");
logBug("一个错误");
logBug("两个个错误");

var logFeautre = log(new Date())("FEATURE");
logFeautre("新增功能1");
logFeautre("新增功能2");

自动柯里化函数

目前我们有将多个普通的函数,转成柯里化函数:

  1. 首先分析我们期望调用它的方式,再来逆向还原我们的代码

    function add(x, y, z) {
      return x + y + z;
    }
    var curryAdd = Currying(add);
    
    curryAdd(10, 20, 30);
    curryAdd(10, 20)(30);
    curryAdd(10)(20)(30);
    
  2. 可以看到,在调用我们的 Currying 函数之后,传入需要被柯里化的函数,就能生成一个新的函数,所以不用说,第一步是返回一个新的函数,这个新的函数接受几个参数呢?这个我们不知道,因为调用方式可以很灵活,所以写成...args

  3. 继续想一下,如果这里我们看到是第一种调用方式的话,就和原函数调用方式是没有任何区别的,所以这里分成两种情况

    1. 如果柯里化后接受参数的个数大于等于函数原本接受的参数个数时:
      • 直接返回原本函数并且带上参数调用后的结果
      • 这里注意一下如果写成 fn(...args) ,如果在使用我们 Currying 函数的时候绑定了 this,那么这时候这个 this 就会丢失,所以这里我们要绑定一下 this 再执行
    2. 否则:
      • 这里就是参数少于原本参数的情况,递归执行 curried 函数,并将传入的参数和之前的参数一起传递给 curried
      • 同理 这里最好也绑定一下 this
      • 要注意这里是不能使用 bind 的,使用 bind 返回的只是一个函数,要注意 bind 和 call、apply 的区别
function Currying(fn) {
  function curried(...args) {
    if (args.length >= fn.length) {
      return fn.call(this, ...args);
    } else {
      return function (...rest) {
        return curried.call(this, ...args, ...rest);
      };
    }
  }
  return curried;
}

组合函数

组合函数的概念

  • 组合(Compose)函数是在 JavaScript 开发过程中一种对函数的使用技巧、模式:
    • 比如我们现在需要对某一数据进行函数的调用,执行两个函数 fn1 和 fn2,这两个函数是依次执行的
    • 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复
    • 那么是否可以将这两个函数组合起来,自动依次调用呢?
    • 这个过程就是对函数的组合,我们称之为组合函数(Compose Function)
function compose(fn1, fn2) {
  return function (x) {
    return fn2(fn1(x));
  };
}
function double(num) {
  return num * 2;
}
function square(num) {
  return num ** 2;
}
var calcFn = compose(double, square);
console.log(calcFn(20));

实现组合函数

刚才我们实现的 compose 函数比较简单,我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用 compose 函数时,传入了更多的参数

  1. 首先还是从使用出发, 传入需要组合的函数,返回一个新的函数

    function double(num) {
      return num * 2;
    }
    function square(num) {
      return num ** 2;
    }
    var calcFn = compose(double, square);
    calcFn(10);
    
  2. 首先明确需要返回一个新的函数, 并且传入的类型这里我们要限制一下,必须是函数类型

  3. 接下来就要开始调用我们的函数,这里采用 while 循环,在循环之前我们设立一个 result 变量保存一下第一次的值,防止如果它什么都没传的情况发生,当 result 在第一次的时候直接将传入的参数赋值给 result

  4. 进入 while 循环,结束的条件就是传入的函数全都执行完毕,每次将 result 作为参数去执行每一个函数即可

function compose(...fns) {
  for (var i = 0; i < fns.length; i++) {
    if (typeof fns[i] !== "function") {
      throw new TypeError("Expected argumnets are functions");
    }
  }
  return function (...args) {
    var index = 0;
    var result = index ? fns[index].call(this, ...args) : args;
    while (++index < fns.length) {
      result = fns[index].call(this, result);
    }
    return result;
  };
}

var calcFn = compose(double, square);
calcFn(10);
Last Updated:
Contributors: hqchqc