自ES6往后的Js新特性

对现阶段(ES6 - ES12)的新特性进行一点简单的总结

摘自这篇博客(原本不想放的,仔细想想还是放了好,有助于了解Js的历史和ESM到底是什么,说的很清楚)

前言

JavaScript 是当今使用最广泛的、发展最好的前后端(后端主要是 Nodejs)语言,如果我们想要灵活使用 JavaScript,我们首先需要了解的就是 JavaScript 和 ECMAScript(ES) 的基础知识及发展历程。

一、JavaScript 的诞生

JavaScript 因为互联网而生,紧跟着浏览器的出现而问世。回顾它的历史,就要从浏览器的历史讲起。

1990年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最大的电脑网络——互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。最早的网页只能在操作系统的终端里浏览,也就是说只能使用命令行操作,网页都是在字符窗口中显示,这当然非常不方便。

1992年底,美国国家超级电脑应用中心(NCSA)开始开发一个独立的浏览器,叫做 Mosaic。这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览。

1994年10月,NCSA 的一个主要程序员 Marc Andreessen 联合风险投资家 Jim Clark,成立了 Mosaic 通信公司(Mosaic Communications),不久后改名为 Netscape。这家公司的方向,就是在 Mosaic 的基础上,开发面向普通用户的新一代的浏览器 Netscape Navigator。

1994年12月,Navigator 发布了1.0版,市场份额一举超过90%。

Netscape 公司很快发现,Navigator 浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写用户名”。这就需要在网页中嵌入小程序,让浏览器检查每一栏是否都填写了。

管理层对这种浏览器脚本语言的设想是:功能不需要太强,语法较为简单,容易学习和部署。那一年,正逢 Sun 公司的 Java 语言问世,市场推广活动非常成功。Netscape 公司决定与 Sun 公司合作,浏览器支持嵌入 Java 小程序(后来称为 Java applet)。但是,浏览器脚本语言是否就选用 Java,则存在争论。后来,还是决定不使用 Java,因为网页小程序不需要 Java 这么“重”的语法。但是,同时也决定脚本语言的语法要接近 Java,并且可以支持 Java 程序。这些设想直接排除了使用现存语言,比如 Perl、Python 和 TCL。

1995年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种网页脚本语言。Brendan Eich 有很强的函数式编程背景,希望以 Scheme 语言(函数式语言鼻祖 LISP 语言的一种方言)为蓝本,实现这种新语言。

1995年5月,Brendan Eich 只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。

  • 基本语法:借鉴 C 语言和 Java 语言。
  • 数据结构:借鉴 Java 语言,包括将值分成原始值和对象两大类。
  • 函数的用法:借鉴 Scheme 语言和 Awk 语言,将函数当作第一等公民,并引入闭包。
  • 原型继承模型:借鉴 Self 语言(Smalltalk 的一种变种)。
  • 正则表达式:借鉴 Perl 语言。
  • 字符串和数组处理:借鉴 Python 语言。

为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型(subtyping)等等,但是可以利用现有功能找出解决办法。这种功能的不足,直接导致了后来 JavaScript 的一个显著特点:对于其他语言,你需要学习语言的各种功能,而对于 JavaScript,你常常需要学习各种解决问题的模式。而且由于来源多样,从一开始就注定,JavaScript 的编程风格是函数式编程和面向对象编程的一种混合体。

Netscape 公司的这种浏览器脚本语言,最初名字叫做 Mocha,1995年9月改为 LiveScript。12月,Netscape 公司与 Sun 公司(Java 语言的发明者和所有者)达成协议,后者允许将这种语言叫做 JavaScript。这样一来,Netscape 公司可以借助 Java 语言的声势,而 Sun 公司则将自己的影响力扩展到了浏览器。

