Bulandent / blog

高级前端进阶博客,记录个人前端成长之路。包括但不限制于:es6、node、vue、小程序、浏览器、网络、设计模式、算法和数据结构、web安全、性能优化、工程化等。
80 stars 8 forks source link

「建议收藏」送你一份精心总结的3万字ES6实用指南(全) #10

Open Bulandent opened 4 years ago

Bulandent commented 4 years ago

写本篇文章目的是为了夯实基础,基于阮一峰老师的著作 ECMAScript 6 入门 以及 tc39-finished-proposals 这两个知识线路总结提炼出来的重点和要点,涉及到从 ES2015ES2021 的几乎所有知识,基本上都是按照一个知识点配上一段代码的形式来展示,所以篇幅较长,也正是因为篇幅过长,所以就没把 Stage 2Stage 3 阶段的提案写到这里,后续 ES2021 更新了我再同步更新。

有 5 个提案已经列入 Expected Publication Year in 2021 所以本篇中暂且把他们归为 ES2021。

ES6前言

发展史

能写好 JS 固然是重要的,但是作为一个前端,我们也要了解自己所使用语言的发展历程,这里强烈推荐看 《JavaScript 20年》,本书详细记载和解读了自 1995 年语言诞生到 2015 年 ES6 规范制定为止,共计 20 年的 JavaScript 语言演化历程。

版本说明

2011 年,发布了 ECMAScript 5.1 版,而 2015 年 6 月发布了 ES6 的第一个版本又叫 ES2015ES6 其实是一个泛指,指代 5.1 版本以后的下一代标准。TC39 规定将于每年的 6 月发布一次正式版本,版本号以当年的年份为准,比如当前已经发布了 ES2015ES2016ES2017ES2018ES2019ES2020 等版本。

提案发布流程

任何人都可以向 TC39 提案,要求修改语言标准。一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在这里查看 ecma262。关于提案流程可以在这里 TC39_Process 看到更加详细的信息。

ES2015

声明

const:声明一个常量,let:声明一个变量;const/let 声明的常量/变量都只能作用于代码块(块级作用域或函数作用域)里;

if (true) {
    let name = '布兰'
}
console.log(name)  // undefined

const/let 不存在变量提升,所以在代码块里必须先声明然后才可以使用,这叫暂时性死区;

let name = 'bubuzou'
if (true) {
    name = '布兰'
    let name
}
console.log(name)

const/let 不允许在同一个作用域内,重复声明;

function setName(name) {
    let name = ''  // SyntaxError
}

const 声明时必须初始化,且后期不能被修改,但如果初始化的是一个对象,那么不能修改的是该对象的内存地址;

const person = {
    name: '布兰'
}
person.name = 'bubuzou'  
console.log(person.name)  // 'bubuzou'
person = ''  // TypeError

const/let 在全局作用域中声明的常量/变量不会挂到顶层对象(浏览器中是 window )的属性中;

var name = '布兰'
let age = 12
console.log(window.name)  // '布兰'
console.log(window.age)  // undefined

解构赋值

解构类型

解构要点

解构应用

字符串扩展

数值扩展

数组扩展

对象扩展

正则扩展

函数扩展

Symbol

静态方法

Symbol 的内置值

Set

Set 实例属性和方法

let set = new Set([1, 3])
set.add(5)     // Set {1, 3, 5}
set.size       // 3
set.delete(1)  // true,1 已被删除
set.has(1)     // false
set.keys()     // SetIterator {3, 5}
set.clear()
set.size       // 0

Set 应用场景

WeakSet

Set 的区别

实例方法

Map

实例属性和方法

let map = new Map()
map.set({a: 1}, 'a')
map.set({a: 2}, 'b')

for (let [key, value] of map) {
    console.log(key, value)
}
// {a: 1} 'a'
// {a: 2} 'b'

for (let key of map.keys()) {
    console.log(key)
}
// {a: 1}
// {a: 2}

WeakMap

实例方法

Proxy

静态方法

handle 对象的方法

