编写更好的 JavaScript 的实用方法使用 TypeScript 使用现代功能始终假设您的系统是分布式的检查您的代码并强制执行样式测试您的代码另外两个随机的 JS 事情结论

2025-05-27

编写更好的 JavaScript 的实用方法

使用 TypeScript

使用现代功能

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

检查代码并强制执行样式

测试你的代码

另外两个随机的 JS 事物

结论

我很少看到有人谈论提升 JavaScript 水平的实用方法。以下是我用来写出更好 JavaScript 代码的一些常用方法。

使用 TypeScript

提升 JS 水平的首要方法就是不写 JS。对于初学者来说,TypeScript (TS)是 JS 的“编译型”超集(任何用 JS 运行的代码都可以用 TS 运行)。TS 在原生 JS 体验的基础上添加了一个全面的可选类型系统。长期以来,整个生态系统对 TS 的支持并不一致,这让我不太愿意推荐它。值得庆幸的是,这种日子早已过去,大多数框架都开箱即用地支持 TS。既然我们对 TS什么已经有了共识,那么我们来谈谈为什么要使用它。

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

类型安全指的是编译器验证整段代码中所有类型的使用是否“合法”的过程。换句话说,如果你创建一个foo接受数字的函数:

function foo(someNum: number): number {
  return someNum + 5;
}
Enter fullscreen mode Exit fullscreen mode

foo函数只能用数字来调用:

好的

console.log(foo(2)); // prints "7"
Enter fullscreen mode Exit fullscreen mode

不好

console.log(foo("two")); // invalid TS code
Enter fullscreen mode Exit fullscreen mode

除了在代码中添加类型的开销之外,强制类型安全没有任何缺点。另一方面,它的好处也大到不容忽视。类型安全为常见的错误/bug 提供了额外的保护,这对于像 JS 这样不受约束的语言来说,无疑是一大福音。


主演:希亚·拉博夫

Typescript 类型使得重构更大的应用程序成为可能。

重构大型 JS 应用简直就是一场噩梦。重构 JS 的痛苦很大程度上源于它不强制函数签名。这意味着 JS 函数永远不会被真正“滥用”。例如,如果我有一个函数myAPI被 1000 个不同的服务使用:

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
Enter fullscreen mode Exit fullscreen mode

我稍微改变了一下调用签名:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
Enter fullscreen mode Exit fullscreen mode

我必须 100% 确定,每个使用此函数的地方(成千上万个地方)都正确更新了用法。哪怕漏掉一个,我的凭证都可能泄露。以下是 TS 的相同场景:


function myAPITS(someNum: number, someString: string) { ... }
Enter fullscreen mode Exit fullscreen mode


function myAPITS(someString: string, someNum: number) { ... }
Enter fullscreen mode Exit fullscreen mode

如您所见,该myAPITS函数经历了与 JavaScript 对应函数相同的更改。但是,这段代码并没有生成有效的 JavaScript,而是生成了无效的 TypeScript,因为它在数千个位置使用时提供了错误的类型。由于我们之前讨论过的“类型安全”,这 1000 个错误会阻止编译,并且您的凭据不会被泄露(这总是好的)。

TypeScript 让团队架构沟通更加容易。

如果 TS 设置正确,如果不先定义接口和类,编写代码将会非常困难。这也提供了一种共享简洁易沟通的架构方案的方法。在 TS 出现之前,虽然存在其他针对此问题的解决方案,但没有一个能够原生地解决问题,并且无需您额外付出工作。例如,如果我想Request为我的后端提出一种新类型,我可以使用 TS 将以下内容发送给我的团队成员。

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}
Enter fullscreen mode Exit fullscreen mode

我已经不得不编写代码了,但现在我可以分享我的增量进度并获得反馈,而无需投入更多时间。我不知道 TS 是否天生就比 JS 更少“bug”。我坚信,强制开发人员先定义接口和 API 可以写出更好的代码。

总的来说,TS 已经发展成为原生 JS 的一个成熟且更可预测的替代方案。当然,仍然需要熟悉原生 JS,但最近我开始的大多数新项目都是从一开始就使用 TS 的。

使用现代功能

JavaScript 是世界上最流行的编程语言之一(即使不是最流行的)。你可能会认为,一门拥有 20 多年历史、被数亿人使用的语言,到现在应该已经被大多数人“玩转”了,但事实并非如此。近年来,JS(没错,严格来说是 ECMAScript)经历了许多变化和新增功能,从根本上改变了开发者的体验。作为一个两年前才开始接触 JS 的人,我的优势在于没有偏见或期望。这使得我在选择使用或避免使用 JS 特性时,能够更加务实、客观地做出选择。

asyncawait

长期以来,异步、事件驱动的回调是 JS 开发中不可避免的一部分:

传统回调

makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});
Enter fullscreen mode Exit fullscreen mode

我不会花时间解释为什么上述代码有问题(但我之前解释过)。为了解决回调的问题,JS 中引入了一个新概念“Promises”。Promises 允许你编写异步逻辑,同时避免之前困扰基于回调的代码的嵌套问题。

