教你认清这8大杀手锏

前言

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

结语

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容