ES2019及ES2020最新特性

1,148 阅读14分钟

【本文共4000字左右,阅读时间约10分钟】

截止目前为止,以下的17个特性(按时间顺序排列)都已经处于finished阶段,接下来就让我们一睹新特性的风采吧!

es最新特性大纲

一. 8个ES2019新特性

1. Optional catch binding

catch参数可选, 我们在使用try catch捕获错误的时候,即使并不需要错误信息,也必须给catch传参数的,否则就会报错:

const isValidJSON = json => {
  try {
    JSON.parse(json);
  } catch (err) {
    console.log(err)
  }
};

在es2019中,我们可以这么写,是不是感觉舒服了很多:

const isValidJSON = json => {
  try {
    JSON.parse(json);
  } catch {   
  }
};

2. JSON superset

JSON超集:ES2019 规范要求,字符串字面量支持完整的 JSON 字符集,即在支持 ES2019 的环境中,对于双引号/单引号中的U+2028U+2029字符。

我们知道JavaScript 规定以下5个字符,不能在字符串里面直接使用,只能使用转义形式。

U+005C:反斜杠(reverse solidus)
U+000D:回车(carriage return)
U+2028:行分隔符(line separator)
U+2029:段分隔符(paragraph separator)
U+000A:换行符(line feed)

​ 如下:执行解析分隔符都会报语法错误:

 eval(" '\u2029' "); // Uncaught SyntaxError: Invalid or unexpected token
JSON.parse('"\u2029"'); // Uncaught SyntaxError: Invalid or unexpected token
  ES2019支持了这个新特性之后,可以直接执行解析:
   eval(" '\u2029' ");  //""
   JSON.parse('"\u2029"');  // ""

3. Symbol.prototype.description

Symbol的描述属性(只读):用于获取符号对象的描述,更好了解该符号的作用。

新特性之前,假如我们想要知道定义的Symbol的作用,只能通过Symbol.prototype.toString获取,如:

let sym = Symbol('foo');
sym.toString();    // => foo

Es2019中这个新增的description特性,允许我们直接的获取到描述,如:

let sym = Symbol('foo');
sym.description;    // => foo

4. Function-prototype-toString-revision

ES2019中重新修订Function.prototype.toString,toString方法会原样返回源代码中的实际文本片段。即返回注释、空格和语法详细信息。

看个例子,以前的写法中,toStringf()只返回了函数主体:

function /* zhushi*/ foo /*zhushi*/ (){}**
foo.toString(); // function foo(){}

在新特性中,函数的toString方法输出的内容与编写时的一致:

function /* zhushi*/ foo /*zhushi*/ (){}**
foo.toString(); // function /* zhushi*/ foo /*zhushi*/ (){}

// 注意:这里不支持箭头函数js
const bar /* zhushi*/ = /*注释*/ () => {}
bar.toString(); // () => {}

5. Object.fromEntries

Object.fromEntries函数,用于把键值对还原成对象结构,可以理解为是entries的逆方法。

在ES6时候,如果我们想把一个键值对还原成对象,写起来稍微复杂一点:

const getObj = (myArray) => Array.from(myArray).reduce((acc, [key, val]) => Object.assign(acc, {[key]: val}), {});
getObj([['one', 1], ['two', 2], ['three', 3]]) // {one: 1, two: 2, three: 3}

有了新特性之后,我们可以一步到位:

const myArray1 = [['one', 1], ['two', 2], ['three', 3]];
Object.fromEntries(myArray1)  //{one: 1, two: 2, three: 3}

//在只有一个数值的情况下,会默认这个数值为key,值为undefined
const myArray2 = [['one'], ['two'], ['three']];
Object.fromEntries(myArray2)  //{one: undefined, two: undefined, three: undefined}

//在有多个数值的情况下,会默认截取第一个值为key,第二个值为value
const myArray3 = [['one',1,'yi'], ['two',2,'er'], ['three',3,'san']];
Object.fromEntries(myArray3) //{one: 1, two: 2, three: 3}

6. JSON.stringify

加强格式化,主要是修复了一些unicode字符显示的问题。

由于JSON数据必须是UTF-8编码,UTF-8 标准规定,0xD800到0xDFFF之间的码点,不能单独使用,对于不存在的码点或者该区间的码点,JSON会认为这是一个无效的文本字符串。

es2019之前,这些字符将替换为特殊字符:

JSON.stringify('\uD83D')  // '"�"'

现在, 针对以上字符串,JSON.stringify()会返回转义字符串。

JSON.stringify('\uD83d') // ""\ud83d""

7. String.prototype.{trimStart,trimEnd}

字符串原型上新增trimStart()和trimEnd()方法,用于去掉字符串前后的空格

