lodash 源码解析 —— slice
一直听说lodash源码写的非常好,乘着上班之余,阅读了下lodash的源码,感觉写的非常清晰,考虑的非常全面。今天对于slice源码进行解读,记录下自己的学习过程和疑难点。
不知道你有没有一样的困惑,为什么lodash会实现这个slice方法,原生数组不是提供了一个slice方法吗?刚开始我也是有一样的困惑,后面找资料发现这个是有意义的,lodash 的作者已经在 why not the 'baseslice' func use Array.slice(), loop faster than slice? 的 issue 中给出了答案:lodash 的 slice 会将数组当成密集数组对待,原生的slice会将数组当成稀疏数组对待。
稀疏数组VS密集数组
如果数组是稀疏的,那么这个数组中至少有一个以上的位置不存在元素(包括 undefined ),也就是数组长度要比数组个数要多。
定义一个稀疏数组也很简单:
const sparse = new Array(10)
其中 sparse 的 length 为10,但是 sparse 数组中没有元素,是稀疏数组;
定义一个密集数组也很简单:
const dense = new Array(10).fill(undefined)
而 dense 每个位置都是有元素的,虽然每个元素都为undefined,为密集数组 。
那稀疏数组和密集数组有什么区别呢?在 lodash 中最主要考虑的是两者在迭代器中的表现。
稀疏数组在迭代的时候会跳过不存在的元素。
sparse.forEach(function(item){
console.log(item)
})
dense.forEach(function(item){
console.log(item)
})
sparse 根本不会调用 console.log 打印任何东西,但是 dense 会打印出10个 undefined 。
源码和解读
// array 是传入的原数组,相比于原生的slice强大之处是这个方法可以传入非数组,容错率更大
// start是截取的开始下标
// end是截取的尾下标
function slice(array, start, end) {
// 根据array是否是数组取其长度 ,如果不是数组,返回一个空数组
let length = array == null ? 0 : array.length;
if (!length) {
return [];
}
// 如果start未传,则从第0位索引开始
start = start == null ? 0 : start;
// 同理如果end未传,则截取到数组长度索引
end = end === undefined ? length : end;
// 考虑到可能 需要从数组尾部开始截取 start可能为负值 当-start>length 则start为0,否则为length + start ,相信这里是挺好理解的
if (start < 0) {
start = -start > length ? 0 : length + start;
}
// 考虑用户输入的范围超过了数组长度,此时end应该赋值为length,如果输入为负值,则加上length
end = end > length ? length : end;
if (end < 0) {
end += length;
}
// 判断 start 和 end的大小,合理的情况是 start < end
// 所以当开始索引大于结束索引时,不合理的区间,截取长度设置为0
// 合理的话把 end - start 的结果转为十进制数后,赋值给 length
// >>> 这里是无符号的移位 移动0位的意义在于需要将其转化为十进制的整数
length = start > end ? 0 : (end - start) >>> 0;
start >>>= 0;
// 新数组的长度 length,并且创建一个长度为 length 的新数组
let index = -1;
const result = new Array(length);
// 循环加入 开始截取
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
export default slice;
截取并返回新数组
let index = -1
const result = new Array(length)
while (++index < length) {
result[index] = array[index + start]
}
return result
用 while 循环,从 start 位置开始,获取原数组的值,依次存入新的数组中。
因为是通过索引取值,如果遇到稀疏数组,对应的索引值上没有元素时,通过数组索引取值返回的是 undefined, 但这并不是说稀疏数组中该位置的值为 undefined 。
看到这里已经到了尾声了,但考虑到肯定有很多小伙伴对' >>> '这个操作符号会有些困惑,这里多做些解释。
js 做位运算时,小数部分怎么处理呢?
- 答案是直接丢弃
js 中非数值类型如何进行位运算的呢?
- 当 js 需要进行位运算的时候,对于非数值类型,会首先将操作数转成一个整型(就是0)然后在进行运算。
js 中的无符号右移的特别之处?
- js在移动位数大于0的时候与其他语言没有啥区别
- 当移动位数为0且数值为负数的时候,此时js就会出现独特之处 >>>0 实际上并没有发生数位变化,但是 js 却会把符号位替换成 0,这是js的移位特征
// java
8 >>> 0 // 8
5 >>> 1 // 2
-1 >>> 1 // 2147483647
-1 >>> 0 // -1
// js
8 >>> 0 // 8
5 >>> 1 // 2
-1 >>> 1 // 2147483647
// 对比java 发现大大的不同
-1 >>> 0 // 4294967295
js 中无符号右移时,不管正数、负数都会首先将符号位替换成 0,然后再进行移位。也就是说,该运算符永远返回正整数。
相信到这里大家就已经明白了为啥代码中要使用 >>> 运算符了。
总结:
主要核心点是lodash 的 slice 会将数组当成密集数组对待,原生的slice会将数组当成稀疏数组对待。lodash中封装的slice相比于原生的slice的一个优势是在于传入的 array 即使不是数组,也不会抛出异常,而是会返回一个空数组。其实这一点在使用typescript的时候优势不大。start与end也做了处理,这样使得在操作的时候并不会出现乱七八槽的报错。
当然函数式的调用也是使用该方法的一大优势,最起码可以学习这里的编程思想,但不可否认的是该源码如果使用了es6语法会更加简洁,比如可以使用es6的参数默认值,如果加上ts的话该方法会更加健壮,用户使用会更加明了。好了,到这里就结束了,谢谢大家的观看,一起加油一起学习。