如何编写出易读性更强的JavaScript

142 阅读10分钟

每个人都想成为专家。但这到底是什么意思?多年来,我见过两种类型的人被称为 "专家"。专家1是指知道语言中的每一个工具,并确保使用每一点,无论它是否有帮助。专家2也知道每一种语法,但他们对采用什么来解决问题比较挑剔,考虑到许多因素,包括与代码有关的和无关的。

你能猜一猜我们希望哪位专家在我们的团队工作吗?如果你说是专家2,你会是对的。他们是一个专注于提供可读代码的开发人员,这些代码是别人可以理解和维护的JavaScript。他们能把复杂的东西变得简单。但是,"可读 "很少是确定的--事实上,它在很大程度上是基于观察者的眼睛。那么,这让我们怎么办呢?专家们在编写可读代码时应该以什么为目标?是否有明确的正确和错误的选择?

明显的选择

为了改善开发者的体验,TC39近年来在ECMAScript中增加了很多新的功能,包括从其他语言中借用的许多成熟的模式。在ES2019中增加的一个这样的功能是 Array.prototype.flat()它需要一个深度或Infinity 的参数,并对一个数组进行扁平化处理。如果没有给出参数,深度默认为1。

在增加这个功能之前,我们需要用下面的语法将一个数组平铺到一个单层。

let arr = [1, 2, [3, 4]];

[].concat.apply([], arr);
// [1, 2, 3, 4]

当我们添加flat() ,同样的功能可以用一个单一的、描述性的函数来表达。

arr.flat();
// [1, 2, 3, 4]

第二行代码是否更有可读性?答案是肯定的。事实上,两位专家都会同意。

不是每个开发者都会意识到flat() 的存在。但他们不需要,因为flat() 是一个描述性的动词,传达了正在发生的事情的含义。它比concat.apply() 要直观得多。

这是一个罕见的情况,对于新的语法是否比旧的好的问题有一个明确的答案。两位专家,每个人都熟悉这两种语法选项,都会选择第二种。他们会选择更短、更清晰、更容易维护的代码行。

但是,选择和取舍并不总是那么果断。

直觉检查

JavaScript的神奇之处在于它具有难以置信的通用性。它遍布整个网络是有原因的。至于你认为这是一件好事还是坏事,那是另一回事。

但是,随着这种多功能性的出现,也带来了选择的悖论。你可以用许多不同的方式来写同样的代码。你如何确定哪种方式是 "正确的"?除非你了解可用的选项和它们的局限性,否则你甚至无法开始做决定。

让我们用函数式编程中的 map()作为例子。我将走过各种迭代,都会产生相同的结果。

这是我们的map() 例子中最简洁的版本。它使用了最少的字符,全部装入一行。这就是我们的基线。

const arr = [1, 2, 3];
let multipliedByTwo = arr.map(el => el * 2);
// multipliedByTwo is [2, 4, 6]

下一个例子只增加了两个字符:括号。有什么损失吗?得到的又是什么呢?一个有多个参数的函数总是需要使用括号,这有什么区别吗?我认为是的。在这里添加括号几乎没有什么坏处,而且当你不可避免地写一个有多个参数的函数时,它提高了一致性。事实上,当我写这个的时候,Prettier强制执行了这个约束;它不想让我创建一个没有括号的箭头函数。

let multipliedByTwo = arr.map((el) => el * 2);

让我们再往前走一步。我们已经添加了大括号和回车。现在,这开始看起来更像一个传统的函数定义了。现在,有一个和函数逻辑一样长的关键字似乎是多余的。然而,如果函数超过了一行,这种额外的语法又是必须的。我们是否假定我们不会有任何其他超过一行的函数?这似乎令人怀疑。

let multipliedByTwo = arr.map((el) => {
  return el * 2;
});

接下来,我们完全删除了箭头函数。我们使用与之前相同的语法,但我们换掉了function 关键字。这很有意思,因为没有任何情况下这种语法是行不通的;没有任何参数或行数会导致问题,所以一致性是站在我们这边的。这比我们最初的定义更加冗长,但这是一件坏事吗?这对一个新的编码者,或者一个精通JavaScript以外的东西的人来说是什么打击?对JavaScript非常熟悉的人是否会因为这种语法而感到沮丧?

let multipliedByTwo = arr.map(function(el) {
  return el * 2;
});

最后,我们来到了最后一个选项:只传递函数。而timesTwo ,可以使用我们喜欢的任何语法来编写。同样,在任何情况下,传递函数名都不会造成问题。但退一步讲,想想这是否会造成混乱。如果你是这个代码库的新手,是否清楚timesTwo 是一个函数而不是一个对象?当然,map() 是为了给你一个提示,但错过这个细节也不是没有道理的。timesTwo 被声明和初始化的位置如何呢?它容易找到吗?是否清楚它在做什么以及它是如何影响这个结果的?所有这些都是重要的考虑因素。