之所以起这个名字,并不是因为 JavaScript 本身与 Java 语言有多么深的关系(事实上,两者关系并不深,详见下节),而是因为 Netscape 公司已经决定,使用 Java 语言开发网络应用程序,JavaScript 可以像胶水一样,将各个部分连接起来。当然,后来的历史是 Java 语言的浏览器插件失败了,JavaScript 反而发扬光大。

1995年12月4日,Netscape 公司与 Sun 公司联合发布了 JavaScript 语言,对外宣传 JavaScript 是 Java 的补充,属于轻量级的 Java,专门用来操作网页。

1996年3月,Navigator 2.0 浏览器正式内置了 JavaScript 脚本语言。

二、JavaScript 与 ECMAScript 的关系

要讲清楚这个问题,需要回顾历史。1996年11月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript ,这个版本就是1.0版。

该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript ,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA ,不是 Netscape ,这样有利于保证这门语言的开放性和中立性。

因此,ECMAScript 和 JavaScript 的关系是,ECMAScript 是一个简单的 JavaScript 标准规范,JavaScript 是 ECMAScript 的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript )。并且,ECMAScript 持续不断的为 JavaScript 添加新功能。

从 1997年7月 ECMAScript 1.0 发布到现在,ECMAScript 已经正式发布了 11 版,下面我们主要介绍从ES6(ES2015)到ES11(最新 ES2020 )期间,每版发布的新功能。

ES6

这个是比较重要的一个版本,现在很多常用的语法都是基于这一版,下面是其中一部分

  • 模块化
  • 箭头函数
  • 函数参数默认值
  • 模板字符串
  • 解构赋值
  • 延展操作符
  • 对象属性简写
  • Promise
  • Let 与 Const

然后具体讲解的话我们去看看阮一峰的书 ES6

ES7

这个内容比较少,估计搞完ES6大家都累了…

  1. Array.prototype.includes()

这个方法大家用的比较多吧,不用includes其实还可以用index,-1就是不存在嘛

  1. ** 运算符

这个就是乘方,Math.pow()的语法糖,比如2 ** 3 === Math.pow(2, 3) === 8

ES8

  • async/await
  • Object.values()
  • Object.entries()
  • String padding: padStart()padEnd(),填充字符串达到当前长度
  • 函数参数列表结尾允许逗号
  • Object.getOwnPropertyDescriptors()
  • ShareArrayBufferAtomics对象,用于从共享内存位置读取和写入

Es8补充的内容还挺多的

  1. async/await

这个就不用多说了,Js中异步的终极解决方案

async 是一个通过异步执行并隐式返回 Promise 作为结果的函数

然后用await处理Promise.resolve()后的情况,详细不多说,只做简介

  1. Object.values()

很明显,遍历一个对象的值,然后以返回一个值的数组

  1. Object.entries()

这个有意思,是遍历一个对象的key/value对,然后同样以数组(一个二维数组)方式返回,简单的看看使用

1
2
3
4
5
6
7
8
9
// ES8之前
Object.keys(obj).forEach((key) => {
console.log('key:' + key + ' value:' + obj[key])
})

// ES8之后
for (let [key, value] of Object.entries(obj1)) {
console.log(`key: ${key} value:${value}`)
}
  1. String padding: padStart()padEnd()

    允许将空字符串或其他字符串添加到原始字符串的开头或结尾。

1
2
3
4
5
6
7
console.log('0.0'.padStart(4, '10'))
// 10.0
console.log('0.0'.padStart(20))
// 0.0
console.log('0.0'.padStart(20, '10'))
// 101010101010101010.0
// padEnd同理咯
  1. Object.getOwnPropertyDescriptors()

返回 obj对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const obj2 = {
name: 'Jine',
get age() {
return '18'
},
};
console.log(Object.getOwnPropertyDescriptors(obj2))
// 大概这样
{name: {…}, age: {…}}
age:
configurable: true
enumerable: true
get: ƒ age()
set: undefined
[[Prototype]]: Object
name:
configurable: true
enumerable: true
value: "Jine"
writable: true

  1. SharedArrayBuffer对象和Atomics对象

