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

5.2 重点API的剖析

通过前文的示例代码对比,相信有读者已经迫不及待地想要学习Lodash.js的使用方法了,本节就先来看看它的API的构成吧。Lodash.js的API大致可以分为如下几个大类。

  • 数组(Array)操作类
  • 集合(Collection)操作类
  • 函数(Function)操作类
  • 语言(Lang)工具类
  • 数学(Math)类
  • 序列(Sequence)类
  • 字符串(String)操作类
  • 常用工具(Util)类

详细的使用方法直接查看官方文档就可以了,大部分方法并不复杂,我们需要做的只是熟悉它们,然后使用它们,感兴趣的话还可以自己试着实现一下,然后再看看官方的源码是如何实现的,你会发现其中有非常多既有趣又实用的知识。本节将重点介绍官方文档没有详细说明但开发者需要了解的那部分内容。

1. Collection

Lodash.js中的许多方法是基于特定的数据类型来分组的,细心的读者会发现针对数组和对象的方法分别放在了Array组和Object组中,还有一部分方法在归类时划分到了Collection组中,可是JavaScript中并没有Collection这个数据类型,我们要如何使用这类方法呢?事实上,划分到Collection这个类别中的方法,其数据集既可以是Array类型,也可以是Object类型,这样划分的目的是为一些抽象行为提供统一的名称。在JavaScript语言中,数组实例和对象实例的原型链分别如下:

//数组实例的原型链
[].__proto__ = Array.prototype
[].__proto__.__proto__ = Object.prototype
//对象实例的原型链
({}).__proto__ = Object.prototype

从原型链中我们很容易看出,数组实例是可以使用对象方法的,因为它的原型链上有Object.prototype对象,但是对象实例也会因为类似的原因而无法直接使用数组方法,这就使得许多看起来非常相似的逻辑在实现细节上却有很大的差别。例如,典型的遍历、查找、映射、排序、聚合等操作,在数组中我们可以直接使用对应的forEach find、map、sort、reduce等方法,但是在处理对象类型时,几乎只能通过一遍又一遍地在逻辑的外层包裹上“for...in...”和“hasOwnProperty”等代码段来实现。而Lodash.js为这些数组和对象都会用到的方法(不仅仅是Array.prototype上的方法)提供了不同的实现并将其封装起来了,这就为开发者提供了更加友好且一致的API。

2. 不可变数据

初级开发者常常会分不清一个原生方法是会直接改变源数据还是会生成新的数据,为了在开发中避免自己编写的逻辑影响不想修改的数据,许多初级开发者会多次调用深备份方法对数据集进行备份,这种思路是正确的,但是多次深备份所带来的的性能损失却不容忽视。使用Lodash.js就可以避免出现这种混乱。

在函数的实现上,Lodash.js会遵循“不修改原数据”的原则,这就意味着在你将一个数据集传入某个方法后,期望的结果总是会以函数返回值的形式传递回来,如果另一个变量标识符指向了原来的数据集,那么它不会受到任何影响。这样的设计提供了额外的一致性保障,你可以非常确切地知道自己得到的是否是新的数据集。操作嵌套类型的数据时,需要格外小心,最稳妥的办法就是在使用前测试一下某个方法的真实表现。

3. 高阶函数

Function操作类中的方法所涉及的几乎都是高阶函数的知识,也是前端面试必考的知识点——闭包的应用,即使不使用高阶函数,虽然一样也可以实现这些方法的功能,但这样做的代价就是将一些本来只有自己使用的状态变量提升到了更外层的作用域中,这样一来,不仅无法实现私有变量的隔离,而且也很容易带来更多的干扰。

例如,你正在使用Vue框架开发一个组件,在一个鼠标移动事件中,你希望通过函数节流(throttle)的功能来限制一个高性能消耗的事件处理函数的触发频率,如果不使用高阶函数,那么你就必须将记录每次移动事件时间戳的变量挂载在“data”上(即一个更外层的作用域),尽管它的消费者只有鼠标移动事件的回调函数,但是它的挂载方式却使得这个变量可以被其他函数访问或修改。从组件设计的角度来看,这种做法违背了基本的封装原则,增加了不必要的干扰。如果使用高阶函数来实现,那么这个记录时间戳的变量就会被封装在高阶函数内部,如果你使用的是Lodash.js提供的“_.throttle()”方法,那么主逻辑代码中甚至连这个变量都不会出现,组件中的主逻辑代码也会因此变得更加清晰,这样的结构也更符合“高内聚,低耦合”的开发原则。

4. 数据分离和逻辑聚合

“数据分离”是指将数据从逻辑中剥离出来,“逻辑聚合”是指将主逻辑的代码尽量聚合在一起,而把它们的实现细节封装起来。一个复杂的业务逻辑可能会按步骤调用大量的方法,初学者极有可能会写出耦合度超高的巨型函数,又或者将其拆解为若干个步骤,在每个步骤的结尾处调用下一个步骤,这样的代码维护和调试起来非常困难,过多的细节会让你难以聚焦主要的业务逻辑代码。在Lodash.js中,我们既可以使用类似于jQuery的链式调用风格来组装业务逻辑的多个步骤,也可以使用类似于函数式编程中管道(pipe)的风格,无论如何,将细节封装起来,将重要的信息聚合在一起,都可以让代码变得更清晰和易维护。

在Lodash.js中,将数据集传入“_.chain()”方法中,可以开启一段链式调用风格的逻辑,示例[1]代码如下:

var users = [
    { 'user': 'barney',  'age': 36 },
    { 'user': 'fred',    'age': 40 },
    { 'user': 'pebbles', 'age': 1 }
];
var youngest = _
    .chain(users)
    .sortBy('age')
    .map(function(o) {
        return o.user + ' is ' + o.age;
    })
    .head()
    .value();
//输出的结果为:'pebbles is 1'

也可以引用函数式编程(Functional Programming)风格的Lodash.js,并使用“_.pipe”方法将函数按照执行步骤组合在一起,示例代码如下:

const _ = require('lodash/fp');
const youngest = _.pipe([_.sortBy('age'),_.map(o=>o.user+' is '+o.age),_.head]);
console.log(youngest(users));
//输出的结果为: 'pebbles is 1'

函数式编程和面向对象编程是两种不同的编程范式,面向对象编程通常更适合用来描述抽象实体之间的联系,而函数式编程则在数据加工的任务中显得更灵活简洁。无论选择哪种逻辑聚合风格,我们几乎都可以通过编写出更少的应用层代码来表达业务逻辑的主线,搞清楚程序中“做了哪些事”,而不是“做了哪些事以及分别是怎么做的”,另一方面,业务逻辑的聚合也减少了中间变量的使用,使代码更加精简。


[1]示例来自Lodash.js官方文档。