Reflect

静态方法

Class

方法和关键字

类的继承

Module

Iterator和for...of

Promise

Promise 这块知识可以直接看我之前写的一篇文章:深入理解Promise 非常完整。

Generator

实例方法

应用

ES2016

Array.prototype.includes

判断一个数组是否包含某个元素,之前一般是这么做的:

if (arr.indexOf(el) >= 0) {}

// 或者
if (~arr.indexOf(el)) {}

而现在你可以这么做了:

if (arr.includes(el)) {}

indexOf 会返回找到元素在数组中的索引位置,判断的逻辑是是否严格相等,所以他在遇到 NaN 的时候不能正确返回索引,但是 includes 解决了这个问题:

[1, NaN, 3].indexOf(NaN)   // -1
[1, NaN, 3].includes(NaN)  // true

求幂运算符(**)

x ** y 是求 xy 次幂,和 Math.pow(x, y) 功能一致:

// x ** y
let squared = 2 ** 2  // 2 * 2 = 4
let cubed = 2 ** 3    // 2 * 2 * 2 = 8

x **= y 表示求 xy 次幂,并且把结果赋值给 x

// x **= y
let x = 2;
x **= 3  // x 最后等于 8

ES2017

Object.values()

返回一个由对象自身所有可遍历属性的属性值组成的数组:

const person = { name: '布兰' };
Object.defineProperty(person, 'age', {
    value: 12,
    enumrable: false  // age 属性将不可遍历
})
console.log(Object.values(person))  // ['布兰']

// 类似 str.split('') 效果
console.log(Object.values('abc'))  // ['a', 'b', 'c']

Object.entries()

返回一个由对象自身所有可遍历属性的键值对组成的数组:

const person = { name: '布兰', age: 12 }
console.log(Object.entries(person))  // [["name", "布兰"], ["age", 12]]

利用这个方法可以很好的将对象转成正在的 Map 结构:

const person = { name: '布兰', age: 12 }
const map = new Map(Object.entries(person))
console.log(map)  // Map { name: '布兰', age: 12 }

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptor() 会返回指定对象某个自身属性的的描述对象,而 Object.getOwnPropertyDescriptors() 则是返回指定对象自身所有属性的描述对象:

const person = { name: '布兰', age: 12 }

console.log(Object.getOwnPropertyDescriptor(person, 'name'))
// { configurable: true, enumerable: true, value: "布兰", writable: true }

console.log(Object.getOwnPropertyDescriptors(person))
//{ 
//  name: { configurable: true, enumerable: true, value: "布兰", writable: true },
//  age: {configurable: false, enumerable: false, value: 12, writable: false}
//}

配合 Object.create() 可以实现浅克隆:

const shallowClone = (obj) => Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
)

String.prototype.padStart()

str.padStart(length [, padStr]) 会返回一个新字符串,该字符串将从 str 字符串的左侧开始填充某个字符串 padStr(非必填,如果不是字符串则会转成字符串, 传入 undefined 和不传这个参数效果一致)直到达到指定位数 length 为止:

'abc'.padStart(5, 2)          // '22abc'
'abc'.padStart(5, undefined)  // '  abc'
'abc'.padStart(5, {})         // '[oabc'
'abc'.padStart(5)             // '  abc'
'abcde'.padStart(2, 'f')      // 'abcde'

String.prototype.padEnd()

规则和 padStart 类似,但是是从字符串右侧开始填充:

'abc'.padEnd(5, 2)  // 'abc22'

函数参数尾逗号

允许函数在定义和调用的时候时候最后一个参数后加上逗号:

function init(
    param1, 
    param2,  
) { }

init('a', 'b',)

Async函数

共享内存和Atomics对象

ES2018

Promise.prototype.finally()

Promise.prototype.finally() 用于给 Promise 对象添加 onFinally 函数,这个函数主要是做一些清理的工作,只有状态变化的时候才会执行该 onFinally 函数。

function onFinally() {
    console.log(888)  // 并不会执行  
}
new Promise((resolve, reject) => {

}).finally(onFinally)