SharedArrayBuffer 对象用来表示一个通用的,固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。

Atomics 对象提供了一组静态方法用来对 SharedArrayBuffer 对象进行原子操作

没用过,不知道,不清楚

ES9

1.异步迭代

async/await的某些时刻,你可能尝试在同步循环中调用异步函数。例如:

1
2
3
4
5
async function process(array) {
for (let i of array) {
await doSomething(i)
}
}

这段代码不会正常运行,下面这段同样也不会:

1
2
3
4
5
async function process(array) {
array.forEach(async (i) => {
await doSomething(i)
})
}

这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调用完成。

ES2018 引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了 next()方法返回一个 Promise。因此 await可以和 for...of循环一起使用,以串行的方式运行异步操作。例如:

1
2
3
4
5
async function process(array) {
for await (let i of array) {
doSomething(i)
}
}

2.Promise.finally()

一个 Promise 调用链要么成功到达最后一个 .then(),要么失败触发 .catch()。在某些情况下,你想要在无论 Promise 运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。

.finally()允许你指定最终的逻辑:

1
2
3
4
5
6
7
8
9
function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch((err) => {
console.log(err)
})
.finally(() => {})
}

3.Rest/Spread 属性

就是对…运算符的扩展,ES6的时候还只能作用于数组,到了ES9才可以作用于对象

4.正则表达式命名捕获组

JavaScript 正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组,例如:以 YYYY-MM-DD的格式解析日期:

1
2
3
4
5
const reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match[1],
month = match[2],
day = match[3]

这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。

