简介
JavaScript
是世界上发展最快的编程语言之一,不仅可以用于编写运行在浏览器的客户端程序,随着Node.js
的发展,JavaScript
也被广泛应用于编写服务端程序。而随着JavaScript
这门语言的不断发展和完善,在2015
年正式发布的ECMAScript6
已经成为了JavaScript
这门语言的下一代标准,使得JavaScript
用来编写复杂的大型应用程序更加的得心应手。近几年几乎所有使用JavaScript
这门语言开发的项目,都在使用ES
的新特性来开发。
随着ES2015
的发布,标准委员会决定在每年都会发布一个ES
的新版本。但很多开发者并没有真正的理解ES2015+
每个版本都具有哪些新特性,以及这些新特性与ES5
语法的差别,更不清楚这些新特性在实际项目中的应用场景是怎么样的。
由于篇幅原因笔者将ES6~ES12
分成了ES进阶之路一和ES进阶之路二两篇文章,如果对ES6
还不是很清楚了可以先看ES6(ES进阶之路一)
我相信只要你们认真看完了笔者的ES
系列文章,你一定会成为ES
大神。
接下来我们来看看ES7-ES12
吧。篇幅有点长,建议收藏后再看,后期也可以当做字典查阅。
ECMAScript2016(ES7)
Array扩展
Array.prototype.includes()
在 ES7
之前想判断数组中是否包含一个元素,基本可以这样写:
console.log(array1.find(function(item) {
return item === 2
}))
或者
console.log(array1.findIndex(function(item) {
return item === 2
}))
或者
console.log(array1.indexOf(2) > -1)
或者
console.log(array1.filter(function(item) {
return item === 2
}).length > 0)
ES7
引入的Array.prototype.includes()
方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true
,否则返回false
。
const arr = ['es6', 'es7', 'es8']
console.log(arr.includes('es6')) // true
console.log(arr.includes('es9')) // false
includes
方法接收俩个参数,要搜索的值和搜索的开始索引。第二个参数可选。从该索引处开始查找 searchElement
。如果为负值,则从末尾第几个开始查找。
const arr = ['es6', 'es7', 'es8']
console.log(arr.includes('es7', 1)) // true
console.log(arr.includes('es7', 2)) // false
console.log(arr.includes('es7', -1)) // false
console.log(arr.includes('es7', -2)) // true
与indexOf()
比较
indexOf
返回的不是boolean
值,而是下标或-1。
['a', 'b', 'c'].includes('a') //true
['a', 'b', 'c'].indexOf('a') > -1 //true
console.log(arr.indexOf('es7')) // 1
console.log(arr.indexOf('es7') > -1) // true
includes
方法只能判断简单类型的数据,对于复杂类型的数据,比如对象类型的数组,二维数组,这些是无法判断的.
const arr = [1, [2, 3], 4]
arr.includes([2, 3]) //false
arr.indexOf([2, 3]) //-1
- 两者都是采用
===
的操作符来作比较的,不同之处在于:对于NaN
的处理结果不同。我们知道js
中NaN === NaN
的结果是false
,indexOf()
也是这样处理的,但是includes()
不是这样的。
const demo = [1, NaN, 2, 3]
demo.indexOf(NaN) //-1
demo.includes(NaN) //true
总结
如果只想知道某个值是否在数组中存在,而并不关心它的索引位置,建议使用includes()
。如果想获取一个值在数组中的位置,那么只能使用indexOf
方法。
幂运算符**
如果不使用任何函数,如何实现一个数的求幂运算?
function pow(x, y) {
let res = 1
for (let i = 0; i < y; i++) {
res *= x
}
return res
}
pow(2, 10) // 1024
除了自己封装函数来实现,也可是使用 Math.pow()
来完成。
Math.pow() 函数返回基数(base)的指数(exponent)次幂。
console.log(Math.pow(2, 10)) // 1024
在 ES7
可以这样写了:
console.log(2 ** 10) // 1024
注意,幂运算符的两个*号之间不能出现空格,否则语法会报错。
ECMAScript2017(ES8)
Async/Await
在ES2017(ES8)
中引入了 async
函数,使得异步操作变得更加方便。Async/Await
的出现,被很多人认为是js
异步操作的最终且最优雅的解决方案。我们可以简单理解Async/Await = Generator + Promise
。
语法
async
用于声明一个 function
是异步的,await
用于等待一个异步方法执行完成,只有当异步完成后才会继续往后执行。await
不是必须的并且await
只能出现在 async
函数中。
async function() {
const result = await getData()
console.log(result)
}
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
这种用书写同步代码的方式处理异步是不是很舒服呢。
错误处理
Async/Await
没有Promise
那么多的api
,错误需要自己使用try catch
处理。
async function() {
try{
const result = await getData()
console.log(result)
} catch(e) {
console.log(e)
}
}
Async/Await和Promise对比
Async/Await
相较于Promise
的链式调用完全用书写同步代码的方式处理异步使代码分厂优雅易懂。Async/Await
这种用书写同步代码的方式使得await
会阻塞后面代码正常运行,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
下面笔者使用Async/Await
和Promise
作为对比举个例子说明。
function getData() {
return Promise.resolve("模拟获取后端数据");
}
async function fun1() {
console.log("主程序开始执行");
const result = await getData();
console.log(result);
console.log("让异步代码自己去执行,不阻塞我们主程序");
}
fun1(); // 主程序开始执行、模拟获取后端数据、让异步代码自己去执行,不阻塞我们主程序
async function fun2() {
console.log("主程序开始执行");
getData().then((result) => {
console.log(result);
});
console.log("让异步代码自己去执行,不阻塞我们主程序");
}
fun2(); // 主程序开始执行、让异步代码自己去执行,不阻塞我们主程序、模拟获取后端数据
从上面的例子我们可以看出使用Async/Await
的弊端,就是不管后面依不依赖异步结果,Async/Await
都一定会阻塞后面代码的执行。
Async/Await和Generator对比
Async/Await
内置执行器。Generator
函数的执行必须靠执行器(如co
函数库),而Async/Await
函数自带执行器。也就是说,Async/Await 函数的执行,与普通函数一模一样。Async/Await
更好的语义。async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
Object 扩展
之前的语法如何获取对象的每一个属性值
const obj = {
name: 'randy',
age: 24
}
console.log(Object.keys(obj)) // ['name', 'age']
const res = Object.keys(obj).map(key => obj[key])
console.log(res)// ["randy", 24]
ES8
中对象扩展补充了两个静态方法,用于遍历对象:Object.values(),Object.entries()
Object.values()
Object.values()
返回一个数组,其元素是在对象上找到的可枚举属性值。属性的顺序与通过手动循环对象的属性值所给出的顺序相同(for...in
,但是for...in
还会遍历原型上的属性值)。
const obj = {
name: 'randy',
age: 24
}
console.log(Object.values(obj)) // ["randy", 24]
Object.entries()
Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in
循环遍历该对象时返回的顺序一致。(区别在于 for-in
循环也枚举原型链中的属性)
const obj = {
name: 'randy',
age: 24
}
for (let [key, value] of obj) {
console.log(key, value) // Uncaught TypeError: obj is not iterable
}
我们知道 Object
是不可直接遍历的,上述代码足以说明直接遍历触发了错误。如果使用 Object.entries()
则可以完成遍历任务。
const obj = {
name: 'randy',
age: 24
}
for (let [k, v] of Object.entries(obj)) {
console.log(k, v)
// name randy
// age 24
}
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors
用来获取对象所有属性的描述符。
想理解 Object.getOwnPropertyDescriptors
这个方法之前,首先要弄懂什么是描述符descriptor
?
const data = {
Portland: '78/50',
Dublin: '88/52',
Lima: '58/40'
}
还是上述那个对象,这里有 key
和 value
,上边的代码把所有的 key、value
遍历出来,如果我们不想让 Lima
这个属性和值被枚举怎么办?
Object.defineProperty(data, 'Lima', {
enumerable: false
})
Object.entries(data).map(([city, temp]) => {
console.log( `City: ${city.padEnd(16)} Weather: ${temp}` )
// City: Portland Weather: 78/50
// City: Dublin Weather: 88/52
})
很成功,Lima
没有被遍历出来,那么 defineProperty
的第三个参数就是描述符descriptor
。这个描述符包括几个属性:
- value [属性的值]
- writable [属性的值是否可被改变]
- enumerable [属性的值是否可被枚举]
- configurable [描述符本身是否可被修改,属性是否可被删除]
在ES6
中笔者就已经讲过了,关于Object
更多API
可以看看笔者前面写的JS Object API详解
console.log(Object.getOwnPropertyDescriptor(data, 'Lima'))
// {value: "58/40", writable: true, enumerable: false, configurable: true}
这个是获取对象指定属性的描述符,如果想获取对象的所有属性的描述符:
console.log(Object.getOwnPropertyDescriptors(data))
String 扩展
在 ES8
中 String
新增了两个实例函数 String.prototype.padStart
和 String.prototype.padEnd
,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
String.prototype.padStart()
把指定字符串填充到字符串头部,返回新字符串。
str.padStart(targetLength [, padString])
参数 | 含义 | 必选 |
---|---|---|
targetLength | 目标字符要保持的长度值 | Y |
padString | 如果目标字符的长度不够需要的补白字符,默认为空 | N |
const str = 'randy'
console.log(str.padStart(8, 'x')) // xxxrandy
console.log(str.padEnd(8, 'y')) // randyyyy
console.log(str.padStart(8)) // randy
场景1:日期格式化,希望把当前日期格式化城:yyyy-mm-dd的格式:
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = (now.getDate()).toString().padStart(2, '0')
console.log(year, month, day)
console.log( `${year}-${month}-${day}` )
场景2:数字替换
// 数字替换,比如手机号,身份证号
const tel = '13012345678'
const newTel = tel.slice(-4).padStart(tel.length, '*')
console.log(newTel) // *******5678
String.prototype.padEnd()
方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。
str.padEnd(targetLength [, padString])
参数 | 含义 | 必选 |
---|---|---|
targetLength | 目标字符要保持的长度值 | Y |
padString | 如果目标字符的长度不够需要的补白字符,默认为空 | N |
const str1 = 'I am learning es'
console.log(str1.padEnd(20, 'o'))
// I am learning esoooo
const str2 = 'randy'
console.log(str2.padEnd(10))
// "randy "
场景:时间戳统一长度
在JS前端我们处理时间戳的时候单位都是ms毫秒,但是,后端同学返回的时间戳则不一样是毫秒,可能只有10位,以s秒为单位。所以,我们在前端处理这个时间戳的时候,保险起见,要先做一个13位的补全,保证单位是毫秒。
// 伪代码
console.log(new Date().getTime()) // 时间戳 13位的
timestamp = +String(timestamp).padEnd(13, '0')
尾逗号 Trailing commas
ES8
允许函数的最后一个参数有尾逗号(Trailing comma
)。
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。
function clownsEverywhere(
param1,
param2
) {
/* ... */
}
clownsEverywhere(
'foo',
'bar'
)
上面代码中,如果在param2
或bar
后面加一个逗号,就会报错。
如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
function clownsEverywhere(
param1,
param2,
) {
/* ... */
}
clownsEverywhere(
'foo',
'bar',
)
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
ECMAScript2018(ES9)
for await of
异步迭代器(for-await-of
):循环等待每个Promise
对象变为resolved
状态才进入下一步。
我们知道 for...of
是同步运行的,有时候一些任务集合是异步的,那这种遍历怎么办呢?
function Gen(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(time)
}, time)
})
}
function test1() {
let arr = [3, 2, 1];
for (let item of arr) {
console.log(Date.now());
const res = Gen(1000 * item);
console.log(res);
}
}
test()
//1675914446701
//Promise {<pending>}
//1675914446701
//Promise {<pending>}
//1675914446701
//Promise {<pending>}
这里写了几个小任务,分别是 2000ms 、100ms、3000ms后任务结束。在上述遍历的过程中可以看到三个任务是同步启动的,然后输出上也不是按任务的执行顺序输出的,这显然不太符合我们的要求。
聪明的同学一定能想起来 await
的作用,它可以中断程序的执行直到这个 Promise
对象的状态发生改变,我们修改上面的代码:
function Gen(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(time)
}, time)
})
}
async function test1() {
let arr = [3, 2, 1];
for (let item of arr) {
console.log(Date.now()); // 这个会先打印
const res = await Gen(1000 * item);
console.log(res); // res会等异步有了结果再打印
}
}
test()
//1675914546326
//3000
//1675914549343
//2000
//1675914551351
//1000
从返回值看确实是按照任务的先后顺序进行的,其中原理也有说明是利用了 await
中断程序的功能。但是你会发现,时间会先打印,然后等待异步执行,再输出异步结果。
在 ES9
中也可以用 for...await...of
的语法来操作:
function Gen(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(time)
}, time)
})
}
async function test() {
let arr = [Gen(2000), Gen(100), Gen(3000)]
for await (let item of arr) {
console.log(Date.now()); // 这个和res是一起打印的
const res = item;
console.log(res);
}
}
test()
// 1560092345730 2000
// 1560092345730 100
// 1560092346336 3000
从这个结果来看和第二种写法效果差不多,但是工作原理却完全不同,重点观察下输出的时间(Chrome Console), 第二种写法是代码块中有 await 导致等待 Promise 的状态而不再继续执行;第三种写法是整个代码块都不执行,等待 arr 当前的值(Promise状态)发生变化之后,才执行代码块的内容。
回想我们之前给数据结构自定义遍历器是同步的,如果想定义适合 for...await...of
的异步遍历器该怎么做呢?答案是 Symbol.asyncIterator
。
let obj = {
count: 0,
Gen (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve({ done: false, value: time })
}, time)
})
},
[Symbol.asyncIterator] () {
let self = this
return {
next () {
self.count++
if (self.count < 4) {
return self.Gen(Math.random() * 1000)
} else {
return Promise.resolve({
done: true,
value: ''
})
}
}
}
}
}
async function test () {
for await (let item of obj) {
console.log(Date.now(), item)
}
}
// 1560093560200 649.3946561938179
// 1560093560828 624.6310222512955
// 1560093561733 901.9497480464518
Object Rest & Spread
前面在讲 function
的 Rest & Spread
方法,忘记的同学可以去复习下。
在 ES9
新增 Object
的 Rest & Spread
方法,直接看下示例:
const input = {
a: 1,
b: 2
}
const output = {
...input,
c: 3
}
console.log(output) // {a: 1, b: 2, c: 3}
这块代码展示了 spread
语法,可以把 input
对象的数据都拓展到 output
对象,这个功能很实用。
我们再来看下 Object rest
的示例:
const input = {
a: 1,
b: 2,
c: 3
}
let { a, ...rest } = input
console.log(a, rest) // 1 {b: 2, c: 3}
当对象 key-value
不确定的时候,把必选的 key
赋值给变量,用一个变量收敛其他可选的 key
数据,这在之前是做不到的。
Promise扩展
Promise.prototype.finally()
指定不管最后状态如何都会执行的回调函数。
Promise.prototype.finally()
方法返回一个Promise
,在promise
执行结束时,无论结果是fulfilled
或者是rejected
,在执行then()
和catch()
后,都会执行finally
指定的回调函数。这为指定执行完promise
后,无论结果是fulfilled
还是rejected
都需要执行的代码提供了一种方式,避免同样的语句需要在then()
和catch()
中各写一次的情况。
基本语法
p.finally(onFinally)
示例
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
// reject('fail')
}, 1000)
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
}).finally(() => {
console.log('finally')
})
比如我们常用的场景:loading关闭
需要每次发送请求,都会有loading
提示,请求发送完毕,就需要关闭loading
提示框,不然界面就无法被点击。不管请求成功或是失败,这个loading
都需要关闭掉,这时把关闭loading
的代码写在finally
里再合适不过了。
ECMAScript2019(ES10)
Object扩展
Object.fromEntries()
方法 Object.fromEntries()
把键值对列表转换为一个对象,这个方法是和 Object.entries()
相对的。
Object.fromEntries([
['foo', 1],
['bar', 2]
])
// {foo: 1, bar: 2}
案例1:Object
转换操作
const obj = {
name: 'randy',
course: 'es'
}
const entries = Object.entries(obj)
console.log(entries)
// [['name', 'randy'], ['course', 'es']]
// ES10
const fromEntries = Object.fromEntries(entries)
console.log(fromEntries)
// {name: "randy", course: "es"}
案例2:Map
转 Object
const map = new Map()
map.set('name', 'randy')
map.set('course', 'es')
console.log(map)
const obj = Object.fromEntries(map)
console.log(obj)
// {name: "randy", course: "es"}
案例3:过滤
course
表示所有课程,想请求课程分数大于80的课程组成的对象:
const course = {
math: 80,
english: 85,
chinese: 90
}
const res = Object.entries(course).filter(([key, val]) => val > 80)
console.log(res)
console.log(Object.fromEntries(res)
String 扩展
String.prototype.trimStart()
trimStart()
方法从字符串的开头删除空格,trimLeft()
是此方法的别名。
语法
str.trimStart() 或 str.trimLeft()
注意,虽然 trimLeft
是 trimStart
的别名,但是你会发现 String.prototype.trimLeft.name === 'trimStart'
,一定要记住
示例
let str = ' foo '
console.log(str.length) // 8
str = str.trimStart()
console.log(str.length) // 5
String.prototype.trimEnd()
trimEnd()
方法从一个字符串的右端移除空白字符,trimRight
是 trimEnd
的别名。
语法
str.trimEnd() 或 str.trimRight()
注意,虽然 trimRight
是 trimEnd
的别名,但是你会发现 String.prototype.trimRight.name === 'trimEnd'
,一定要记住
示例
let str = ' foo '
console.log(str.length) // 8
str = str.trimEnd()
console.log(str.length) // 6
Array 扩展
Array.prototype.flat()
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
const newArray = arr.flat(depth)
解释
参数 | 含义 | 必选 |
---|---|---|
depth | 指定要提取嵌套数组的结构深度,默认值为 1 | N |
示例
const numbers = [1, 2, [3, 4, [5, 6]]]
console.log(numbers.flat())
// [1, 2, 3, 4, [5, 6]]
此时 flat
的参数没有设置,取默认值 1,也就是说只扁平化向下一级,遇到 [3, 4, [5, 6]]
这个数组会扁平会处理,不会再继续遍历内部的元素是否还有数组
const numbers = [1, 2, [3, 4, [5, 6]]]
console.log(numbers.flat(2))
// [1, 2, 3, 4, 5, 6]
当 flat
的参数大于等于 2,返回值就是 [1, 2, 3, 4, 5, 6]
了。
Array.prototype.flatMap()
flatMap()
方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。从方法的名字上也可以看出来它包含两部分功能一个是 map
,一个是 flat
(深度为1)。
语法
const new_array = arr.flatMap(function callback(currentValue[, index[, array]]) {// 返回新数组的元素 }[, thisArg])
解释
参数 | 含义 | 必选 |
---|---|---|
callback | 可以生成一个新数组中的元素的函数,可以传入三个参数:currentValue、index、array | Y |
thisArg | 遍历函数 this 的指向 | N |
示例
const numbers = [1, 2, 3]
numbers.map(x => [x * 2]) // [[2], [4], [6]]
numbers.flatMap(x => [x * 2]) // [2, 4, 6]
这个示例可以简单对比下 map 和 flatMap 的区别。当然还可以看下下面的示例:
let arr = ['今天天气不错', '', '早上好']
arr.map(s => s.split(''))
// [["今", "天", "天", "气", "不", "错"],[""],["早", "上", "好"]]
arr.flatMap(s => s.split(''))
// ["今", "天", "天", "气", "不", "错", "", "早", "上", "好"]
Function扩展
Function.prototype.toString()
函数是对象,并且每个对象都有一个 toString()
方法,因为它最初存在于Object.prototype.toString()
上。所有对象(包括函数)都是通过基于原型的类继承从它继承的。这意味着我们以前已经有 funcion.toString()
方法了。
Function.prototype.toString()
方法返回一个表示当前函数源代码的字符串。
这意味着还将返回注释、空格和语法详细信息。
function foo() {
// es10新特性
console.log('randy')
}
console.log(foo.toString())
// 直接在方法名toString()
console.log(Number.parseInt.toString())
可选的Catch Binding
在 ES10
之前我们都是这样捕获异常的:
try {
// tryCode
} catch (err) {
// catchCode
}
在这里 err
是必须的参数,在 ES10
可以省略这个参数
try {
console.log('Foobar')
} catch {
console.error('Bar')
}
案例:验证参数是否为json格式
这个需求我们只需要返回true
或false
,并不关心catch
的错误。
const validJSON = json => {
try {
JSON.parse(json)
return true
} catch {
return false
}
}
const json = '{"name":"randy", "course": "es"}'
console.log(validJSON(json))
JSON扩展
JSON.stringify() 增强能力
JSON.stringify
在 ES10
修复了对于一些超出范围的 Unicode
展示错误的问题。因为 JSON
都是被编码成 UTF-8
,所以遇到 0xD800–0xDFFF
之内的字符会因为无法编码成 UTF-8
进而导致显示错误。在 ES10
它会用转义字符的方式来处理这部分字符而非编码的方式,这样就会正常显示了。
// \uD83D\uDE0E emoji 多字节的一个字符
console.log(JSON.stringify('\uD83D\uDE0E')) // 笑脸
// 如果我们只去其中的一部分 \uD83D 这其实是个无效的字符串
// 之前的版本 ,这些字符将替换为特殊字符,而现在将未配对的代理代码点表示为JSON转义序列
console.log(JSON.stringify('\uD83D')) // "\ud83d"
Symbol扩展
Symbol.prototype.description
我们知道,Symbol
的描述只被存储在内部的 Description
,没有直接对外暴露,我们只有调用 Symbol
的 toString()
时才可以读取这个属性:
const name = Symbol('es')
console.log(name.toString()) // Symbol(es)
console.log(name) // Symbol(es)
console.log(name === 'Symbol(es)') // false
console.log(name.toString() === 'Symbol(es)') // true
现在可以通过 description
方法获取 Symbol
的描述:
const name = Symbol('es')
console.log(name.description) // es
console.log(name.description === 'es') // true
ECMAScript2020(ES11)
String扩展
String.prototype.matchAll()
matchAll()
方法返回一个包含所有匹配正则表达式及分组捕获结果的迭代器
语法
str.matchAll(regexp)
解释
参数 | 含义 | 必选 |
---|---|---|
regexp | 正则表达式对象 | Y |
注意
- 如果所传参数不是一个正则表达式对象,则会隐式地使用
new RegExp(obj)
将其转换为一个RegExp
- 返回值一个迭代器,但是不可重用,结果耗尽需要再次调用方法,获取一个新的迭代器
示例
const str = `
<html>
<body>
<div>第一个div</div>
<p>这是一个p</p>
<span>span</span>
<div>第二个div</div>
<body>
</html>
`
请找出所有的div元素。
function selectDiv(regExp, str) {
let matches = [];
for (let match of str.matchAll(regExp)) {
console.log(match);
matches.push(match[1]);
}
return matches;
}
const res = selectDiv(/<div>(.*)<\/div>/g, str);
console.log(res); // ['第一个div', '第二个div']
Dynamic Import
按需 import
提案几年前就已提出,如今终于能进入ES
正式规范。这里个人理解成“按需”更为贴切。现代前端打包资源越来越大,打包成几M的JS资源已成常态,而往往前端应用初始化时根本不需要全量加载逻辑资源,为了首屏渲染速度更快,很多时候都是按需加载,比如懒加载图片等。而这些按需执行逻辑资源都体现在某一个事件回调中去加载。
页面上有一个按钮,点击按钮才去加载ajax
模块。
const oBtn = document.querySelector('#btn')
oBtn.addEventListener('click', () => {
import('./ajax').then(mod => {
// console.log(mod)
mod.default('static/a.json', res => {
console.log(res)
})
})
})
当然,webpack
目前已很好的支持了该特性。
BigInt
在 ES10
增加了新的原始数据类型:BigInt
,表示一个任意精度的整数,可以表示超长数据,可以超出2的53次方。
Js 中 Number类型只能安全的表示-(2^53-1)至 2^53-1 范的值
console.log(2 ** 53) // es7 幂运算符
console.log(Number.MAX_SAFE_INTEGER) // 最大值-1
使用 BigInt 有两种方式:
方式一:数字后面增加n
const bigInt = 9007199254740993n
console.log(bigInt)
console.log(typeof bigInt) // bigint
console.log(1n == 1) // true
console.log(1n === 1) // false
方式二:使用 BigInt 函数
const bigIntNum = BigInt(9007199254740993)
console.log(bigIntNum)
Promise扩展
Promise.allSettled()
学习了ES
新特性,我们都知道 Promise.all()
具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常reject
,所有任务都会挂掉,Promise
直接进入 reject
状态。
Promise.allSettled()
方法就是不管是否成功失败,都会返回结果并进入then
方法。
场景:现在页面上有三个请求,分别请求不同的数据,如果一个接口服务异常,整个都是失败的,都无法渲染出数据
Promise.all([
Promise.reject({
code: 500,
msg: '服务异常'
}),
Promise.resolve({
code: 200,
data: ['1', '2', '3']
}),
Promise.resolve({
code: 200,
data: ['4', '5', '6']
})
]).then(res => {
console.log(res)
console.log('成功')
}).catch(err => {
console.log(err)
console.log('失败')
})
我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态
Promise.allSettled([
Promise.reject({
code: 500,
msg: '服务异常'
}),
Promise.resolve({
code: 200,
data: ['1', '2', '3']
}),
Promise.resolve({
code: 200,
data: ['4', '5', '6']
})
]).then(res => {
console.log(res) // [{…}, {…}, {…}]
const data = res.filter(item => item.status === 'fulfilled')
console.log(data) // [{…}, {…}]
}).catch(err => {
console.log(err)
console.log('失败')
})
globalThis
Javascript
在不同的环境获取全局对象有不通的方式:
- node 中通过 global
- web 中通过 window, self 等.
self:打开任何一个网页,浏览器会首先创建一个窗口,这个窗口就是一个window对象,也是js运行所依附的全局环境对象和全局作用域对象。self 指窗口本身,它返回的对象跟window对象是一模一样的。也正因为如此,window对象的常用方法和函数都可以用self代替window。
self.setTimeout(() => {
console.log(123)
}, 1000)
以前想要获取全局对象,可通过一个全局函数
const getGlobal = () => {
if (typeof self !== 'undefined') {
return self
}
if (typeof window !== 'undefined') {
return window
}
if (typeof global !== 'undefined') {
return global
}
throw new Error('无法找到全局对象')
}
const globals = getGlobal()
console.log(globals)
globalThis
提供了一个标准的方式来获取不同环境下的全局 this
对象(也就是全局对象自身)。不像 window
或者 self
这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis
,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的 this
就是 globalThis
。
console.log(globalThis)
可选链 Optional chaining
可让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。
const user = {
address: {
street: 'xx街道',
getNum() {
return '80号'
}
}
}
在之前的语法中,想获取到深层属性或方法,不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property...
这种错误,这极有可能让你整个应用挂掉。
const street = user && user.address && user.address.street
const num = user && user.address && user.address.getNum && user.address.getNum()
console.log(street, num)
用了 Optional Chaining
,上面代码会变成
const street2 = user?.address?.street
const num2 = user?.address?.getNum?.()
console.log(street2, num2)
可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段。根据上面可以看出,用可选链可以大量简化类似繁琐的前置校验操作,而且更安全。
空值合并运算符(Nullish coalescing Operator)
空值合并运算符 ??
是一个逻辑运算符。当左侧操作数为 null
或 undefined
时,其返回右侧的操作数。否则返回左侧的操作数。
当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。
const b = 0
const a = b || 5
console.log(a) // 5
上面的例子并不符合我们的需求,因为它把0也当做无效值了。
空值合并运算符 ??
我们仅在第一项为 null
或 undefined
时设置默认值
const b = 0
const a = b || 5
console.log(a) // 0
ECMAScript2021(ES12)
String扩展
String.prototype.replaceAll()
replace(pattern, 'xxx')
和replaceAll(pattern, 'xxx')
都是用来替换字符串中某些字符。pattern
可以是一个字符串或一个正则表达式,而替换项可以是一个字符串或一个应用于每个匹配项的函数。
既然有了replace
,那为什么还要有replaceAll
呢?这两者有什么区别呢?
当pattern
是正则的时候没有区别
const str1 = "hello word";
console.log(str1.replace(/o/g, "哦")); // hell哦 w哦rd
console.log(str1.replaceAll(/o/g, "哦")); // hell哦 w哦rd
但是当pattern
是字符串的时候是有区别的。replace()
方法仅替换一个字符串中某模式 pattern
的首个实例,replaceAll()
会返回一个新字符串,该字符串中用一个替换项替换了原字符串中所有匹配了某模式的部分。replaceAll()
相当于增强了 replace()
的特性,全量替换。
const str1 = "hello word";
console.log(str1.replace("o", "哦")); // hell哦 word
console.log(str1.replaceAll("o", "哦")); // hell哦 w哦rd
Promise扩展
Promise.any()
Promise.any()
和Promise.race()
类似都是返回第一个结果,但是Promise.any()
只返回第一个成功的,尽管某个 promise
的 reject
早于另一个 promise
的 resolve
,仍将返回那个首先 resolve 的 promise。
如果都被reject
则会抛出All promises were rejected
错误。
Promise.any([
Promise.reject("1"),
Promise.resolve("2"),
Promise.resolve("3"),
])
.then((res) => console.log(res)) // 2
.catch((err) => console.error(err));
Promise.any([
Promise.resolve("1"),
Promise.resolve("2"),
Promise.resolve("3"),
])
.then((res) => console.log(res)) // 1
.catch((err) => console.error(err));
Promise.any([
Promise.reject("错误 1"),
Promise.reject("错误 2"),
Promise.reject("错误 3"),
])
.then((res) => console.log(res))
.catch((err) => console.error(err));
// AggregateError: All promises were rejected
逻辑操作符和赋值表达
&&=
&&=
可以理解为有值再赋值的意思
let num1 = 1;
let num2 = 2;
num1 &&= num2;
console.log(num1); // 2
let num1 = 0;
let num2 = 2;
num1 &&= num2;
console.log(num1); // 0
等价于
num1 && (num1 = num2);
或者
if (num1) {
num1 = num2;
}
||=
||=
可以理解为没值再赋值的意思
let num3 = 3;
let num4 = 4;
num3 ||= num4;
console.log(num1); // 3
let num3 = 0;
let num4 = 4;
num3 ||= num4;
console.log(num3); // 4
等价于
num3 || (num3 = num4);
或
if (!num3) {
num3 = num4;
}
??=
??=
跟我们前面说的空值合并运算符 ??
类似,只有在左边的值严格等于 null
或 undefined
时起作用进行赋值。
let num5 = 0;
let num6 = null;
let num7 = undefined;
num5 ??= 10;
console.log(num5); // 0
num6 ??= 10;
console.log(num6); // 10
num7 ??= 10;
console.log(num7); // 10
等价于
num ?? (num = 10);
或
if(num === null || num === undefined) {
num = 10
}
数值分隔符
我们定义number
类型数据的时候,可以使用_
当做分隔符,让数据更美观易懂,并不会影响该数据的值。
const num1 = 1000000000;
const num2 = 1000_000_000;
console.log(num1); // 1000000000
console.log(num2); // 1000000000
WeakRef
WeakRef
允许创建对象的弱引用。弱引用笔者在前面讲weakSet、weakMap
的时候说过了,就是在进行垃圾回收的时候不会考虑该对象是否还在WeakRef
中使用。
我们必须用 new
关键字创建新的 WeakRef
,然后使用deref()
读取引用的对象。
let weakref = new WeakRef({name: 'randy', age: 27})
weakref.deref() // {name: 'randy', age: 27}
weakref.deref().age // 27
ECMAScript2022(ES13)
class扩展
类成员声明
在ES13
之前,我们只能在构造函数里面声明类的成员,而不能像其他大多数语言一样在类的最外层作用域里面声明成员。不过ES13
出来之后,这都不算什么事儿了。现在我们终于可以突破这个限制,写下面这样的代码了:
class Car {
color = 'blue';
age = 2;
}
const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2
私有属性和私有方法
ES13
之前,我们是不可能给类定义私有成员的。所以一些程序员为了表示某个成员变量是一个私有属性,会给该属性名添加一个下划线(_
)作为后缀。可是这只能作为程序员之间的君子之约来使用,因为这些变量其实还是可以被外界访问到的。不过在ES13
中,我们只需要给我们的属性/方法添加一个hashtag(#)
前缀,这个属性/方法就变成私有的了。当我们的属性变为私有后,任何外界对其的访问都会出错哦。
class Person {
#firstName = 'randy';
#lastName = 'su';
#say() {
console.log('say hello')
}
get name() {
this.#say();
return `${this.#firstName} ${this.#lastName}`;
}
}
const person = new Person();
console.log(person.name); // say hello randy su
// 下面都会报错
// SyntaxError: Private field '#firstName' must be
// declared in an enclosing class
console.log(person.#firstName);
console.log(person.#lastName);
console.log(person.#say);
静态私有属性和私有方法
跟私有属性和方法一样,我们只需要给我们的静态属性/方法添加一个hashtag(#)
前缀,这个静态属性/方法就变成私有的了。只能在类内部访问啦。
类的静态方法可以使用this
关键字访问其他的私有或者公有静态成员,而在类的实例方法中则可以通过this.constructor
来访问这些静态属性.
class Person {
static #count = 0;
static getCount() {
return this.#count;
}
constructor() {
this.constructor.#incrementCount();
}
static #incrementCount() {
this.#count++;
}
}
const person1 = new Person();
const person2 = new Person();
console.log(Person.getCount()); // 2
// 下面都会报错
console.log(Person.#count);
console.log(Person.#incrementCount);
判断是否有私有变量
前面我们说了,可以定义私有属性和方法,但是在外部是没办法直接访问的,那么我们怎么知道某对象是否具有某私有属性呢?
在ES13
中,我们可以通过in
操作符来判断对象是否具有某私有属性。
class Car {
#color;
hasColor() {
return #color in this;
}
}
const car = new Car();
console.log(car.hasColor()); // true
支持定义静态代码块
ES13
允许在类中通过static
关键字定义一系列静态代码块,这些代码块只会在类被创造的时候执行一次。
一个类可以定义任意多的静态代码块,这些代码块会和穿插在它们之间的静态成员变量一起按照定义的顺序在类初始化的时候执行一次。我们还可以使用super
关键字来访问父类的属性。
class Vehicle {
static defaultColor = 'blue';
}
class Car extends Vehicle {
static colors = [];
static {
this.colors.push(super.defaultColor, 'red');
}
static {
this.colors.push('green');
}
console.log(Car.colors); ['blue', 'red', 'green']
}
Async Await扩展
支持在最外层写await
在ES13
之前,我们的await
必须写在async
方法里面,否则会报错。但是ES13
允许直接在最外层写await
,是不是很爽呢?
function setTimeoutAsync(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
})
}
await setTimeoutAsync(3000);
Array和String扩展
at函数
我们可以使用at
函数来索引元素,支持数组和字符串。
const arr = ['a', 'b', 'c', 'd'];
// 第一个元素
console.log(arr.at(0)); // a
// 倒数第一个元素
console.log(arr.at(-1)); // d
// 倒数第二个元素
console.log(arr.at(-2)); // c
const str = 'randy';
// 第一个元素
console.log(str.at(0)); // r
// 倒数第一个元素
console.log(str.at(-1)); // y
// 倒数第二个元素
console.log(str.at(-2)); // d
注意传正数从前往后找,下标从0
开始。传负数从后往前找,下标从-1
开始。
RegExp扩展
支持返回开始和结束索引
简单来说这个新属性就是允许我们告诉RegExp
在返回match
对象的时候,给我们返回匹配到的子字符串的开始和结束索引。
ES13
之前,我们只能获取正则表达式匹配到的子字符串的开始索引:
const str = 'sun and moon';
const regex = /and/;
const matchObj = regex.exec(str);
// [ 'and', index: 4, input: 'sun and moon', groups: undefined ]
console.log(matchObj);
ES13
后,我们就可以给正则表达式添加一个d
的标记来让它在匹配的时候给我们既返回匹配到的子字符串的起始位置还返回其结束位置:
const str = 'sun and moon';
const regex = /and/d;
const matchObj = regex.exec(str);
/**
[
'and',
index: 4,
input: 'sun and moon',
groups: undefined,
indices: [ [ 4, 7 ], groups: undefined ]
]
*/
console.log(matchObj);
你看,设置完d
标记后,多了一个indices
的数组,里面就是匹配到的子字符串的范围了!
Object扩展
Object.hasOwn()
Object.hasOwn()
函数接收两个参数,一个是对象,一个是属性,如果这个对象本身就有这个属性的话,这个函数就会返回true
,否则就返回false
。
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
console.log(Object.hasOwn(obj, 'color')); // true
console.log(Object.hasOwn(obj, 'name')); // false
好奇的小伙伴就会问了,不是有hasOwnProperty()
可以判断某对象是否具有某属性吗?为什么还是要出一个这样的方法。
其实原因有两点,
hasOwnProperty()
方法是Object
原型上的方法,所以可以被覆盖,如果覆盖了就达不到我们想要的结果了。- 如果我们创建了一个原型为
null
的对象(Object.create(null)
),也会获取不到该方法而报错。
Error扩展
Error对象的cause属性
ES13
后,Error
对象多了一个cause
属性来指明错误出现的原因。这个属性可以帮助我们为错误添加更多的上下文信息,从而帮助使用者们更好地定位错误。这个属性是我们在创建error
对象时传进去的第二个参数对象的cause
属性:
function userAction() {
try {
apiCallThatCanThrow();
} catch (err) {
throw new Error('New error message', { cause: '请求出错啦' });
}
}
try {
userAction();
} catch (err) {
console.log(err);
console.log(`Cause by: ${err.cause}`); // Cause by: 请求出错啦
}
Array扩展
数组支持倒序查找
在JS
中,我们可以使用数组的find()
函数来在数组中找到第一个满足某个条件的元素。同样地,我们还可以通过findIndex()
函数来返回这个元素的位置。可是,无论是find()
还是findIndex()
,它们都是从数组的头部开始查找元素的,可是在某些情况下,我们可能有从数组后面开始查找某个元素的需要。
ES13
出来后,我们终于有办法处理这种情况了,那就是使用新的findLast()
和findLastIndex()
函数。这两个函数都会从数组的末端开始寻找某个满足条件的元素。
const letters = [
{ value: 'z' },
{ value: 'y' },
{ value: 'x' },
{ value: 'y' },
{ value: 'z' },
];
// 倒序查找
const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');
console.log(found); // { value: 'y' }
console.log(foundIndex); // 3
总结
笔者用一张图来总结ES7-ES12
所有知识点。
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!