杭州IT培训
美国上市IT培训机构

400-111-8989

都听说过 lodash,但你会用吗?

  • 时间:2022-11-18 14:12
  • 发布:互联网
  • 来源:精品干货

0. 引言

Lodash 是 JavaScript 社区出名的一个工具库,提供了许多高效、高兼容性的工具函数。

不过,随着浏览器和 web 技术的发展,一些人开始反对在项目中使用 lodash。主要原因有:

  • ES6 之后拓展了 JavaScript 特性,许多功能不再需要额外工具库。
  • 现代浏览器对 ES 语法的支持性提升。
  • Babel 等编译工具能将 ES6 编译成 ES5,更优雅地解决了 ES6 语法的兼容性问题。
  • 为了几个工具函数而引入了整个 lodash,增加了项目的体积。

本文来探讨一下,我们该不该在项目中使用 lodash,以及如何正确使用 lodash。是的,这个看起来有点标题党的标题,有两层含义:

  • 你会在项目中使用 lodash 吗?
  • 你会正确地使用 lodash 吗?

1. 我们还需要 lodash 吗?

个人认为,应该优先使用 ES 原生语法,同时,在大部分项目中仍然推荐使用 lodash 作为拓展工具库。原因如下:

  • Lodash 不止 ES6,有更多 ES6 难以实现的功能,比如常见的深拷贝。
  • 提高开发效率、简化代码。lodash 中的函数,都是社区开发者从多年的实践中提炼出来的常用功能,并且经过广泛的考验和优化,使用库函数往往比自己实现有更好的性能和鲁棒性。
  • Lodash 支持多种模块化方案,配合 tree shaking 技术或者使用单独的函数模块,几乎不会导致冗余代码。

不管怎么样,lodash 目前仍然保持着 4 千多万的周下载了,就足以见得它的流行程度。

2. 还是不想用 lodash ?

即使你坚持不肯使用 lodash,我认为仍然有必要了解 lodash 提供了哪些功能,这些功能你会经常在开发中遇到。这个时候,你可以从 You Don't Need Lodash Underscore[1] 中查看如何使用原生语法实现。

3. Lodash 按需引入

常见的引入 lodash 的方式是:

// 方式1:引入整个lodash对象 import _ from "lodash"; // 方式2:按名称引入特定的函数 import { cloneDeep } from "lodash"; 

这两种方式都会引入整个 lodash 库。Lodash 含有许多函数,项目里一般只会用到其中的小部分,为了避免引入不必要的代码,lodash 提供了多种支持按需加载的方式。

使用打包插件实现按需加载(推荐)

插件 babel-plugin-lodash[2] 和 lodash-webpack-plugin[3] 能够在打包时去掉不必要的 lodash 代码,减小产物体积。

指用具体的功能模块

// 只引入 array 模块的功能 import array from "lodash/array"; // 只引入 cloneDeep 函数 import cloneDeep from "lodash/cloneDeep"; 

这种方式只会引入引用路径对应的模块,无需使用插件,也不会有冗余代码。缺点是每个 import 语句只能引入一个函数,可能导致多个 import 语句。

使用单独的函数库

Lodash 为每个方法提供了单独的 npm 包[4],你可以只下载你想要的函数。

不推荐在项目中使用这种方式。首先,它并不像看起来一样轻量,lodash 中的公共代码会存在于每一个函数包中。其次,每个方法都是独立的依赖包,意味着多次安装、多个 package.json 依赖项、多个 node_modules 包目录。

4. 实用功能总结

Lodash 含有 Array, Collection, Date, Function, Lang, Math, Number, Object, String 等多个功能模块,总共几百个功能函数。官方文档上以字典顺序排序,不容易总结记忆。这里总结一些 ES6 不容易实现的实用功能。

4.1. 字符串操作

4.1.1. 大小写转换

String.toLowerCase/toUpperCase 只能进行简单大小写转换,lodash 还提供了

_.lowerFirst(string);
_.upperFirst(string); // 第一个字符大写,其它字符小写 _.capitalize(string); 

4.1.2. 命名风格转换

