前言
underscore.js源码分析第三篇,前两篇地址分别是
😔看了很多篇技术文章,却依然写不好前端。
从步入程序猿这个大坑开始到现在,已经看过数不清的技术文章和书籍,有的是零散的知识,有的是系列权威的教程,但为毛还写不好挚爱的前端,听说过一句话,这个世界又不是只有你一个人深爱而不得。但纵使如此,我也要技术这条路上一路走到黑。直到天涯迷了路,海角翻了船。
开始
今天想说几个类似我们平常的工作中经常用到的几个宝贝,姑且把他叫做杀手锏好了,因为实在是特别好用呀,他们分别是...
- each
- map
- reduce
- reduceRight
- find
- filter
- every
- 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的实现思路
- 创建一个�即将返回的数组
- 遍历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); // 接着开始进入自定义的迭代函数,请往上看
};
}
结语
夜深人静,有点困乏了。希望这篇文章对大家有点作用。如果对前几篇源码分析的文章感兴趣,欢迎前往顶部地址查看
不介意的话,在文章开头的源码地址那里点一个小星星吧😀
不介意的话,在文章开头的源码地址那里点一个小星星吧😀
不介意的话,在文章开头的源码地址那里点一个小星星吧😀