承诺

makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});
Enter fullscreen mode Exit fullscreen mode

Promises 相对于回调的最大优势是可读性和可链接性。

Promises 虽然很棒,但仍有一些不足之处。最终,编写 Promises 仍然感觉不太“原生”。为了解决这个问题,ECMAScript 委员会决定添加一种新的使用 Promises 的方法,async并且await

asyncawait

try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}
Enter fullscreen mode Exit fullscreen mode

一个警告是,你await必须申报任何事情async

上例中 makeHttpRequest 的必需定义

async function makeHttpRequest(url) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

也可以await直接使用 Promise,因为async函数实际上只是一个精美的 Promise 包装器。这也意味着,async/await代码和 Promise 代码在功能上是等效的。所以请放心使用,async/await无需感到内疚。

letconst

在 JS 的大部分存在时间里,只有一个变量作用域限定符varvar它在处理作用域方面有一些非常独特/有趣的规则。 的作用域行为var不一致且令人困惑,并导致了 JS 整个生命周期中出现意外行为和错误。但从 ES6 开始,有了varconst和 的替代方案let。现在几乎没有再使用的必要了var,所以不要再使用了。任何使用var、 的逻辑都可以转换为等效的基于constlet的代码。

至于何时使用constvs let,我总是首先声明所有内容const. const,因为它限制性更强,而且“不可变”,通常可以产生更好的代码。在“真实场景”中,使用 的情况并不多let,我估计只有 1/20 的变量是用 声明的let。其余的都是const. 。

我之所以说它const是“不可变的”,是因为它的工作方式与constC/C++ 不同。const对于 JavaScript 运行时来说,这意味着对该const变量的引用永远不会改变。这并不意味着存储在该引用中的内容永远不会改变。对于原始类型(数字、布尔值等),const确实转化为不可变性(因为它是一个单一的内存地址)。但对于所有对象(类、数组、字典),const并不保证不可变性。

箭头=>函数

箭头函数是 JS 中声明匿名函数的一种简洁方法。匿名函数描述的是未明确命名的函数。通常,匿名函数作为回调或事件钩子传递。

原始匿名函数

someMethod(1, function () { // has no name
  console.log('called');
});
Enter fullscreen mode Exit fullscreen mode

在大多数情况下,这种风格并没有什么“问题”。原生匿名函数在作用域方面的行为“有点奇怪”,这可能会导致许多意想不到的 bug。有了箭头函数,我们再也不用担心这个问题了。以下是用箭头函数实现的相同代码:

匿名箭头函数

someMethod(1, () => { // has no name
  console.log('called');
});
Enter fullscreen mode Exit fullscreen mode

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

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

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

单行的箭头函数包含一个隐式return语句。单行箭头函数无需使用括号或分号。

我想澄清一下。这不是个var例,原生匿名函数(特别是类方法)仍然有其有效的用例。话虽如此,我发现,如果你总是默认使用箭头函数,最终的调试工作会比默认使用原生匿名函数少得多。

与往常一样,Mozilla 文档是最好的资源

扩展运算符...

提取一个对象的键值对,并将其添加为另一个对象的子对象,这是一种非常常见的场景。历史上,有几种方法可以实现这一点,但这些方法都非常笨重:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }
Enter fullscreen mode Exit fullscreen mode

这种模式极其常见,所以上面的方法很快就会变得乏味。幸亏有了“扩展运算符”,我们再也不需要使用它了:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }
Enter fullscreen mode Exit fullscreen mode

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

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

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

模板文字(模板字符串)

字符串是最常见的编程结构之一。正因如此,许多语言对原生声明字符串的支持仍然很差,这真是令人尴尬。很长一段时间以来,JS 都属于“蹩脚字符串”一族。但模板字面量的加入,让 JS 自成一派。模板字面量原生且便捷地解决了编写字符串时遇到的两个最大问题:添加动态内容以及编写跨多行代码的字符串:

const name = 'Ryland';
const helloString =
`Hello
 ${name}`;
Enter fullscreen mode Exit fullscreen mode

我觉得代码本身就说明了一切。多么令人惊叹的实现啊。

对象解构

对象解构是一种从数据集合(对象、数组等)中提取值的方法,而无需迭代数据或明确访问其键:

老办法

function animalParty(dogSound, catSound) {}

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

animalParty(myDict.dog, myDict.cat);
Enter fullscreen mode Exit fullscreen mode

解构

function animalParty(dogSound, catSound) {}

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

const { dog, cat } = myDict;
animalParty(dog, cat);
Enter fullscreen mode Exit fullscreen mode

等等,还有更多。你还可以在函数签名中定义解构:

解构 2

function animalParty({ dog, cat }) {}

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

animalParty(myDict);
Enter fullscreen mode Exit fullscreen mode

它也适用于数组:

解构 3

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

console.log(a); // prints 10
Enter fullscreen mode Exit fullscreen mode

还有许多其他现代功能值得您利用。以下是一些我认为比较突出的功能:

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

