每个人都想成为专家,但什么才是专家呢?这些年来,我见过两种被称为“专家”的人。专家一是指对语言中的每一个工具都了如指掌的人,而且无论是否有帮助,都一定要用好每一点。专家二也知道每一个语法,但他们对采用什么来解决问题比较挑剔,会考虑很多因素,包括与代码有关的和无关的。

你能猜猜我们想让哪位专家加入我们的团队吗?如果你说是专家二,那你猜对了。他们是专注于编写可读性好的 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

这个代码解决了这个问题。它很好用,很清楚它的作用,我的同事们不用查就能明白。对于解构语法,我可以做并不代表我应该做。

代码不是一切

正如我们所看到的那样,专家二的解决方案很少能单凭代码就能明显地看出;但每个专家会写哪些代码,还是有明显的区别。这是因为代码是给机器看的,而人类要解释它。所以还有一些非代码因素需要考虑!

你为一个 JavaScript 开发团队所做的语法选择,与你为一个不沉浸于细枝末节的多语言团队应该做的选择是不同的。

让我们以扩展运算符(...) 与 concat() 为例。

扩展运算符是几年前添加到 ECMAScript 中的,它得到了广泛的应用。它是一种实用的语法,它可以做很多不同的事情。其中之一就是连接数组。

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

虽然扩展运算符很强大,但它并不是一个很直观的符号。所以除非你已经知道它的作用,否则它并没有极大的帮助。虽然两位专家可能会安全地假设一个 JavaScript 专家团队熟悉这种语法,但专家二可能会质疑一个多语言程序员团队是否如此。相反,专家二可能会选择 concat() 方法来代替,因为它是一个描述性动词,你可以从代码的上下文中理解。

这段代码给我们提供了和上面扩展运算符例子一样的数字结果。

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

而这只是人为因素影响代码选择的一个例子。例如,一个由很多不同团队接触的代码库,可能必须持有更严格的标准,不一定能跟上最新最强的语法。然后,你站在源代码以外的视角,考虑你的工具链中的其他因素,这些因素会让在这些代码上工作的人感到更轻松或者更困难。有一些代码,可以以一种敌视测试的方式进行结构化。有一些代码,让你在未来的扩展或功能添加时陷入困境。有的代码性能较差,不能处理不同的浏览器。所有这些都会成为专家二提出建议的因素。

专家二还考虑了命名的影响。但说实话,即使是他们也不能在大多数时候把这一点做对。

结语

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

那么,这对我们这些自认为是专家或有志于成为专家的人来说意味着什么呢?这意味着编写代码需要问自己很多问题。它意味着要以一种真实的方式考虑你的开发者受众。你能写出的最好的代码是完成一些复杂的业务,但本质上是那些检查你的代码库的人所能理解的代码。

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

英文原文:https://alistapart.com/article/human-readable-javascript

可读性友好的JavaScript:两个专家的故事的更多相关文章

  1. javascript两种定时器的使用及其清除

    <!--示例代码如下:--><!DOCTYPE html> <html> <body> <p>A script on this page s ...

  2. JavaScript两个变量的值交换的多种方式

    前言 该文是在看别人博客的时候发现的,很有趣的一篇文章,这里摘录到自己的简书中,供给各位读者学习本文主要描述,如何不使用中间值,将两个变量的值进行交换.前三种只适用于number类型的数值交换,第四和 ...

  3. JavaScript两种方法来定义一个函数

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  4. JavaScript两个变量交换值(不使用临时变量)

    概要  本文主要描述,如何不使用中间值,将两个变量的值进行交换. 一.普通做法 var a = 1, b = 2, tmp; tmp = a; a = b; b = tmp;  普通的做法就是声明多一 ...

  5. javascript两种声明函数的方式的一次深入解析

    声明函数的方式 javascript有两种声明函数的方式,一个是函数表达式定义函数,也就是我们说的匿名函数方式,一个是函数语句定义函数,下面看代码: /*方式一*/ var FUNCTION_NAME ...

  6. JavaScript两数相加(踩坑)记录

    Adding two numbers concatenates them instead of calculating the sum JavaScript里两个变量 var a = 2: var b ...

  7. [javascript]两段 javaScript 代码的逻辑比较

    两段 javaScript 代码的逻辑比较: #1 if(tagName.length < 3){    $(this).parent().addClass('active');    tagN ...

  8. Javascript两个感叹号的用法(!!)

    var foo; alert(!foo);//undefind情况下或者null,一个感叹号返回的是true; alert(!goo);//undefind情况下,一个感叹号返回的也是true; 但是 ...

  9. 讲解JavaScript两个圆括号、自调用和闭包函数

    一.JavaSript圆括号的使用 先来看一组通过函数声明来定义的函数: 先附代码: 运行结果如下: 这里我们可以看出: Ø  若没有加圆括号,则返回的是这个函数的内容 Ø  若加上圆括号,则返回的是 ...

随机推荐

  1. transient的作用及序列化

    1.transient 介绍 Java中的transient关键字,transient是短暂的意思.对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略. 因此,trans ...

  2. 验证销售部门的数据查看权限-脚本demo

    1 # coding:utf-8 2 ''' 3 @file: run_old.py 4 @author: jingsheng hong 5 @ide: PyCharm 6 @createTime: ...

  3. bluestein算法

    我们熟知的FFT算法实际上是将一个多项式在2n个单位根处展开,将其点值对应相乘,并进行逆变换.然而,由于单位根具有"旋转"的特征(即$w_{m}^{j}=w_{m}^{j+m}$) ...

  4. 别找了,这可能是全网最全的鸿蒙(OpenHarmony)刷机指南

    目录: 1. 配置编译环境 2. 编译HarmonyOS源代码 3. 烧录HarmonyOS 4.下载文中资源 5.作者文章合集 摘要:相信很多同学都玩过鸿蒙(HarmonyOS)了,不过估计大多数同 ...

  5. 使用OkHttp和OkHttpGo获取OneNET云平台数据

    图1是OneNET官网关于NB-IoT文档关于批量查询设备最新数据的介绍,可以看到GET方法的URL地址和两个头部信息(图2是Htto请求消息结构).所以在写url时,还要添加两行头部字段名,不然获取 ...

  6. Docker的架构

    一.Docker引擎 docker引擎是一个c/s结构的应用,主要组件见下图: Server是一个常驻进程 REST API 实现了client和server间的交互协议 CLI 实现容器和镜像的管理 ...

  7. docker封装nuxt项目使用jenkins发布

    一.概述 vue项目可以打一个dist静态资源包,直接使用Nginx发布即可. 但是nuxt项目无法像vue那样,可以打一个dist静态资源包. 需要安装Node.js,并使用npm install ...

  8. 01.从0实现一个JVM语言之架构总览

    00.一个JVM语言的诞生过程 文章集合以及项目展望 源码github地址 这一篇将是架构总览, 将自顶向下地叙述自制编译器的要素; 文章目录 01.从0实现一个JVM语言之架构总览 架构总览目前完成 ...

  9. CCF(地铁修建):向前星+dijikstra+求a到b所有路径中最长边中的最小值

    地铁修建 201703-4 这题就是最短路的一种变形,不是求两点之间的最短路,而是求所有路径中的最长边的最小值. 这里还是使用d数组,但是定义不同了,这里的d[i]就是表示从起点到i的路径中最长边中的 ...

  10. OpenGL中的坐标系统详细概括:包括Z缓冲

    一: 首先就是关于几个坐标系统的概括: 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的.这些坐标相对于世界的全局 ...