这里有几个如何更好地编写JavaScript的实用方法

169 阅读14分钟

原文:stackoverflow.blog/2019/09/12/…

作者:Ryland Goldstein

翻译:奶爸码农

我是Ryland Goldstein,在Binaris进行Reshuffle的产品研发人员。这是我在Stack Overflow的第二篇文章。让我们深入探讨一下提升JS开发能力的实用方法!

我没有看到很多人谈论提高JavaScript的方法,这是我用来更好的编写JS的一些实践方法。

使用TypeScript

这是你可以通过不编写JS来改善JS的第一件事。对于初学者来说,TypeScript(TS)是JS的“已编译”超集(在JS中运行的所有内容都可以在TS中运行)。TS在原生JS体验的基础上增加了一个全面的可选类型检查系统。长期以来,整个生态系统中对TS的支持都不够好,以至于我不建议大家使用。幸运的是,那些日子已经过去很久了,大多数框架都开箱即用地支持TS。现在我们都在讨论了什么是TS,下面我们来谈谈为什么要使用它。

TypeScript可以保证类型安全

类型安全性可以确保在编译阶段验证整个代码段中所有类型是否都以合法方式使用。换句话说,如果您创建一个数字参数的函数foo:

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

函数foo只接受number类型的参数

正确的例子
console.log(foo(2)); // 打印 "7"
错误的例子
console.log(foo("two")); // 不正确的TS代码

除了在代码中添加类型检查的开销之外,类型安全性的负面影响几乎为零。另一方面,好处太大了,类型安全性提供了针对常见错误的额外保护,这对于像JS这样有缺陷的语言来说是一种幸运。

TypeScript让重构大型项目成为可能

重构大型JS应用程序可能是一场噩梦。重构JS的最大苦恼是由于它不强制检查函数参数签名。这意味着绝对不能滥用JS函数。例如,如果我有一个函数myAPI提供给1000个不同的调用方使用:

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%确定使用此功能的每个地方(成千上万个地方),我都正确地更新了用法。如果我错过一个,系统就会崩溃。

如果使用TS的话:

修改前
function myAPITS(someNum: number, someString: string) { ... }
修改后
function myAPITS(someString: string, someNum: number) { ... }

如您所见,myAPITS函数与JavaScript对应函数进行了相同的更改。但是,此代码不是生成有效的JavaScript,而是导致生成无效的TypeScript,因为它所使用的成千上万个位置现在提供了错误的提示。 而且由于我们前面讨论的类型安全性,这数千种情况将阻止编译,这样你可以从容进行修改。

TypeScript使得团队架构沟通更为顺畅

正确配置TS后,如果不先定义接口和类就很难编写代码。这提供了一种共享简洁,可交流的体系结构建议的方法。在TS之前,存在解决此问题的其他解决方案,但是没有一个解决方案可以在不使您做额外工作的情况下自动解决。 例如,如果我想为后端提出新的请求类型,则可以使用TS将以下内容发送给队友。

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

我必须已经编写代码,但是现在我可以共享我的增量进度并获得反馈,而无需花费更多时间。我不知道TS本质上是否比JS更不容易出错。我坚信,强制开发人员首先定义接口和API会产生更好的代码。总体而言,TS已发展成为比原生JS的更为成熟且更可预测的替代方案。开发人员绝对仍然需要熟练使用原生JS,但是我最近开始的大多数新项目从一开始就是TS。

使用现代语法

JavaScript是世界上最流行(即使不是最多)的编程语言之一。您可能希望到现在为止,大多数人已经知道了数以亿计的人使用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中添加了一个新概念Promises。使用Promise,您可以编写异步逻辑,同时避免以前困扰基于回调的代码的嵌套问题。

Promises

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

与回调相比,Promises的最大优点是可读性和可链式调用。尽管Promise很棒,但他们仍有一些不足之处。对于许多人来说,Promise的体验仍然让人联想到回调。具体来说,开发人员正在寻求Promise模型的替代方案。为了解决这个问题,ECMAScript委员会决定添加一种新的利用promise,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) {
  // ...
}

由于异步功能实际上只是精美的Promise包装器,因此也可以直接等待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的限制要严格得多且“固定不变”,这通常会导致更好的代码。没有大量的“真实场景”需要使用let,我会说我用let声明的1/20变量,其余的都是const。

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

箭头=>函数

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

原生匿名函数

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

在大多数情况下,这种风格没有任何“错误”。原生匿名函数在作用域方面表现出“有趣”,这可能会(或已经)导致许多意外错误。 借助箭头功能,我们不必再为此担心。这是使用箭头功能实现的相同代码:

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

除了更加简洁之外,箭头功能还具有更多实用的作用域行为。箭头函数从定义它们的范围继承作用域。 在某些情况下,箭头函数功能可能更简洁:

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

我想说清楚,这不是可变的情况;仍然存在原生匿名函数(特别是类方法)的有效用例。话虽这么说,但我发现,如果始终使用默认箭头功能,则与默认使用原始匿名函数相比,您进行的调试要少得多。

展开操作符 ...

