今年 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: ? 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.

转载于:https://juejin.im/post/5cf28b7ff265da1b8b2b467b

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值