前言

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

那些不起眼的小工具?

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

本篇原文链接

源码地址

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

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

<!--more-->

开始

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

  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); // 接着开始进入自定义的迭代函数,请往上看
    };
  }

结语

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

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

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

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


前端胖头鱼
3.7k 声望6.2k 粉丝