想要单独去掉字符串前后的空格,截止到es6,我们可以这样写:

const str = "   string   ";
str.trimLeft();    // => "string   "
str.trimRight();    // => "   string"

也可以自己写个函数:

String.prototype.TrimStart = function (c) {
        if (c == null || c == "") {
            var str = this.replace(/^s*/, '');
            return str;
        }
        else {
            var rg = new RegExp("^" + c + "*");
            var str = this.replace(rg, '');
            return str;
        }
    }

新特性中,trimStart()和trimEnd()这两个函数的作用等同于trimLeft()和trimRight(),可以理解为后者是前者的别名。

const str = "   string   ";
str.trimStart();    // => "string   "
str.trimEnd();     // => "   string"

String.prototype.trimLeft.name === 'trimStart' //true
String.prototype.trimRight.name === 'trimEnd'  //true

8. Array.prototype.{flat,flatMap}

flat函数,返回一组新的展开的数组。在新特性之前,我们要展开一个二维数组,可能会这么写:

const  iterFun = (arr) => {
 while(arr.some(item=> Array.isArray(item))){
      arr=[].concat(...arr)
  }
  return arr
}
const arr1 =  [1, 2, [3, 4]];
iterFun(arr1); //[1,2,3,4]
const arr2 = [1, 2, [3, 4, [5, 6, [7, 8]]]];
iterFun(arr2); // [1, 2, 3, 4, 5, 6, 7, 8]

有了flat函数之后,展开函数可就简单多了。ps:flat函数默认展开一层,若想完全展开,传入Infinity即可:

const arr1 = [1, 2, [3, 4]];
arr1.flat(); // [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6, [7, 8]]]];
arr2.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8]

flatMap函数,可以看做是flat函数和map函数的结合体,在展开数组后,可对数组的各项进行操作,如:

const arr = [[4.25,7.88],[ 19.99], 25.5];
arr.flatMap(value => Math.round(value)) //[4,8, 20, 26]
arr.flat().map(value => Math.round(value)) //[4,8, 20, 26]

再看一个关于flatMap函数的例子,这次的效果就相当于对数组的各项操作之后,再进行展开:

const sentence = ["This is a", "regular", "sentence"];
sentence.flatMap(x => x.split(" ")); // ["This","is","a","regular", "sentence"]

arr.map(x => x.split(" ")).flat()  // ["This","is","a","regular", "sentence"]
arr.flat().map(x => x.split(" "))  //[["This","is","a"], ["regular"], ["sentence"]

二. 9个ES2020新特性

1. String.prototype.matchAll

matchAll方法接收正则表达式,返回一个迭代对象

我们先用match方法举个例子:

const str = 'Dr. Smith and Dr. Anderson';
const re1 = /(Dr\. )\w+/g;
const res1 = str.match(re1);  //["Dr. Smith", "Dr. Anderson"]

const re2= /(Dr\. )\w+/;
const res2 =str.match(re2);  //["Dr. Smith", "Dr. ", index: 0, input: "Dr. Smith and Dr. Anderson", groups: undefined]

从res1和res2的结果中可以看出,res1没有匹配到子项(即Dr. ),res2中返回了第一个匹配项。

新特性matchAll,兼顾了以上两种返回结果,既可以获取全部匹配项,同时返回所有子项。

const re = /(Dr\. )\w+/g;
const str = 'Dr. Smith and Dr. Anderson';
const matches = str.matchAll(re);

for (const match of matches) {
  console.log(match);
}

// ["Dr. Smith", "Dr. ", index: 0, input: "Dr. Smith and Dr. Anderson", groups: undefined]
// ["Dr. Anderson", "Dr. ", index: 14, input: "Dr. Smith and Dr. Anderson", groups: undefined]

2. Dynamic-import

import(module) 按需加载,用于动态加载由应用合并(Bundling)中代码切分阶段所生成的模块

通过import方法异步导入(支持关键字await)的模块返回一个promise对象,我们可以在任何地方(不同于只在模块头部定义导入,而是可在代码任何位置)调用,比如某个点击事件中,示例代码如下:

onClick = () => {
import('/modules/my-module.js')
.then(module => {
 // Do something **with** the module. 
 })
}

let module = await import('/modules/my-module.js');

3. BigInt

BigInt 是一种内置对象,它提供了一种表示大于2^53-1的整数的方法。BigInt类型作为一种新的数据类型,它可以表示任意大的整数。

typeof  9007199254740993n; // 'bigint'

ES2019之前,Js 中 Number类型只能安全的表示-(2^53-1)至 2^53-1 范的值,超出这个范围的整数计算或者表示会丢失精度,如

