(深入JavaScript 四)JS中的纯函数、函数柯里化、组合函数

89 阅读5分钟

纯函数

JavaScript这门语言是符合函数式编程的,在函数式编程中有一个概念叫做纯函数。并不是说纯函数只有JavaScript中才会有,只要是符合函数式编程范式的语言都能运用纯函数的概念去书写程序,在这里我们主要在JavaScript中进行讨论。

纯函数的特点

  1. 确定的输入一定有确定的输出
  2. 函数在执行中不能产生副作用

副作用

JavaScript中的副作用(Side Effect)指的是函数除了返回值之外,还对函数外部的状态进行了修改或产生了其他影响的情况。比如说,一个函数修改了全局变量、向服务端发送了 Ajax 请求、改变了浏览器的网页内容等都属于副作用。

副作用并不是一件坏事情,实际上很多有用的操作都包含副作用,比如产生动画效果、获取用户输入、发送网络请求等。

但是,过度的副作用会增加代码的复杂度和可读性,也容易引发程序的bug,所以我们在编写 JavaScript 程序时,需要尽量避免不必要的副作用,并且尽可能减少副作用的影响范围。

纯函数示例

对于上述概念看下列代码

var strs = ["aaa","bbb","ccc"]
var strs1 = strs.slice(2)
var strs2 = strs.splice(2)

console.log(strs1)//["ccc"]
console.log(strs2)//["ccc"]
console.log(strs)//["aaa","bbb"]

strs2 = strs.splice(2)
console.log(strs2)//[]

function foo(num1,num2){
    strs2 = 123
    return num1 + num2
}

slice和splice都是传入参数后对数组进行截断操作的,不同的是splice会改变原数组slice不会改变原数组,对于slice,我们每次调用strs.slice(2)都会得到相同的结果,并且它也没有产生我们上面说的副作用,那么就可以说slice这个方法就是一个纯函数。而对于splice,我们通过strs在第二次调用它的时候,输出为[],与我们第一次调用的结果不一样,所以违背了我们上面对纯函数的定义,所以这里就可以说splice它不是一个纯函数。而对于上面的函数foo,它虽然每次返回的结果都是相同的,但是它改变了不属于函数内部的变量strs2,产生了副作用,所以它也不是一个纯函数。

纯函数的优点

  1. 易测试,结果只依赖于输入,测试时能保证相同的输出。
  2. 能缓存,相同的输入一定会产生相同的输出,可以针对相同输入将结果缓存起来。
  3. 移植性高,因为不依赖外部环境,只依赖本身,所以可以直接将纯函数移植到另一个地方去使用。
  4. 易维护,无副作用,维护起来不需要考虑太多其它因素。

柯里化

柯里化也是属于函数式编程里面一个非常重要的概念。将接收多个参数的函数改为只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就叫做函数的柯里化。如下代码显示:

function add(x, y, z) {
  return x + y + z
}

var result = add(10, 20, 30)
console.log(result)//60

//将add函数进行柯里化
function sum(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}
var result2 = sum(10)(20)(30)
console.log(result2)//60

柯里化运用场景

其实从上面代码就可以看出,柯里化的过程会产生闭包,而我们就可以运用闭包会缓存传入的参数来简化代码。比如:

function add(num1,num2){
    return num1 + num2
}
var result = add(5,5)
var result2 = add(5,10)

function add1(num1) {
    return function (num2) {
      return num1 + num2;
    };
}

var add2 = add1(5);
console.log(add2(5));
console.log(add2(10));

从上面代码我们可以看到,我们在确定第一个参数为5,但执行add的时候,每次执行都还是需要传入两个参数,但我们将add函数柯里化后生成了add1函数,通过add1函数我们返回了一个函数add2,这时我们就通过闭包将第一个参数5给缓存了起来,每次调用add2我们就不必再传入5这个参数。其实我们也能经常看到一些用第三方库的时候,我们调用一些第三方库传入一些配置参数得到一个函数,我们再调用这个函数,它们内部其实就用了柯里化的原理。

简单封装一个将普通函数进行柯里化的工具函数

function curring(fn) {
    function curried(...arg) {
      if (arg.length >= fn.length) {
        //解决传入的函数的this绑定问题
        return fn.call(this, ...arg);
      } else {
        return function (...arg2) {
          return curried(...arg, ...arg2);
        };
      }
    }
    return curried;
}
function add(x, y, z) {
    return x + y + z;
}
//使用
var addCurried = curring(add);
var result = addCurried(10)(20)(30);
console.log(result)//60 结果没有问题

组合函数

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式,比如下列代码,我们对 一个数先平方再乘2,每次我们想进行这一操作的时候,都需要重复的调用double和square函数,但我们可以通过组合的形式将这两个函数组合起来自动依次调用,而对函数组合的过程就就称为组合函数。

function double(num){
    return num * 2
}
function square(num){
    return num ** 2
}
var result  = double(square(5))

其实这原理不难,很简单,这里简单封装一个满足组合函数要求的,将多个函数组合后返回一个组合函数:

  function createCompose(...fns) {
    var length = fns.length;
    for (var i = 0; i < length; i++) {
      if (typeof fns[i] !== "function") {
        throw new TypeError("Expected arguments are functions");
      }
    }

    function compose(...args) {
      var index = 0;
      var result = length ? fns[index].apply(this, args) : args;
      while (++index < length) {
        console.log("index:", index);
        result = fns[index].call(this, result);
      }
      return result;
    }
    return compose;
  }

  function double(m) {
    return m * 2;
  }

  function square(n) {
    return n ** 2;
  }
  //使用方法
  var newFn = createCompose(double, square);
  console.log(newFn(5));//50