JavaScript 的演变:从 Lodash 和 Underscore 到 vanilla

**由 Kapeel Kokane✏️撰写**

想象一下这个场景——你正计划开始一个新项目。无论它是一个前端项目还是一个后端仓库,你要做的第一件事是什么?

那么,首先你要寻找一个入门套件或样板,它们可以帮助你快速入门,而不是做所有繁重的工作。在我们的例子中,假设你正在启动一个新的前端项目。

在这种情况下,您可以使用 Next.js,因为现在谁还会使用 create-react-app 呢?甚至连官方的 React 文档都不推荐它。接下来呢?您立即安装 Tailwind CSS,因为谁愿意从头开始编写 CSS?然后是状态管理。您可以使用 Redux、MobX、Recoil,甚至是新手 Zustand。我也会做同样的事情。

但是我们接下来要做什么呢?我们通常会安装一个实用程序包来帮助我们完成一些常见任务。谁想编写自己的映射、排序函数、防抖实用程序,甚至深度克隆方法?

但是我们应该使用哪个实用程序包呢?有很多选择:Lodash、Underscore、Ramda 等。最受欢迎的两个是 Lodash 和 Underscore,这也是我们今天要探索的。我们将比较它们的功能,并探讨这些包现在是否有必要。

比较 Lodash 和 Underscore

Underscore 由 Jeremy Ashkenas(Backbone.js 的创建者)于 2009 年创建,旨在提供一组 JavaScript 当时所缺乏的实用函数。它也是为与 Backbone.js 配合使用的,但它逐渐成为需要实用函数的开发人员的最爱,这些开发人员只需调用这些实用函数即可完成工作,而不必担心内部实现和浏览器兼容性。

Lodash 由 John-David Dalton 于 2012 年创建,是 Underscore 的一个分支。它旨在提供更一致的 API 和更好的性能。它还提供了一些不属于 Underscore 的附加实用程序。

捆绑包大小

如果我们比较这两个软件包的 npm 存储库,我们可以看到 Lodash 的包大小(1.41 MB)比 Underscore(906kB)更大。这意味着,当在 npm 项目中使用时,Lodash 在安装节点模块时会占用额外的 500 千字节网络带宽。但是,有了额外的大小,我们获得了一些额外的功能。接下来让我们谈谈它们。

功能

与 Underscore 相比,Lodash 提供了一些附加功能,包括:

  • _.clone--用于深度克隆对象
  • _.merge——可用于合并两个具有共同键的对象
  • _.set——为我们想要的任何路径设置一个值
  • 除此之外,Lodash 还提供了一些额外的字符串实用程序,例如 `[_.kebabCase](https://lodash.com/docs/4.17.15#kebabCase)` 和 `[_.camelCase](https://lodash.com/docs/4.17.15#camelCase)`,它们可以将任何提供的字符串转换为特定的大小写样式。还有一个 `[_.capitalize](https://lodash.com/docs/4.17.15#capitalize)` 方法可以将任何字符串的首字母大写。

    以下是使用这些实用程序的几个示例,这些实用程序目前无法通过 Underscore 实现:

    // _.clone example
    const user = {
      name: 'John Doe',
      age: 30,
      email: 'test@gmail.com',
      address: {
        city: 'New York',
        country: {
          code: 'US',
          name: 'United States'
        }
      }
    };
    const clonedUser = _.clone(user);
    // _.set example
    const user = {
      name: 'John Doe',
      age: 30,
    };
    _.set(user, 'address.city', 'New York');
    _.set(user, 'address.country.code', 'US');
    _.kebabCase('Hello World'); // hello-world
    _.camelCase('Hello World'); // helloWorld
    _.capitalize('hello world'); // Hello world

    当我们研究常用实用方法的 vanilla JS 替代方案时,我们将在文章后面查看合并示例。

    受欢迎程度

    如果我们看一下 npm 下载量,Lodash 的下载量为每周 7030 万次,而 Underscore 的下载量则少得多,为每周 1450 万次。这是有道理的,因为一旦 Lodash 可用,人们就会选择“较新”版本的实用程序包,而不是旧版本,旧版本会自动变得“过时”。

    我们真的需要实用程序包吗?

    现在,让我们看看这些库最初创建的主要用例——实用函数。过去,JavaScript 规范缺少一些基本功能。即使它确实通过将它们添加到规范中引入了新功能,但并非所有浏览器都实现了它们。让我们看看其中的一些。

    根据条件过滤数组

    ES5 中引入了 `Array.prototype.filter` 方法。它创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。但是,它直到 2015 年才在所有主流浏览器中普遍可用。在此之前,如果您想编写函数代码来过滤数组,则必须使用 Underscore 或 Lodash 中的实用方法,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const evenNumbers = _.filter(numbers, (number) => number % 2 === 0);

    但在 ES2015 之后,它可以轻松地用原始 JavaScript 实现,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const evenNumbers = numbers.filter((number) => number % 2 === 0);

    将数组精简为单个结果

    `Array.prototype.reduce` 方法也出现了同样的情况。在 2015 年之前,如果你想编写函数式代码来减少数组,你必须使用 Underscore 或 Lodash 中的实用方法,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const sum = _.reduce(numbers, (acc, number) => acc + number, 0);

    在ES2015之后,它可以很容易地用原生JavaScript实现:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const sum = numbers.reduce((acc, number) => acc + number, 0);

    对每个数组元素执行操作

    现在让我们看一下 `Array.prototype.forEach` 方法。如果我们想对数组的每个元素执行操作,我们必须使用 `for` 循环或 Underscore 或 Lodash 中的实用方法,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    _.forEach(numbers, (number) => console.log(number));

    但是在 ES2015 之后,它可以用原始 JavaScript 实现,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    numbers.forEach((number) => console.log(number));

    检查变量是否为数组

    如果我们想检查一个变量是否是一个数组,我们必须使用 Underscore 或 Lodash 的实用方法,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const isNumbersArray = _.isArray(numbers);

    使用 ES5,我们可以轻松地用原始 JavaScript 实现它,如下所示:

    const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const isNumbersArray = Array.isArray(numbers);

    从对象中选取属性

    现在让我们看一些对象操作。如果我们想从对象中挑选一些键,我们必须使用 Underscore 或 Lodash 中的实用方法,如下所示:

    const user = {
      name: 'John Doe',
      age: 30,
      email: '
    };
    const pickedUser = _.pick(user, ['name', 'age']);

    这将使用用户对象的键创建一个新对象,在本例中,键是“name”和“age”。但是,从 ES5 开始,我们可以轻松地在原生 JavaScript 中实现它,如下所示:

    const user = {
      name: 'John Doe',
      age: 30,
      email: 'test@gmail.com'
    };
    const pickedUser = Object.fromEntries(Object.entries(user).filter(([key]) => ['name', 'age'].includes(key)));

    合并两个对象

    如果我们想要合并两个对象,以便新对象具有来自两个对象的键的超集,我们必须使用 Lodash 中的实用方法,如下所示:

    const user = {
      name: 'John Doe',
      age: 30,
      email: 'test@gmail.com'
    }
    const newUser = {
      age: 31
    }
    const mergedUser = _.merge({}, user, newUser);

    请注意,Underscore 没有此功能,但在 ES5 中,我们可以使用扩展运算符在原始 JavaScript 中轻松实现它,如下所示:

    const mergedUser = {...user, ...newUser};

    我们还可以使用 `[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)` 方法,如下所示:

    const mergedUser = Object.assign({}, user, newUser);

    深度克隆对象

    如果我们想要深度克隆一个对象,我们必须使用 Underscore 或 Lodash 的实用方法,如下所示:

    const user = {
      name: 'John Doe',
      age: 30,
      email: 'test@gmail.com'
    };
    const clonedUser = _.cloneDeep(user);

    但是,现代 JavaScript 具有诸如“parse”和“stringify”之类的 JSON 实用程序,可用于深度克隆对象,如下所示:

    const clonedUser = JSON.parse(JSON.stringify(user));

    现在,让我们讨论一些更复杂的用例。

    限制函数调用

    如果我们想要限制一个函数,即将函数执行次数限制为给定时间范围内一次,我们必须使用 Underscore 或 Lodash 中的实用方法,如下所示:

    const throttledFunction = _.throttle(() => console.log('Throttled function'), 1000);

    在这种情况下,该函数每秒只会执行一次,即 1000 毫秒。

    也可以用原生 JavaScript 实现相同的功能,如下所示:

    function throttle(func, delay) {
      let lastTime = 0;
    
      return function() {
        const now = Date.now();
        if (now - lastTime >= delay) {
          lastTime = now;
          func.apply(this, arguments);
        }
      };
    }
    
    const throttledFunction = throttle(() => console.log('Throttled function'), 1000);

    函数调用防抖

    如果我们想要对一个函数进行去抖动,即自上次函数执行后在特定时间内停止该函数的执行,我们必须使用 Underscore 或 Lodash 中的实用方法,如下所示:

    const debouncedFunction = _.debounce(() => console.log('Debounced function'), 1000);

    但同样的功能也可以用原生 JavaScript 实现,如下所示:

    function debounce(func, delay) {
      let timeoutId;
    
      return function() {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          func.apply(this, arguments);
        }, delay);
      };
    }
    
    const debouncedFunction = debounce(() => console.log('Debounced function'), 1000);

    结论

    那么,我们真的需要像 Lodash 或 Underscore 这样的实用程序库吗?到 2024 年也不需要了。

    随着 ES2015 及更高版本的 JavaScript 的推出,该语言取得了长足的进步。它引入了许多可用于执行常见任务的实用方法。即使您需要一些额外的功能,您也可以使用该语言的最新功能在原生 JavaScript 中轻松实现它们,就像我们在本文中对扩展运算符所做的那样。

    因此,下次您在设置新项目时发现自己需要使用 Lodash 或 Underscore 时,请三思。节省一些网络带宽(来自 npm 模块)并编写自己的实用函数。这不仅可以帮助您更好地理解语言,而且还可以让您成为更好的开发人员。

    您是否添加新的 JS 库来构建新功能或提高性能?如果他们做相反的事情怎么办?

    毫无疑问,前端变得越来越复杂。当您向应用添加新的 JavaScript 库和其他依赖项时,您需要更高的可见性,以确保您的用户不会遇到未知问题。

    LogRocket 是一个前端应用程序监控解决方案,可以让您重播 JavaScript 错误,就像它们发生在您自己的浏览器中一样,以便您可以更有效地对错误做出反应。

    LogRocket with JS Libraries Demo

    LogRocket 可与任何应用程序完美配合,无论使用哪种框架,并且具有可记录来自 Redux、Vuex 和 @ngrx/store 的其他上下文的插件。您无需猜测问题发生的原因,而是可以汇总和报告问题发生时应用程序的状态。LogRocket 还可以监控应用程序的性能,报告客户端 CPU 负载、客户端内存使用情况等指标。

    自信构建 — 开始免费监控。