41

怎样编写更好的 JavaScript 代码[每日前端夜话0xA4]

 4 years ago
source link: https://www.tuicool.com/articles/VrU3Arn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

每日前端夜话 0xA4

每日前端夜话,陪你聊前端。

每天晚上18:00准时推送。

正文共:5069 字

预计阅读时间:15 分钟

作者:Ryland G

翻译:疯狂的技术宅

来源: dev.to

aayAZj6.png!web

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

使用TypeScript

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

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

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

1function foo(someNum: number): number {
2  return someNum + 5;
3}

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

good

1console.log(foo(2)); // prints "7"

no good

1console.log(foo("two")); // invalid TS code

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

Zf6fI3E.jpg!web 无法无天-主演:shia lebouf

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

Typescript 类型,可以重构更大的程序

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

1function myAPI(someNum, someString) {
2  if (someNum > 0) {
3    leakCredentials();
4  } else {
5    console.log(someString);
6  }
7}

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

1function myAPI(someString, someNum) {
2  if (someNum > 0) {
3    leakCredentials();
4  } else {
5    console.log(someString);
6  }
7}

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

之前

1function myAPITS(someNum: number, someString: string) { ... }

之后

1function myAPITS(someString: string, someNum: number) { ... }

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

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

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

1interface BasicRequest {
2  body: Buffer;
3  headers: { [header: string]: string | string[] | undefined; };
4  secret: Shhh;
5}

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

总的来说,TS 已经发展成为一种成熟且更可预测的 vanilla JS替代品。肯定仍然需要 vanilla JS,但是我现在的大多数新项目都是从一开始就是 TS。

使用现代功能

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

async 和 await

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

传统的回调

1makeHttpRequest('google.com', function (err, result) {
2  if (err) {
3    console.log('Oh boy, an error');
4  } else {
5    console.log(result);
6  }
7});

我不打算花时间来解释上述问题(我以前写过此类文章【 http://cdevn.com/parallel-computing-simplified-starring-gordon-ramsay 】)。为了解决回调问题,JS 中增加了一个新概念 “Promise”。Promise 允许你编写异步逻辑,同时避免以前基于回调的代码嵌套问题的困扰。

Promises

1makeHttpRequest('google.com').then(function (result) {
2  console.log(result);
3}).catch(function (err) {
4  console.log('Oh boy, an error');
5});

Promise 优于回调的最大优点是可读性和可链接性。

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

async 和 await

1try {
2  const result = await makeHttpRequest('google.com');
3  console.log(result);
4} catch (err) {
5  console.log('Oh boy, an error');
6}

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

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

1async function makeHttpRequest(url) {
2  // ...
3}

也可以直接 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 匿名函数

1someMethod(1, function () { // has no name
2  console.log('called');
3});

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

匿名箭头函数

1someMethod(1, () => { // has no name
2  console.log('called');
3});

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

在某些情况下,箭头函数可以更简洁:

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

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

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

像以往一样,Mozilla 文档是最好的资源【 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

展开操作符

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

1const obj1 = { dog: 'woof' };
2const obj2 = { cat: 'meow' };
3const merged = Object.assign({}, obj1, obj2);
4console.log(merged) // prints { dog: 'woof', cat: 'meow' }

这种模式非常普遍,但也很乏味。感谢“展开操作符”,再也不需要这样了:

1const obj1 = { dog: 'woof' };
2const obj2 = { cat: 'meow' };
3console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }

最重要的是,这也可以与数组无缝协作:

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

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

文字模板(字符串模板)

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

1const name = 'Ryland';
2const helloString =
3`Hello
4 ${name}`;

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

对象解构

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

旧方法

1function animalParty(dogSound, catSound) {}
2
3const myDict = {
4  dog: 'woof',
5  cat: 'meow',
6};
7
8animalParty(myDict.dog, myDict.cat);

解构

1function animalParty(dogSound, catSound) {}
2
3const myDict = {
4  dog: 'woof',
5  cat: 'meow',
6};
7
8const { dog, cat } = myDict;
9animalParty(dog, cat);

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

解构2

1function animalParty({ dog, cat }) {}
2
3const myDict = {
4  dog: 'woof',
5  cat: 'meow',
6};
7
8animalParty(myDict);

它也适用于数组:

解构3

1[a, b] = [10, 20];
2
3console.log(a); // prints 10

还有很多你应该使用现代功能。以下是我认为值得推荐的:

  • Rest Parameter【 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters

  • Import Over Require【 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

  • 查找数组元素【 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find

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

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

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

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

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

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

1let runningTotal = 0;
2for (let i = 0; i < myArray.length; i += 1) {
3  if (i === 50 && runningTotal > 50) {
4    runningTotal = 0;
5  }
6  runningTotal += Math.random() + runningTotal;
7}

如果按顺序执行迭代,此代码仅生成预期结果。如果你尝试执行多次迭代,则处理器可能会根据不准确的值进入错误地分支,从而使结果无效。如果这是 C 代码,我们将会进行不同的讨论,因为使用情况不同,编译器可以使用循环实现相当多的技巧。在 JavaScript 中,只有绝对必要时才应使用传统的 for 循环。否则使用以下构造:

map

1// in decreasing relevancy :0
2const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
3const resultingPromises = urls.map((url) => makHttpRequest(url));
4const results = await Promise.all(resultingPromises);

带索引的 map

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

for-each

1const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
2// note this is non blocking
3urls.forEach(async (url) => {
4  try {
5    await makHttpRequest(url);
6  } catch (err) {
7    console.log(`${err} bad practice`);
8  }
9});

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

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

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

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

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

1var 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(https://github.com/avajs)

测试驱动只是简单的框架,可以提供非常高级别的结构和工具。它们通常与其他特定测试工具结合使用,这些工具根据你的实际需求而有所不同。

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

替代品:Jest,Mocha,Jasmine

Spies 和 Stubs — Sinon

Sinon on Github(https://github.com/sinonjs/sinon)

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

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

替代方案:testdouble

模拟 — Nock

Nock on Github(https://github.com/nock/nock?source=post_page---------------------------)

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

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

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

网络自动化 -  Selenium

Selenium on Github(https://github.com/SeleniumHQ/selenium)

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

原文: https://dev.to/taillogs/practical-ways-to-write-better-javascript-26d4

下面夹杂一些私货:也许你和高薪之间只差这一张图

2019年京程一灯课程体系上新,这是我们第一次将全部课程列表对外开放。

愿你有个好前程,愿你月薪30K。我们是认真的 ! BbquyaF.png!web

zMFVruu.jpg!web

在公众号内回复“体系”查看高清大图

长按二维码,加大鹏老师微信好友

拉你加入前端技术交流群

唠一唠怎样才能拿高薪

JFNJFbv.jpg!web

小手一抖,资料全有。长按二维码关注 前端先锋 ,阅读更多技术文章和业界动态。

MFryQjN.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK