如何写出小而清晰的函数?(JS 版)
本文以 JavaScript 为例,介绍了该如何优化函数,使函数清晰易读,且更加高效稳定。
软件的复杂度一直在持续增长。代码质量对于保证应用的可靠性、易扩展性非常重要。
然而,几乎每一个开发者,包括我自己,在职业生涯中都见过低质量的代码。这东西就是个坑。低质量代码具备以下极具杀伤力的特点:
- 函数超级长,而且塞满了各种乱七八糟的功能。 
- 函数通常有一些副作用,不仅难以理解,甚至根本没法调试。 
- 含糊的函数、变量命名。 
- 脆弱的代码:一个小的变更,就有可能出乎意料的破坏其他应用组件。 
- 代码覆盖率缺失。 
它们听起来基本都是:“我根本没法理解这段代码是如何工作的”,“这段代码就是一堆乱麻”,“要修改这一段代码实在太难了” 等等。
我就曾遇到过这样的情况,我的一个同事由于无法继续将一个基于Ruby 的 REST API 做下去,继而离职。这个项目是他从之前的开发团队接手的。
修复现有的 bug ,然后引入了新的 bug,添加新的特性,就增加了一连串新 bug,如此循环(所谓的脆弱代码)。客户不希望以更好的设计重构整个应用,开发人员也做出明智的选择——维持现状。
好吧,这种事儿经常发生,而且挺糟糕的。那我们能做点什么呢?
首先,需要谨记于心:只是让应用运转起来,和尽心保证代码质量是两个完全不同的事。一方面,你需要实现产品需求。但是另一方面,你应该花点时间,确保函数功能简单、使用易读的变量和函数命名,避免函数的副作用等等。
函数(包括对象方法)是让应用运转起来的齿轮。首先你应当将注意力集中在他们的结构和整体布局上。这篇文章包括了一些非常好的示例,展示如何编写清晰、易于理解和测试的函数。
1. 函数应当很小,非常小
避免使用包含大量的功能的大函数,应当将其功能分割为若干较小的函数。大的黑盒函数难于理解、修改,特别是很难测试。
假设这样一个场景,需要实现一个函数,用于计算 array、map 或 普通 JavaScript 对象的权重。总权重可通过计算各成员权重获得:
- null 或者 未定义变量计 1 点。 
- 基本类型计 2 点。 
- 对象或函数计 4 点。 
例如,数组 [null, ‘Hello World’, {}] 的权重这样计算:1(null) + 2(string 是基本类型) + 4(对象) = 7。
Step 0: 最初的大函数
我们从最糟的实例开始。所有的逻辑都被编码在函数 getCollectionWeight() 中:
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function(sum, item) {
if (item == null) {
return sum + 1;
}
if (typeof item === 'object' || typeof item === 'function') {
return sum + 4;
}
return sum + 2;
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
问题显而易见,getCollectionWeight() 函数超级长,而且看起来像一个装满“意外”的黑盒子。可能你也发现了,第一眼根本就搞不明白它要干什么。再试想一下,应用里有大把这样的函数。
在工作中遇到这样的代码,就是在浪费你的时间和精力。反之,高质量的代码不会令人不适。高质量代码中,那些精巧、自文档极好的函数非常易于阅读和理解。
Step 1:根据类型计算权重,抛弃那些“迷之数字”。
现在,我们的目标是:把这个巨型函数,拆分为较小的、独立的、可重用的一组函数。第一步,将根据类型计算权重的代码提取出来。这个新的函数命名为 getWeight()。
我们再看看这几个“迷之数字”: 1, 2, 4。在不知道整个故事背景的前提下,仅靠这几个数字提供不了任何有用的信息。幸好 ES2015 允许定义静态只读引用,那你就能简单的创造几个常量,用有意义的名称,替换掉那几个“迷之数字”。(我特别喜欢“迷之数字”这个说法:D)
我们来新建一个较小的函数 getWeightByType(),并用它来改进 getCollectionWeight():
// Code extracted into getWeightByType()
function getWeightByType(value) {
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
}
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function(sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
看起来好多了,对吧? getWeightByType() 函数是一个独立的组件,仅仅用于决定各类型的权重值。而且它是可复用的,你可以在其他任何函数中使用它。
getCollectionWeight() 稍微瘦了点身。
WEIGHT_NULL_UNDEFINED, WEIGHT_PRIMITIVE 还有 WEIGHT_OBJECT_FUNCTION 都是具备自文档能力的常量,通过它们的名字就可以看出各类型的权重。你就不需要猜测 1、2、4 这些数字的意义。
Step 2: 继续切分,使之具备扩展性
然而,这个升级版依然有不足的地方。假如你打算对一个 Set,甚至其他用户自定义集合来实现权值计算。getCollectionWeight() 会快速膨胀,因为它包含了一组获得权值的具体逻辑。
让我们将获得 maps 权重的代码提取到 getMapValues(),将获得基本 JavaScript 对象权值的代码则放到 getPlainObjectValues() 中。看看改进后的版本吧。
function getWeightByType(value) {
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
}
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
// Code extracted into getMapValues()
function getMapValues(map) {
return [...map.values()];
}
// Code extracted into getPlainObjectValues()
function getPlainObjectValues(object) {
return Object.keys(object).map(function (key) {
return object[key];
});
}
function getCollectionWeight(collection) {
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = getMapValues(collection);
} else {
collectionValues = getPlainObjectValues(collection);
}
return collectionValues.reduce(function(sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15];
let myMap = new Map([ ['functionKey', function() {}] ]);
let myObject = { 'stringKey': 'Hello world' };
getCollectionWeight(myArray); // => 7 (1 + 4 + 2)
getCollectionWeight(myMap); // => 4
getCollectionWeight(myObject); // => 2
现在再来看 getCollectionWeight() 函数,你会发现已经比较容易明白它的机理,看起来就像一段有趣的故事。
每一个函数的简单明了。你不需要花费时间去挖掘代码,理解代码的工作。这就是清新版代码该有的样子。
Step 3: 优化永无止境
就算到了现在这种程度,依然有很大优化的空间!
你可以创建一个独立的函数  getCollectionValues(),使用 if/else 语句区分集合中的类型:
function getCollectionValues(collection) {
if (collection instanceof Array) {
return collection;
}
if (collection instanceof Map) {
return getMapValues(collection);
}
return getPlainObjectValues(collection);
}
那么, getCollectionWeight() 应该会变得异常纯粹,因为它唯一的工作:用 getCollectionValues() 获得集合中的值,然后依次调用求和累加器。
你也可以创建一个独立的累加器函数:
function reduceWeightSum(sum, item) {
return sum + getWeightByType(item);
}
理想情况下 getCollectionWeight() 函数中不应该定义函数。
最后,最初的巨型函数,已经被转换为如下一组小函数:
除了这些代码质量上的优化之外,你也得到不少其他的好处:
- 通过代码自文档,getCollectionWeight() 函数的可读性得到很大提升。 
- getCollectionWeight() 函数的长度大幅减少。 
- 如果你打算计算其他类型的权重值,getCollectionWeight() 的代码不会再剧烈膨胀了。 
- 这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。 
- 当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。 
- 这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。 
- 你可以参照 CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。 
这些建议可以帮助你,战胜应用的复杂性。
原则上,你的函数不应当超过 20 行——越小越好。
现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。
2. 函数应当是简单的
让我们稍微放松一下,思考下应用的定义到底是什么?
每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。
一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。
换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。
为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。
我们来举个例子。假设你需要实现一个函数,使数组仅保留素数(2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如何用低一级抽象的若干步骤实现 getOnlyPrime() 函数呢?我们这样做:
为了实现 getOnlyPrime() 函数, 我们用 isPrime() 函数来过滤数组中的数字。
非常简单,只需要对数字数组执行一个过滤函数 isPrime() 即可。
你需要在当前抽象层实现 isPrime() 的细节吗?不,因为 getOnlyPrime() 函数会在不同的抽象层实现一些列步骤。否则,getOnlyPrime() 会包含过多的功能。
在头脑中谨记简单函数的理念,我们来实现 getOnlyPrime() 函数的函数体:
function getOnlyPrime(numbers) {
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如你所见, getOnlyPrime() 非常简单,它仅仅包含低一级抽象层的步骤:数组的 .filter() 方法和 isPrime() 函数。
现在该进入下一级抽象。
数组的 .filter() 方法由 JavaScript 引擎提供,我们直接使用即可。当然,标准已经准确描述了它的行为。
现在你可以深入如何实现 isPrime() 的细节中了:
为了实现 isPrime() 函数检查一个数字 n 是否为素数,只需要检查 2 到 Math.sqrt(n) 之间的所有整数是否均不能整除n。
有了这个算法(不算高效,但是为了简单起见,就用这个吧),我们来为 isPrime() 函数编码:
function isPrime(number) {
if (number === 3 || number === 2) {
return true;
}
if (number === 1) {
return false;
}
for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) {
if (number % divisor === 0) {
return false;
}
}
return true;
}
function getOnlyPrime(numbers) {
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime() 很小也很清晰。它只从更低一级抽象中获得必要的一组步骤。
只要你按照这些规则,将函数变的简洁清晰,复杂函数的可读性将得到很大提升。将代码进行精确的抽象分级,可以避免出现大块的、难以维护的代码。
3. 使用简练的函数名称
函数名称应该非常简练:长短适中。理想情况下,名称应当清楚的概括函数的功用,而不需要读者深入了解函数的实现细节。
对于使用骆驼风格的函数名称,以小写字母开始: addItem(),saveToStore() 或者 getFirstName() 之类。
由于函数都是某种操作,因此名称中至少应当包含一个动词。例如 deletePage(),verifyCredentials()。需要 get 或 set 属性的时候,请使用 标准的 set 和 get 前缀:getLastName() 或 setLastName()。
避免在生产代码中出现有误导性的名称,例如 foo(),bar(),a(),fun() 等等。这样的名称没有任何意义。
如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。
4. 总结
当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。
如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。
当然,最正确的解决方案应该是,从项目一开始就以正确的方式实现应用。除了花一些时间在实现上,也应该花一些精力在组建合理的函数结构上:如我们所建议的——让它们保持短小、清晰。
成竹在胸,落笔有神.
ES2015 实现了一个非常棒的模块系统,它明确建议,小函数是优秀的工程实践。
记住,干净、组织良好的代码通常需要投入大量时间。你会发现这做起来有难度。可能需要很多尝试,可能会迭代、修改一个函数很多次。
然而,没有什么比乱麻一样的代码更让人痛心的了,那么这一切都是值得的!
如何写出小而清晰的函数?(JS 版)的更多相关文章
- css直接写出小三角
		在开发移动端项目时,总是遇到很多小三角,之前一直用图片,感觉好麻烦,今天尝试了直接用CSS写出小三角!先看看如何写出各种小三角! /*箭头向上*/ .arrow-up { ; ; border-lef ... 
- 写出完整版的strcpy函数及其他如:strcat,strcmp,strstr的函数实现
		(---牛客网中刷题---)写出完整版的strcpy函数 如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案: 2分 1 2 3 4 void strcpy( char *st ... 
- js中的写出想jquery中的函数一样调用
		1.IIFE: Immediately-Invoked function Expression 函数模块自调用 2.代码实现 <!DOCTYPE html> <html lang=& ... 
- 4.写出完整版的strcpy函数
		(1) 2~4分 void strcpy(char *strDest, char *strSrc) { while((*strDest++ = *strSrc++)!='\0'); } //将源字符串 ... 
- MySQL写出高效SQL
		mysql设计标准事务处理标准索引使用标准约束设计sql语句标准 怎么写出高效SQL清晰无误的了知业务需求满足业务需求,不做无用功知道表数据量和索引基本情况知道完成SQL需要扫描的数据量级SQL执行计 ... 
- 每个人都可以用C语言写的推箱子小游戏!今天你就可以写出属于自己项目~
		C语言,作为大多数人的第一门编程语言,重要性不言而喻,很多编程习惯,逻辑方式在此时就已经形成了.这个是我在大一学习 C语言 后写的推箱子小游戏,自己的逻辑能力得到了提升,在这里同大家分享这个推箱子小游 ... 
- 输入一个数字n  如果n为偶数则除以2,若为奇数则加1或者减1,直到n为1,求最少次数  写出一个函数
		题目: 输入一个数字n 如果n为偶数则除以2,若为奇数则加1或者减1,直到n为1,求最少次数 写出一个函数 首先,这道题肯定可以用动态规划来解, n为整数时,n的解为 n/2 的解加1 n为奇数时 ... 
- 描述性统计分析-用脚本将统计量函数批量化&分步骤逐一写出
		计算各种描述性统计量函数脚本(myDescriptStat.R)如下: myDescriptStat <- function(x){ n <- length(x) #样本数据个数 m &l ... 
- JS函数 编程练习 使用javascript代码写出一个函数:实现传入两个整数后弹出较大的整数。
		编程练习 使用javascript代码写出一个函数:实现传入两个整数后弹出较大的整数. 任务 第一步: 编写代码完成一个函数的定义吧. 第二步: 我们来补充函数体中的控制语句,完成函数功能吧. 提示: ... 
随机推荐
- URAL 1242 Werewolf(DFS)
			Werewolf Time limit: 1.0 secondMemory limit: 64 MB Knife. Moonlit night. Rotten stump with a short b ... 
- HDU-5806 NanoApe Loves Sequence Ⅱ(two-pointer或二分)
			题目大意:给一个整数序列,统计<k,m>子序列的数目.<k,m>序列是满足第k大的数字不比m小的连续子序列. 题目分析:维护一个不小于m的数的个数的后缀和数组,可以枚举序列起点 ... 
- IOS中如何获取手机的当前IP
			网上有许多类似的帖子.在搜索了资料以后.觉得下面的方法是最简单的. 使用的时候直接把类方法拖到自己新建的分类中就行. .h文件 #import <Foundation/Foundation.h& ... 
- 越狱Season 1-Episode 5: English, Fitz or Percy
			Season 1, Episode 5: English, Fitz or Percy -Pope: I assume this is about your transfer request for ... 
- Qt_Window@Qt Command Prompt从命令行创建工程
			#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplicatio ... 
- HttpServletRequest/HttpServletResponse乱码问题解决
			1.request.setCharacterEncoding只对POST请求起作用.GET请求用new String(paramterData.getBytes("iso8859-1&quo ... 
- 伪类选择器:root的妙用
			css3的元素旋转功能非常强大,也非常吸引人,但是很多时候因为浏览器使用率的问题,我们必需要想办法兼容一些低版本的浏览器,特别是ie这朵奇葩. 想要实现元素旋转本来很简单的一个属性就能实现,那就是tr ... 
- gitignore 忽略文件夹
			上面问题,按理说只要在.gitignore文件中加上 /dockAni/Library/ 即可实现. 但是我这样做了以后却发现sourceTree中仍然会列出Library中的文件. 最后在这个帖子中 ... 
- ant脚本编写
			使用ant脚本前的准备 1.下载一个ant安装包.如:apache-ant-1.8.4-bin.zip.解压到E盘. 2.配置环境变量.新增ANT_HOME:E:\apache-ant-1.8.4:P ... 
- 通过CSS禁用页面模块的复制和粘贴功能
			样式代码: -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; -khtml-user-select: ... 