var num = Number.MAX_SAFE_INTEGER; // 9007199254740991
num = num + 1; //  9007199254740992
num = num + 1; // 9007199254740992,无法正常运算
9007199254740992 === 9007199254740993 //true,无法正常比教

ES2020中为了解决大整数计算的问题,引入了BigInt 类型,它有两种定义方法

1.字面量表示,在数字的末尾带有一个n const bigIntNum = 9007199254740993n; 2.通过BigInt函数创造,参数(必传)可为整型、十六进制、二进制的字符串形式

const bigNum = BigInt(1); //1n
const bigHex = BigInt("0x1f"); //31n
const bigBin= BigInit('0b110011') //51n
const bigDeci= BigInit(1.5) // The number 1.5 cannot be converted to a BigInt because it is not an integer

算数运算,如 _ * / **,注意不能和Number实例进行混合运算

1.操作数必须都是bigints类型
add = 9n + 12 //Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
2.bigints不储存整数
division = -9n/2n   //-4n  

比较运算,如 > < == ===

1n === 1 //false,两数的类型不一样
1n == 1 // true, 仅比较两数值
2n > 1  // true
23243454698789789789n < 86786758575675646456456n //true

逻辑运算,如 || && !

   or = 1n || 2n // 1n
   and = 0 && 2n // 0

位移运算,如 >> << ,注意不能和Number实例进行混合运算,也不能用于无符号右移

   left = 2n << 5n // 64n
   right = 2n >> 5n // 0n
   noSymbolRight = 2n >>> 5n; // Uncaught TypeError: BigInts have no unsigned right shift, use >> instead
   mixRight = 2n >> 1 // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

注意:bigInt类型不能用于math方法中

   Math.abs(-120n) //Uncaught TypeError: Cannot convert a BigInt value to a number at Math.abs 

4. Promise.allSettled

Promise.allSettled(iterable),iterable中每个成员都是promise。该方法保证了在并发任务中,无论一个任务正常或者异常,都会返回对应的的状态(fulfilled 或者 rejected)与结果(value 或者 reason)。

我们都知道 Promise.all 和promise.race,前者具有并发执行异步任务的能力,但是只要某个任务被拒绝,所有任务都会挂掉,Promise直接进入reject状态;后者一旦迭代器中的某个任务被解决或拒绝,返回的promise就会解决或拒绝。

Promise.all([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => console.log(arr))
 .catch(error => console.log(error) )//  b

Promise.race([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => console.log(arr))  // a
 .catch(error => console.log(error) )

那有没有一个方法可以输出所有任务的执行结果呢?Promise.allSettled()就是解决这个问题的。该方法返回一个promise,将给定的所有promise执行后的结果(无论是解决还是拒绝)解析称一个对象,放在数组里返回:

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => console.log(arr));  //  { status: 'fulfilled', value:  'a' },  { status: 'rejected',  reason: 'b' }

5. GlobalThis

globalThis 提供一种标准化方式访问全局对象,不需要考虑环境的问题 之前,我们要获取全局对象,需要考虑到环境的问题,比如node 中通过 global, web中通过 window, self ,webwork中通过self获取。所以常常是写个函数,通过判断环境来返回全局对象

const getGlobal = () => { 
 if(typeof self !== 'undefined') { return self; } 
 if(typeof window !== 'undefined') { return window; } 
if (typeof global !== 'undefined') { return global; } 
 throw new Error('unable to locate global objec'); 
};

es2020中,通过globalThis来访问全局对象,不用担心环境问题啦

//浏览器:
 globalThis === window  //true  
globalThis === self  //true 
//node:  
globalThis === global  //true
//webwork:
 globalThis === self  //true

6. For-in-order

ECMA-262 几乎全部未指明 for (a in b) 顺序 ,由于不同的引擎实现方式不同,所以for in顺序也会存在差异。es2020中明确只要保证for in迭代过程中保持以下几个特点,就能保证各大引擎的输出的顺序有一定的规律性。

  • 迭代对象或原型的任何内容都不是代理,类数组,模块名称空间对象或主机对象

  • 对象及其原型链没有更改,即不将属性添加到原型链上,可以添加到对象本身

  • 属性的可枚性没有变化

  • 没有属性被删除,然后重新添加

  • 没有从原型链中的任何内容删除属性

    //迭代对象为类数组,postMessage发送消息后,a的所有权不再属于发送方。
    let a = new Uint8Array(3);
      let done = false;
      for (let key in a) {
        console.log(key);
        if (!done) {
          postMessage(null, '*', [a.buffer]);
          done = true;
        }
      }   //  0
    
    //迭代时修改了可枚举属性
    let a = { x: 0, y: 0, z: 0 };
      for (let key in a) {
        console.log(key);
        Object.defineProperty(a, 'y', { enumerable: false, configurable: true, writable:     true, value: 0 });
      } //{ x: 0, z: 0,y: 0,  };
    
    // 迭代时修改了原型
    let derived = { x: 0 };
    let baseOne = { y: 0 };
    let baseTwo = { z: 0 };
    Object.setPrototypeOf(derived, baseOne);
    for (let key in derived) {
      Object.setPrototypeOf(derived, baseTwo);
      console.log(key);
    }  //{'x':0}  
    

输出的规律也有以下几个特点:

  • 在返回之前被删除的属性,且不再其进行重新添加,该属性不返回
  • 对于非整数属性,按插入顺序返回属性
  • 在迭代后再添加到要迭代对象上的属性,不返回。
let x = [];
x.a = 0;
x['2'] = 0;
x.c = 0;
x.b = 0;
x[0] = 0;
x[-1] = 0;
x[3] = 0;

for (let key in x) {
  console.log(key.toString());
  delete x[3];
  delete x.a;
  Object.defineProperty(x, 3, { enumerable: true, configurable: true, writable:  true, value: 0 });
x.a= 0;  
x.d = 0;
} // 0 2 3 c b -1

7. Optional Chaining

可选链:通过操作符?.让我们在查询多层级的对象时,不再需要进行冗余的各种前置校验。? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段,否则返回undefined.

之前在查询多层级对象时,我们一般会这么写:

const obj2 = {
  prop1: {
    prop2: {
      prop3: 1
    }
  }
};
const val = obj2 && obj2.props1 &&  obj2.props1.props2 &&  obj2.props1.props2.props3; //1

支持了可选链之后,写起来就轻松多了

const val = obj2?.prop1?.prop2?.prop3); //1