编写并行应用程序时,你的目标是优化一次性处理的工作量。如果你有 4 个可用核心,而你的代码只能利用其中的一个核心,那么 75% 的潜力就被浪费了。这意味着阻塞式同步操作是并行计算的终极敌人。但考虑到 JS 是一门单线程语言,代码无法在多核上运行。那么,这样做的意义何在?

JS 是单线程的,但不是单文件(就像学校里的排队一样)。即使它不是并行的,它仍然是并发的。发送 HTTP 请求可能需要几秒钟甚至几分钟,如果 JS 停止执行代码直到请求返回响应,该语言将无法使用。

JavaScript 通过事件循环解决了这个问题。事件循环循环遍历已注册的事件,并根据内部调度/优先级逻辑执行它们。这使得我们能够“同时”发送数千个 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];
}
Enter fullscreen mode Exit fullscreen mode

原生 for 循环是编程中并行性最差的结构之一。在我上一份工作中,我领导的团队花了数月时间尝试将传统的Rfor 循环转换为自动并行的代码。这基本上是一个不可能解决的问题,只有等待深度学习的改进才能解决。并行化 for 循环的困难源于一些存在问题的模式。顺序 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;
}
Enter fullscreen mode Exit fullscreen mode

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

地图

// 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);
Enter fullscreen mode Exit fullscreen mode

带索引的地图

// 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);
Enter fullscreen mode Exit fullscreen mode

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`);
  }
});
Enter fullscreen mode Exit fullscreen mode

我将解释为什么这些是对传统 for 循环的改进。这类循环并非按顺序(顺序)执行每次“迭代”,而是map将所有元素作为单独的事件提交给用户定义的 map 函数。这直接向运行时传递了这样的信息:各个“迭代”彼此之间没有任何联系或依赖,从而允许它们并发运行。在很多情况下,for 循环的性能与mapor 循环相当(甚至更好) forEach。我仍然认为,现在损失一些循环周期,对于使用定义良好的 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"
Enter fullscreen mode Exit fullscreen mode

eslint 如何为您的开发周期增值,这一点显而易见。本质上,它确保您遵循关于什么是“好的”实践,什么是“不好的”实践的规则。正因如此,linter 天生就带有主观性。正如所有观点一样,请谨慎看待,linter 也可能存在错误。

Prettier 是一款代码格式化程序。它不太关注“正确性”,而更注重统一性和一致性。Prettier 不会抱怨使用var,它会自动对齐代码中的所有括号。在我的个人开发过程中,我总是在将代码推送到 Git 之前最后一步运行 Prettier。在很多情况下,甚至在每次提交到代码库时自动运行 Prettier 也是合理的。这确保了所有进入源代码管理的代码都具有一致的样式和结构。

测试你的代码

编写测试是改进 JS 代码的一种间接但极其有效的方法。我建议你熟悉各种测试工具。你的测试需求各不相同,没有哪一款工具可以满足所有需求。JS 生态系统中已经有大量成熟的测试工具,因此选择工具主要取决于个人喜好。一如既往,请独立思考。

试车手——Ava

Github 上的 AvaJS

测试驱动程序只是提供高层次结构和实用程序的框架。它们通常与其他特定的测试工具结合使用,这些工​​具会根据您的测试需求而有所不同。

Ava 在表现力和简洁性之间取得了完美的平衡。Ava 的并行和隔离架构是我最喜爱的源泉。运行速度更快的测试可以节省开发人员的时间和公司的成本。Ava 拥有大量优秀的功能,例如内置断言,同时又保持了极简的风格。

替代品: Jest、摩卡、茉莉花

间谍与存根 - Sinon

Sinon 在 Github 上

间谍为我们提供“功能分析”,例如某个函数被调用的次数、被什么调用以及其他有见地的数据。

Sinon 是一个功能丰富的库,但只有少数几个功能非常出色。具体来说,sinon 在间谍和存根方面表现尤为出色。它的功能集丰富,但语法简洁。这对于存根来说尤其重要,因为它们部分存在是为了节省空间。

替代方案:testdouble

莫克斯-诺克

Nock 在 Github 上

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

HTTP mocking 可能非常麻烦,但 Nock 可以减轻它的痛苦。Nock 直接覆盖了requestNode.js 的内置函数,并拦截了传出的 HTTP 请求。这反过来又让你能够完全控制响应。

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

Web 自动化 - Selenium

Github 上的 Selenium

我对推荐 Selenium 的感受很复杂。作为 Web 自动化领域最受欢迎的选择,它拥有庞大的社区和在线资源。可惜的是,它的学习曲线相当陡峭,而且实际使用时需要依赖很多外部库。话虽如此,它是唯一真正免费的选择,所以除非你正在进行企业级的 Web 自动化,否则 Selenium 就足够了。

另外两个随机的 JS 事物

  • 很少应该使用null,差null
  • JavaScript 中的数字很糟糕,总是使用基数参数parseInt

结论

画你自己的。

文章来源:https://dev.to/taillogs/practical-ways-to-write-better-javascript-26d4
PREV
开发人员最佳读物
NEXT
异步、并行、并发详解 - 戈登·拉姆齐主演