怎样编写更好的 JavaScript 代码

做者:Ryland Gjavascript

翻译:疯狂的技术宅前端

原文:dev.to/taillogs/pr…java

未经许可严禁转载node

我看到没有多少人谈论改进 JavaScript 代码的实用方法。如下是我用来编写更好的 JS 的一些顶级方法。git

使用TypeScript

改进你 JS 代码要作的第一件事就是不写 JS。TypeScript(TS)是JS的“编译”超集(全部能在 JS 中运行的东西都能在 TS 中运行)。 TS 在 vanilla JS 体验之上增长了一个全面的可选类型系统。很长一段时间里,整个 JS 生态系统对 TS 的支持不足以让我以为应该推荐它。但值得庆幸的是,那养的日子已通过去好久了,大多数框架都支持开箱即用的 TS。假设咱们都知道 TS 是什么,如今让咱们来谈谈为何要使用它。github

TypeScript 强制执行“类型安全”。

类型安全描述了一个过程,其中编译器验证在整个代码段中以“合法”方式使用全部类型。换句话说,若是你建立一个带有 number 类型参数的函数 foo编程

function foo(someNum: number): number {
  return someNum + 5;
}
复制代码

只应使给 foo 函数提供 number 类型的参数:后端

good前端工程化

console.log(foo(2)); // prints "7"
复制代码

no good数组

console.log(foo("two")); // invalid TS code
复制代码

除了向代码添加类型的开销以外,使用类型安全没有任何缺点。额外的好处太大了而不容忽视。类型安全提供额外级别的保护,以防止出现常见的错误或bug,这是对像 JS 这样没法无天的语言的祝福。

没法无天-主演:shia lebouf

电影:没法无天,主演 shia lebouf

Typescript 类型,能够重构更大的程序

重构大型 JS 程序是一场真正的噩梦。重构 JS 过程当中引发痛苦的大部分缘由是它没有强制按照函数的原型执行。这意味着 JS 函数永远不会被“误用”。若是我有一个由 1000 种不一样的服务使用的函数 myAPI

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
复制代码

我稍微改变了函数的原型:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
复制代码

这时我必须 100% 肯定每一个使用此函数的位置(足足有1000个)都正确地更新了用法。哪怕我漏掉一个地方,函数也可能就会失效。这与使用 TS 的状况相同:

以前

function myAPITS(someNum: number, someString: string) { ... }
复制代码

以后

function myAPITS(someString: string, someNum: number) { ... }
复制代码

正如你所看到的,我对 myAPITS 函数进行了与 JavaScript 对应的相同更改。可是这个代码不是产生有效的 JavaScript,而是致使无效的 TypeScript,由于如今使用它的 1000 个位置提供了错误的类型。并且因为咱们以前讨论过的“类型安全”,这 1000 个问题将会阻止编译,而且你的函数不会失效(这很是好)。

TypeScript使团队架构沟通更容易。

正确设置 TS 后,若是事先没有定义好接口和类,就很难编写代码。这也提供了一种简洁的分享、交流架构方案的方法。在 TS 出现以前,也存在解决这个问题的其余方案,可是没有一个可以真正的解决它,而且还须要你作额外的工做。例如,若是我想为本身的后端添加一个新的 Request 类型,我可使用 TS 将如下内容发送给一个队友。

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}
复制代码

尽管我不得不编写一些代码,可是如今能够分享本身的增量进度并得到反馈,而无需投入更多时间。我不知道 TS 本质上是否能比 JS 更少出现“错误”,不给我强烈认为,迫使开发人员首先定义接口和 API,从而产生更好的代码是颇有必要的。

总的来讲,TS 已经发展成为一种成熟且更可预测的 vanilla JS替代品。确定仍然须要 vanilla JS,可是我如今的大多数新项目都是从一开始就是 TS。

使用现代功能

JavaScript 是世界上最流行的编程语言之一。你可能会认为,有大约数百万人使用的 JS 如今已经有 20 多岁了,但事实偏偏相反。JS 已经作了不少改变和补充(是的我知道,从技术上说是 ECMAScript),从根本上改变了开发人员的体验。做为近两年才开始编写 JS 的人,个人优点在于没有偏见或指望。这致使了我关于要使用哪一种语言更加务实。