8. Nullish coalescing Operator

空值合并运算符(??):解决了在赋值时,空字符串、 flase、0在逻辑运算符被视为false无法取值只能取默认值的问题

举个例子,如下的fun方法,如果num为null或未定义时取默认值,但是传参为0情况下输出的却是默认值,当然这不是我们想要的结果:

const fun = (num) => {
return num || '默认值'
}
fun(0) // '默认值'

采用es2020新特性,就完美解决,??表示在只有null或者undefined才会取默认值

const fun = (num) => {
return num ?? '默认值'
}
fun(0) //  0
fun(null) //  '默认值'

9. Import-meta

import.meta:模块的元数据对象,包含了依赖于当前宿主环境的模块元数据。 当前其中最受开发人员关注的是 url 属性,url 给出了当前模块文件的 URL 字符串。开发人员可使用该 URL 字符串去导入相关模块,或是根据模块当前信息有条件地执行一些操作。ES2020 提案中给出了如下示例代码,获取当前模块对应的 hamster.jpg 文件

在之前的用法中,假如要获取某个脚本上的数据,我们需要这么写:

<script data-option="value" src="library.js"></script>
const value = document.currentScript.dataset.option;

在ES2020中,提出两点 1.不再通过全局变量的形式获取数据,而是通过Import.meta获取元数据,2.提供通用性拓展机制,而不是对每个元数据进行标准化。

这两点怎么理解呢,举个例子就明白了了。拿ES2020提案中给出了官方示例,针对上面第一点,通过import.meta.url获取当前模块文件的URL字符串,然后根据该字符串去导入相关模块,第二点通过import.meta.scriptElement.dataset.size获取元数据size的值。

//当加载a模块时,将加载同级图片hamster.jpg,并根据脚本数据size显示图像的大小
<script type="module" src="a.mjs" data-size="500"></script>
(async () => {
   const response = await fetch(new URL("../hamsters.jpg", import.meta.url));
   const blob = await response.blob();
   const size = import.meta.scriptElement.dataset.size || 300;
  
   const image = new Image();
   image.src = URL.createObjectURL(blob);
   image.width = image.height = size;
  
   document.body.appendChild(image);
 })();

具体项目中使用的话,需要安装依赖npm install babel-preset-env --save-dev,然后在babelrc文件中加入"presets": ['env']即可,就是让babel根据运行环境自动将ES2015+的代码转换为es5。

注意:

1.目前babel是不支持bigint的字面量语法的转换,所以想要使用bigint类型,还是采用函数BigInt()的方式吧。

2.由于规范未说明该import.meta对象内部应包含什么内容,所以babel未正式提供用于语法转换的插件,目前该特性暂不可用。tips:在vscode中会对js文件默认开启ts校验,在代码里使用新特性语法,会报错ts错误,所以需要在首选项-设置-扩展-TypeScript: 关闭Javascript › Validate: Enable选项。

结束语:感谢大家看到这里,以上就是我要介绍的17个ES新特性,希望能带给大家一点收获!