编程中,常见的多单词命名风格有:

  • 蛇形写法(snake case):单词之间用下划线连接,如 foo_bar
  • 烤肉串写法(kebab case):单词之间使用横线连接,如 foo-bar
  • 驼峰写法(camel case):从二个单词开始,每个单词的首字母大写,如 fooBar 。
  • 大驼峰写法(pascal case):每个单词首字母大写,如 FooBar

除了大驼峰[5],其他三种风格都有对应的转换函数:

_.snakeCase(string);
_.kebabCase(string);
_.camelCase(string); // 利用upperFirst和camelCase 实现 pascalCase const pascalCase = (string) => _.upperFirst(_.camelCase(string)); // examples _.snakeCase("fooBar"); // 'foo_bar' _.camelCase("Foo Bar"); // 'fooBar' _.kebabCase("__FOO_BAR__"); // 'foo-bar' 

另外,还有两个不常用的全大写和全小写写法(以空格为分隔符),它们与 _.toLower/toUpper 的区别是会识别并转换字符串中的分隔符。

_.lowerCase(string);
_.upperCase(string); // examples _.lowerCase("--Foo-Bar--"); // 'foo bar' _.upperCase("fooBar"); // 'FOO BAR' 

4.2. 算术与数字

算术运算:

// 求总和 _.sum(array); // 求平均值 _.mean(array); 

常用的数字操作:

// 返回一个[lower,upper]之间的随机数 // 如果lower和upper中有浮点数,或者floating为true,返回浮点数,否则,返回整数 _.random(lower=0,upper=1 [,floating]) // 生成一个范围数组 _.range([start=0,]end,step=1) // 把一个数字就近限制在某个区间内 _.clamp(number,[lower=0,] upper) // examples _.clamp(-10, -5, 5);    // -5 _.clamp(10, -5, 5);    // 5 

4.3. 数组操作

4.3.1. 集合运算

// 交集 intersection _.intersection(...arrays);
_.intersectionWith(...arrays [, comparator]);
_.intersectionBy(...arrays [, iteratee]); // 并集 _.union(...arrays);
_.unionWith(...arrays [, comparator]);
_.unionBy(...arrays [, iteratee]); // 集合差,A - B 表示属于集合A但不属于集合B的元素集合 _.difference(array, ...operands);
_.differenceWith(array, ...operands [, comparator]);
_.differenceBy(array, ...operands [, iteratee]); 

这三簇函数都不改变原来的数组,而是返回一个新的数组作为运算结果。其中,交并集的运算结果不含重复元素,集合差取决于第一个集合。

函数命名具有一定的规约,以交集为例:

  • intersection: 执行常规运算,采用浅比较判断元素是否相等。
  • intersectionWidth: 调用 comparator 函数进行元素比较,可以自定义比较方式。
  • intersectionBy: 每个元素先经过 iteratee 函数处理,对转换后的数组进行比较运算,最后以转换前的第一个元素为结果。
_.intersection([2, 1, 1], [2, 3], [2, 4]); // => [2] const objects = [
  { x: 1, y: 2 },
  { x: 2, y: 1 },
]; const others = [
  { x: 1, y: 1 },
  { x: 1, y: 2 },
];
_.intersectionWith(objects, others, _.isEqual); // => [{ 'x': 1, 'y': 2 }]  , 结果引用objects中的元素 _.intersectionBy([2.1, 1.2], [2.3, 3.4], [3.2, 2.4], Math.floor); // => [2.1] 

4.3.2. 分片/分区/分组

分片(chunk)是指把数组中的每 n 个元素分为一组(一片),如果不能整除,最后剩下的元素单独一片。

_.chunk(array [, size=1]) // example _.chunk(['a', 'b', 'c', 'd','e'], 2); // => [["a", "b"], ["c", "d"], ["e"]] 

分区(partition)是利用一个断言函数迭代每个元素,根据断言的 true 和 false,把元素分成两组。

_.partition(collection [, predicate]) // example _.partition([4,5,6,7],num=>num>5) // =>[[6, 7], [4, 5]] 

分组(group) 则是用一个函数遍历每个元素,得到的结果作为该元素所在组的 key,相同 key 元素归为同一组。

