1

今年 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.allPromise.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.allSettledPromise.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

这个特性其实包括两个点: WeakRefFinalizationGroup

弱引用有什么用?一个对象如果有弱引用,并不能保证它不被 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

Author: 👦 Xin Zhang

Link: 💫 https://zhan.gxin.xyz/s/new-j... 【网站被墙中,没xx上不去

Copyright notice: 🏀 All articles in this blog are licensed under CC BY-SA 4.0 unless stating additionally.


正义de味方
32 声望0 粉丝

zhang xin 同学,热爱前端,赞美太阳