写JavaScript函数不得不知的高级技巧

2,987 阅读3分钟

对于我们程序员来说,写函数是再熟悉不过的事情了,不管我们要实现什么样的功能,都需要通过函数来完成。在JavaScript里面,函数拥有非常高的特权,甚至是一等公民,因此也跟Kotlin一样支持多种编程范式。

今天我主要想跟大家聊聊一些写函数时的高级技巧,大概有如下几个内容:

  • 纯函数
  • 高阶函数
  • 函数缓存
  • 懒函数
  • 柯里化
  • 函数组合

纯函数

纯函数要满足两个条件:

  1. 给相同的参数返回相同的结果
  2. 不产生任何副作用

来看如下代码:

function double(num){
  return num * 2 
}

这边只要给num的值不变,它返回的结果也不会变,而且这个函数执行的过程中没有对外界造成影响,所以它是一个纯函数。

而:

const counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()

20200929120128.jpg

这个函数每次执行时结果都不一样,所以不是纯函数。

而:

let count = 0;
function isYoung(user){
  if(user.age <= 20){
    count++;
    return true
  }
  return false
}

这里虽然每次给定相同的输入都给出相同的结果,但是它操作了外部的变量,产生了一个副作用,所以它也不是纯函数。

纯函数有什么好处?

为什么我们要区分纯函数跟其它函数?因为纯函数在我们编码过程中可以提高代码的质量。

  1. 纯函数更清晰更易于理解

每个纯函数都完成了一个特定的任务,并且我们可以通过输入预测结果

  1. 对于纯函数编译器可以做优化

比如说我们有如下代码:

for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}

如果fun不是纯函数,那么fun(10)将会被执行1000次,但是如果fun是一个纯函数,那么由于对于给定的输入它的输出是确定的,所以上面的代码可以被优化成:

const result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}
  1. 纯函数更易于测试

纯函数的测试不依赖于外部因素,多亏了纯函数的特性,我们给纯函数编写单元测试时只要简单地给个输入然后判断输出是否与预期一致就好了。

还用上面的double(num)函数为例,我们写单元测试就只需要这么写:

const x = 1;
assert.equals(double(x),2);

如果不是纯函数,我们就会有许多外部的因素需要考虑,mock数据之类的。

高阶函数

高阶函数至少要满足下面条件中的一个:

  1. 接受函数作为参数
  2. 把函数作为结果返回

不了解函数式编程的同学可能感觉有些怪异,函数本来是计算结果的,返回另一个函数,这有什么用场?哎,用处可大了,使用高阶函数可以让我们的代码变得更加简单灵活。

我们还是来看个具体的例子吧,假设我们有一个数组,我们想用它来创建一个新的数组,这个新数组中每个元素是之前的数组对应位置的元素+1。

不用高阶函数的话,我们大概会这么写:

const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] + 1);
}

但是JavaScript的数组对象有一个map方法,这个map方法接受一个回调,会对当前数组对象的每一个元素应用这个回调,返回一个新数组。

const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item + 1;
});
console.log(arr2);

我们的代码是不是看起来更简洁了?这个map函数就是一个高阶函数,map有映射的意思,我们扫一眼很快就能明白这段代码声明了对于原来对象的转换,基于原来的数组对象的元素创建一个新的数组。高阶函数的强大可不止这么点,咱们接着往下看。

函数缓存

假设我们有个很耗时的纯函数:

function computed(str) {    
    // 就当这里是很耗时的计算      
    console.log('执行了10分钟')
    // 这是计算结果
    return '算出来了'
}

为了避免不必要的重复计算,我们可以缓存一些之前已经计算过的结果。这样再后面再遇到相同的计算时,我们可以从缓存中直接取出结果。我们在这儿需要编写一个名为cached的函数去包装我们实际要调用的函数,这个函数把目标函数作为参数,返回一个新的函数。在这个cached函数里,我们缓存之前函数调用的结果。

function cached(fn){
  // 这边使用一个对象做缓存
  const cache = Object.create(null);

  //返回一个对目标函数加上了缓存逻辑的函数
  return function cachedFn (str) {

    //如果缓存里没有,我们会执行目标函数
    if ( !cache[str] ) {
        let result = fn(str);

        //把计算结果缓存起来
        cache[str] = result;
    }

    return cache[str]
  }
}

