如果你现在写JavaScript,那么你值得花时间去了解该语言在过去几年里的所有更新。自2015年以来,随着ES6的发布,ECMAScript规范的新版本每年都会发布。每一次迭代都会为该语言增加新的功能、新的语法和生活质量的改进。大多数浏览器和Node.js中的JavaScript引擎很快就会跟上,你的代码也应该跟上,这才公平。这是因为,随着JavaScript的每一次新的迭代,都会有新的成语和新的方法来表达你的代码,而且很多时候,这些变化可能会让你和你的合作者更容易维护代码。
以下是ECMAScript的一些最新特性,通过归纳,JavaScript和Node.js,你可以利用它们来编写更干净、更简洁、更可读的代码。
1.块状得分的声明
自从该语言诞生以来,JavaScript开发人员一直使用var 来声明变量。关键词var 有其怪异之处,其中最有问题的是使用它所创建的变量的范围:
var x = 10
if (true) {
var x = 15 // inner declaration overrides declaration in parent scope
console.log(x) // prints 15
}
console.log(x) // prints 15
由于用var 定义的变量不是块范围的,在一个较窄的范围内重新定义它们会影响到外部范围的值。
现在我们有两个新的关键字来替代var ,即let 和const ,它们不会有这个缺点:
let y = 10
if (true) {
let y = 15 // inner declaration is scoped within the if block
console.log(y) // prints 15
}
console.log(y) // prints 10
const 和 的语义不同,用 声明的变量在它们的作用域中不能被重新赋值。这并不意味着它们是不可改变的,只是说它们的引用不能被改变。let const
const x = []
x.push("Hello", "World!")
x // ["Hello", "World!"]
x = [] // TypeError: Attempted to assign to readonly property.
2.箭头函数
箭头函数是最近引入JavaScript的另一个非常重要的特性。它们有很多优点。首先,也是最重要的,它们使JavaScript的功能方面看起来更漂亮,写起来更简单:
let x = [1, 2, 3, 4]
x.map(val => val * 2) // [2, 4, 6, 8]
x.filter(val => val % 2 == 0) // [2, 4]
x.reduce((acc, val) => acc + val, 0) // 10
在上述所有的例子中,箭头函数都是以独特的箭头命名的=> ,用简洁的语法取代了传统函数:
- 如果函数主体是一个单一的表达式,那么范围括号
{}和return关键字是隐含的,不需要写。 - 如果函数有一个参数,那么参数括号
()是隐含的,不需要写。 - 如果函数主体表达式是一个字典,则必须用括号括起来
()。
箭头函数的另一个显著优势是它们不定义作用域,而是存在于父作用域中。这就避免了很多使用this 关键字时可能出现的陷阱。箭头函数没有对this 的绑定。在箭头函数内部,this 的值与父作用域中的值相同。因此,箭头函数不能作为方法或构造函数使用。箭头函数不能与apply 、bind 或call 一起使用,并且对super 没有绑定。
它们也有一些其他的限制,比如缺少传统函数可以访问的arguments 对象,以及不能从函数体中yield 。
因此,箭头函数不是标准函数的1:1替代品,而是对JavaScript功能集的欢迎补充。
3.可选的链式结构
想象一下一个深度嵌套的数据结构,比如这里的person 对象。考虑到你想访问这个人的名字和姓氏。你会像这样在JavaScript中写这个:
person = {
name: {
first: 'John',
last: 'Doe',
},
age: 42
}
person.name.first // 'John'
person.name.last // 'Doe'
现在想象一下,如果person 对象不包含一个嵌套的name 对象,会发生什么:
person = {
age: 42
}
person.name.first // TypeError: Cannot read property 'first' of undefined
person.name.last // TypeError: Cannot read property 'last' of undefined
为了避免这样的错误,开发人员不得不使用下面这样的代码,它不必要地冗长,难以阅读,而且写起来也不愉快--这是一个非常糟糕的形容词组合。
person && person.name && person.name.first // undefined
可选链是JavaScript的一个新特性,它消除了这种畸形现象。可选链在遇到null 或undefined 的值时,就会缩短挖掘过程,并返回undefined ,而不会引发错误。
person?.name?.first // undefined
这样一来,代码就更加简洁明了了。
4.空值凝聚(Null-ish coalescing
在引入null-ish凝聚操作符之前,JavaScript开发者使用OR操作符|| ,在输入值不存在的情况下返回到一个默认值。这有一个重要的警告,即即使是合法但虚假的值也会导致回退到默认值:
function print(val) {
return val || 'Missing'
}
print(undefined) // 'Missing'
print(null) // 'Missing'
print(0) // 'Missing'
print('') // 'Missing'
print(false) // 'Missing'
print(NaN) // 'Missing'
现在,JavaScript提出了null凝聚操作符?? ,它提供了一个更好的选择,因为它只在前面的表达式为null-ish时才会导致回退。这里的null-ish指的是null 或undefined 的值:
function print(val) {
return val ?? 'Missing'
}
print(undefined) // 'Missing'
print(null) // 'Missing'
print(0) // 0
print('') // ''
print(false) // false
print(NaN) // NaN
这样,你可以确保如果你的程序接受虚假的值作为合法的输入,你不会最终用回退来代替它们。
5.逻辑性赋值
假设你想给一个变量赋值,当且仅当该值目前是空的。一个合乎逻辑的方法是这样写的:
if (x === null || x == undefined) {
x = y
}
如果你知道短路的工作原理,你可能会想用一个更简洁的版本来代替这3行代码,使用null-ish凝聚操作符:
x ?? (x = y) // x = y if x is nullish, else no effect
这里我们使用null-ish凝聚运算符的短路功能,如果x 是null-ish,就执行第二部分x = y 。这段代码相当简洁,但还是不太容易阅读和理解。逻辑上的null-ish赋值消除了对这种变通方法的需要:
x ??= y // x = y if x is nullish, else no effect
沿着同样的思路,JavaScript还引入了逻辑AND赋值&&= 和逻辑OR赋值||= 操作符。这些运算符只有在满足特定条件时才执行赋值,否则没有任何作用。
x ||= y // x = y if x is falsy, else no effect
x &&= y // x = y if x is truthy, else no effect
专业提示:如果你以前写过Ruby,你已经看到了||= 和&&= 操作符,因为Ruby没有falsy values的概念。
6.命名的捕获组
让我们先快速回顾一下正则表达式中的捕获组。捕获组是字符串的一部分,它与括号中的一部分regex相匹配:
let re = /(\d{4})-(\d{2})-(\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
result[0] // '2020-03-14', the complete match
result[1] // '2020', the first capture group
result[2] // '03', the second capture group
result[3] // '14', the third capture group
正则表达式支持命名的捕获组也有一段时间了,这是一种通过名称而不是索引来引用捕获组的方式。现在,随着ES9的出现,这个功能已经进入了JavaScript。现在,结果对象包含一个嵌套的组对象,每个捕获组的值都被映射到它的名字上:
let re = /(?\d{4})-(?\d{2})-(?\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
result.groups.year // '2020', the group named 'year'
result.groups.month // '03', the group named 'month'
result.groups.day // '14', the group named 'day'
这个新的API与另一个新的JavaScript特性--非结构化赋值--配合得非常好:
let re = /(?\d{4})-(?\d{2})-(?\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
let { year, month, day } = result.groups
year // '2020'
month // '03'
day // '14'
7.async &await
JavaScript的一个强大的方面是它的异步性。这意味着许多可能是长期运行或耗时的函数可以返回一个Promise,而不会阻塞执行:
const url = 'https://the-one-api.dev/v2/book'
let prom = fetch(url)
prom // Promise {}
// wait a bit
prom // Promise {: Response}, if no errors
// or
prom // Promise {: Error message}, if any error
在这里,对fetch的调用返回一个Promise,这个Promise在创建时的状态是 "待定"。很快,当API返回响应时,它就转变为 "已完成 "状态,并且可以访问它所包装的响应。在承诺的世界里,你会做类似这样的事情来进行API调用,并将响应解析为JSON:
const url = 'https://the-one-api.dev/v2/book'
let prom = fetch(url)
prom // Promise {: Response}
.then(res => res.json())
.then(json => console.log(json)) // prints response, if no errors
.catch(err => console.log(err)) // prints error message, if any error
在2017年,JavaScript宣布了两个新的关键字async 和await ,这使得处理和使用Promises更容易、更流畅。它们不是承诺的替代品;它们只是在强大的承诺概念之上的语法糖。
所有的代码都发生在一系列的 "then "函数中,而不是await ,使其看起来像同步的JavaScript。作为一个额外的好处,你可以使用try...catch 和await ,而不是在'catch'函数中处理错误,如果你直接使用Promises的话。使用await 的相同代码看起来是这样的:
const url = 'https://the-one-api.dev/v2/book'
let res = await fetch(url) // Promise {: Response} -await-> Response
try {
let json = await res.json()
console.log(json) // prints response, if no errors
} catch(err) {
console.log(err) // prints error message, if any error
}
async 关键字是同一个硬币的另一面,因为它包装了任何要在Promise中发送的数据。考虑一下下面这个用于添加几个数字的异步函数。在现实世界中,你的代码会做一些更复杂的事情:
async function sum(...nums) {
return nums.reduce((agg, val) => agg + val, 0)
}
sum(1, 2, 3) // Promise {: 6}
.then(res => console.log(res) // prints 6
let res = await sum(1, 2, 3) // Promise {: 6} -await-> 6
console.log(res) // prints 6
这些新功能只是冰山一角。我们甚至还没有触及表面。JavaScript是不断发展的,每年都有新的功能被添加到语言中。要跟上不断引入的新功能和习语的手动操作是很困难的。
如果有一些工具能够为我们处理这个问题,那不是很好吗?别担心,有的。我们已经详细讨论了如何使用ESLint在你的JavaScript repo中设置静态代码分析。它非常有用,应该是你的工具链中不可缺少的工具。但说实话,设置ESLint的自动修复管道和流程需要时间和精力。除非你喜欢这种管道,否则你最好写代码并将管道外包给......DeepSource!
DeepSource可以帮助你实现代码审查的自动化,为你节省大量的时间。只要在版本库的根部添加一个.deepsource.toml 文件,DeepSource就会立即对其进行扫描。扫描将在你的代码中找到改进的范围,并通过有用的描述帮助你修复它们。