Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

教你认清这8大杀手锏 #5

Open
qianlongo opened this issue Apr 29, 2018 · 0 comments
Open

教你认清这8大杀手锏 #5

qianlongo opened this issue Apr 29, 2018 · 0 comments

Comments

@qianlongo
Copy link
Owner

前言

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

结语

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant