怎样编写更好的 JavaScript 代码
Posted xuwenming
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了怎样编写更好的 JavaScript 代码相关的知识,希望对你有一定的参考价值。
使用TypeScript
改进你 JS 代码要做的第一件事就是不写 JS。TypeScript(TS)是JS的“编译”超集(所有能在 JS 中运行的东西都能在 TS 中运行)。 TS 在 vanilla JS 体验之上增加了一个全面的可选类型系统。很长一段时间里,整个 JS 生态系统对 TS 的支持不足以让我觉得应该推荐它。但值得庆幸的是,那养的日子已经过去很久了,大多数框架都支持开箱即用的 TS。假设我们都知道 TS 是什么,现在让我们来谈谈为什么要使用它。
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
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,async
和 await
的新方法:
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 只有一个变量限定符 var
。 var
在处理方面有一些非常独特且有趣的规则。 var
的作用域行为是不一致而且令人困惑的,在 JS 的整个生命周期中导致了意外行为和错误。但是从 ES6 开始有了 var
的替代品:const
和 let
。几乎没有必要再使用 var
了。使用 var
的任何逻辑都可以转换为等效的 const
和 let
代码。
至于何时使用 const
和 let
,我总是优先使用 const
。 const
是更严格的限制和 “永固的”,通常会产生更好的代码。我仅有 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
还有很多你应该使用现代功能。以下是我认为值得推荐的:
- Rest Parameter
- Import Over Require
- 查找数组元素
始终假设你的系统是分布式的
编写并行化程序时,你的目标是优化你一次性能够完成的工作量。如果你有 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 或风格更重要。最终没人能够准确地编写代码,所以优化它是一个不切实际的目标。
有很多人问他们是否应该用 eslint 或 prettier。对我来说,它们的目的是有很大区别的,因此应该结合使用。 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 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 最适合这个工作。
以上是关于怎样编写更好的 JavaScript 代码的主要内容,如果未能解决你的问题,请参考以下文章