_.groupBy(collection [, iteratee]) // example _.groupBy([6.1, 4.2, 6.3], Math.floor); // => { '4': [4.2], '6': [6.1, 6.3] } 

4.3.3. 有序数组查找/去重

在保证数组有序的情况下,查找和去重可以采用二分法,降低复杂度。Lodash 也提供了一些针对有序(升序)数组的操作。

sortedIndex / sortedLastIndex 可以操作基本的 number 数组和 string 数组:

// 返回插入该元素后仍然能保持数组有序的第一个下标位置 _.sortedIndex(array, value); // 类似 sortedIndex,但返回最后一个能保持顺序的下标位置 _.sortedLastIndex(array, value); // example _.sortedIndex([1, 20, 20, 100, 500], 20); // 1 _.sortedLastIndex([1, 20, 20, 100, 500], 20); // 3 

上面两个函数都只能在数字和字符串数组中使用,对于对象数组,可以用一个函数表示元素之间的排序依据:

// 以 iteratee 转化后的结果排序 _.sortedIndexBy(array, value [, iteratee])
_.sortedLastIndexBy(array, value [, iteratee]) // example _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, function(o) { return o.x; }); // => 0 

注意,sortedIndex/sortedLastIndex 并不能直接用于元素查找,比如上面返回下标 3,但 array[3] 是 100 而不是 20。

有序数组查找用 sortedIndexOf/sortedLastIndexOf,它的功能与 indexOf/lastIndexOf 一样,不过采用了二分查找。

_.sortedIndexOf(array, value);
_.sortedLastIndexOf(array, value); // example _.sortedIndexOf([4, 5, 5, 5, 6], 5); //1 

sortedUniq/sortedUniqBy 可以对有序数组去重。

_.sortedUniq(array)
_.sortedUniqBy(array [, iteratee]) // example _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); // => [1.1, 2.3] 

4.3.4. 元素操作:取样/打乱/计数

// 随机返回一个元素 _.sample(collection) // 随机返回n个元素 _.sampleSize(collection, [n=1]) // 打乱数组 _.shuffle(collection) // 计数 _.countBy(collection [, iteratee])
_.countBy([6.1, 4.2, 6.3], Math.floor); // => { '4': 1, '6': 2 } 

4.4. 对象操作

4.4.1. 对象转换

开发中经常需要从已有对象改造,得到我们期望的结构。Lodash 中有很多函数可以派上用场:

// 克隆 _.clone(value);
_.cloneWith(value, customizer);
_.cloneDeep(value);
_.cloneDeepWith(value, customizer); // 同 Object.assign,把 sources 对象中的自有属性赋值到 object 中 _.assign(object, ...sources); // 转化后赋值 _.assignWith(_.assignWith(object, sources, customizer)); // 类似_.assign, 但会赋值继承属性 _.assignIn(object, ...sources);
_.assignInWith(object, ...sources, customizer); // 当object中不存在值时,才会赋值,经常用于合并默认值 _.defaults(object, ...sources); // _.default 不适用多层对象,需要使用_.defaultsDeep _.defaultsDeep(object, ...sources); // 合并对象,类似 _.assign,但对象会递归深入,数组会被拼接 _.merge(object, ...sources);
_.mergeWith(object, ...sources); 

注意,上面这些函数都会直接修改 object 参数。

与此不同,pick 和 omit 操作则返回新对象,不修改参数:

// 从对象中取出对应路径的值,合成一个新对象 _.pick(object, [paths]); // 用一个断言函数决定要不要取这个属性,predicate(value,key) _.pickBy(object, predicate); // 去掉指定属性,把余下部分合成一个新对象,性能差于 pick _.omit(object, [paths]);
_.omitBy(object, predicate); // examples _.pick({ a: 1, b: "2", c: 3 }, ["a", "c"]); // => { 'a': 1, 'c': 3 } _.pickBy({ a: 1, b: "2", c: 3 }, _.isNumber); // => { 'a': 1, 'c': 3 } _.omit({ a: 1, b: "2", c: 3 }, ["a", "c"]); // => { 'b': '2' } 

另外,对象还能像数组一样进行 map :