finally() 会生成一个 Promise 新实例,finally 一般会原样后传父 Promise,无论父级实例是什么状态:

let p1 = new Promise(() => {})
let p2 = p1.finally(() => {})
setTimeout(console.log, 0, p2)  // Promise {<pending>}

let p3 = new Promise((resolve, reject) => {
    resolve(3)
})
let p4 = p3.finally(() => {})
setTimeout(console.log, 0, p3)  // Promise {<fulfilled>: 3}

上面说的是一般,但是也有特殊情况,比如 finally 里返回了一个非 fulfilledPromise 或者抛出了异常的时候,则会返回对应状态的新实例:

let p1 = new Promise((resolve, reject) => {
    resolve(3)
})
let p2 = p1.finally(() => new Promise(() => {}))
setTimeout(console.log, 0, p2)  // Promise {<pending>}

let p3 = p1.finally(() => Promise.reject(6))
setTimeout(console.log, 0, p3)  // Promise {<rejected>: 6}

let p4 = p1.finally(() => {
    throw new Error('error')
})
setTimeout(console.log, 0, p4)  // Promise {<rejected>: Error: error}

参考:深入理解Promise

异步迭代器

想要了解异步迭代器最好的方式就是和同步迭代器进行对比。我们知道可迭代数据的内部都是有一个 Symbol.iterator 属性,它是一个函数,执行后会返回一个迭代器对象,这个迭代器对象有一个 next() 方法可以对数据进行迭代,next() 执行后会返回一个对象,包含了当前迭代值 value 和 标识是否完成迭代的 done 属性:

let iterator = [1, 2][Symbol.iterator]()
iterator.next()  // { value: 1, done: false }
iterator.next()  // { value: 2, done: false }
iterator.next()  // { value: undefinde, done: true }

上面这里的 next() 执行的是同步操作,所以这个是同步迭代器,但是如果 next() 里需要执行异步操作,那就需要异步迭代了,可异步迭代数据的内部有一个 Symbol.asyncIterator 属性,基于此我们来实现一个异步迭代器:

class Emitter {
    constructor(iterable) {
        this.data = iterable
    }
    [Symbol.asyncIterator]() {
        let length = this.data.length,
            index = 0;

        return {
            next:() => {
                const done = index >= length
                const value = !done ? this.data[index++] : undefined
                return new Promise((resolve, reject) => {
                    resolve({value, done})
                })
            }
        }
    }
}

异步迭代器的 next() 会进行异步的操作,通常是返回一个 Promise,所以需要对应的处理函数去处理结果:

let emitter = new Emitter([1, 2, 3])
let asyncIterator = emitter[Symbol.asyncIterator]()
asyncIterator.next().then(res => {
    console.log(res)  // { value: 1, done: false }
})
asyncIterator.next().then(res => {
    console.log(res)  // { value: 2, done: false }
})
asyncIterator.next().then(res => {
    console.log(res)  // { value: 3, done: false }
})

另外也可以使用 for await...of 来迭代异步可迭代数据:

let asyncIterable = new Emitter([1, 2, 3])
async function asyncCount() {
    for await (const x of asyncIterable ) {
        console.log(x)
    }
}
asyncCount()
// 1 2 3

另外还可以通过异步生成器来创建异步迭代器:

class Emitter {
    constructor(iterable) {
        this.data = iterable
    }
    async *[Symbol.asyncIterator]() {
        let length = this.data.length,
            index = 0;

        while (index < length) {
            yield this.data[index++]
        }
    }
}
async function asyncCount() {
    let emitter = new Emitter([1, 2, 3])
    const asyncIterable = emitter[Symbol.asyncIterator]()
    for await (const x of asyncIterable ) {
        console.log(x)
    }
}
asyncCount()
// 1 2 3

参考:

s修饰符(dotAll模式)

正则表达式新增了一个 s 修饰符,使得 . 可以匹配任意单个字符:

/foo.bar/.test('foo\nbar')   // false
/foo.bar/s.test('foo\nbar')  // true

