今年 IO19 大会上的 What’s new in JavaScript (Google I/O ’19) 讲座继续介绍了一些前沿的 JS 开发技术,两位 v8 项目组的大牛(还是去年俩)给我们介绍了 15 个新特性,我只了解过 6 个 ?,快来看看你了解过几个,是否跟上现代 web 的步伐了呢?
首先大牛们上来先吹一波 V8,列下数据:
- chrome 75 的编译速度是 chrome 61 的两倍
- chrome 74/Node 12 的 async 性能是 chrome 55/Node 7 的 11 倍
- chrome 76 的内存使用比 chrome 70 减少 20%
Emmm,好像很厉害的样子,好我们开始正题... ☃️
? Private Class Field
ES6 引入了 class,比如写一个自增计数器:
class IncreasingCounter {
constructor() {
this._count = 0
}
get value() {
console.log('Hi')
return this._count
}
increment() {
this._count++
}
}
复制代码
tc39 引入 class field 的提案,把在 constructor
中声明的属性拿到外面,这样类的定义更加清晰,而且不用强制给此字段初始值:
class IncreasingCounter {
_count = 0
get value() {
console.log('Hi')
return this._count
}
increment() {
this._count++
}
}
复制代码
上面 _count
并不是私有的,要想让 _count
私有,只需要给变量加个 #
前缀:
class IncreasingCounter {
#count = 0
get value() {
console.log('Hi')
return this.#count
}
increment() {
this.#count++
}
}
复制代码
是的就这么简单... ✌?
const counter = new IncreasingCounter()
counter.#count
// SyntaxError
counter.#count = 42
// SyntaxError
复制代码
class field 这个特性对于继承的情况特别友好,考虑下面的情况:
class Animal {
constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
super(name)
this.likesBaths = false
}
meow() {
console.log('Meow!')
}
}
复制代码
我们把 likesBaths
提到外面,然后就可以直接省去 constructor
了:
class Cat extends Animal {
likesBaths = false
meow() {
console.log('Meow!')
}
}
复制代码
? Chrome 74,Node 12 已实现,还有很多关于 class 的新特性正在开发,比如 private methods/getters/setters
? MatchAll Regex
考虑下面的正则匹配,我们可以匹配出所有的 'hex 单词' :
const string = 'Magic hex numbers: DEADBEEF CAFE'
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu
for (const match of string.match(regex)) {
console.log(match)
}
// Output:
//
// 'DEADBEEF'
// 'CAFE'
复制代码
但是有时候我们需要更多的信息,比如 index、input ,我们知道可以使用 Regex.prototype.exec
来做到:
const string = 'Magic hex numbers: DEADBEEF CAFE'
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu
let match
while ((match = regex.exec(string))) {
console.log(match)
}
// Output:
//
// ["DEADBEEF", index: 19, input: "Magic hex numbers: DEADBEEF CAFE"]
// ["CAFE", index: 28, input: "Magic hex numbers: DEADBEEF CAFE"]
复制代码
不过这个形式是不是太麻烦了呢?对的,并且一般情况下还要注意无限循环的问题,while ((match = regex.exec(string)) !== null)
,所有有了 String.prototype.matchAll()
:
const string = 'Magic hex numbers: DEADBEEF CAFE'
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu
for (const match of string.matchAll(regex)) {
console.log(match)
}
// Output:
//
// ["DEADBEEF", index: 19, input: "Magic hex numbers: DEADBEEF CAFE"]
// ["CAFE", index: 28, input: "Magic hex numbers: DEADBEEF CAFE"]
复制代码
? Chrome 73、FireFox 67、Node 12 已实现
? Numeric Literals
这一部分主要是提高数字可读性,比如对于下面的大数字:
1000000000000
1019436871.42
复制代码
你能一下子看出来它是多少吗?? 并不能。所以我们使用一个分隔符 _
(U+005F) 来帮助提高可读性:
1_000_000_000_000
1_019_436_871.42
let budget = 1_000_000_000_000
console.log(budget === 10 ** 12) // true
复制代码
? Chrome 75 已实现
? BigInt Formatting
看一个例子:
1234567890123456789 * 123
// 151851850485185200000
复制代码
很明显这是错的,结尾至少得是 7 吧。因为 js 中 number 最大值是 2^53 ,所以引入 BigInt 来处理这种情况, BigInt 的数后面带 n
:
1234567890123456789n * 123n
// 151851850485185185047n
typeof 123n === 'bigint'
// true
复制代码
使用到 BigInt 的数一般都很大,可读性也不好,你能一下子看出来 1234567890123456789n
是多少吗?? 并不能。所以需要格式化。
我们知道 toLocaleString()
专注格式化,这下也支持 BigInt 了:
12345678901234567890n.toLocaleString('en')
// '12,345,678,901,234,567,890'
12345678901234567890n.toLocaleString('de')
// '12.345.678.901.234.567.890'
复制代码
不过要知道,这种方式效率并不高,我们可以使用 Intl
的 format API,这个 API 现在也支持格式化 BigInt 了,
const nf = new Intl.NumberFormat('fr')
nf.format(12345678901234567890n)
// '12 345 678 901 234 567 890'
复制代码
对于使用分隔符的 BigInt 也适用:
12_345_678_901_234_567_890n.toLocaleString('en')
// '12,345,678,901,234,567,890'
12_345_678_901_234_567_890n.toLocaleString('de')
// '12.345.678.901.234.567.890'
const nf = new Intl.NumberFormat('fr')
nf.format(12_345_678_901_234_567_890n)
// '12 345 678 901 234 567 890'
复制代码
? BigInt 在 Chrome 67 、 Firefox 68 、 Node 10 已实现,而后面两种格式,在 Chrome 76 、 Firefox Nightly 已实现。目前 GoogleChromeLabs 发布了一个包用来支持 BigInt: JSBI
? Flat & FlatMap
flat 一个数组是很常见的功能,以前我们可以通过 reduce + concat
方式来做到,现在引入 flat()
方法来实现这个常用的功能:
// Flatten one level:
const array = [1, [2, [3]]]
array.flat()
// [1, 2, [3]]
复制代码
而 flatMap()
做的事情就是 map + flat
,并且性能优化过:
const duplicate = x => [x, x]
;[2, 3, 4].map(duplicate)
// [[2, 2], [3, 3], [4, 4]]
;[2, 3, 4].map(duplicate).flat()
// [2, 2, 3, 3, 4, 4]
;[2, 3, 4].flatMap(duplicate)
// [2, 2, 3, 3, 4, 4]
复制代码
? Chrome 69 、 Firefox 62 、Safari 12 、Node 11 均已实现(第一次见 Safari ?)
? FromEntries
Object.entries()
很早就有了,现在又有了类似的 Object.fromEntries()
方法,它用来把一个 key-value 对的 list 转化为对象,他做的事情和 Object.entries()
是相反的:
const object = { x: 42, y: 50 }
const entries = Object.entries(object)
// [['x', 42], ['y', 50]]
const result = Object.fromEntries(entries)
// { x: 42, y: 50 }
复制代码
在转换对象的时候很有用:
const object = { x: 42, y: 50, abc: 900 }
const result = Object.fromEntries(
Object.entries(object)
.filter(([key, value]) => key.length === 1)
.map(([key, value]) => [key, value * 2])
)
// { x: 84, y: 100 }
复制代码
还可以做 object 与 Map 的互转:
const object = { language: 'JavaScript', coolness: 9001 }
// Convert the object into a map:
const map = new Map(Object.entries(object))
// Convert the map back into an object:
const objectCopy = Object.fromEntries(map)
// { language: "JavaScript", coolness: 9001 }
复制代码
? Chrome 73 、Firefox 63 、Safari 12 、Node 12 已实现
? GlobalThis
跨环境的全局对象都不一样,所以需要判断,以前需要这么做:
const getGlobalThis = () => {
if (typeof self !== 'undefined') return self
if (typeof window !== 'undefined') return window
if (typeof global !== 'undefined') return global
if (typeof this !== 'undefined') return this
throw new Error('unable to locate global object')
}
const theGlobalThis = getGlobalThis()
复制代码
现在直接这么写就行了:?
const theGlobalThis = globalThis
复制代码
? chrome 71 、Firefox 65 、Safari 12 、Node 12 已实现
? Stable Sort
之前 Array/TypedArray.prototype.sort
方法的排序是不稳定的,什么意思?看下面例子:
const doggos = [
{ name: 'Abby', rating: 12 },
{ name: 'Bandit', rating: 13 },
{ name: 'Choco', rating: 14 },
{ name: 'Daisy', rating: 12 },
{ name: 'Elmo', rating: 12 },
{ name: 'Falco', rating: 13 },
{ name: 'Ghost', rating: 14 }
]
doggos.sort((a, b) => b.rating - a.rating)
复制代码
注意初始数组 name 是有序的,根据 rating 排序后,如果俩 rating 一样,我们期望根据 name 排序,但实际上呢:
;[
{ name: 'Ghost', rating: 14 }, // ?
{ name: 'Choco', rating: 14 }, // ?
{ name: 'Bandit', rating: 13 },
{ name: 'Falco', rating: 13 },
{ name: 'Abby', rating: 12 },
{ name: 'Daisy', rating: 12 },
{ name: 'Elmo', rating: 12 }
]
复制代码
你多试几次,结果可能都不一样...
因为之前 V8 在对于比较小的数组使用稳定排序,但是对于一些长数组使用的是不稳定的快速排序,而在 Chrome 70 以后,换成了稳定的 TimSort ,所以现在排序结果稳定了,不用再去用第三方库或者自己实现稳定排序了~
? Chrome 、 Firefox 、 Safari 、 Node 现在都实现稳定排序
? Intl.RelativeTimeFormat
这个 API 也是个格式化,看例子就懂了:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day')
// 'yesterday'
rtf.format(0, 'day')
// 'today'
rtf.format(1, 'day')
// 'tomorrow'
rtf.format(-1, 'week')
// 'last week'
rtf.format(0, 'week')
// 'this week'
rtf.format(1, 'week')
// 'next week'
复制代码
我们来看下 zh
下的输出:
const rtf = new Intl.RelativeTimeFormat('zh', { numeric: 'auto' })
rtf.format(-1, 'day')
// '昨天'
rtf.format(0, 'day')
// '今天'
rtf.format(1, 'day')
// '明天'
rtf.format(-1, 'week')
// '上周'
rtf.format(0, 'week')
// '本周'
rtf.format(1, 'week')
// '下周'
复制代码
怎么样?我知道你肯定懂了。?
? Chrome 71 、 Firefox 65 、 Node 12 已实现
? Intl.ListFormat
是的,这个 API 还是个格式化,也很容易懂:
const lfEnglish = new Intl.ListFormat('en', { type: 'disjunction' })
lfEnglish.format(['Ada', 'Grace'])
// 'Ada or Grace'
lfEnglish.format(['Ada', 'Grace', 'Ida'])
// 'Ada , Grace or Ida'
复制代码
同样在 zh
下,
const lfChinese = new Intl.ListFormat('zh', { type: 'disjunction' })
lfChinese.format(['Ada', 'Grace'])
// 'Ada或Grace'
lfChinese.format(['Ada', 'Grace', 'Ida'])
// 'Ada、Grace或Ida'
复制代码
上面的 type
参数如果是 conjunction
那么就会是 and/和
,如果是 unit
就直接拼在一起。
? Chrome 72 、 Node 12 已实现
? Intl.DateTimeFormat -> formatRange
没错,依然是个格式化 API,这个 API 可以让时间段的展示更加简便和智能:
const start = new Date(startTimestamp)
// 'May 7, 2019'
const end = new Date(endTimestamp)
// 'May 9, 2019'
const fmt = new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
const output = `${fmt.format(start)} - ${fmt.format(end)}`
// 'May 7, 2019 - May 9, 2019'
const output2 = fmt.formatRange(start, end)
// 'May 7 - 9, 2019' ?
复制代码
? Chrome 76 已实现
? Intl.Locale
它会打印出一些本地化的信息:
const locale = new Intl.Locale('es-419-u-hc-h12', {
calendar: 'gregory'
})
locale.language
// 'es'
locale.calenda
// 'gregory'
locale.hourCycle
// 'h12'
locale.region
// '419'
locale.toString()
// 'es-419-u-hc-h12'
复制代码
这个字符叫做 Unicode Language and Locale Identifiers ,太过专业,有兴趣的朋友去了解一下:
? Chrome 74 、 Node 12 已实现
? Top-Level await
写过 async/await
的朋友肯定碰到过这种情况,为了一行 await
,要搞一个 async
函数出来,然后调用:
async function main() {
const result = await doSomethingAsync()
doSomethingElse()
}
main()
复制代码
或者使用 IIFE 来实现:
;(async function() {
const result = await doSomethingAsync()
doSomethingElse()
})
复制代码
但是这个提案使得下面的写法成为现实:
const result = await doSomethingAsync()
doSomethingElse()
复制代码
当然它现在仍然是 提案,这个话题从去年就被讨论的蛮激烈,很多人支持,也有很多人认为这是个 FOOTGUN! Typescript 给其添加了 waiting for ts39
标签, deno 也在观望,可以说这个提案关系重大...
? stage-2
? Promise.allSettled/Promise.any
我们知道 Promise.all
和 Promise.race
可以批量处理 promise,
const promises = [fetch('/component-a.css'), fetch('/component-b.css'), fetch('/component-c.css')]
try {
const styleResponse = await Promise.all(promises)
enableStyles(styleResponse)
renderNewUi()
} catch (reason) {
displayError(reason)
}
try {
const result = await Promise.race([performHeavyComputation(), rejectAfterTimeout(2000)])
renderResult(result)
} catch (error) {
renderError(error)
}
复制代码
但是这俩个方法有一个共同的缺点,就是会在一些情况下短路,Promise.all
中的 promises 只要有一个 reject 了,其他的就会立刻中止,而 Promise.race
只要有一个 resolve 了,其他的也会立刻中止,这种行为称为短路(short-circuit)。
很明显,有很多情况下我们不希望短路行为的发生,现在有两个新的提案解决上述问题: Promise.allSettled
和 Promise.any
。
const promises = [fetch('/api-call-1'), fetch('/api-call-2'), fetch('/api-call-3')]
// Imagine some of these requests fail, and some succeed.
await Promise.allSettled(promises)
// ALL API calls have finished(either failed or succeeded).
removeLoadingIndicator()
复制代码
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c')
]
try {
const first = await Promise.any(promises)
// Any of the promises was fulfilled.
console.log(first)
// e.g. 'b'
} catch (error) {
// All of the promises were rejected
console.log(error)
}
复制代码
? Promise.settled 在 Chrome 76 、 Firefox Nightly 已实现, Promise.any 还在开发,stage-3
? WeakRef
这个特性其实包括两个点: WeakRef
和 FinalizationGroup
。
弱引用有什么用?一个对象如果有弱引用,并不能保证它不被 GC 摧毁,释放内存,倒是在被摧毁前,弱引用总能返回这个对象,即使这个对象没有任何强引用。
由于这个特性,弱引用很适合缓存,或者映射一些大的对象。
比如下面这个操作,
function getImage(name) {
const image = performExpensiveOperation(name)
return image
}
复制代码
为了提高性能,我们使用缓存,
const cache = new Map()
function getImageCached(name) {
if (cache.has(name)) return cache.get(name)
const image = performExpensiveOperation(name)
cache.set(name, image)
return image
}
复制代码
但是在 Map
中,key/value 都是强引用,name 和 image 对象一直不会被 GC 收集,最后导致的结果就是内存泄漏。
WeakMap
也不行啊,我们知道 WeakMap
要求 key 得是对象,string 啊,对不起告辞... ? 所以我们使用 WeakRef
来解决这个问题。
我们创建一个 WeakRef
来弱引用 image 对象,存在 Map 中,这样的话 GC 就能回收 image 对象了。
const cache = new Map()
function getImageCached(name) {
let ref = cache.get(name)
if (ref !== undefined) {
const deref = ref.deref()
if (deref !== undefined) return deref
}
const image = performExpensiveOperation(name)
ref = new WeakRef(image)
cache.set(name, ref)
return image
}
复制代码
当然我们也需要解决 key 的问题,key 不会自动被 GC 回收,这时候就需要使用 FinalizationGroup
来解决。
const cache = new Map()
const finalizationGroup = new FinalizationGroup(iterator => {
for (const name of iterator) {
const ref = cache.get(name)
if (ref !== undefined && ref.deref() === undefined) {
cache.delete(name)
}
}
})
复制代码
finalizationGroup
接受一个回调函数,当它注册了 image,而这个 image 被 GC 回收时,会调用此回调函数,也就是找到对应的 name 删掉。
const cache = new Map()
function getImageCached(name) {
let ref = cache.get(name)
if (ref !== undefined) {
const deref = ref.deref()
if (deref !== undefined) return deref
}
const image = performExpensiveOperation(name)
ref = new WeakRef(image)
cache.set(name, ref)
finalizationGroup.register(image, name)
return image
}
复制代码
? 这个功能相当的实用,不过目前正在开发中, stage-2
? Summary
新特性有很多都可以使用了,polyfill 也基本都能找得到,很多都是优化过的方案,在构建 modern web 的今天,你还犹豫啥?
去年两位大牛也讲了很多新特性,1 年过去了,现在有 5 个特性 modern browser 已经完全支持了:
- async {it,gen}erators
- Promise#finally
- optional catch binding
- String#trim{Start,End}
- object rest & spread
说 1️⃣ 个和去年讲座的变化,今年在提及浏览器兼容性的时候少了 Edge & Opera。
如果你学到了新东西,点个赞呗~ ?
Reference
- 视频地址(油管)
- Chrome Status
- proposal-top-level-await
- proposal-promise-allSettled
- proposal-intl-locale
- proposal-numeric-separator
- proposal-promise-any
- proposal-weakrefs
- proposal-class-fields
- proposal-bigint
- Getting things sorted in V8
Author: ? Xin Zhang
Link: ? zhan.gxin.xyz/s/new-js-io… 【网站被墙中,没梯子上不去
Copyright notice: ? All articles in this blog are licensed under CC BY-SA 4.0 unless stating additionally.