提取一个对象的键/值对并将它们添加为另一个对象的子代是一种非常常见的事情。从历史上看,有几种方法可以做到这一点,但是所有这些方法都相当笨拙:

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}`;

对象解构

对象解构是一种从数据集合(对象,数组等)中提取值的方法,而无需遍历数据或显式访问其健值:

老方法

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);

你还可以在函数参数签名中进行解构:

function animalParty({ dog, cat }) {}

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

animalParty(myDict);

解构也能和数组配合使用:

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

console.log(a); // prints 10

永远默认你的系统是分布式的

编写并行化的应用程序时,您的目标是一次优化您的工作量。如果您有四个可用的核心,而您的代码只能使用一个核心,那么您的潜力就浪费了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];
}

原始for循环是编程中存在的最少并行构造之一。在我的上一份工作中,我带领一个团队花费了几个月的时间尝试将传统的R lang for循环转换为自动并行代码。从根本上讲,这是一个不可能解决的问题,只有等待深度学习的改善才能解决。并行化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;
}

如果按顺序执行该代码,并且每次迭代执行,则只会产生预期的结果。 如果尝试一次执行多个迭代,则处理器可能会基于不正确的值错误地分支,这会使结果无效。 如果这是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 with index
// 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之类的结构无需按顺序执行每个迭代,而是采用所有元素并将它们作为单独的事件提交给用户定义的map函数。 在大多数情况下,各个迭代之间没有固有的联系或依赖性,因此它们可以并行运行。 这并不是说您无法使用for循环来完成同一件事。 实际上,它看起来像这样:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

async function testCall() {
  // do async stuff here
}

for (let i = 0; i < 10; i += 1) {
  testCall();
}

如您所见,for循环并不能阻止我以正确的方式进行操作,但是它肯定也不会使其变得更容易。 与map版本比较:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
items.map(async (item) => {
 // do async stuff here
});

如您所见,map就可以了。 如果要在所有单个异步操作完成之前进行阻塞,则map的优势将变得更加明显。 使用for循环代码,您将需要自己管理一个数组。 这是map版本:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
 const allResults = await Promise.all(items.map(async (item) => {
  // do async stuff here
 }));

真的很容易,在许多情况下,与map或forEach相比,for循环的性能相同(或可能更高)。 我仍然认为,现在丢失几个周期值得使用定义良好的API的优点。 这样,将来对该数据访问模式实现的任何改进都会使您的代码受益。 for循环过于笼统,无法对同一模式进行有意义的优化。

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

静态检查你的代码

没有一致风格(外观)的代码很难阅读和理解。因此,以任何语言编写高端代码的一个关键方面是保持一致且明智的风格。由于JS生态系统的广度,对于lint风格和style细节有很多选择。我要强调的是,与使用特制的lint/style相比,使用lint并强制使用一种style(其中的任何一种)更为重要。归根结底,没有人会完全按照我的意愿来编写代码,因此对此进行优化是不切实际的目标。

我看到很多人问他们应该使用eslint还是prettier。对我来说,它们的用途非常不同,因此应结合使用。ESlint大部分时间都是传统的linter。它将确定与style无关,而与正确性有关的代码问题。例如,我将eslint与Airbnb规则结合使用。使用该配置,以下代码将强制linter失败:

var fooVar = 3; // airbnb rules forebid "var"

很明显,eslint如何为您的开发周期增加价值。 从本质上讲,它可以确保您遵循关于什么是好习惯的准则。 因此,lint在本质上是强制执行的。 与所有意见一样,将其与盐一同食用。 Linter可能是错误的。

Prettier是代码格式化程序。 它较少关注正确性,而更担心均匀性和一致性。 Prettier不会抱怨使用var,但是它将自动对齐代码中的所有方括号。 在我的个人开发过程中,在将代码推送到Git之前,我总是使用Prettier来美化代码格式。 在许多情况下,让Prettier在每次对存储库的每次提交时自动运行甚至是有意义的。 这样可以确保所有进入源代码管理的代码都具有一致的样式和结构。

测试你的代码

编写测试是改善您编写的JS代码的一种间接但非常有效的方法。 我建议你需要适应使用各种各样的测试工具。 您的测试需求会有所不同,并且没有一种工具可以处理所有问题。 JS生态系统中有大量完善的测试工具,因此选择工具主要取决于个人喜好。与往常一样,请自己考虑。

Test Driver – Ava

Test drivers是一种简单的框架,它可以提供高级别的结构和实用性。 它们通常与其他特定的测试工具结合使用,具体取决于您的测试需求。

Ava是表达力和简洁性的完美平衡。 Ava的并行,隔离的体系结构是我大部分爱的源泉。 运行速度更快的测试可节省开发人员时间和公司资金。 Ava拥有大量不错的功能,例如内置断言,同时设法将其保持在最小限度。

其他选项: Jest, Mocha, Jasmine

Mocks – Nock

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

Http模拟可能是一个真正的痛苦,但是Nock减轻了痛苦。 Nock直接覆盖nodejs的内置请求,并拦截传出的http请求。这又使您可以完全控制响应。

其他选项: 我也不知道其他 🙁

测试自动化 – Selenium

我对推荐Selenium感到喜忧参半。 由于它是网络自动化的最受欢迎的选项,因此它拥有庞大的社区和在线资源集。 不幸的是,学习曲线相当陡峭,并且它依赖于许多实际使用的外部库。 话虽如此,它是唯一真正的免费选项,因此,除非您要进行企业级的网络自动化,否则Selenium会是一个好选择。

其他选项: Cypress, PhantomJS

永无终点的旅途

与大多数事情一样,编写更好的JavaScript是一个持续的过程。 代码始终可以变得更加简洁,一直在添加新功能,并且永远没有足够的测试。 看起来似乎势不可挡,但是由于有很多潜在的方面需要改进,因此您可以按照自己的步调真正进步。 一次接着一步,在不知不觉中,您将成为JavaScript的王者。

该博客文章最初出现在Ryland的个人网站和Dev.to上。 在这两个站点上都可以找到他的更多著作。 如果您想为Stack Overflow博客撰写文章,请发送电子邮件至pitches@stackoverflow.com。

奶爸码农』从事互联网研发工作10+年,经历IBM、SAP、陆金所、携程等国内外IT公司,目前在美团负责餐饮相关大前端技术团队,定期分享关于大前端技术、投资理财、个人成长的思考与总结。