上面这又被称为 dotAll 模式,表示点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式:

/foo.bar/s.dotAll  // true

具名组匹配

正则表达式可以使用捕获组来匹配字符串,但是想要获取某个组的结果只能通过对应的索引来获取:

let re = /(\d{4})-(\d{2})-(\d{2})/
let result = re.exec('2015-01-02')
// result[0] === '2015-01-02'
// result[1] === '2015'
// result[2] === '01'
// result[3] === '02'

而现在我们可以通过给捕获组 (?<name>...) 加上名字 name ,通过名字来获取对应组的结果:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let result = re.exec('2015-01-02')
// result.groups.year === '2015'
// result.groups.month === '01'
// result.groups.day === '02'

配合解构赋值可以写出非常精简的代码:

let {groups: {year, month, day}} = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec('2015-01-02')
console.log(year, month, day)  // 2015 01 02

具名组也可以通过传递给 String.prototype.replace 的替换值中进行引用。如果该值为字符串,则可以使用 $<name> 获取到对应组的结果:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let result = '2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// result === '02/01/2015'

参考:proposal-regexp-named-groups

后行断言

后行断言: (?<=y)xx 只有在 y 后面才能匹配:

/(?<=\$)\d+/.exec('I have $100.')  // ['100']

后行否定断言: (?<!y)xx 只有不在 y 后面才能匹配:

/(?<!\$)\d+/.exec('I have $100.')  // ['00']

Unicode属性转义

允许正则表达式匹配符合 Unicode 某种属性的所有字符,\p{...} 是匹配包含,\P{...} 是匹配不包含的字符,且必须搭配 /u 修饰符才会生效:

/\p{Emoji}+/u.exec('😁😭笑死我了🤣😂不行了')  // ['😁😭']
/\P{Emoji}+/u.exec('😁😭笑死我了🤣😂不行了')  // ['笑死我了']

这里可以查询到更多的 Unicode 的属性 Full_Properties

对象扩展运算符

对象的扩展运算符可以用到解构赋值上,且只能应用到最后一个变量上:

let {x, ...y} = {x: 1, a: 2, b: 3}
console.log(y)  // {a: 2, b: 3}

对象扩展运算符不能解构原型上的属性:

let obj = { x: 1 }
obj.__proto__ = { y: 2 }
let {...a} = obj
console.log(a.y)  // undefined

应用一:可以实现浅拷贝,但是不会拷贝原始属性:

let person = Object.create({ name: '布兰' })
person.age = 12

// 浅拷贝写法一
let { ...pClone1 } = person
console.log(pClone1)  // { age: 12 }
console.log(pClone1.name)  // undefined

// 浅拷贝写法二
let pClone2 = {...person}
console.log(pClone2)  // { age: 12 }
console.log(pClone2.name)  // undefined

应用二:合并两个对象:

let ab = {...a, ...b}

// 等同于
let ab = Object.assign({}, a, b);

应用三:重写对象属性

let aWithOverrides = { ...a, x: 1, y: 2 };

应用四:给新对象设置默认值

let aWithDefaults = { x: 1, y: 2, ...a };

应用五:利用扩展运算符的解构赋值可以扩展函数参数:

function baseFunction({ a, b }) {}
function wrapperFunction({ x, y, ...restConfig }) {
    // 使用 x 和 y 参数进行操作
    // 其余参数传给原始函数
    return baseFunction(restConfig)
}

参考:

放松对标签模板里字符串转义的限制

ECMAScript 6 入门

ES2019

允许省略catch里的参数

异常被捕获的时候如果不需要做操作,甚至可以省略 catch(err) 里的参数和圆括号:

try {

} catch {

}

JSON.stringify()变动

UTF-8 标准规定,0xD8000xDFFF 之间的码点,不能单独使用,必须配对使用。 所以 JSON.stringify() 对单个码点进行操作,如果码点符合 UTF-8 标准,则会返回对应的字符,否则会返回对应的码点:

