2

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

 1 year ago
source link: https://www.fly63.com/article/detial/12233
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 中查看如何使用原生语法实现。

3. Lodash 按需引入

最常见的引入 lodash 的方式是:

// 方式1:引入整个lodash对象
import _ from "lodash";

// 方式2:按名称引入特定的函数
import { cloneDeep } from "lodash";

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

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

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

B)指用具体的功能模块

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

// 只引入 cloneDeep 函数
import cloneDeep from "lodash/cloneDeep";

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

C)使用单独的函数库

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

 不推荐在项目中使用这种方式。首先,它并不像看起来一样轻量,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。

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

_.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 = ""));
作者:czpcalm
链接:https://juejin.cn/post/7143579596217122853

链接: https://www.fly63.com/article/detial/12233


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK