功能概述
compact 函数是 Lodash 中的一个实用数组方法,主要用于创建一个新数组,其中包含原数组中所有非假值(truthy)的元素。在 JavaScript 中,假值包括 false
、null
、0
、""
(空字符串)、undefined
和 NaN
。这个函数可以帮助我们快速过滤掉数组中的所有假值,保留真值元素。
源码实现
js
function compact(array) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (value) {
result[resIndex++] = value;
}
}
return result;
}
实现原理解析
原理概述
compact 函数的实现原理非常直观:遍历输入数组的每个元素,检查每个元素是否为真值(truthy),如果是,则将其添加到结果数组中。这种实现利用了 JavaScript 的布尔转换机制,任何非假值在布尔上下文中都会被视为 true
。
整个过程可以概括为:
- 创建一个空数组作为结果容器
- 遍历原数组的每个元素
- 对每个元素进行布尔值检查
- 将所有通过检查的元素(真值)添加到结果数组
- 返回过滤后的新数组
代码解析
1. 变量初始化
js
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
这段代码初始化了四个关键变量:
index
:当前遍历的索引,初始值为 -1(因为后面会先自增再使用)length
:输入数组的长度,如果数组为 null 或 undefined,则设为 0resIndex
:结果数组的当前索引位置,用于追踪结果数组的填充位置result
:用于存储过滤后元素的空数组
这里的 array == null
是一个巧妙的技巧,它同时检查了 array === null
和 array === undefined
,因为在 ==
比较中,undefined
和 null
是相等的。这样可以防止在传入 null
或 undefined
时尝试访问它们的 length
属性而导致错误。
示例:
js
// 当传入正常数组时
compact([1, 2, 3]); // length = 3
// 当传入 null 时
compact(null); // length = 0
// 当传入 undefined 时
compact(undefined); // length = 0
2. 遍历数组并过滤
js
while (++index < length) {
var value = array[index];
if (value) {
result[resIndex++] = value;
}
}
这是函数的核心部分,使用 while 循环遍历数组:
++index < length
:先将索引自增,然后检查是否小于数组长度var value = array[index]
:获取当前索引位置的元素值if (value)
:检查该值是否为真值(非假值)result[resIndex++] = value
:如果是真值,则将其添加到结果数组,并将结果索引自增
这里的 if (value)
利用了 JavaScript 的类型转换机制,会自动将 value
转换为布尔值。在 JavaScript 中,以下值会被转换为 false
:
false
null
undefined
0
NaN
""
(空字符串)
而其他所有值(包括所有对象,即使是空对象 {}
)都会被转换为 true
。
示例:
js
// 遍历 [0, 1, false, 2, '', 3] 数组
// index = -1, resIndex = 0, result = []
// 第一次循环: value = 0, 0 是假值,不添加到结果
// 第二次循环: value = 1, 1 是真值,添加到结果, result = [1], resIndex = 1
// 第三次循环: value = false, false 是假值,不添加到结果
// 第四次循环: value = 2, 2 是真值,添加到结果, result = [1, 2], resIndex = 2
// 第五次循环: value = '', '' 是假值,不添加到结果
// 第六次循环: value = 3, 3 是真值,添加到结果, result = [1, 2, 3], resIndex = 3
3. 返回结果
js
return result;
最后,函数返回过滤后的新数组,其中只包含原数组中的真值元素。
性能优化点
compact 函数的实现中有几个值得注意的性能优化点:
-
预分配结果数组 :虽然结果数组初始为空,但通过使用
resIndex
变量直接设置数组索引,避免了使用push()
方法,减少了函数调用开销。这种优化主要体现在三个方面:
-
避免函数调用开销 :
push()
是一个方法调用,每次调用都会有创建函数调用栈的开销;而直接使用索引赋值是一个简单的内存操作,没有函数调用开销。 -
减少隐藏操作 :
push()
方法内部需要处理各种边缘情况,如数组长度检查、可能的数组扩容等;直接索引赋值更加直接,减少了这些隐藏操作。 -
更好的 JIT 优化:JavaScript 引擎的即时编译器(JIT)对简单的索引赋值操作通常能进行更好的优化;相比之下,方法调用可能会阻碍某些优化。
这种优化在 V8 引擎(Chrome 和 Node.js 使用的 JavaScript 引擎)的文档中有所提及,V8 团队的博客(v8.dev/blog)中讨论了如何...
我们可以通过一个简单的性能测试来验证这一点:
js// 使用push方法的实现 function compactWithPush(array) { var index = -1, length = array == null ? 0 : array.length, result = []; while (++index < length) { var value = array[index]; if (value) { result.push(value); // 使用push方法 } } return result; } // 使用索引赋值的实现(Lodash的方式) function compactWithIndex(array) { var index = -1, length = array == null ? 0 : array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index]; if (value) { result[resIndex++] = value; // 使用索引直接赋值 } } return result; } // 性能测试 function performanceTest() { // 创建一个大数组,包含随机的真值和假值 const testArray = []; for (let i = 0; i < 1000000; i++) { testArray.push(Math.random() > 0.5 ? i : 0); } console.time("使用push方法"); compactWithPush(testArray); console.timeEnd("使用push方法"); console.time("使用索引赋值"); compactWithIndex(testArray); console.timeEnd("使用索引赋值"); } // 在浏览器控制台或Node.js环境中运行此函数可以看到性能差异 // performanceTest();
在大多数现代浏览器中,直接索引赋值通常会比
push()
方法快 10-30%,尤其是在处理大型数组时。这种性能差异在性能关键的应用中是非常有意义的。 -
-
单次遍历:整个过程只需要遍历一次数组,时间复杂度为 O(n),其中 n 是输入数组的长度。
-
直接布尔检查 :使用
if (value)
而不是更复杂的条件检查,利用 JavaScript 的类型转换机制,简化了代码并提高了性能。 -
空值处理 :通过
array == null ? 0 : array.length
优雅地处理了 null 和 undefined 输入,避免了额外的条件检查。
使用示例
js
// 过滤各种假值
_.compact([0, 1, false, 2, "", 3]);
// => [1, 2, 3]
// 处理包含 null 和 undefined 的数组
_.compact([null, undefined, 1, 2]);
// => [1, 2]
// 处理包含 NaN 的数组
_.compact([NaN, 1, 2]);
// => [1, 2]
// 处理包含对象的数组(所有对象都是真值)
_.compact([{}, [], 0, 1]);
// => [{}, [], 1]
// 处理空数组
_.compact([]);
// => []
// 处理 null 或 undefined
_.compact(null);
// => []
_.compact(undefined);
// => []
注意事项
- 保留 0 和空字符串 :如果你需要保留数字 0 或空字符串,compact 函数不适合使用,因为它会过滤掉这些值。这种情况下,应该使用
filter()
方法并提供自定义的过滤条件。
js
// 保留 0 但过滤其他假值
const filterButKeepZero = (array) =>
array.filter(
(value) =>
value !== false &&
value !== null &&
value !== undefined &&
value !== "" &&
!Number.isNaN(value)
);
filterButKeepZero([0, 1, false, 2, "", 3]);
// => [0, 1, 2, 3]
-
不改变原数组:compact 函数不会修改原始数组,而是返回一个新数组。这符合函数式编程的不可变原则,但在处理大型数组时可能会有内存开销。
-
处理特殊对象 :所有对象(包括空对象和数组)在布尔上下文中都被视为真值,因此 compact 不会过滤掉它们。如果需要更复杂的过滤逻辑,应该使用
filter()
方法。
js
// compact 不会过滤掉空对象或空数组
_.compact([{}, [], 1]);
// => [{}, [], 1]
// 如果需要过滤掉空对象和空数组
const filterEmptyObjects = (array) =>
array.filter((value) => {
if (typeof value === "object" && value !== null) {
return Object.keys(value).length > 0;
}
return Boolean(value);
});
filterEmptyObjects([{}, [], { a: 1 }, [1], 0, 1]);
// => [{ a: 1 }, [1], 1]
总结
Lodash 的 compact 函数是一个简单但非常实用的工具,它利用 JavaScript 的类型转换机制,通过一次遍历就能过滤掉数组中的所有假值。虽然实现简单,但它在日常开发中有广泛的应用场景,特别是在数据清洗和条件逻辑处理方面。
理解 compact 函数的实现原理,不仅能帮助我们更好地使用这个工具,还能让我们学习到 Lodash 中的代码优化技巧和函数式编程思想。在实际开发中,我们可以根据具体需求,选择使用 compact 或者自定义更复杂的过滤逻辑。