使用Lodash编写安全、高效、优雅的JavaScript应用

在编写JS应用时,相信很多开发者会遇到“Uncaught TypeError: Cannot read property ‘prop’ of null” ,这种情况一般是没有做安全预判断。然而如果每一处都要加 obj != null && obj != undefined,不仅编写繁琐,也让代码的信噪比降低

为了让应用safety first,同时编写代码能够优雅高效,JS社区终于出现了一个瑞士军刀–Lodash。它的前身其实是underscore,这个和Lodash的概念差不多,其实API有点不一致而已,我主要介绍以下Lodash^_^

Lodash使用非常简单,你只需要记住它的变量是_就可以了(下划线)。在Lodash的编程哲学里,安全是第一的,而且融入了函数式编程。通过使用Lodash,你能感觉到它所有的API都可以大幅度减少无用的代码,让你的JS应用从1000行减到200行。我相信你会喜欢Lodash,而且在使用的过程中会逐渐理解贯彻它的编程哲学的

使用资料

Lodash官方仓库:
https://github.com/lodash/lodash

Lodash参考文档:
https://www.css88.com/doc/lodash/#_bindfunc-thisarg-partials

API简介

你在使用Lodash一段时间会发现,_.map既可以用在Object也可以用在Array,_.pickBy可以过滤Object不需要的key值,同时哪怕你传进去是null/undefined/NaN,Lodash也不会直接报错中断当前执行,而且返回一个空的结果(数字0,空数组,空对象等),可以确保你不会收到异常的情况。

在Lodash一些方法,比如_.difference,_.differenceBy, _.differenceWith,它们分别是默认处理函数,提供指定属性或者函数结果值来判断,手动编写compartor来进行判断

接下来,我介绍一些常用的API和它们的用途,其他的API如果感兴趣可以看上面的参考文档^_^

_.isNil

判断传参是不是null或者undefined,这对于常规的判断非常有用

_.get(object,path,[defaultvalue])

这是最常用的API,能够让你对一个数组或者数字,进行路径path的值访问,而且如果这个path的值是为空的,将会使用defaultvalue,举例:

// path的格式:
// 比如a.0.c对应的path路径就是 obj = {a:[{c: 2033}]}

// 数组的_.get例子
var arr = [{myval:3223}]
_.get(arr,'0.myval'); // 输出3223
_.get(arr,'0.other'); // 输出undefined
_.get(arr,'0.other','default') // 输出default

// 对象的_.get例子
var obj = {a:{c:323}}
_.get(obj,'a.c') // 输出323
// defaultvalue同上,不再阐述

_.map/_.every

该方法可以循环数组或者对象,其实在ES标准也有定义map和every,其实它们要求是不为null和undefined,调用时需要进行判断。every可以返回bool值来决定是否继续,用法和_.map差不多,我拿_.map来进行举例:

var obj = {a:23,b:123}
_.map(obj,(x,d,n)=>{
  console.log(x,d);
})
// 结果
// 23 "a"
// 123 "b"

var arr = ['a','b']
_.map(arr,(x,d,n)=>{
  console.log(x,d);
}) 
// 结果
// 'a'
// 'b'

_.mapKeys/_.mapValues

能够对传参的key和value进行处理,它不仅局限于obj,也能用在arr,举例:

var obj = {a:32,b:100}
_.mapKeys(obj,(x,d)=>'key_'+d) // {key_a:32,key_b:100}
_.mapValues(obj,(x,d)=>'value_'+x) // {a:'value_32',b:'value_100'}

var arr = [103,452,233];
_.mapKeys(arr,(x,d)=>'key'+d) // {key_0: 103, key_1: 452, key_2: 233},从数组变成object

_.groupBy

这个方法绝对是非常有用,我常用于将扁平的记录转成带层次的树节点,如果要自己实现我觉得绝对会花费很多时间,比如例子:

var arr = [
  {type: '网络部',id: 36},
  {type: '财务部',id: 14},
  {type: '研发部',id: 23},
  {type: '网络部',id: 32}, 
  {type: '研发部',id: 31},
  {type: '研发部',id: 13},
  {type: '网络部',id: 20}
] // 假设通过ajax,从后端拿出来这一堆扁平的数组

_.groupBy(arr,x=>x.type) 
// 只需要调用这个命令,就能让你获取object,key为type
// 结果输出:{网络部: Array(3), 财务部: Array(1), 研发部: Array(3)}

_.map(.groupBy(arr,x=>x.type),(x,d)=>({label:d,value:x}))
// 可以使用这个命令,再次扁平化,但是这个扁平化是进行分类的
/**
 0: {label: "网络部", value: Array(3)}
 1: {label: "财务部", value: Array(1)}
 2: {label: "研发部", value: Array(3)} 
 */

_.merge

可以将多个对象合并在一起,比如_.merge({},{a:32},{b:100})等于{a:32,b:100}

_.clone/_.cloneDeep

_.clone是shadow clone(浅拷贝),
_.cloneDeep是深拷贝。以前我还知道JSON.parse(JSON.stringify(obj))也可以深拷贝,但是会丢失函数(应该可以在第二个参数进行格式化定义函数)

_.flatten/_.flattenDeep

能够将深层次的数组展开,比如 [[12],[434]]转换成[12,434],这在一些数据结构比较嵌套的情况非常有用,我一般用在Tree的model展开会用到这个API

_.defaults/_.defaultsDeep

在传参需要初始化时属性的时候,会用到这个API。比如说,我规定传进来的Object必须要有这个属性值,如果没有我就提供默认值给它,如下面的例子:

function handlefunc(obj){
   _.defaultsDeep(obj,{
      info:{
         name: 'defaultname'
      }
   })
   console.log(obj.info.name) // 不管传进来的object有没有这个属性,我是一定能获取到的,这也是lodash提升安全性的体现
}

_.difference/_.differenceBy

比如_.difference(a,b),给出所有在a存在而b不存在的元素集合,比如举例:

_.difference([3, 2, 1], [4, 2]);
// 结果输出[3, 1]

_.intersection/_.intersectionBy

找出所有数组都有元素,比如举例:

_.intersection([2, 1], [4, 2], [1, 2]);
// 结果输出[2] 

其他API简介

_.first # 获取第一个元素
_.tail # 获取除了第一个元素以外其他所有元素
_.dropRight # 获取除了最后一个元素以外其他所有元素
_.eachRight # 可以从尾部元素开始一直往前循环,以前是要用for循环,然而现在采用了函数式的形式,非常简练
_.isEmpty # 判断是否为空数组或者数字0

链式调用

函数式编程还不够优雅,我们可以上链式调用,让内容转换直接一行代码就都解决了,可能以前还需要做merge和map,需要写几十上百的代码,然而现在就可以非常简洁的处理好,下面简单说一下链式调用的方式^_^

_.chain # 所有链式调用开头都是这个chain方法,在调用之后你可以一直连续调用其他API方法,直到调用.value才会返回值
_.value # 表示链式调用已经结束
_.tap # 在使用过程中,如果想在函数式过程中的某个节点,进行更自由的定义,可以使用tap。tap会传入当前链式的当前值,你可以对它进行任何操作,然后继续传给下一个链的处理函数
_.thru # 作用和_.tap一样,不过你需要return一个结果值,这个结果值将会传递给下一个链的处理函数

在chain调用之后的API,都免去了第一个参数(大部分情况下),所以你只需要传入其他参数即可

# 举例
var arr = [1,'2',3,4];

_.chain(arr).filter(x=>_.isNumber(x)).map(x=>x+100).value()
// 结果:[101, 103, 104]

_.chain(arr).filter(x=>_.isNumber(x)).map(x=>x+100).thru(x=>_.size(x)).value()
// 结果:3,因为在thru返回length

在具体应用的场景

1,Ajax返回的结果值

如果后端给的数据,数据结构比较复杂需要各种循环转换,又或者数据经常缺少一个属性等,那么可以使用Lodash进行简化循环操作,用_.defaultsDeep来初始化数据的默认值

2,在JSX里的循环

假设在JSX的render方法里面,你从redux里拿到一个list值,但是这个list万一是null或者undefined,你使用list.map就会报错,这时候可以采用_.map(list,func)的方式。我们可以不关注list为什么是null或者undefined,我们只关心我们要list循环生成JSX,这样就显得非常直观,不需要做太多判断

3,对表单逻辑需要大量判断和检查

在复杂表单里,你经常需要对复杂的formmodel做判断。这些判断有些来自于业务需求,有些来自于校验功能,尽管现在前端有许多工具可以避免做基础的校验,但是不可避免还是需要接触复杂表单域的判断。

比如你要判断是否为null/undefined,数组过滤null/undefined之后是否为空,选中的值是否合法等,如果使用lodash,将会大幅度降低你的代码量

文章总结

我非常喜欢Lodash的编程哲学,它能将开发的关注点放在真正的地方,让编程优雅自然,希望你也可以喜欢

发表评论

邮箱地址不会被公开。 必填项已用*标注