Lodash源码阅读-compact

功能概述

compact 函数是 Lodash 中的一个实用数组方法,主要用于创建一个新数组,其中包含原数组中所有非假值(truthy)的元素。在 JavaScript 中,假值包括 falsenull0""(空字符串)、undefinedNaN。这个函数可以帮助我们快速过滤掉数组中的所有假值,保留真值元素。

源码实现

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. 创建一个空数组作为结果容器
  2. 遍历原数组的每个元素
  3. 对每个元素进行布尔值检查
  4. 将所有通过检查的元素(真值)添加到结果数组
  5. 返回过滤后的新数组

代码解析

1. 变量初始化

js 复制代码
var index = -1,
  length = array == null ? 0 : array.length,
  resIndex = 0,
  result = [];

这段代码初始化了四个关键变量:

  • index:当前遍历的索引,初始值为 -1(因为后面会先自增再使用)
  • length:输入数组的长度,如果数组为 null 或 undefined,则设为 0
  • resIndex:结果数组的当前索引位置,用于追踪结果数组的填充位置
  • result:用于存储过滤后元素的空数组

这里的 array == null 是一个巧妙的技巧,它同时检查了 array === nullarray === undefined,因为在 == 比较中,undefinednull 是相等的。这样可以防止在传入 nullundefined 时尝试访问它们的 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 循环遍历数组:

  1. ++index < length:先将索引自增,然后检查是否小于数组长度
  2. var value = array[index]:获取当前索引位置的元素值
  3. if (value):检查该值是否为真值(非假值)
  4. 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 函数的实现中有几个值得注意的性能优化点:

  1. 预分配结果数组 :虽然结果数组初始为空,但通过使用 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%,尤其是在处理大型数组时。这种性能差异在性能关键的应用中是非常有意义的。

  2. 单次遍历:整个过程只需要遍历一次数组,时间复杂度为 O(n),其中 n 是输入数组的长度。

  3. 直接布尔检查 :使用 if (value) 而不是更复杂的条件检查,利用 JavaScript 的类型转换机制,简化了代码并提高了性能。

  4. 空值处理 :通过 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);
// => []

注意事项

  1. 保留 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]
  1. 不改变原数组:compact 函数不会修改原始数组,而是返回一个新数组。这符合函数式编程的不可变原则,但在处理大型数组时可能会有内存开销。

  2. 处理特殊对象 :所有对象(包括空对象和数组)在布尔上下文中都被视为真值,因此 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 或者自定义更复杂的过滤逻辑。

相关推荐
magic 2453 分钟前
CSS-复合选择器、元素显示模式、背景
前端·css·html·html5
傻小胖4 分钟前
CSS面试题
前端·css
古柳_Deserts_X14 分钟前
坏消息又断更8个多月,好消息准备重新开始更新 Shader 教程啦
前端·javascript·three.js
用户04696943357220 分钟前
nike hyper adapt,Nike Hyper Adapt:智能鞋履的创新体验!
javascript
范文杰21 分钟前
[最佳实践] 5倍效率+覆盖率90%,大部分程序员不知道的 Cursor 单测生成黑科技
前端
lx凌星27 分钟前
ahooks 的 useAntdTable 实现优雅的表格分页和搜索
前端
逍遥江湖29 分钟前
【js篇】二常见面试题
javascript
WolvenSec31 分钟前
Web基础:CSS快速入门
前端·css·tensorflow
Daci32 分钟前
Next.js全栈实战:手把手集成NextAuth实现Google/GitHub一键登录
前端·next.js
页面魔术32 分钟前
无虚拟DOM到底能快多少?
前端·vue.js