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

对于咱们程序员来讲,写函数是再熟悉不过的事情了,无论咱们要实现什么样的功能,都须要经过函数来完成。在JavaScript里面,函数拥有很是高的特权,甚至是一等公民,所以也跟Kotlin同样支持多种编程范式。javascript

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

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

纯函数

纯函数要知足两个条件:程序员

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

来看以下代码:编程

function double(num){
  return num * 2 
}
复制代码

这边只要给num的值不变,它返回的结果也不会变,并且这个函数执行的过程当中没有对外界形成影响,因此它是一个纯函数。数组

而:缓存

const counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()
复制代码

20200929120128.jpg

这个函数每次执行时结果都不同,因此不是纯函数。markdown

而:app

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~

相关文章
相关标签/搜索