Lodash 源码阅读-baseSlice
功能概述
baseSlice 函数是 Lodash 中的一个内部工具函数,用于创建数组的切片(slice)。它是 slice、initial、tail 等多个数组操作函数的核心实现,负责处理数组切片的底层逻辑。与 JavaScript 原生的 Array.prototype.slice 方法类似,baseSlice 可以从指定的起始位置到结束位置(不包括结束位置)提取数组的一部分,并返回一个新数组。它支持负数索引,并能够处理各种边界情况,确保返回的切片符合预期。
前置学习
在深入理解 baseSlice 函数之前,建议先了解以下相关概念:
- JavaScript 数组切片:理解 Array.prototype.slice 的工作原理
- 位运算 :特别是无符号右移运算符(
>>>
)的用法和效果 - JavaScript 中的数组索引:包括正向索引和负向索引的概念
源码实现
js
function baseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : length + start;
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : (end - start) >>> 0;
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
实现原理解析
原理概述
baseSlice 函数的实现采用了多步骤的处理策略,确保在各种输入情况下都能正确地创建数组切片。其核心原理是:
- 首先处理起始位置(start)和结束位置(end)的边界情况,包括负数索引的转换
- 然后计算结果数组的长度,并使用位运算确保长度为非负整数
- 最后创建一个新数组,并从原数组中复制相应的元素
这种实现方式既能处理常规的切片操作,又能优雅地处理各种边界情况,如负数索引、超出范围的索引等。通过使用位运算和预分配数组长度等技巧,baseSlice 还能在性能上有所优化。
代码解析
1. 变量初始化
js
var index = -1,
length = array.length;
这段代码初始化了两个变量:
index
:用于遍历数组的索引,初始值为-1(为了配合后面的前置递增操作)length
:原数组的长度,用于后续的边界检查和计算
2. 处理负数起始位置
js
if (start < 0) {
start = -start > length ? 0 : length + start;
}
这段代码处理了负数起始位置的情况:
- 如果
start
小于 0,表示从数组末尾开始计数 - 如果
-start
大于数组长度,说明起始位置超出了数组的开头,此时将start
设为 0 - 否则,将
start
转换为等价的正向索引:length + start
例如,对于数组[a, b, c, d]
(长度为 4):
- 起始位置-2 表示从倒数第二个元素开始,即索引 2(c)
- 起始位置-6 超出了数组长度,会被设置为 0,即从数组开头开始
3. 处理结束位置
js
end = end > length ? length : end;
if (end < 0) {
end += length;
}
这段代码处理了结束位置的边界情况:
- 如果
end
大于数组长度,将其限制为数组长度 - 如果
end
小于 0,将其转换为等价的正向索引:end + length
这确保了结束位置始终在有效范围内,并且能够正确处理负数索引。
4. 计算结果数组长度
js
length = start > end ? 0 : (end - start) >>> 0;
start >>>= 0;
这段代码计算了结果数组的长度,并使用位运算进行优化:
- 如果
start
大于end
,则长度为 0(空数组) - 否则,长度为
end - start
>>> 0
是无符号右移 0 位的操作,它能将值转换为无符号 32 位整数,确保结果为非负整数start >>>= 0
同样确保起始位置为非负整数
位运算的详细作用
在这段代码中,>>> 0
(无符号右移零位)操作看起来似乎没有实际的位移动作,但它实际上有几个重要作用:
-
类型转换:将任何值强制转换为 32 位无符号整数
- 对于非数字值,会先尝试转换为数字,然后再转换为无符号整数
- 对于小数,会截断小数部分(向下取整)
- 对于 NaN 和 Infinity,会转换为 0
-
处理负数:将负数转换为对应的正数表示
- 例如,-1 经过
>>> 0
后会变成 4294967295(2^32 - 1) - 但在 baseSlice 中,由于前面的逻辑已经确保了
end - start
不会为负数,所以这里主要是确保结果为正整数
- 例如,-1 经过
-
边界处理:确保结果在 JavaScript 数组索引的有效范围内
- 如果结果超过 2^32 - 1,会自动取模,确保结果在有效范围内
这种位运算技巧在 JavaScript 中经常用于确保数组索引和长度值的有效性,特别是在需要处理各种边界情况的底层函数中。
以下是一些具体示例,说明 >>> 0
的效果:
js
// 全部被注释掉的原因会被格式化成奇怪的格式
//5 >>> 0 // 5 - 正整数保持不变
//-5 >>> 0 // 4294967291 - 负数转换为大正数
//3.7 >>> 0 // 3 - 小数被截断
//NaN >>> 0 // 0 - NaN 转换为 0
//Infinity >>> 0 // 0 - Infinity 转换为 0
//"42" >>> 0 // 42 - 字符串先转换为数字,再转换为无符号整数
在 baseSlice 函数中,使用这种位运算有以下优势:
- 性能优化:位运算通常比其他类型转换方法(如 Math.floor 或 parseInt)更快
- 代码简洁:一个操作符完成了类型转换和边界检查
- 安全保障:确保数组长度和索引始终为有效的非负整数,避免因为无效索引导致的错误
5. 创建并填充结果数组
js
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
最后,这段代码创建并填充了结果数组:
- 首先创建一个指定长度的新数组
- 然后使用 while 循环,将原数组中从
start
开始的元素复制到新数组中 - 最后返回填充好的新数组
通过预先分配数组长度,而不是使用 push 方法动态增长数组,可以在处理大型数组时提高性能。
使用示例
由于 baseSlice 是 Lodash 的内部函数,通常不会直接使用它,而是通过 slice、initial、tail 等公开 API 间接调用。但为了理解其工作原理,以下是一些模拟的使用示例:
1. 基本切片操作
js
// 模拟baseSlice函数
function myBaseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : length + start;
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : (end - start) >>> 0;
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
var array = [1, 2, 3, 4, 5];
// 从索引1到索引3(不包括3)
myBaseSlice(array, 1, 3); // [2, 3]
// 从开头到索引3(不包括3)
myBaseSlice(array, 0, 3); // [1, 2, 3]
// 从索引2到末尾
myBaseSlice(array, 2, array.length); // [3, 4, 5]
2. 处理负数索引
js
var array = [1, 2, 3, 4, 5];
// 从倒数第二个元素到末尾
myBaseSlice(array, -2, array.length); // [4, 5]
// 从索引1到倒数第二个元素(不包括倒数第二个)
myBaseSlice(array, 1, -2); // [2, 3]
// 从倒数第三个元素到倒数第一个元素(不包括倒数第一个)
myBaseSlice(array, -3, -1); // [3, 4]
3. 处理边界情况
js
var array = [1, 2, 3, 4, 5];
// 起始位置超出数组开头
myBaseSlice(array, -10, 3); // [1, 2, 3]
// 结束位置超出数组末尾
myBaseSlice(array, 2, 10); // [3, 4, 5]
// 起始位置大于结束位置
myBaseSlice(array, 3, 1); // []
// 起始位置等于结束位置
myBaseSlice(array, 2, 2); // []
4. 在其他 Lodash 函数中的应用
js
// 模拟initial函数(获取除最后一个元素外的所有元素)
function myInitial(array) {
return array && array.length ? myBaseSlice(array, 0, -1) : [];
}
// 模拟tail函数(获取除第一个元素外的所有元素)
function myTail(array) {
return array && array.length ? myBaseSlice(array, 1, array.length) : [];
}
var array = [1, 2, 3, 4, 5];
myInitial(array); // [1, 2, 3, 4]
myTail(array); // [2, 3, 4, 5]