做者:Ryland Gjavascript
翻译:疯狂的技术宅前端
原文:dev.to/taillogs/pr…java
未经许可严禁转载node
我看到没有多少人谈论改进 JavaScript 代码的实用方法。如下是我用来编写更好的 JS 的一些顶级方法。git
改进你 JS 代码要作的第一件事就是不写 JS。TypeScript(TS)是JS的“编译”超集(全部能在 JS 中运行的东西都能在 TS 中运行)。 TS 在 vanilla JS 体验之上增长了一个全面的可选类型系统。很长一段时间里,整个 JS 生态系统对 TS 的支持不足以让我以为应该推荐它。但值得庆幸的是,那养的日子已通过去好久了,大多数框架都支持开箱即用的 TS。假设咱们都知道 TS 是什么,如今让咱们来谈谈为何要使用它。github
类型安全描述了一个过程,其中编译器验证在整个代码段中以“合法”方式使用全部类型。换句话说,若是你建立一个带有 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
重构大型 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 个问题将会阻止编译,而且你的函数不会失效(这很是好)。
正确设置 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 的人,个人优点在于没有偏见或指望。这致使了我关于要使用哪一种语言更加务实。
很长一段时间里,异步、事件驱动的回调是 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
并不会让你感到不安。
对于大多数 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的时间会更少。
提取一个对象的键值对,并将它们做为另外一个对象的子对象添加,是一种很常见的状况。有几种方法能够实现这一目标,但它们都很是笨重:
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。
没有一致风格的代码难以阅读和理解。所以,用任何语言编写高端代码的一个关键就是具备一致和合理的风格。因为 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 是表达力和简洁性的完美平衡。 Ava 的并行和独立的架构是个人最爱。快速运行的测试能够节省开发人员的时间和公司的资金。Ava 拥有许多不错的功能,例如内置断言等。
替代品:Jest,Mocha,Jasmine
Sinon on Github(github.com/sinonjs/sin…
Spies 为咱们提供了“功能分析”,例如调用函数的次数,调用了哪些函数以及其余有用的数据。
Sinon 是一个能够作不少事的库,但只有少数的事情作得超级好。具体来讲,当涉及到 Spies 和 Stubs 时,sinon很是擅长。功能集丰富并且语法简洁。这对于 Stubs 尤为重要,由于它们为了节省空间而只是部分存在。
替代方案:testdouble
Nock on Github(github.com/nock/nock?s…
HTTP 模拟是伪造 http 请求中某些部分的过程,所以测试人员能够注入自定义逻辑来模拟服务器行为。
http 模拟多是一种真正的痛苦,nock 使它不那么痛苦。 Nock 直接覆盖 nodejs 内置的 request
并拦截传出的 http 请求。这使你能够彻底控制 http 响应。
替代方案:我真的不知道 :(
Selenium on Github(github.com/SeleniumHQ/…
我对推荐 Selenium 有着一种复杂的态度。因为它是 Web 自动化最受欢迎的选择,所以它拥有庞大的社区和在线资源集。不幸的是学习曲线至关陡峭,而且它依赖许多外部库。尽管如此,它是惟一真正的免费选项,因此除非你作一些企业级的网络自动化,不然仍是 Selenium 最适合这个工做。