ES2018 允许命名捕获组使用符号 ?<name>,在打开捕获括号 (后立即命名,示例如下:

1
2
3
4
5
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match.groups.year,
month = match.groups.month, // 04
day = match.groups.day

任何匹配失败的命名组都将返回 undefined

命名捕获也可以使用在 replace()方法中。例如将日期转换为美国的 MM-DD-YYYY 格式:

1
2
3
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
d = '2018-04-30',
usDate = d.replace(reDate, '$<month>-$<day>-$<year>')

5.正则表达式反向断言

目前 JavaScript 在正则表达式中支持先行断言(lookahead)。这意味着匹配会发生,但不会有任何捕获,并且断言没有包含在整个匹配字段中。例如从价格中捕获货币符号:

1
2
3
const reLookahead = /\D(?=\d+)/,
match = reLookahead.exec('$123.89')
console.log(match[0])

ES2018 引入以相同方式工作但是匹配前面的反向断言(lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字:

1
2
3
const reLookbehind = /(?<=\D)\d+/,
match = reLookbehind.exec('$123.89')
console.log(match[0])

以上是 肯定反向断言,非数字 \D必须存在。同样的,还存在 否定反向断言,表示一个值必须不存在,例如:

1
2
3
const reLookbehindNeg = /(?<!\D)\d+/,
match = reLookbehind.exec('$123.89')
console.log(match[0])

6.正则表达式 dotAll 模式

正则表达式中点 .匹配除回车外的任何单字符,标记 s改变这种行为,允许行终止符的出现,例如:

1
2
/hello.world/.test('hello\nworld');
/hello.world/s.test('hello\nworld');

7.正则表达式 Unicode 转义

到目前为止,在正则表达式中本地访问 Unicode 字符属性是不被允许的。ES2018添加了 Unicode 属性转义——形式为 \p{...}\P{...},在正则表达式中使用标记 u (unicode) 设置,在 \p块儿内,可以以键值对的方式设置需要匹配的属性而非具体内容。例如:

1
2
const reGreekSymbol = /\p{Script=Greek}/u
reGreekSymbol.test('π')

此特性可以避免使用特定 Unicode 区间来进行内容类型判断,提升可读性和可维护性。

8.非转义序列的模板字符串

之前, \u开始一个 unicode 转义, \x开始一个十六进制转义, \后跟一个数字开始一个八进制转义。这使得创建特定的字符串变得不可能,例如 Windows 文件路径 C:\uuu\xxx\111。更多细节参考模板字符串。

ES10

  • 行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与 JSON 匹配
  • 更加友好的 JSON.stringify
  • 新增了Array 的 flat()方法和 flatMap()方法
  • 新增了String 的 trimStart()方法和 trimEnd()方法
  • Object.fromEntries()
  • Symbol.prototype.description
  • String.prototype.matchAll
  • Function.prototype.toString()现在返回精确字符,包括空格和注释
  • 简化 try {} catch {},修改 catch 绑定
  • 新的基本数据类型 BigInt
  • globalThis
  • import()
  • Legacy RegEx
  • 私有的实例方法和访问器

1.行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与 JSON 匹配

以前,这些符号在字符串文字中被视为行终止符,因此使用它们会导致 SyntaxError 异常。

2.更加友好的 JSON.stringify

如果输入 Unicode 格式但是超出范围的字符,在原先 JSON.stringify 返回格式错误的 Unicode 字符串。现在实现了一个改变 JSON.stringify 的第3阶段提案,因此它为其输出转义序列,使其成为有效 Unicode(并以UTF-8表示)

3.新增了Array的 flat()方法和 flatMap()方法

flat()flatMap()本质上就是是归纳(reduce) 与 合并(concat)的操作。

Array.prototype.flat()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

1
2
3
4
5
6
7
8
9
10
var arr1 = [1, 2, [3, 4]]
arr1.flat()

var arr2 = [1, 2, [3, 4, [5, 6]]]
arr2.flat()

var arr3 = [1, 2, [3, 4, [5, 6]]]
arr3.flat(2)
//使用 Infinity 作为深度,展开任意深度的嵌套数组
arr3.flat(Infinity)
  • 其次,还可以利用 flat()方法的特性来去除数组的空项
1
2
var arr4 = [1, 2, , 4, 5]
arr4.flat()

Array.prototype.flatMap()

flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 和 深度值1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。 这里我们拿 map 方法与 flatMap 方法做一个比较。

1
2
3
4
5
6
7
var arr1 = [1, 2, 3, 4]

arr1.map((x) => [x * 2])

arr1.flatMap((x) => [x * 2])
// 只会将 flatMap 中的函数返回的数组 “压平” 一层
arr1.flatMap((x) => [[x * 2]])

4.新增了String的 trimStart()方法和 trimEnd()方法

新增的这两个方法很好理解,分别去除字符串首尾空白字符,这里就不用例子说声明了。

5. Object.fromEntries()

Object.entries()方法的作用是返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)。

Object.fromEntries() 则是 Object.entries() 的反转。

Object.fromEntries() 函数传入一个键值对的列表,并返回一个带有这些键值对的新对象。这个迭代参数应该是一个能够实现 @iterator 方法的的对象,返回一个迭代器对象。它生成一个具有两个元素的类似数组的对象,第一个元素是将用作属性键的值,第二个元素是与该属性键关联的值。

  • 通过 Object.fromEntries, 可以将 Map 转化为 Object:
1
2
3
4
5
6
const map = new Map([
['foo', 'bar'],
['baz', 42],
])
const obj = Object.fromEntries(map)
console.log(obj)
  • 通过 Object.fromEntries, 可以将 Array 转化为 Object:
1
2
3
4
5
6
7
const arr = [
['0', 'a'],
['1', 'b'],
['2', 'c'],
]
const obj = Object.fromEntries(arr)
console.log(obj)

6. Symbol.prototype.description

通过工厂函数 Symbol()创建符号时,您可以选择通过参数提供字符串作为描述:

1
const sym = Symbol('The description')

以前,访问描述的唯一方法是将符号转换为字符串:

1
assert.equal(sym.description, 'The description')

现在引入了getter Symbol.prototype.description以直接访问描述:

1
assert.equal(sym.description, 'The description')

7. String.prototype.matchAll

matchAll() 方法返回一个包含所有匹配正则表达式及分组捕获结果的迭代器。 在 matchAll 出现之前,通过在循环中调用 regexp.exec 来获取所有匹配项信息(regexp 需使用 /g 标志:

1
2
3
4
5
6
const regexp = RegExp('foo*', 'g')
const str = 'table football, foosball'
while ((matches = regexp.exec(str)) !== null) {
console.log(`Found ${matches[0]}. Next starts at ${regexp.lastIndex}.`)
// expected output: "Found foo. Next starts at 19."
}

如果使用 matchAll ,就可以不必使用 while 循环加 exec 方式(且正则表达式需使用/g标志)。使用 matchAll 会得到一个迭代器的返回值,配合 for…of, array spread, or Array.from() 可以更方便实现功能:

1
2
3
4
5
6
7
8
9
10
11
const regexp = RegExp('foo*', 'g')
const str = 'table football, foosball'

let matches = str.matchAll(regexp)
for (const match of matches) {
console.log(match)
}
// Array [ "foo" ]
// Call matchAll again to create a new iterator
matches = str.matchAll(regexp)
Array.from(matches, (m) => m[0])

matchAll 可以更好的用于分组

1
2
3
4
5
6
7
var regexp = /t(e)(st(\d?))/g
var str = 'test1test2'
str.match(regexp)

let array = [...str.matchAll(regexp)]
array[0]
array[1]

8. Function.prototype.toString()现在返回精确字符,包括空格和注释

1
2
3
4
5
6
7
function /* comment */ foo /* another comment */() {}
console.log(foo.toString())
// ES2019 会把注释一同打印
console.log(foo.toString())
// 箭头函数
const bar = /* another comment */ () => {}
console.log(bar.toString())

9.修改 catch 绑定

在 ES10 之前,我们必须通过语法为 catch 子句绑定异常变量,无论是否有必要。很多时候 catch 块是多余的。ES10 提案使我们能够简单的把变量省略掉。

不算大的改动。

之前是

1
try {} catch (e) {}

现在是

1
try {} catch {}

10.新的基本数据类型 BigInt

现在的基本数据类型(值类型)不止5种(ES6 之后是六种)了哦!加上BigInt一共有七种基本数据类型,分别是:String、Number、Boolean、Null、Undefined、Symbol、BigInt

ES11

1.Promise.allSettled

Promise.all 缺陷

都知道 Promise.all 具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise 直接进入 reject 状态。

想象这个场景:你的页面有三个区域,分别对应三个独立的接口数据,使用 Promise.all 来并发三个接口,如果其中任意一个接口服务异常,状态是 reject,这会导致页面中该三个区域数据全都无法渲染出来,因为任何 reject 都会进入 catch 回调, 很明显,这是无法接受的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.all([
Promise.reject({ code: 500, msg: '服务异常' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] }),
])
.then((ret) => {
// 如果其中一个任务是 reject,则不会执行到这个回调。
RenderContent(ret)
})
.catch((error) => {
// 本例中会执行到这个回调
// error: {code: 500, msg: "服务异常"}
})

Promise.allSettled 的优势

我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态(fulfilled 或者 rejected)与结果(业务 value 或者 拒因 reason),在 then 里面通过 filter 来过滤出想要的业务逻辑结果,这就能最大限度的保障业务当前状态的可访问性,而 Promise.allSettled 就是解决这问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Promise.allSettled([
Promise.reject({ code: 500, msg: '服务异常' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] }),
]).then((ret) => {
/*
0: {status: "rejected", reason: {...}}
1: {status: "fulfilled", value: {...}}
2: {status: "fulfilled", value: {...}}
*/
// 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
RenderContent(
ret.filter((el) => {
return el.status !== 'rejected'
})
)
})