// iteratee(value,key,obj) 返回的结果作为新对象的key _.mapKeys(object, iteratee); // iteratee(value,key,obj) 返回的结果作为新对象的value _.mapValues(object, iteratee); // example _.mapKeys({ a: 1, b: 2 }, function (value, key) {
  return key + value;
}); // => { 'a1': 1, 'b2': 2 } 

4.4.2. 遍历对象

// 遍历自有属性,类似 for...in 加 hasOwnProperty判断。 _.forOwn(object, iteratee); // 查找符合条件的key,类似数组的findIndex _.findKey(object, iteratee);
_.findLastKey(object, iteratee); // 查找对象中的函数属性 _.functions(object);
_.functionsIn(object); 

4.4.3. 安全的 get/set

在 JavaScript 中,读取和设置某一个路径下的值,是不安全的:

const object={a:1}; const=object.b.someKey;   // TypeError: Cannot read properties of undefined object.c.someKey=v;       // TypeError: Cannot set properties of undefined 

Lodash 为我们提供了更安全的 get 和 set 操作:

// 当path对应的值不存在时,返回undefined,而不是报错。 _.get(object,path [, defaultValue]); // 一层层set,而不是报错 _.set(object,path,value); // 根据该路径现在的value,更新为updater返回后的值,updater(value)=>newValue _.update(object,path,updater) 

尽管最近的可选链 "?." 语法能取代 get 函数,但 set 操作依然没有较好的原生支持。

4.5. 函数操作

4.5.1. 控制函数执行

有时候,我们希望在特定条件下函数才执行,比如常见的防抖和节流:

  • 防抖(debounce):当函数调用时,等待一段时间再执行实际操作(内部函数),如果这段时间内函数再次被调用,则本次调用不执行实际操作,新调用重新开始等待。
  • 节流(throttle):一段时间内多次调用函数,只执行一次实际操作。
_.debounce(func [, wait=0] [, options={}])
_.throttle(func [, wait=0] [, options={}]) // examples // 只会在停住之后重新布局 window.addEventListener('resize', _.debounce(calculateLayout, 150)); // 会持续更新位置,但150ms更新一次,避免卡顿 window.addEventListener('resize', _.throttle(updatePosition, 150)); 

也可以根据调用次数控制是否执行操作。

// 函数只调用一次 _.once(func); // 只在前n次调用 _.before(func, n); // 只在n次之后才调用 _.after(func, n); 

延迟执行:

// 在本次调用堆栈被清空后执行 _.defer(func, ...args); // 等待wait ms 后执行,同setTimeout _.delay(func, wait, ...args); 

_.memorize 能缓存函数结果,避免重复计算,是一种常见的性能优化手段。

// resolver用于计算缓存key,当key相同时,使用缓存。默认使用func的第一个参数为key _.memorize(func [, resolver]) 

4.5.2. 函数参数转换

// 柯里化 _.curry(func, (arity = func.length)); // 绑定部分参数,但不绑定this _.partial(func, ...args); // 只接收前n个参数,忽略额外参数 _.ary(func, n); // 只接收第一个参数,同 _.ary(func,1) _.unary(func); 

4.6. 通用工具

流水线:

_.flow([funcs]); const pascalCase = _.flow(_.upperFirst, _.camelCase); 

生成一个唯一 ID:

_.uniqueId((prefix = ""));

预约申请免费试听课

怕钱不够?就业挣钱后再付学费!    怕学不会?从入学起,达内定制课程!     担心就业?达内多家实践企业供你挑选 !

【免责声明】本文部分系转载,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及作品内容、版权和其它问题,请在30日内与联系我们,我们会予以更改或删除相关文章,以保证您的权益!"
上一篇:浅析VO、DTO、DO、PO的概念、区别和用处
下一篇:英语不好能学Java吗?知道这70个单词足够了!

Java 后端有哪些不用学的技术?

分布式存储之数据切片

策略模式:巧妙替代你的if-else

sorted()之正序倒序

  • 扫码领取资料

    回复关键字:视频资料

    免费领取 达内课程视频学习资料

Copyright © 2023 Tedu.cn All Rights Reserved 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有

选择城市和中心
江西省

贵州省

广西省

海南省