async 和 await

很长一段时间里,异步、事件驱动的回调是 JS 开发中不可避免的一部分:

传统的回调

makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});
复制代码

我不打算花时间来解释上述问题(我之前写过此类文章)。为了解决回调问题,JS 中增长了一个新概念 “Promise”。 Promise 容许你编写异步逻辑,同时避免之前基于回调的代码嵌套问题的困扰。

Promises

makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});
复制代码

Promise 优于回调的最大优势是可读性和可连接性。

虽然 Promise 很棒,但它们仍然有待改进。到如今为止,写 Promise 仍然感受不到“原生”。为了解决这个问题,ECMAScript 委员会决定添加一种利用 promise,asyncawait 的新方法:

async 和 await

try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}
复制代码

须要注意的是,你要 await 的任何东西都必须被声明为 async

在上一个例子中须要定义 makeHttpRequest

async function makeHttpRequest(url) {
  // ...
}
复制代码

也能够直接 await 一个 Promise,由于 async 函数实际上只是一个花哨的 Promise 包装器。这也意味着,async/await 代码和 Promise 代码在功能上是等价的。因此随意使用 async/await 并不会让你感到不安。

let 和 const

对于大多数 JS 只有一个变量限定符 varvar 在处理方面有一些很是独特且有趣的规则。 var 的做用域行为是不一致并且使人困惑的,在 JS 的整个生命周期中致使了意外行为和错误。可是从 ES6 开始有了 var 的替代品:constlet。几乎没有必要再使用 var 了。使用 var 的任何逻辑均可以转换为等效的 constlet 代码。

至于什么时候使用 constlet,我老是优先使用 constconst 是更严格的限制和 “永固的”,一般会产生更好的代码。我仅有 1/20 的变量用 let 声明,其他的都是 const

我之因此说 const 是 “永固的” 是由于它与 C/C++ 中的 const 的工做方式不一样。 const 对 JavaScript 运行时的意义在于对 const 变量的引用永远不会改变。这并不意味着存储在该引用中的内容永远不会改变。对于原始类型(数字,布尔等),const 确实转化为不变性(由于它是单个内存地址)。但对于全部对象(类,数组,dicts),const 并不能保证不变性。

箭头函数 =>

箭头函数是在 JS 中声明匿名函数的简明方法。匿名函数即描述未明确命名的函数。一般匿名函数做为回调或事件钩子传递。

vanilla 匿名函数

someMethod(1, function () { // has no name
  console.log('called');
});
复制代码

在大多数状况下,这种风格没有任何“错误”。 Vanilla 匿名函数在做用域方面表现得“有趣”,这可能致使许多意外错误。有了箭头函数,咱们就没必要再担忧了。如下是使用箭头函数实现的相同代码:

匿名箭头函数

someMethod(1, () => { // has no name
  console.log('called');
});
复制代码

除了更简洁以外,箭头函数还具备更实用的做用域行为。箭头函数从它们定义的做用域继承 this

在某些状况下,箭头函数能够更简洁:

const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"
复制代码

第 1 行的箭头函数包含一个隐式的 return 声明。不须要具备单线箭头功能的括号或分号。

在这里我想说清楚,这和 var 不同,对于 vanilla 匿名函数(特别是类方法)仍有效。话虽这么说,但若是你老是默认使用箭头函数而不是vanilla匿名函数的话,最终你debug的时间会更少。

像以往同样,Mozilla 文档是最好的资源

展开操做符

提取一个对象的键值对,并将它们做为另外一个对象的子对象添加,是一种很常见的状况。有几种方法能够实现这一目标,但它们都很是笨重:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }
复制代码

这种模式很是广泛,但也很乏味。感谢“展开操做符”,不再须要这样了:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }
复制代码

最重要的是,这也能够与数组无缝协做:

const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]
复制代码

它可能不是最重要的 JS 功能,但它是我最喜欢的功能之一。

文字模板(字符串模板)