2.可选链

可选链 可让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。

日常开发中,我们经常会遇到这种查询

1
var name = user && user.info && user.info.name

又或是这种

1
var age = user && user.info && user.info.getAge && user.info.getAge()

这是一种丑陋但又不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property... 这种错误,这极有可能让你整个应用挂掉。

用了 Optional Chaining ,上面代码会变成

1
2
var name = user?.info?.name
var age = user?.info?.getAge?.()

可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段。根据上面可以看出,用可选链可以大量简化类似繁琐的前置校验操作,而且更安全。

3.空值合并运算符

当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。比如下面代码中查询玩家等级。

1
var level = (user.data && user.data.level) || '暂无等级'

在 JS 中,空字符串、0 等,当进行逻辑操作符判断时,会自动转化为 false。在上面的代码里,如果玩家等级本身就是 0 级, 变量 level 就会被赋值 暂无等级 字符串,这是逻辑错误。

1
2
3
4
5
6
7
8
var level;
if (typeof user.level === 'number') {
level = user.level;
} else if (!user.level) {
level = '暂无等级';
} else {
level = user.level;
}

来看看用空值合并运算符如何处理

1
2
3
4
5
// {
// "level": 0
// }
var level = `${user.level}级` ?? '暂无等级'
// level -> '0级'

