重要的理论

  • 函数式的思想:输入值 —— 流水线式处理 —— 输出值
  • 函数式编程一个关键:用较低级的函数逐步定义和使用离散功能。(就是尽量拆解、抽象,通用、相似的部分都抽象出来)
  • 对数据集合的操作:最好对数据集合构建抽象关系运算符,就像 SQL 一样

underscore 常用函数使用场景和案例

只有想不到没有做不到,熟能生巧,多用用就知道该用什么函数解决问题了

  • reduce
  • identity
  • map
  • filter

  • chain 链式调用。
    • 建议写成链式调用,而不是声明多余的中间值。
    • 配合 tap,在 chain 的过程中可以获取中间值进行处理

        _.chain([])
            .push('Take one down.')
            .tap(function(lyrics){
                if(n > 1){
                    lyrics.push((n-1) + 'bottles of beer on the wall.');
                }
            })
            .value(); // 用 .value() 获取 _.chain 包装的数据的值
      
  • _.range 生成数字数组 _.range([start], stop, [step])
    • 使用场景:

        // 配合 _.map, 指定重复次数
        _.map(_.range(3), function(){return 'hello'});
      

通用技巧

不要怕,不过就是组合来组合去

方法变函数

  • 优先使用函数而不是方法。把方法变成函数:

      /**
       * 接收一个方法,在任何给定的对象上调用它 ⭐️
       * 意义在于:
       *     把方法变成了函数,比如下面的 map,本来是 arr.map 现在变成了一个函数
       *     并且做好了兼容,对象调用本身没有的方法时,就返回 undefined
       */
      function invoker(name, method){
          return function(target){
              if(!existy(target)){
                  console.warn('must provide a target');
                  return ;
              }
              var targetMethod = target[name]; // 得到方法
              var args = _.rest(arguments); // 调用对象就要放到参数中,而原来的参数就是 _.rest(arguments)
    
              return doWhen(existy(targetMethod) && method===targetMethod, function(){
                  return targetMethod.apply(target, args);
              });
          };
      }
      var rev = invoker('reverse', Array.prototype.reverse);
    
      rev([3, 4, 5]); // [5, 4, 3]
    

数组降维

  • 场景:传入参数是数组,用 arguments 获取参数就变成了二维数组,这时需要把 arguments 降维。其实传入平面参数,arguments 就是一维的,但是如果参数一定要是数组呢?

      // 没有降维
      var addArrayElements = function(){
          return _.reduce(arguments, add, 0); // arguments 是二维数组,[[1, 2, 3]] 要降维成 [1, 2, 3]
      };
      addArrayElements([1, 2, 3, 4]); // 01,2,3,4
    
  • apply 封装函数降维

      // 封装 func,用 apply 的方式调用 func,就会把参数降维
      function splat(func){
          return function(arr){
              return func.apply(null, arr); // arr 就变成了平面元素
          };
      }
      // arguments 降维了,arguments 就是一维数组
      var addArrayElements = splat(function(){
          return _.reduce(arguments, add, 0);
      });
    
      addArrayElements([1, 2, 3, 4]); //10
    
  • apply 配合 identity

      var addArrayElements = function(){
          var args = _.identity.apply(null, arguments); // arguments 是 [ [1, 2, 3, 4] ],降成了 [1, 2, 3, 4]
          return _.reduce(args, add, 0);
      };
      addArrayElements([1, 2, 3, 4]); // 10
    

通用比较器

  • 比较器跟谓词函数有关,独立定义比较器 的问题就是返回值是不确定的,只能作为一种约定。需要统一比较器的返回值。

      // 比较器,返回 -1/0/1,可以传到 sort 之类的函数中
      // 问题:这个比较器不够通用
      function compareLessThanOrEqual(x, y){
          if(x < y) return -1; // 万一返回值被别人修改成 -2 了呢,另外再写一个比较器,又要注意返回值的统一
          if(x > y) return 1;
          return 0;
      }
    
  • 解决思路:把比较器拆成 谓词 + 结果,变的是谓词的定义,根据谓词的执行结果(true/false)返回的结果是不变的(都是 -1/0/1)。这样,以后只需要定义一个谓词就可以生成一个新的比较器。

      // 更好的比较器, pred 是谓词
      function comparator(pred){
          return function(x, y){
              if(truthy(pred(x, y)))
                  return -1;
              else if (truthy(pred(x,y)))
                  return 1;
              else
                  return 0;
          }
      }
    
      // 谓词,返回布尔值
      function lessOrEqual(x, y){
          return x <= y;
      }
      [1, -2, 11].sort( comparator(lessOrEqual) );
    

像操作数据库一样操作一张表

补集

  • 从一个数组中获取数字组成新数组,补集:从一个数组中获取非数字组成新数组

    • 低级写法

        var a = ['a', 1, 'c'];
      
        _.filter(a, _.isNumber); // [1]
      
        // 补集。如果另外有一个 _.isObject 的补集,难道又要重新写一段类似的代码?
        _.filter(a, function(x){
            return !_.isNumber(x);
        }); // ['a', 'c']
      
    • 高级写法。抽象出一个补集函数

        // 把这段抽象
        // 这段的作用:返回谓词函数执行结果的相反值,这个谓词函数应该参数化
        function(x){
            return !_.isNumber(x);
        }
      		
        // 变成
        function complement(pred){
            return function(){
                return !pred.apply(null, _.toArray(arguments));
            };
        }
        _.filter(a, complement(_.isNumber); // ['a', 'c']
      

this

  • this 的值跟创建时的上下文有关,但会根据调用者改变,常常引起错误

    • 改变 this 的值,用 apply/ call
      fun.apply(obj, ...); // this 就指向 obj 了
      fun.call(obj, ...); // this 就指向 obj 了
    
    • 绑定 this 的值,用 _.bind_.bindAll(绑定多个方法到它自己的对象上)

闭包、私有变量

  • 用闭包实现私有变量,保护变量

      var ball = function(){
          var count = 0; // 私有变量
          return {
              inc: function(n){
                  return count += n;
              }
          };
      };
      ball.inc(2); // 外部只能用 inc 访问 count
    

默认参数

  • 有时传入的参数不对,比如是 null,就会破坏整个结果,例如

      var nums = [1, 2, 3, null, 5];
      console.log(
          '因为有个 null,所以乘积结果是错误的',
          _.reduce(nums, function(product, n){
              return product * n;
          })
      );
    
  • 解决:需要设置默认参数

      /**
       * 封装 func,提供默认参数
       * 只能解决一级 null/undefined
       * 对于 {name: null, age: 18} 这种二级 null 无法解决
       */
      function fnull(func /*, defaults*/){
          var defaults = _.rest(arguments); // 默认参数
    
          return function(/*args*/){ // 返回包装后的守卫函数
              // 只有在 守卫函数 被调用时才会遍历默认值,即只在需要的时候发生
              // 其实:按低级的思想,就是在函数开头先把默认函数处理一遍,而这里把这个处理的过程抽象出来了,所以不用在定义单个函数时再去处理了,只要声明默认参数就可以了
              var args = _.map(arguments, function(e, i){
                  return existy(e) ? e : defaults[i]; // 默认参数代替 null/undefined
              });
              return func.apply(null, args);
          };
      }
      var nums = [1, 2, 3, null, 5];
      var saveMult = fnull(function(product, n){
          return product * n;
      }, 1, 1);
      console.log(
          '使用 fnull 得到正确结果',
          _.reduce( nums, saveMult)
      );
    
      /**
       * 解决二级 null
       * 只要遍历参数对象,把单个空值换成对应的默认值即可
       * fnull(_.identity, defaultValue); 就可以把单个值转换成默认值
       */
      function defaults(d){ // 默认配置对象
          return function(o, k){ // object key
              var val = fnull(_.identity, d[k]); // 守卫函数,会把空参数替换成默认参数
              return o && val(o[k]);
          };
      }
      function findName(person){
          var lookup = defaults({name: 'jack'});
          return lookup(person, 'name'); // 找到 person 的 name 属性
      }
      findName({name: null}); // jack
    

对象校验器

  • 场景:需要校验传入的参数是否符合要求。低级方法:在函数开头对传入参数进行一一校验,不通过就返回错误提示。升级:需要把校验抽象出来,验证器是验证器,函数是函数,区分开来!

  • 解决:

      /**
       * 参数:若干个验证器(谓词函数)
       *     validators 是谓词函数,且带有一个 message 属性,表示错误信息
       * return: 一个 checker 函数,他会用这些验证器进行验证,如果验证错误就会添加错误信息到数组中,最后返回该数组
       * 所以返回空数组就表明通过了所有验证器
       */
      function checker(/*validators*/){ // validator 是 谓词函数
          var validators = _.toArray(arguments); // 要 toArray,因为 arguments 不是数组
    
          return function(obj){
              // 用 reduce 是一个棒棒的选择!因为它可以记录 errs
              return _.reduce(validators, function(errs, check){ // check 就是当前的验证器
                  if(check(obj)){
                      return errs; // 通过 check, errs 不变
                  }else{
                      // check 是一个验证器,它是一个函数,他还有一个 message 属性,表示错误信息
                      return _.chain(errs).push(check.message).value(); // 不通过,添加 err
                  }
              }, []);
          };
      }
    	
      // validators 要带有一个 message 属性,表示错误信息,每次都要设置很麻烦,所以抽象一个 validator 生成器, 不需要另外设置 message
      function validator(message, fun){
          // 为什么不是直接 f = fun?
          // 应该是因为这一段:
          //      message 是一个非常普通的属性名,如果给它设置值可能会抹掉正常的值,所以要新建一个 function
          var f = function(/* args */){
              return fun.apply(fun, arguments);
          };
    
          f.message = message;
          return f;
      }
    
      var gonnaFail = checker( validator('ZOMG!', always(false)) );
    	
      gonnaFail(100); // ["ZOMG!"]
    

头痛:高阶函数阶级多了就晕了。。。

链接