前端跨界开发指南:JavaScript工具库原理解析与实战
上QQ阅读APP看书,第一时间看更新

5.1 Lodash.js是什么

本节先分析为什么需要使用Lodash.js,接着再通过实例来对比使用Lodash.js和原生JavaScript的区别。

5.1.1 概述

正如Lodash.js官方主页上所写的,它是一个具有一致性、模块化且高性能的JavaScript实用工具库。一致性,是指无论是在浏览器环境下,还是在服务端的Node.js环境下,开发者都可以通过统一的API来使用它。模块化,是指你可以仅在程序中引用Lodash.js提供的工具函数的子集,而不需要在所有场景中加载整个库。开发中,大多数计算逻辑背后的算法都不是唯一的,不同的实现方式之间自然也就有了性能的比较,开发者当然都希望自己能使用性能比较好的方法,但并不是每个人都有能力判定哪个最优,Lodash.js在内部帮助大家完成了这件事情。

说到实用性,Lodash.js所做的工作实际上很抽象,就是将重复率非常高的方法、算法和处理逻辑等提取出来形成一个方法集合,将原本暴露在业务逻辑代码中的循环语句和条件判断语句隐藏起来,而对外提供一致且便于记忆的API,这样不仅可以使业务逻辑代码变得更加精简且易读,也降低了多人合作开发时因个人能力差异造成的混乱。当你在检视或维护代码时,不再需要猜测别人代码中那一层层的for循环和if判断的真正意图。

在真实的开发合作中,其他人不了解你所认为的“理所应当”的事情是一种非常普遍的现象,因为与你合作的人甚至可能都不是前端工程师。对于for(var...)循环、for(let...)循环、for...of...、forEach和map这些看起来非常相似的方法,一些非前端开发者很有可能分不清它们之间的区别,以及它们对数据源的影响。如果所有人统一使用“_.each()”或“_.map()”进行遍历,则不仅能降低JavaScript的编写难度,而且数据的变化相对来说也更容易追踪。当然,在ES6+版本的规范早已普及的今天,许多同名的方法都已经标准化,原生的JavaScript语法配合Babel插件使用可以让代码变得更加简洁和优雅,但在一些去重、分组、递归遍历等更为复杂的场景中,使用Lodash.js往往能使语义变得更清晰。

5.1.2 代码的较量

我们先通过一个示例直观地感受一下不同的编程方式是如何处理同一个业务逻辑需求的,以便于更深入地了解Lodash.js带来的便利。

假设后端在响应中返回了一个包含了多个对象的数组类型的数据,它的结构如下,dinner属性中记录了每个人最近3天每餐所吃的食物,记录中的每一项既可能是字符串,也可能是字符串组成的数组(代表所吃的食物不止一种):

[{
    id:0,
    name:'Tony',
    dinner:['apple',['peach','blueberry'],//....]
},{
    //....
},//....]

现在为了分析目标群体的饮食结构,我们需要把在dinner属性中出现过的所有食物都记录下来,并按照字母顺序对其排序,相同的食物只需要出现一次即可。下面就来看看不同的开发者可能会如何实现这样的功能。

1. 初级开发者的代码

function getAllFood(data){
    let resultMap = {};
    //遍历每一条记录的dinner字段,然后使用对象实现去重功能
    for(let i = 0; i < data.length; i++){
        for(let j = 0; j <data[i].dinner.length; j++){
            let foods = data[i].dinner[j];
            if(typeof foods === 'string' && !resultMap[foods]){
                //处理字符串类型的情况
                resultMap[foods] = 1;
            }else if (isArray(foods)){
                //处理数组类型的情况
                foods.forEach(item=>{
                   if(!resultMap[item]){
                       resultMap[item] = 1;
                    }
                });
            }
        }
    }
    //从对象中生成并返回最终结果
    return Object.keys(resultMap).sort((a,b)=>a.localeCompare(b));
}
//判断传入的数据是不是一个数组
function isArray(data){
    return Object.prototype.toString.call(data).slice(8,-1) === 'Array'
}

上面的代码大约有30行,尽管实现了示例中所要求的功能,但暴露了太多实现细节。开发者将无关紧要的代码细节都平铺在函数中了,其他人可能需要完整地阅读整段代码才能搞清楚开发者的意图。同时,开发者并没有对可能出现的状况进行足够的预判,上面统计部分的代码最多只能实现二维嵌套数组的解析,尽管它可以满足当前的需求,但是很难应对未来可能出现的变化,换句话说就是程序的健壮性不足。另一方面,getAllFood方法承担了过多实现业务逻辑的责任,违背了“单一职责”的模块化设计原则,以至于很难直接被复用,后续再出现类似的需求时,几乎只能选择复制粘贴,然后再进行逐句检查和修改,如果团队在构建流水线上加入了重复代码检查等环节,那么这样的代码恐怕无法通过检查。

2. 中级开发者的代码

let originData = require('./data.js');
function getAllFood(data) {
    return sortAndUnique(flatmap(data.map(item=>item.dinner),[]));
}
//排序去重
function sortAndUnique(arr){
    let resultMap = {};
    arr.forEach(i=>resultMap[i]=1);
    return Object.keys(resultMap).sort((a, b) => a.localeCompare(b));
}
//数组扁平化
function flatmap(arr, result){
    if(isArray(arr)){
        arr.map(item=>{
            flatmap(item,result);
        });
    }else{
        result.push(arr);
    }
    return result;
}
//判断传入的数据是不是一个数组
function isArray(data) {
    return Object.prototype.toString.call(data).slice(8, -1) === 'Array';
}

中级开发者的代码大约也有30行,但代码质量明显比前面的要好。这份代码遵循了“单一职责”的开发原则,将数组的类型判断、数组的扁平化、数组的排序和去重等可重用的方法从业务逻辑中剥离出来,形成了独立的方法。同时,主业务逻辑本身只用了一行代码,而且通过函数名就可以非常直观地了解其功能。提取出来的功能方法,其实现逻辑不止一种,仅数组去重就能够写出5种以上的方式,即使开发者在此使用的算法效率不高,后续的优化工作也可以直接针对这个方法进行,而不需要去重写业务逻辑代码。这样的代码就已经非常好了。

3. 高级开发者的代码

//高级开发者的实现
let originData = require('./data.js');
const _ = require('lodash');
function getAllFood(){
    return _.chain(originData)
            .map('dinner')
            .flattenDeep()
            .sortBy()
            .sortedUniq()
            .value();
}
console.log(getAllFood(originData));

当使用Lodash.js封装好的工具来实现同样的逻辑时,代码总共不超过10行,主业务逻辑部分只有一个链式调用。在高级开发者实现的代码中,首先是调用“_.chain”方法声明一段链式调用逻辑,然后通过map方法将原数组中的指定字段提取出来(本质上是一种数组映射),通过flattenDeep方法将数组展平为一维数组,通过sortBy方法对集合进行排序,通过sortedUniq方法对一个已经排好序的集合实现去重功能(已排序序列的去重操作可以通过对相邻项进行比较来完成,相较于针对乱序集合去重的算法,排序去重的效率更高),Lodash.js中链式调用的语法并不是立即执行的,它需要调用value方法来启动计算。与中级开发者编写的代码相比,使用Lodash.js后,开发者不需要再重复实现常用的工具方法,而且可以使用链式调用的方式将方法连接在一起,提高了代码的可读性。当大家都使用类似的API来组织代码时,代码的规范性和可读性自然就提高了。