用空值合并运算在逻辑正确的前提下,代码更加简洁。

空值合并运算符可选链 相结合,可以很轻松处理多级查询并赋予默认值问题。

1
var level = user.data?.level ?? '暂无等级'

4.dynamic-import

按需 import 提案几年前就已提出,如今终于能进入 ES 正式规范。这里个人理解成 “按需“ 更为贴切。现代前端打包资源越来越大,打包成几 M 的 JS 资源已成常态,而往往前端应用初始化时根本不需要全量加载逻辑资源,为了首屏渲染速度更快,很多时候都是按需加载,比如懒加载图片等。而这些按需执行逻辑资源都体现在某一个事件回调中去加载。

1
2
3
4
5
6
7
8
9
el.onclick = () => {
import(`/path/current-logic.js`)
.then((module) => {
module.doSomthing()
})
.catch((err) => {
// load error;
})
}

当然,webpack 目前已很好的支持了该特性。

5.globalThis

JavaScript 在不同的环境获取全局对象有不同的方式,NodeJS 中通过 global, Web 中通过 window, self 等,有些甚至通过 this 获取,但通过 this 是及其危险的,this 在 JavaScript 中异常复杂,它严重依赖当前的执行上下文,这些无疑增加了获取全局对象的复杂性。

过去获取全局对象,可通过一个全局函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var getGlobal = function () {
if (typeof self !== 'undefined') {
return self
}
if (typeofwindow !== 'undefined') {
returnwindow
}
if (typeof global !== 'undefined') {
return global
}
thrownewError('unable to locate global object')
}

var globals = getGlobal()

// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis

globalThis 目的就是提供一种标准化方式访问全局对象,有了 globalThis后,你可以在任意上下文,任意时刻都能获取到全局对象。

6.BigInt

JavaScript 中 Number 类型只能安全的表示-(2^53-1)2^53-1 范的值,即 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER,超出这个范围的整数计算或者表示会丢失精度。

1
2
3
4
5
6
7
8
9
var num = Number.MAX_SAFE_INTEGER // -> 9007199254740991

num = num + 1 // -> 9007199254740992

// 再次加 +1 后无法正常运算
num = num + 1 // -> 9007199254740992

// 两个不同的值,却返回了true
9007199254740992 === 9007199254740993 // -> true

为解决此问题,ES2020 提供一种新的数据类型:BigInt。使用 BigInt 有两种方式:

  1. 在整数字面量后面加n
