美文网首页
教你认清这8大杀手锏

教你认清这8大杀手锏

作者: 谦龙 | 来源:发表于2017-05-11 09:37 被阅读17次

    前言

    underscore.js源码分析第三篇,前两篇地址分别是

    那些不起眼的小工具?

    (void 0)与undefined之间的小九九

    本篇原文链接

    源码地址

    😔看了很多篇技术文章,却依然写不好前端。

    从步入程序猿这个大坑开始到现在,已经看过数不清的技术文章和书籍,有的是零散的知识,有的是系列权威的教程,但为毛还写不好挚爱的前端,听说过一句话,这个世界又不是只有你一个人深爱而不得。但纵使如此,我也要技术这条路上一路走到黑。直到天涯迷了路,海角翻了船。

    开始

    今天想说几个类似我们平常的工作中经常用到的几个宝贝,姑且把他叫做杀手锏好了,因为实在是特别好用呀,他们分别是...

    1. each
    2. map
    3. reduce
    4. reduceRight
    5. find
    6. filter
    7. every
    8. some

    接下来我们从下划线underscore.js的视角,一步步看他们的内部运行的原理是什么....

    1 _.each(list, iteratee, [context])

    遍历list中的所有元素,按顺序用遍历输出每个元素,如果传递了context,则将iteratee函数中的this绑定到context上。

    先来看一下怎么使用

    
    let arr = ['name', 'sex']
    let obj = {
      name: 'qianlongo',
      sex: 'boy'
    }
    
    // 不传入context
    // 遍历数组
    _.each(arr, console.log) 
    // name 0 (2) ["name", "sex"]
    // sex 1 (2) ["name", "sex"]
    
    // 遍历对象
    _.each(obj, console.log)
    // qianlongo name {name: "qianlongo", sex: "boy"}
    // boy sex  {name: "qianlongo", sex: "boy"}
    
    
    // 传入context
    _.each(arr, function (val, key, arr) {
      console.log(this[val])
    }, obj)
    // qianlongo
    // boy
    

    可以看出下划线的each和原生的数组forEach有些类似也有不同的地方

    原生的forEach只可以遍历数组,而下划线的each还可以遍历对象。接下来你想不想一起看下下划线是怎么实现的。come on!!!

    源码

    _.each = _.forEach = function(obj, iteratee, context) {
      // 优化遍历函数iteratee,将iteratee中的this动态设置为context
      iteratee = optimizeCb(iteratee, context); 
      var i, length;
      if (isArrayLike(obj)) { // 如果是类数组类型的obj
        for (i = 0, length = obj.length; i < length; i++) {
          // iteratee接收的三个参数分别是 数组的值,数组的索引,以及数组本身
          iteratee(obj[i], i, obj); 
        }
      } else { // 支持对象类型的数据迭代
        var keys = _.keys(obj); // 拿到obj自身的所有keys
        for (i = 0, length = keys.length; i < length; i++) {
          // iteratee接收的三个参数分别是 obj的属性值,obj的属性,obj本身
          iteratee(obj[keys[i]], keys[i], obj);
        }
      }
      return obj; // 最后将obj返回
    };
    
    
    

    😉,其实也没有那么难理解是吧!开始map函数之旅吧

    2 _.map(list, iteratee, [context])

    通过iteratee将list中的每个值映射到一个新的数组中(注:产生一个新的数组。y = f(x),类似高中学过的知识,将x通过f()映射为一个新的数

    使用案例

    
    let arr = ['qianlongo', 'boy']
    let obj = {
      name: 'qianlongo',
      sex: 'boy'
    }
    
    // list是个数组的时候
    _.map(arr, (val, index) => {
      return `hello : ${val}`
    })
    // ["hello : qianlongo", "hello : boy"]
    
    // list是个对象的时候
    _.map(obj, (val, key, obj) => {
      return `hello : ${val}`
    })
    // ["hello : qianlongo", "hello : boy"]
    
    

    当然还可以传入第三个参数context,其本质如each一般,也是让iteratee函数中的this动态设置为context

    源码

    
     _.map = _.collect = function(obj, iteratee, context) {
      // 可以将这里的内部cb函数理解为绑定iteratee的this到context
      iteratee = cb(iteratee, context);
      // 非类数组对象就获取obj的keys,这里如果是类数组最后得到的keys为undefined
      var keys = !isArrayLike(obj) && _.keys(obj),
          length = (keys || obj).length,
          results = Array(length); // 创建一个和obj长度空间一样的数组
      for (var index = 0; index < length; index++) {
        // 注意这里,keys存在则代表obj是个对象,所以要拿到keys中的值,否则是类数组的话,直接用index索引就好了
        var currentKey = keys ? keys[index] : index;
        // 看到了吗,这里将iteratee执行后的返回值塞到了results数组中
        results[index] = iteratee(obj[currentKey], currentKey, obj);
      }
      return results; // 最后将映射之后的数组返回
    };
    
    
    

    通过源码可以看到map的实现思路

    1. 创建一个�即将返回的数组
    2. 遍历list(可以为数组也可以为对象),将list的元素输入到传进来的iteratee函数中,并将其执行后的返回值填充进数组。这个iteratee负责映射规则

    3 _.every(list, [predicate], [context])

    当list中的所有的元素都可以通过predicate的检测,那么结果返回true,否则false

    使用案例

    let arr = [-1, -3, -6, 0, 3, 6, 9]
    let obj = {
      name: 'qianlongo',
      sex: 'boy'
    }
    
    let result = _.every(arr, (val, key, arr) => {
      return val > 0
    })
    // false
    
    let result2 = _.every(obj, (val, key, obj) => {
      return val.indexOf('o') > -1
    })
    // true
    
    

    使用起来蛮简单的,传入一个谓词函数(返回值是一个布尔值的函数),最后得到true或者false。

    源码

    _.every = _.all = function(obj, predicate, context) {
      // 可以将这里的内部cb函数理解为绑定iteratee的this到context
      predicate = cb(predicate, context);
      // 短路写法,非类数组则获取其keys
      var keys = !isArrayLike(obj) && _.keys(obj),
          length = (keys || obj).length;
      for (var index = 0; index < length; index++) {
        // keys若能转化为"真" 则说明obj是对象类型
        var currentKey = keys ? keys[index] : index; 
        // 只要有一个不满足就返回false,中断迭代
        if (!predicate(obj[currentKey], currentKey, obj)) return false;
      }
      return true; // 否则所有元素都通过判断返回true
    };
    

    4 _.some(list, [predicate], [context])

    如果list中有任何一个元素通过 predicate的检测就返回true。否则返回false,和every恰好有点相反的意思。

    使用案例

    let arr = [-1, -3, -6, 0, 3, 6, 9]
    let obj = {
      name: 'qianlongo',
      sex: ''
    }
    
    let result = _.some(arr, (val, key, arr) => {
      return val > 0
    })
    // true 因为至少有一个元素 >0
    
    let result2 = _.some(obj, (val, key, obj) => {
      return val.indexOf('o') > -1
    })
    // true 两个都包含'o' 当然返回true
    
    

    源码中是怎么实现的呢,与every唯一不同的地方在返回true还是falase之处?

    源码

    _.some = _.any = function(obj, predicate, context) {
      predicate = cb(predicate, context);
      var keys = !isArrayLike(obj) && _.keys(obj),
          length = (keys || obj).length;
      for (var index = 0; index < length; index++) {
        var currentKey = keys ? keys[index] : index;
        if (predicate(obj[currentKey], currentKey, obj)) return true; // 只要有一个满足条件就返回true
      }
      return false; // 所有都不满足则返回false
    };
    
    

    5 _.find(list, predicate, [context])

    遍历list中的元素,返回第一个通过predicate函数检测的值。

    使用案例

    
    let arr = [-1, -3, -6, 0, 3, 6, 9]
    let obj = {
      sex: 'boy',
      name: 'qianlongo'
    }
    let result = _.find(arr, (val, key, arr) => {
      return val > 0
    })
    // 3
    let result2 = _.find(obj, (val, key, obj) => {
      return val.indexOf('o') > -1
    })
    // boy
    

    源码

    
    _.find = _.detect = function(obj, predicate, context) {
      var key;
      if (isArrayLike(obj)) {
        // 当传入的是类数组的时候,调用findIndex方法,结果是>= -1的数组
        key = _.findIndex(obj, predicate, context);
      } else {
        // 当传入的是一个对象的时候,调用findKey,结果是一个字符串属性或者undefined
        key = _.findKey(obj, predicate, context);
      }
      // 返回符合条件的value,否则没有返回值,即默认的undefined
      if (key !== void 0 && key !== -1) return obj[key]; 
    };
    
    
    

    _.findIndex_.findKey在后面会一一分析,目前理解find函数知道他们怎么用就好。

    6 _.filter(list, predicate, [context])

    遍历list,返回包含所有通过predicate检测的元素(结果是个数组)

    使用案例

    
    let arr = [-1, -3, -6, 0, 3, 6, 9]
    let obj = {
      sex: 'boy',
      name: 'qianlongo',
      age: 100
    }
    let result = _.filter(arr, (val, key, arr) => {
      return val > 0
    })
    // [3, 6, 9]
    let result2 = _.filter(obj, (val, key, obj) => {
      return `${val}`.indexOf('o') > -1 // 使用模板字符串是防止100没有indexOf方法而报错
    })
    // ["boy", "qianlongo"]
    
    
    

    聪明的你是不是已经想到了源码是怎么实现的了 😉

    源码

    _.filter = _.select = function(obj, predicate, context) {
      var results = [];
      // 绑定predicate的this作用域到context
      predicate = cb(predicate, context);
      // 用each方法对obj进行遍历
      _.each(obj, function(value, index, list) {
        // 符合predicate过滤条件的,就把对应的值塞到results数组中
        if (predicate(value, index, list)) results.push(value);
      });
      return results; // 最后返回
    };
    
    

    最后是reduce和reduceRight,两个相对来说更难一些的api,虽然已经过了12点了,手动困乏😪, 我们咬咬牙坚持一下,把最后两个说完

    7 _.reduce(list, iteratee, [memo], [context]),

    别名为 inject 和 foldl, reduce方法把list中元素归结为一个单独的数值。Memo是reduce函数的初始值,reduce的每一步都需要由iteratee返回。这个迭代传递4个参数:memo, value 和 迭代的index(或者 key)和最后一个引用的整个 list

    8 _.reduceRight(list, iteratee, memo, [context])

    reducRight是从右侧开始组合的元素的reduce函数

    使用案例

    var arr = [0, 1, 2, 3, 4, 5],
      sum = _.reduce(arr, (init, cur, i, arr) => {
        return init + cur;
      });   
      
      // 15
    

    我们来看一下上面的执行过程是怎样的。

    第一回合

    // 因为initialValue没有传入所以回调函数的第一个参数为数组的第一项
    init = 0;
    cur = 1;
    => init + cur = 1;
    
    

    第二回合

    init = 1;
    cur = 2;
    => init + cur = 3;
    
    
    

    第三回合

    init = 3;
    cur = 3;
    => init + cur = 6;
    
    

    第四回合

    init = 6;
    cur = 4;
    => init + cur = 10;
    
    

    第五回合

    init = 10;
    cur = 5;
    => init + cur = 15;
    
    

    😭妈妈啊,终于执行完了,这么多回合才结束,哪像人家格斗高手瞬间就把太极大师整挂了

    知道了一步步执行流程,我们来看下源码到底是怎么实现的。

    源码

    // 源码还是通过调用createReduce生成的,所以主要是看createReduce这个函数
    _.reduce = _.foldl = _.inject = createReduce(1);
    
    
    

    这尼玛看起来好吓人啊,不怕,我们一点点来分析

    function createReduce(dir) {
        // Optimized iterator function as using arguments.length
        // in the main function will deoptimize the, see #1991.
        function iterator(obj, iteratee, memo, keys, index, length) { // 真正执行迭代的地方
          for (; index >= 0 && index < length; index += dir) {
            var currentKey = keys ? keys[index] : index; // 如果keys存在则认为是obj形式的参数,所以读取keys中的属性值,否则类数组只需要读取索引index即可
            memo = iteratee(memo, obj[currentKey], currentKey, obj); // 接着就是执行外部传入的回调了,并将结果赋值为memo,也就是我们最后要到的值
          }
          return memo;
        }
    
        return function(obj, iteratee, memo, context) {
          iteratee = optimizeCb(iteratee, context, 4); // 首先绑定一下this作用域
          var keys = !isArrayLike(obj) && _.keys(obj), // 如果不是类数组就读取其keys
              length = (keys || obj).length,
              index = dir > 0 ? 0 : length - 1; // 默认开始迭代的位置,从左边第一个开始还是右边第一个
          // Determine the initial value if none is provided.
          if (arguments.length < 3) { // 如果没有传入初始化值,则将第一个值(左边第一个或者右边第一个)作为初始值
            memo = obj[keys ? keys[index] : index];
            index += dir; // 从索引为1开始或者索引为length - 2开始迭代
          }
          return iterator(obj, iteratee, memo, keys, index, length); // 接着开始进入自定义的迭代函数,请往上看
        };
      }
    
    

    结语

    夜深人静,有点困乏了。希望这篇文章对大家有点作用。如果对前几篇源码分析的文章感兴趣,欢迎前往顶部地址查看

    不介意的话,在文章开头的源码地址那里点一个小星星吧😀

    不介意的话,在文章开头的源码地址那里点一个小星星吧😀

    不介意的话,在文章开头的源码地址那里点一个小星星吧😀

    相关文章

      网友评论

          本文标题:教你认清这8大杀手锏

          本文链接:https://www.haomeiwen.com/subject/emantxtx.html