JSON.stringify('\u{1f600}')  // ""😀""
JSON.stringify('\u{D834}')  // ""\ud834""

Symbol.prototype.description

Symbol 实例新增了一个描述属性 description

let symbol = Symbol('foo')
symbol.description  // 'foo'

Function.prototype.toString()

函数的 toString() 会原样输出函数定义时候的样子,不会省略注释和空格。

Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象:

let person = { name: '布兰', age: 12 }
let keyValueArr = Object.entries(person)   // [['name', '布兰'], ['age', 12]]
let obj = Object.fromEntries(arr)  // { name: '布兰', age: 12 }

常用可迭代数据结构之间的装换:

let person = { name: '布兰', age: 12 }

// 对象 -> 键值对数组
let keyValueArr = Object.entries(person)  // [['name', '布兰'], ['age', 12]]

// 键值对数组 -> Map
let map = new Map(keyValueArr)  // Map {"name": "布兰", "age": 12}

// Map -> 键值对数组
let arr = Array.from(map)  // [['name', '布兰'], ['age', 12]] 

// 键值对数组 -> 对象
let obj = Array.from(arr).reduce((acc, [ key, val ]) => Object.assign(acc, { [key]: val }), {})  // { name: '布兰', age: 12 }

参考:Object.fromEntries

字符串可直接输入行分隔符和段分隔符

JavaScript 规定有 5 个字符,不能在字符串里面直接使用,只能使用转义形式。

但是由于 JSON 允许字符串里可以使用 U+2028U+2029,所以使得 JSON.parse() 去解析字符串的时候可能会报错,所以 ES2019 允许模板字符串里可以直接这两个字符:

JSON.parse('"\u2028"')  // ""
JSON.parse('"\u2029"')  // ""
JSON.parse('"\u005C"')  // SyntaxError

String.prototype.trimStart

消除字符串头部空格,返回一个新字符串;浏览器还额外增加了它的别名函数 trimLeft()

let str = '  hello world '
let newStr = str.trimStart()
console.log(newStr, newStr === str) 
// 'hello world '  false

String.prototype.trimEnd

消除字符串尾部空格,返回一个新字符串;浏览器还额外增加了它的别名函数 trimRight()

let str = '  hello world '
let newStr = str.trimEnd()
console.log(newStr, newStr === str) 
// '  hello world'  false

Array.prototype.flat()

arr.flat(depth) 按照 depth (不传值的话默认是 1)深度拍平一个数组,并且将结果以新数组形式返回:

// depth 默认是 1
const arr1 = [1, 2, [3, 4]]
console.log(arr1.flat())  // [1, 2, 3, 4]

// 使用 Infinity,可展开任意深度的嵌套数组;自动跳过空数组;
const arr2 = [1, , [2, [3, [4]]]]
console.log(arr2.flat(Infinity))
// [1, 2, 3, 4]

reduce 实现拍平一层数组:


const arr = [1, 2, [3, 4]]

// 方法一
let newStr = arr.reduce((acc, cur) => acc.concat(cur), [])

// 方法二
const flattened = arr => [].concat(...arr)
flattened(arr)

参考:flat

Array.prototype.flatMap()

flatMap(callback) 使用映射函数 callback 映射每个元素,callback 每次的返回值组成一个数组,并且将这个数组执行类似 arr.flat(1) 的操作进行拍平一层后最后返回结果:

const arr1 = [1, 2, 3, 4]

arr1.flatMap(x => [x * 2])
// 将 [[2], [4], [6], [8]] 数组拍平一层得到最终结果:[2, 4, 6, 8]

参考:flatMap

ES2020

String.prototype.matchAll()

String.prototype.matchAll() 方法,可以一次性取出所有匹配。不过,它返回的是一个 RegExpStringIterator 迭代器同是也是一个可迭代的数据结构,所以可以通过 for...of 进行迭代:

let str = 'test1test2'
let regexp = /t(e)(st(\d?))/g
let iterable = str.matchAll(regexp)
for (const x of iterable) {
    console.log(x)
}
// ['test1', 'e', 'st1', '1', index: 0, input: 'test1test1', groups: undefined]
// ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', groups: undefined]

注意当使用 matchAll(regexp) 的时候,正则表达式必须加上 /g 修饰符。

也可以将这个可迭代数据转成数组形式:

// 方法一
[...str.matchAll(regexp)]

// 方法二
Array.from(str.matchAll(regexp))

动态import()

标准用法的 import 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。

比如按需加载一个模块可以这样:

if (xxx) {
    import('./module.js')
}

import() 是异步导入的,结果会返回一个 Promise

import('/module.js')
.then((module) => {
    // Do something with the module.
})

动态 import() 的应用场景挺多的,比如 Vue 中的路由懒加载就是使用的动态导入组件。另外由于动态性不便于静态分析工具和 tree-shaking 工作,所以不能滥用。

BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 $2^{53}$ - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

为了区分 Number,定义一个 BigInt 需要在整数后面加上一个 n,或者用函数直接定义:

const num1 = 10n
const num2 = BigInt(20)

NumberBigInt 之间能进行比较,但他们之间是宽松相等;且由于他们表示的是不同类型的数字,所以不能直接进行四则运算:

10n == 10         // true
10n === 10        // false
10n > 8           // true
10 + Number(10n)  // 20
10 + 10n          // TypeError

Promise.allSettled

Promise.allSettled(iterable) 当所有的实例都已经 settled,即状态变化过了,那么将返回一个新实例,该新实例的内部值是由所有实例的值和状态组合成的数组,数组的每项是由每个实例的状态和内部值组成的对象。

function init(){
    return 3
}
let p1 = Promise.allSettled([
    new Promise((resolve, reject) => {
        resolve(9)
    }).then(res => {}),
    new Promise((resolve, reject) => {
        reject(6)
    }),
    init()
])
let p2 = p1.then(res => {
    console.log(res)
}, err => {
    console.log(err)
})
// [
//      {status: "fulfilled", value: undefined}, 
//      {status: "rejected", reason: 6}, 
//      {status: "fulfilled", value: 3}
// ]

只要所有实例中包含一个 pending 状态的实例,那么 Promise.allSettled() 的结果为返回一个这样 Promise {<pending>} 的实例。

globalThis

在以前,从不同的 JavaScript 环境中获取全局对象需要不同的语句。在 Web 中,可以通过 windowself 或者 frames 取到全局对象,但是在 Web Workers 中,只有 self 可以。在 Node.js 中,它们都无法获取,必须使用 global

而现在只需要使用 globalThis 即可获取到顶层对象,而不用担心环境问题。

// 在浏览器中
globalThis === window  // true

import.meta

import.meta 是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的 URLimport.meta 必须在一个模块里使用:

// 没有声明 type="module",就使用 import.meta 会报错
<script type="module" src="./js/module.js"></script>

// 在module.js里
console.log(import.meta)  
// {url: "http://localhost/3ag/js/module.js"}

如果需要在配置了 Webpack 的项目,比如 Vue 里使用 import.meta 需要加一个包且配置一下参数,否则项目编译阶段会报错。

包配置详情参考:@open-wc/webpack-import-meta-loader

比如我用的是 4.x 版本的 vue-cli,那我需要在 vue.config.js 里配置:

module.exports = {
    chainWebpack: config => {
        config.module
            .rule('js')
            .test(/\.js$/)
            .use('@open-wc/webpack-import-meta-loader')
                .loader('@open-wc/webpack-import-meta-loader')
                .end()
    }
}

可选链操作符(?.)

通常我们获取一个深层对象的属性会需要写很多判断或者使用逻辑与 && 操作符,因为对象的某个属性如果为 null 或者 undefined 就有可能报错:

let obj = {
    first: {
        second: '布兰'
    }
}

// 写法一
let name1 = ''
if (obj) {
    if (obj.first) {
        name1 = obj.first.second
    }
}

// 写法二
let name2 = obj && obj.first && obj.first.second