1
var bigIntNum = 9007199254740993n
  1. 使用 BigInt 函数。
1
2
var bigIntNum = BigInt(9007199254740)
var anOtherBigIntNum = BigInt('9007199254740993')

通过 BigInt, 我们可以安全的进行大数整型计算。

1
2
var bigNumRet = 9007199254740993n + 9007199254740993n // -> -> 18014398509481986n
bigNumRet.toString() // -> '18014398509481986'

注意:

  1. BigInt 是一种新的数据原始(primitive)类型。
1
typeof 9007199254740993n // -> 'bigint'
  1. 尽可能避免通过调用函数 BigInt 方式来实例化超大整型。因为参数的字面量实际也是 Number 类型的一次实例化,超出安全范围的数字,可能会引起精度丢失。

7.String.prototype.matchAll

The matchAll() method returns an iterator of all results matching a string against a regular expression, including capturing groups. ——MDN

思考下面代码:

1
2
3
4
5
var str = '<text>JS</text><text>正则</text>'
var reg = /<\w+>(.*?)<\/\w+>/g

console.log(str.match(reg))
// -> ["<text>JS</text>", "<text>正则</text>"]

可以看出返回的数组里包含了父匹配项,但未匹配到子项(group)。移除全局搜索符”g“试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var str = '<text>JS</text><text>正则</text>'
// 注意这里没有全局搜素标示符"g"
var reg = /<\w+>(.*?)<\/\w+>/
console.log(str.match(reg))

// 上面会打印出
/*
[
"<text>JS</text>",
"JS",
index: 0,
input:
"<text>JS</text><text>正则</text>",
groups: undefined
]
*/

这样可以获取到匹配的父项,包括子项(group),但只能获取到第一个满足的匹配字符。能看出上面无法匹配到<text>正则</text>

如果获取到全局所有匹配项,包括子项呢?

ES2020 提供了一种简易的方式:String.prototype.matchAll, 该方法会返回一个迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var str = '<text>JS</text><text>正则</text>'
var allMatchs = str.matchAll(/<\w+>(.*?)<\/\w+>/g)

for (const match of allMatchs) {
console.log(match)
}

/*
第一次迭代返回:
[
"<text>JS</text>",
"JS",
index: 0,
input: "<text>JS</text><text>正则</text>",
groups: undefined
]
第二次迭代返回:
[
"<text>正则</text>",
"正则",
index: 15,
input: "<text>JS</text><text>正则</text>",
groups: undefined
]
*/

能看出每次迭代中可获取所有的匹配,以及本次匹配的成功的一些其他元信息。

ES12

1. replaceAll

返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉

1
2
const str = 'hello world';
str.replaceAll('l', ''); // "heo word"

2. Promise.any

Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise

1
2
3
4
5
6
7
8
9
10
const promise1 = new Promise((resolve, reject) => reject('我是失败的Promise_1'));
const promise2 = new Promise((resolve, reject) => reject('我是失败的Promise_2'));
const promiseList = [promise1, promise2];
Promise.any(promiseList)
.then(values=>{
console.log(values);
})
.catch(e=>{
console.log(e);
});

img

3. WeakRefs

使用WeakRefs的Class类创建对对象的弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)

4. 逻辑运算符和赋值表达式

逻辑运算符和赋值表达式,新特性结合了逻辑运算符(&&,||,??)和赋值表达式而JavaScript已存在的 复合赋值运算符有:

1
2
3
4
5
6
7
8
9
10
11
a ||= b
//等价于
a = a || (a = b)

a &&= b
//等价于
a = a && (a = b)

a ??= b
//等价于
a = a ?? (a = b)

5. 数字分隔符

数字分隔符,可以在数字之间创建可视化分隔符,通过_下划线来分割数字,使数字更具可读性

1
2
3
4
5
const money = 1_000_000_000;
//等价于
const money = 1000000000;

1_000_000_000 === 1000000000; // true

img