字符串是最多见的编程结构之一。这就是为何它如此使人尴尬,以致于本地声明字符串在许多语言中仍然得不到很好的支持的缘由。在很长一段时间里,JS 都处于“糟糕的字符串”系列中。可是文字模板的添加使 JS 成为它本身的一个类别。本地文字模板,方便地解决了编写字符串,添加动态内容和编写桥接多行的两个最大问题:

const name = 'Ryland';
const helloString =
`Hello ${name}`;
复制代码

我认为代码说明了一切。多么使人赞叹。

对象解构

对象解构是一种从数据集合(对象,数组等)中提取值的方法,无需对数据进行迭代或显的式访问它的 key:

旧方法

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict.dog, myDict.cat);
复制代码

解构

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

const { dog, cat } = myDict;
animalParty(dog, cat);
复制代码

不过还有更多方式。你还能够在函数的签名中定义解构:

解构2

function animalParty({ dog, cat }) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict);
复制代码

它也适用于数组:

解构3

[a, b] = [10, 20];

console.log(a); // prints 10
复制代码

还有不少你应该使用现代功能。如下是我认为值得推荐的:

始终假设你的系统是分布式的

编写并行化程序时,你的目标是优化你一次性可以完成的工做量。若是你有 4 个可用的 CPU 核心,而且你的代码只能使用单个核心,则会浪费 75% 的算力。这意味着,阻塞、同步操做是并行计算的最终敌人。但考虑到 JS 是单线程语言,不会在多个核心上运行。那这有什么意义呢?

尽管 JS 是单线程的,它仍然是能够并发执行的。发送 HTTP 请求可能须要几秒甚至几分钟,在这期间若是 JS 中止执行代码,直到响应返回以前,语言将没法使用。

JavaScript 经过事件循环解决了这个问题。事件循环,即循环注册事件并基于内部调度或优先级逻辑去执行它们。这使得可以“同时”发送1000个 HTTP 请求或从磁盘读取多个文件。这是一个问题,若是你想要使用相似的功能,JavaScript 只能这样作。最简单的例子是 for 循环:

let sum = 0;
const myArray = [1, 2, 3, 4, 5, ... 99, 100];
for (let i = 0; i < myArray.length; i += 1) {
  sum += myArray[i];
}
复制代码

for 循环是编程中存在的最不并发的构造之一。在上一份工做中,我带领一个团队花了几个月的时间尝试将 R 语言中的 for-loops 转换为自动并行代码。这基本上是一个不可能的任务,只有经过等待深度学习技术的改善才能解决。并行化 for 循环的难度来自一些有问题的模式。用 for 循环进行顺序执行的状况是比较罕见的,但它们没法保证循环的可分离性:

let runningTotal = 0;
for (let i = 0; i < myArray.length; i += 1) {
  if (i === 50 && runningTotal > 50) {
    runningTotal = 0;
  }
  runningTotal += Math.random() + runningTotal;
}
复制代码

若是按顺序执行迭代,此代码仅生成预期结果。若是你尝试执行屡次迭代,则处理器可能会根据不许确的值进入错误地分支,从而使结果无效。若是这是 C 代码,咱们将会进行不一样的讨论,由于使用状况不一样,编译器可使用循环实现至关多的技巧。在 JavaScript 中,只有绝对必要时才应使用传统的 for 循环。不然使用如下构造:

map

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url) => makHttpRequest(url));
const results = await Promise.all(resultingPromises);
复制代码

带索引的 map

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url, index) => makHttpRequest(url, index));
const results = await Promise.all(resultingPromises);
复制代码

for-each

const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
// note this is non blocking
urls.forEach(async (url) => {
  try {
    await makHttpRequest(url);
  } catch (err) {
    console.log(`${err} bad practice`);
  }
});
复制代码

下面我将解释为何这是对传统 for 循环的改进:不是按顺序执行每一个“迭代”,而是构造诸如 map 之类的全部元素,并将它们做为单独的事件提交给用户定义的映射函数。这将直接与运行时通讯,各个“迭代”彼此之间没有链接或依赖,因此可以容许它们同时运行。我认为如今应该抛弃一些循环,应该去使用定义良好的 API。这样对任何将来数据访问模式实现的改进都将使你的代码受益。 for 循环过于通用,没法对同一模式进行有意义的优化。

