一、ES 2016
1、Array.prototype.includes 和 String.prototype.includes
在之前我们比较一个数组中是否包含某个元素的常用做法是:
if (arr.indexOf(el) !== -1) {
...
}
或者
if (~arr.indexOf(el)) {
...
}
//此处~ 为取反,即-1取反结果为0,0取反为-1,1取反会-2,2取反为-3
indexOf 结果返回是个数字,我们必须要和其他数字进行比较,才能见到得到是否"存在",即-1 表示不存在,大于-1 表示存在。
由于 indexOf 在查找元素的比对过程中,使用的是严格相等"===",比如
[1,2,3].indexOf('1') //就会找不到。因为1和'1'并不相等。
同样在当数组中含有 NaN 是,也找不到,即
[NaN].indexOf(NaN) //找不到,因为NaN===NaN为false。
由此可见 indexOf 并不能完美的找出是否包含某个元素值。
所以我们便有了下面的方式
arr.includes(el);
includes 结果直接返回 boolean,true 表示存在,false 表示不存在,非常直观。includes 是通过,类型和值是否“一致”,而不是“严格相等”
[1,2,3].includes('1') //false
[1,2,3].includes(1) //true
[1,2,NaN].includes(NaN) //true
所以再以后的使用中,
indexOf 应倾向于寻找 元素的位置,可能后续还要通过这个位置进行其他计算的场景,而 includes 应倾向于寻找 元素是否存在,只需要知道存在或者不存在,其他不必知道
既然数组有 indexOf,字符串也有 indexOf,那么同样
数组有 includes,字符串也有 includes
"1234567".includes("12") //true
2、指数运算符
在以前,js 要求取 m 的 n 次方,得这么写
Math.pow(m,n)
比如 2 的 3 次方
Math.pow(2,3) //8
而现在只需要 指数操作符"**",即两个星号即可
2 ** 3 //表示2的3次方
3 ** 2 //表示3的2次方
由于- + _ /,都有 -=,+=, _=, /=
同样 也有 "="
let a = 2;
a **= 2; // a = a ** 2
二、ES 2017
1、Object.values / Object.keys() / Object.entries
将 object 的 key 或者 value 转成一个可以遍历的数组。
Object.values({id:1,name:2}); // [1,2]
Object.keys({id:1,name:2}); // ["id", "name"]
Object.entries({id:1,name:2}); // [["id", 1], ["name", 2]]
2、字符串“填充”
- String.prototype.padStart(); 使用给定的字符串,填充原字符串头部,整体达到一定的长度
- String.prototype.padEnd();使用给定的字符串,填充原字符串尾部,整体达到一定的长度
- String.prototype.trimRight();移除尾部的空白(和 trimEnd 一样,为了兼容其他浏览器)
- String.prototype.trimLeft();移除首部的空白(和 trimStart 一样,为了兼容其他浏览器)
- String.prototype.trimEnd(); 移除尾部的空白
- String.prototype.trimStart();移除首部的空白
- String.prototype.trim(); 移除两端的空白
//padStart
"abc".padStart(10); // " abc" 在原字符串"abc"的头部填充空白,以使整个字符串的长度变为10
"abc".padStart(10, "foo"); // "foofoofabc" 在原字符串"abc"的头部填充"foo",以使整个字符串的长度变为10
"abc".padStart(6, "123465"); // "123abc" 在原字符串"abc"的头部填充"123465",以使整个字符串的长度变为10,如果中途够了,就不进行填充了
"abc".padStart(8, "0"); // "00000abc"
"abc".padStart(1); // "abc"
//padEnd
"abc".padEnd(10); // "abc " 同padStart,只不过填充到末尾
"abc".padEnd(10, "foo"); // "abcfoofoof" 同padStart,只不过填充到末尾
"abc".padEnd(6, "123456"); // "abc123" 同padStart,只不过填充到末尾
"abc".padEnd(1); // "abc" 同padStart,只不过填充到末尾
//trimStart
" abc ".trimStart(); // "abc "
" abc ".trimEnd(); // " abc"
" abc ".trim(); // "abc"
3、Object.getOwnPropertyDescriptors
获取对象的所有自身属性的描述符。包括 configurable(可配置),enumerable(可枚举),writable(可写)
Object.getOwnPropertyDescriptors({ id: 1, name: 2 });
/*
{
"id": {
"value": 1,
"writable": true,
"enumerable": true,
"configurable": true
},
"name": {
"value": 2,
"writable": true,
"enumerable": true,
"configurable": true
}
}
*/
4、在函数末尾参数后增加逗号
在之前的函数是这样的
function clownPuppiesEverywhere(param1, param2) {}
//或者这样
function clownPuppiesEverywhere(param1, param2) {}
param2 后面如果加逗号会报错,而现在不会报错,它是被允许的
function clownPuppiesEverywhere(param1, param2) {}
//或者这样
function clownPuppiesEverywhere(param1, param2) {}
这样如果我们有新参数,只需要往后加就行了,不必往上一行末尾加逗号(真懒,加个逗号又不费事,鸡肋特性)
5、async / await
将异步变为同步神器。再也不需要像 promise 那样回调了
async function getList(){ //用async指明当前方法中有异步,但是当做同步执行
await getId(); //如果getId是个promise,则只有状态为resolve时,才进入下一行。如果是普通函数,则默认是resolved
let result = await getName(); //还可以有返回值。其值为resolved(xx)中的参数
}
await 的并行处理
//因为多个await都是依次往后台发送。如果这几个请求之前没有先后关联顺序,则完全没必要。可以做成并行发送
async getName() {
let str = "";
await Promise.all([this.getPromise(),this.getPromise(),this.getPromise()]);
return str;
}
6、共享内存和原子
太复杂,我也没看懂
如果你想查看,这是详细
文档
三、ES 2018
1、正则表达式 点匹配符 s
在正则表达式中,“.” 点,代表 匹配除换行符之外的任何单个字符。
如下:
/.n/.test('an'); //. 匹配到了a
/.n/.test('1n'); //. 匹配到了1
/.n/.test('nn'); //. 匹配到了n
/.n/.test('\nn'); //.不会匹配 \n 返回false
假如有个需求,需要匹配任何字符。那怎么办.....
有个骚操作。
/foo[^]bar/.test('foo\nbar'); //返回true
用 [^] 替换掉. 它表示匹配包含n 在内的任意字符。
可这也太秀了,正常人都想不到这么写。这么写出来,别人也看不懂。
所以便有了下面的写法:
/foo.bar/s.test('foo\nbar'); //返回true
正则的后面加个 s 标志,你可以将 s 理解为 stop,终止的意思,即表示除了其他的,还能匹配终止符 n,换句话说现在这个. 啥都能匹配到
我们可以总结下,到此为止,正则表达式标志符有:
g 全局搜索。
i 不区分大小写搜索。
m 多行搜索。
s 允许 . 匹配换行符。
u 使用unicode码的模式进行匹配。
y 执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标志。
2、扩展运算符
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }
或者
let [x,y,...z] = [ 1,2,3,4 ];
x; // 1
y; // 2
z; // [3, 4]
let n = [ x, y, ...z ];
n; // [1, 2, 3, 4]
...x 始终代表这是个展开式,而 x 代表原式(对象,或者数组),
类似于高数中的泰勒展开式, ...x 就是那个展开式,x 就是那个原式。
比如:
function getName({a,b,...others}){
console.log(others)
}
getName({a:1,b:2,c:3,d:4})
由于...others 是展开式 c:3,d:4,那么 others 自然就是原式{c: 3, d: 4}
3、Promise.prototype.finally
在 promise 结束时,无论结果是 resovled 或者是 rejected,finally 都会执行指定的回调函数。
这避免了同样的语句需要在 then()和 catch()中各写一次的情况。
所以 finally 中最适合需要进行"清空"的操作。比如 request 请求完成或失败后,都要将 loading 设置为 false。或者关闭打开的文件流对象,或者进行日志记录等。
finally()方法和 then()一样,执行后,也返回一个 Promise。
所以一般流程是
new Promise((resolved, rejected)=>{
//...
//return resolved(res); 如果执行成功
//return rejected(); 如果执行失败
//...
}).then(res=>{
//resolved的回调, res的值为resolved传入的参数
}).catch(err=>{
//rejected的回调。err为rejected传入的失败的原因
}).finally(()=>{
//由于不关注resolved还是rejected。所以没有参数。
})
4、for-await-of
我们经常会通过 for....of 进行迭代,如下:
let arr = [1,2,3];
for(let num of arr) {
console.log(num);
}
输出 1,2,3
我们可以想象下,加入 arr 中的每一项不是普通类型的对象,而是 promise 呢?
//分别创建三个promise对象
let p1 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(1)
},1000)
});
let p2 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(2)
},1000)
});
let p3 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(3)
},1000)
});
let arr = [p1,p2,p3];
for(let num of arr) {
console.log(num);
}
那执行结果会怎么样呢?
你将会在控制台直接看到下面三个结果。
> Promise {}
> Promise {}
> Promise {}
也就是说 arr 在迭代时,分别将 p1,p2,p3 输出控制台了,而 p1,p2,p3 是谁,当然是 promise 啊,只不过还没执行 resolved 而已,处于 pending 状态。
如果我们想在 arr 迭代时,依次输出 p1,p2,p3 的 resolved 或者 rejected 结果呢?
我们可以使用下面的方式
//分别创建三个promise对象
let p1 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(1)
},1000)
});
let p2 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(2)
},1000)
});
let p3 = new Promise((resolved,rejected)=>{
setTimeout(()=>{
resolved(3)
},1000)
});
let arr = [p1,p2,p3];
for await(let num of arr) {
console.log(num);
}
在 for 后面,左圆括号前面,加个 await。
这样在每次 arr 迭代时,等待 num 的结果为 resolved 或者 rejected 时,才进入 for 的语句块。
结果输出为: 1 2 3
我们可以想想在没有 for-await-of 之前,是怎么用的?
let num = null;
let arr = [p1,p2,p3];
num = await arr[0];
console.log(num);
let num = await arr[1];
console.log(num);
let num = await arr[2];
console.log(num);
是不是?但是如果 arr 中几个这么写还行,可如果多了呢,显然不如 for await of 方便。
四、ES 2019
1、可选的 catch 参数
try {
throw new Error("出错了")
} catch (err) {
console.log(err.message);
}
我们通常会在 catch 中通过 err 来获取或者打印错误信息。
但是有些场景下我们不需要 catch 中的 err,信息,只是捕获错误,然后做些其他的事。此时 err 参数被闲置,unused,未被使用。
针对这种情况,es 规范允许你省略 err 参数。
try {
throw new Error("出错了")
} catch () {
console.log('321');
}
==Uncaught SyntaxError: Unexpected token ')'==
报错了,纳尼?
注意不是上面这种省略,而应该是下面的省略。
try {
throw new Error("出错了")
} catch{
console.log('321');
}
catch 后面直接跟{即可。
坑我给你们填上了,别在掉进去了。
2、json 超集
由于 json 可以支持 "u2028"(行分隔符)和 "u2029"(段落分隔符)两个特殊字符。
我们可以看看这两个字符长什么样
方方正正的,一个写着 LSEP,一个写着 PSEP.
创建个 json,包含 u2028.
没问题,可以解析。
但是这两个字符之前在 js 中是不能写的。所以 ECMA 在标准中实现了他们,以便在 js 中也能正常使用。
3、Symbol.prototype.description
symbol 是一种基本数据类型。 每个从 Symbol()返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。
我们可以通过下面方式获取 symbol 的描述。
Symbol('desc').description //desc
4、Function.prototype.toString 的修订
以前由于 function 继承了从 object 来的 toString 方法;所以 toString 为"[object Object]"
function getName(){
return "hello";
};
getName.toString() // "[object Object]"
而修订后的 function toString 覆盖了从 object 继承来的 toString 方法。返回一个包含定义函数的源文本段。
5、Object.fromEntries
let obj = {
id: 1,
name: "hello",
};
let entries = Object.entries(obj); // [["id",1],["name","hello"]]
同样我们可以使用 fromEntries 还原 obj
let obj = Object.fromEntries([
["id", 1],
["name", "hello"],
]); // {id: 1, name: "hello"}
6、Array.prototype.flat / Array.prototype.flatMap
具体可参考 mdn 链接
五、ES 2020
1、String.prototype.matchAll
let regexp = /te/;
let str = 'test1test2';
str.match(regexp) // ['te'] 匹配到第一个位置就停止,并返回匹配到的字符串
加个 g 标志
let regexp = /te/g;
let str = 'test1test2';
str.match(regexp) //["te", "te"] 返回所有匹配到的字符串
matchAll 会返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。且正则表达式必须含有标志 g,否则报错
let regexp = /te/g;
let str = 'test1test2';
str.matchAll(regexp) // RegExpStringIterator{} 但会迭代器。
可以通过解构方式取出迭代器。
let regexp = /te/g;
let str = 'test1test2';
let arr = [...str.matchAll(regexp) ] //
arr[0] // {0: "te", groups: undefined, index: 0, input: "test1test2", length: 1}
arr[1] // {0: "te", groups: undefined, index: 5, input: "test1test2", length: 1}
这样我们不仅能获取到 匹配的字符串,还是通过 index 知道匹配的位置。
2、import()
在 js 中,引入模块的方式通常是:
import _ from "lodash"
在代码运行之前,在编译阶段,就已经绑定引入本地作用域。
但是如果我们想在运行阶段动态的引入呢?
答案是没有办法。
如果你使用的是 webpack,则可以使用 webpack 的 require("xxx") 动态的将模块引入。
所以 ECMA 直接在 js 核心库中,实现了这个 api。即使不适用 webpack,也能直接在浏览器上运行 import().
在 chrome 控制台我们可以直接使用。
同时发现,import()返回的是个 promise 对象,
所以我们可以这么用
import("/static/js/app.js").then(res=>{
//xxx
})
或者
await import("/static/js/app.js");
//后续代码
3、BigInt
在 javascript 中,interget 的最大数为 2^53
Math.pow(2,53) // 9007199254740992
如果加 1,发现没变。
Math.pow(2,53)+1 // 9007199254740992
假如要表示 比 Math.pow(2,53) 还要大的数值怎么办?
可以使用 BigInt 类型来表示
const theBiggestInt = 9007199254740991n; //后面追加 n,表示这是个bigInt类型
const alsoHuge = BigInt(9007199254740991); //直接通过构造器创建。
在 bigInt 中,最小的 1,用 1n 来表示,如下面的运算
1n+1n==2n
2n-1n == 1n
2n * 2n == 4n
4n / 2n == 2n
9007199254740992n +1n==9007199254740993n // //2^53加1 结果为
特殊情况
1n+1n==2 //true
0n == 0 //true
//但是下面为false
1n+1n==2 //false
0n === 0 //false
特别的
1n+1 会直接报错
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
3、Promise.allSettled
在 promise 中的组合器有下面几种:
- Promise.all 参数中的所有 promise 完成,才执行回调。
- Promise.race 只要有一个完成,或者一个失败,就马上执行回调。
- Promise.all 只要有个完成,或者所有的失败,才执行回调。
- Promise.allSettled 永远不会中途结束,会全部完成,返回所有给定的 promise 已被完成或失败后的数组。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, "1oo");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(reject, 1000, "foo");
});
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) => {
//results [{"status":"fulfilled","value":"1oo"},{"status":"rejected","reason":"foo"}]
});
3、globalThis
在以前,从不同的 JavaScript 环境中获取全局对象需要不同的语句。在 Web 中,可以通过 window、self 或者 frames 取到全局对象,但是在 Web Workers 中,只有 self 可以。在 Node.js 中,它们都无法获取,必须使用 global。
globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身)
这样在浏览器中, globalThis 就是 window 或者 self,
在 Web Workers 中, globalThis 就是 self,
在 nodejs 中,globalThis 就是 global。
快去把你项目中的 window,self,global 等替换成 globalThis 吧
4、for-in
for...in 语句以任意顺序遍历一个对象的除 Symbol 以外的可枚举属性。
在对象中使用 for....in 并没有什么问题
for(let item in {id:1,name:'123'}){
console.log(item) //依次输出 id, name
}
不建议 for....in 和数组搭配一起使用。为什么呢,请看下面的例子:
for(let item in ['a','b','c']){
console.log("-===",item) //依次输出 '0' '1' '2',
//注意 '0' '1' '2'并非下标,而是枚举位置
}
我们再给数组添对象添加个可枚举属性
let arr = [1,2,3];
Object.defineProperty(arr, "getName", {
value: "hello",
enumerable: true
});
//等同于
let arr = [1,2,3];
Array.prototype.getName = "hello";
//紧接着我们执行下面
for(let item in arr){
console.log(item) //依次输出 '0' '1' '2' 'getName'
}
发现没?? 是不出错了,如果我们项目中,在 util 的模块中,给 Array.prototype 上增加了一些 polyfill,那么我们使用 for....in 遍历数组,就必然出错。
当然我们可以通过其他方式避免。
比如
for(let i=0;i{
//
})
5、可选链操作符 '?.'
在以前我们进行条件判断时:
let obj = { "circle": { "x": 0, "y": 0 }, "radius": 50 };
if(obj && obj.circle && obj.circle.x){
//然后才敢拿着 obj.circle.x 进行操作。否则假如obj为null,obj.circle 就会直接报错。
}
而现在不用这么麻烦
let obj = { "radius": 50 };
if(obj?.circle?.x){
// obj.circle.x 进行操作
}
?.表示是否有这个属性,如果没有(undifined),或者有,但值为 null。则直接短路不会再继续向下。从而保护了代码不被出错。
let obj = { "circle": { "y": 0 }, "radius": 50 };
if(obj?.circle?.x){
}else{
//不会报错。在执行到 ?.circle时得到undefiend,直接短路。返回undefiend, 进入 else
}
同样,数组也可以
let arr = ['a','b'];
arr?.[1] // 'b'
最后注意,可选链不能用于赋值操作
let arr = ['a','b'];
arr?.[1] = 'c' //报错 Uncaught SyntaxError: Invalid left-hand side in assignment
6、空值合并操作符 '??'
它是个逻辑操作符,类似于 ||
|| 操作符,只有当左侧为假值(0, '', NaN, false, null,undefined)是才返回右侧的值.
0 || 'A' //A
'' || 'A' //A
NaN || 'A' //A
false || 'A' //A
null || 'A' //A
undefined || 'A' //A
而 ??只有左侧为空值(只有 null 和 undefined)时才
0 ?? 'A'; // 0
'' ?? 'A' // ''
NaN ?? 'A' //NaN
false ?? 'A' //false
null ?? 'A' //A
undefined ?? 'A' //A
6、import.meta
具体查看
文档
参考 ECMA 规范:
https://github.com/tc39/proposals/blob/master/finished-proposals.md