?. 操作符允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。如果某个属性为 null 或者 undefined 则结果直接为 undefined。有了可选链操作符就可以使得表达式更加简明了,对于上面例子用可选链操作符可以这么写:

let name3 = obj?.first?.second

空值合并操作符(??)

对于逻辑或 || 运算符,当对运算符左侧的操作数进行装换为 Boolean 值的时候,如果为 true,则取左边的操作数为结果,否则取右边的操作数为结果:

let name = '' || '布兰'
console.log(name)  // '布兰'

我们都知道 ''0nullundefinedfalseNaN 等转成 Boolean 值的时候都是 false,所以都会取右边的操作数。这个时候如果要给变量设置默认值,如果遇到本身值就可能是 ''0 的情况那就会出错了,会被错误的设置为默认值了。

?? 操作符就是为了解决这个问题而出现的,x ?? y 只有左侧的操作数为 nullundefined 的时候才取右侧操作数,否则取左侧操作数:

let num = 0 ?? 1
console.log(num)  // 0

ES2021

如下这几个提案已经确定了会在 2021 年发布,所以把他们归到 ES2021 中。

String.prototype.replaceAll

之前需要替换一个字符串里的全部匹配字符可以这样做:

const queryString = 'q=query+string+parameters'

// 方法一
const withSpaces1 = queryString.replace(/\+/g, ' ')

// 方法二
const withSpaces2 = queryString.split('+').join(' ')

而现在只需要这么做:

const withSpace3 = queryString.replaceAll('+', ' ')

replaceAll 的第一个参数可以是字符串也可以是正则表达式,当是正则表达式的时候,必须加上全局修饰符 /g,否则报错。

参考:string-replaceall

Promise.any()

Promsie.any()Promise.all() 一样接受一个可迭代的对象,然后依据不同的入参会返回不同的新实例:

WeakRef

我们知道一个普通的引用(默认是强引用)会将与之对应的对象保存在内存中。只有当该对象没有任何的强引用时,JavaScript 引擎 GC 才会销毁该对象并且回收该对象所占的内存空间。

WeakRef 对象允许你保留对另一个对象的弱引用,而不会阻止被弱引用的对象被 GC 回收。WeakRef 的实例方法 deref() 可以返回当前实例的 WeakRef 对象所绑定的 target 对象,如果该 target 对象已被 GC 回收则返回 undefined

let person = { name: '布兰', age: 12 }
let wr = new WeakRef(person)
console.log(wr.deref())  
// { name: '布兰', age: 12 }

正确使用 WeakRef 对象需要仔细的考虑,最好尽量避免使用。这里面有诸多原因,比如:GC 在一个 JavaScript 引擎中的行为有可能在另一个 JavaScript 引擎中的行为大相径庭,或者甚至在同一类引擎,不同版本中 GC 的行为都有可能有较大的差距。GC 目前还是 JavaScript 引擎实现者不断改进和改进解决方案的一个难题。

参考:

逻辑赋值符

逻辑赋值符包含 3 个:

看如下示例,加深理解:

let x = 0
x &&= 1  // x: 0
x ||= 1  // x: 1
x ??= 2  // x: 1

let y = 1
y &&= 0     // y: 0
y ||= null  // y: null
y ??= 2     // y: 2

数值分隔符(_)

对于下面一串数字,你一眼看上去不确定它到底是多少吧?

const num = 1000000000

那现在呢?是不是可以很清楚的看出来它是 10 亿:

const num = 1_000_000_000

数值分隔符(_)的作用就是为了让数值的可读性更强。除了能用于十进制,还可以用于二级制,十六进制甚至是 BigInt 类型:

let binarary = 0b1010_0001_1000_0101
let hex = 0xA0_B0_C0
let budget = 1_000_000_000_000n

使用时必须注意 _ 的两边必须要有类型的数值,否则会报错,以下这些都是无效的写法:

let num = 10_
let binarary = 0b1011_
let hex = 0x_0A0B
let budget = 1_n

参考文章