map 和 forEach 以外还有其余有效的异步选择,例如 for-await-of。

Lint 你的代码并强制使用一致的风格

没有一致风格的代码难以阅读和理解。所以,用任何语言编写高端代码的一个关键就是具备一致和合理的风格。因为 JS 生态系统的广度,有许多针对 linter 和样式细节的选项。我不能强调的是,你使用一个 linter 并强制执行同一个样式(随便哪一个)比你专门选择的 linter 或风格更重要。最终没人可以准确地编写代码,因此优化它是一个不切实际的目标。

有不少人问他们是否应该用 eslintprettier。对我来讲,它们的目的是有很大区别的,所以应该结合使用。 Eslint 是一种传统的 “linter”,大多数状况下,它会识别代码中与样式关系不大的问题,更多的是与正确性有关。例如,我使用eslint与 AirBNB 规则。若是用了这个配置,如下代码将会强制 linter 失败:

var fooVar = 3; // airbnb rules forebid "var"
复制代码

很明显,eslint 为你的开发周期增长价值。从本质上讲,它确保你遵循关于“is”和“isn't”良好实践的规则。所以 linters 本质上是执拗的,只要你的代码不符合规则,linter 可能就会报错。

Prettier 是一个代码格式化程序。它不太关心“正确性”,更关注一致性。 Prettier 不会对使用 var 提出异议,但会自动对齐代码中的全部括号。在个人开发过程当中,在将代码推送到 Git 以前,老是处理得很​​漂亮。不少时候让 Prettier 在每次提交到 repo 时自动运行是很是有意义的。这确保了进入源码控制系统的全部代码都有一致的样式和结构。

测试你的代码

编写测试是一种间接改进你代码但很是有效的方法。我建议你熟悉各类测试工具。你的测试需求会有所不一样,没有哪种工具能够处理全部的问题。 JS 生态系统中有大量完善的测试工具,所以选择哪一种工具主要归结为我的偏好。一如既往,要为你本身考虑。

Test Driver - Ava

测试驱动 — Ava

AvaJS on Github

测试驱动只是简单的框架,能够提供很是高级别的结构和工具。它们一般与其余特定测试工具结合使用,这些工​​具根据你的实际需求而有所不一样。

Ava 是表达力和简洁性的完美平衡。 Ava 的并行和独立的架构是个人最爱。快速运行的测试能够节省开发人员的时间和公司的资金。Ava 拥有许多不错的功能,例如内置断言等。

替代品:Jest,Mocha,Jasmine

Spies 和 Stubs — Sinon

Sinon on Githubgithub.com/sinonjs/sin…

Spies 为咱们提供了“功能分析”,例如调用函数的次数,调用了哪些函数以及其余有用的数据。

Sinon 是一个能够作不少事的库,但只有少数的事情作得超级好。具体来讲,当涉及到 Spies 和 Stubs 时,sinon很是擅长。功能集丰富并且语法简洁。这对于 Stubs 尤为重要,由于它们为了节省空间而只是部分存在。

替代方案:testdouble

模拟 — Nock

Nock on Githubgithub.com/nock/nock?s…

HTTP 模拟是伪造 http 请求中某些部分的过程,所以测试人员能够注入自定义逻辑来模拟服务器行为。

http 模拟多是一种真正的痛苦,nock 使它不那么痛苦。 Nock 直接覆盖 nodejs 内置的 request 并拦截传出的 http 请求。这使你能够彻底控制 http 响应。

替代方案:我真的不知道 :(

网络自动化 - Selenium

Selenium on Githubgithub.com/SeleniumHQ/…

我对推荐 Selenium 有着一种复杂的态度。因为它是 Web 自动化最受欢迎的选择,所以它拥有庞大的社区和在线资源集。不幸的是学习曲线至关陡峭,而且它依赖许多外部库。尽管如此,它是惟一真正的免费选项,因此除非你作一些企业级的网络自动化,不然仍是 Selenium 最适合这个工做。

欢迎关注前端公众号:前端先锋,领取前端工程化实用工具包。