const timesTwo = (el) => el * 2;
let multipliedByTwo = arr.map(timesTwo);

正如你所看到的,这里没有明显的答案。但是为你的代码库做出正确的选择意味着了解所有的选项和它们的限制。并且知道一致性需要小括号和大括号以及return 关键字。

在编写代码时,你必须要问自己一些问题。关于性能的问题通常是最常见的。但是当你看到功能相同的代码时,你的判断应该是基于人类--人类如何消费代码。

也许较新的并不总是更好的

到目前为止,我们已经找到了一个明确的例子,说明两位专家都会选择最新的语法,即使它并不广为人知。我们还看了一个例子,它提出了很多问题,但没有那么多答案。

现在是时候深入研究我以前写过的代码了......并将其删除。这是使我成为第一个专家的代码,使用一个鲜为人知的语法来解决一个问题,损害了我的同事和我们代码库的可维护性。

解构赋值可以让你从对象(或数组)中解压缩值。它通常看起来像这样。

const {node} = exampleObject;

它在一行中初始化了一个变量并给它赋了一个值。但它不一定要这样。

let node
;({node} = exampleObject)

最后一行代码使用析构法将一个变量赋值,但变量声明发生在它之前的一行。这并不是一件罕见的事情,但许多人并没有意识到你可以这样做。

但仔细看看这段代码。对于不使用分号结束行的代码,它强制使用了一个尴尬的分号。它用小括号包住了命令,并加上了大括号;这完全不清楚它在做什么。这不容易读懂,而且,作为一个专家,它不应该出现在我写的代码中。

let node
node = exampleObject.node

这段代码解决了这个问题。它是有效的,它的作用很清楚,我的同事也会理解它而不需要去查。对于解构语法,我能够做到并不意味着我应该做到

代码不是一切

正如我们所看到的,仅凭代码,专家2的解决方案很少是显而易见的;但每个专家会写哪些代码,仍然有明显的区别。这是因为代码是供机器阅读和人类解释的。所以有一些非代码的因素需要考虑!

你为一个JavaScript开发者团队所做的语法选择,与你为一个不精通细枝末节的多语言者团队所做的选择是不同的。

让我们以spread vs.concat() 为例。

Spread是几年前加入ECMAScript的,它被广泛采用。它是一种实用的语法,因为它可以做很多不同的事情。其中之一就是串联一些数组。

const arr1 = [1, 2, 3];
const arr2 = [9, 11, 13];
const nums = [...arr1, ...arr2];

尽管spread很强大,但它并不是一个非常直观的符号。因此,除非你已经知道它的作用,否则它不是超级有用的。虽然两位专家都可以有把握地假设一个由JavaScript专家组成的团队熟悉这种语法,但专家2可能会质疑一个由多语言程序员组成的团队是否真的如此。相反,专家2可能会选择concat() 方法,因为它是一个描述性的动词,你可能可以从代码的上下文中理解。

这个代码片段给我们的nums结果与上面的spread例子相同。

const arr1 = [1, 2, 3];
const arr2 = [9, 11, 13];
const nums = arr1.concat(arr2);

而这只是人为因素影响代码选择的一个例子。例如,一个被很多不同团队接触过的代码库,可能要持有更严格的标准,不一定能跟上最新和最伟大的语法。然后,你要超越主要的源代码,考虑你的工具链中的其他因素,使从事该代码工作的人的生活更容易,或更难。有的代码可以以一种对测试不利的方式进行结构化。有的代码会使你在未来的扩展或功能增加方面陷入困境。有的代码性能较差,不能处理不同的浏览器,或者不能访问。所有这些都是专家2号提出的建议中的因素。

专家2还考虑了命名的影响。但说实话,即使是他们在大多数时候也无法做到这一点。

结论

专家并不是通过使用规范的每一个部分来证明自己;他们是通过对规范的充分了解来证明自己,以便明智地部署语法并做出合理的决定。这就是专家如何成为倍增器--他们如何造就新的专家。

那么,这对我们这些自认为是专家或有志于成为专家的人来说意味着什么呢?这意味着写代码需要问自己很多问题。这意味着要真正考虑到你的开发者受众。你能写出的最好的代码是能完成一些复杂的事情,但那些检查你的代码库的人本来就能理解。

不,这并不容易。而且往往没有一个明确的答案。但这是你在编写每个函数时应该考虑的问题。