我们可以看到之后再输入相同的参数后我们可以直接拿到计算结果了。

懒函数

函数体里面会包含各种各样的条件语句,有时候这些条件语句仅仅需要执行一次,比如说我们写单例的时候判断某个对象是否为空,如果为空我们就创建一个对象,那其实我们知道后续只要程序还在运行,这个对象是不可能为空的,但是我们每次使用时都还会判断是否为空,都会执行我们的条件判断。我们可以稍微提升一下性能通过在第一次执行后删除这些条件判断,这样后面就不判断是否为空直接拿来即用了,这就是懒函数

我们把上面的描述用简单的代码表现出来:

let instance = null;
function user() {
    if ( instance != null) {
      return instance;
    } else {
      instance = new User()
      return instance;
    }
}

上面的代码在每次执行的时候都会执行条件判断,这边还好,如果我们的条件判断非常复杂,那其实也是一个不小的性能影响,这时候我们就可以使用懒函数的小技巧来优化代码:

var user = function() {
    var instance = new User();
    user = function() {
        return instance;
    };
    return user();
}

这样在第一次执行后,我们用一个新函数重写了之前的函数,后面再执行这个函数的时候我们都会直接返回一个固定的值,这无疑会提高我们代码的性能。所以后续我们遇到一些只用执行一次的条件语句,我们都可以用懒函数来优化它,通过使用一个新函数来覆盖原有的函数来移除条件语句。

函数柯里化

柯里化简单来说就是把一个接受多个参数的函数转化成一串接受单个参数的函数,这么说可能有点绕,其实就是把一个一次性接受一堆参数的函数,转化成接受第一个参数返回一个接受第二个参数的函数,这个函数返回一个接受第三个参数返回一个接受第四个参数的函数,以此类推。

可能好多同学第一次遇到不知道它有什么用,能一次调用完为什么要整这么花里胡哨呢?

  1. 柯里化可以让我们避免重复传相同的值
  2. 这其实上是创建了一个高阶函数,方便我们处理数据

我们来看一个简单的求和的函数,它接受三个数字作为参数并返回它们的和。

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

多几个少几个参数都可以成功调用它:

sum(1,2,3) --> 6 
sum(1,2) --> NaN
sum(1,2,3,4) --> 6 //多余的参数被忽略了

那么怎样我们才能把它转化成一个柯里化的版本呢?

function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}

看个例子:

我们可以获得跟之前一梭子传递所有参数一样的结果,同时我们还可以在任何一步中缓存之前计算的结果,比如我们这次要传入(1,2,3,6),那我们是可以避免对前面三个参数进行重复计算的。

函数组合

假设我们需要实现一个把给定数字乘10然后转成字符串输出的功能,那我们需要做的有两件事:

  • 给定数字乘10
  • 数字转字符串

我们拿到手大概会这么写:

const multi10 = function(x) { return x * 10; };
const toStr = function(x) { return `${x}`; };
const compute = function(x){
    return toStr(multi10(x));
};

这边只有两步,所以看起来不复杂,实际情况是如果有更多的操作的话,层层嵌套很难看也容易出错,类似于这样fn3(fn2(fn1(fn0(x))))。为了避免这种情况,把调用层级扁平化,我们可以写一个compose函数专门用来把函数调用组合到一起:

const compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};

之后我们的compute函数就可以这么写了:

let `compute` = compose(toStr, multi10);
compute(8);

通过使用compose函数我们可以把两个函数组合成一个函数,这让代码从右往左执行,而不是层层计算某个函数的结果作为另一个函数的参数,这样代码也更加直观。但是现在compose仅仅支持两个参数,没关系我们可以写一个支持任意参数的版本:

function compose(...funs){
    return (x)=>funs.reduce((acc, fun) => fun(acc), x)
}

现在我们的compose函数对于参数个数不再有限制了:

通过函数组合,我们可以可以声明式地指定函数间的关系,代码的可读性也大大提高,也方便我们后续对代码进行扩展跟重构,而且在React里面,当我们的高阶组件变多的时候,一个套着一个就很难看,我们就可以通过类似的方式来让我们的高阶组件层级扁平化。

好啦,今天的分享就到这里啦,我们可以看到还是有很多我们可以玩转的技巧的,把这些技巧运用起来,让我们的代码更加优雅吧~ happy coding~