30个ES6—ES12 新功能

30个ES6—ES12 新功能

你需要知道的30个ES6—ES12开发技巧!


ECMAScript 是一种由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言,这种语言被称为 JavaScript 。简单来说,ECMAScript 是 JavaScript 的标准与规范,JavaScript 是 ECMAScript 标准的实现和扩展。

自2015年开始,ECMAScript发布的版本如下:

发布时间正式名称版本名称名称缩写
2015ECMAScript2015ECMAScript6ES2015、ES6
2016ECMAScript2016ECMAScript7ES2016、ES7
2017ECMAScript2017ECMAScript8ES2017、ES8
2018ECMAScript2018ECMAScript9ES2018、ES9
2019ECMAScript2019ECMAScript10ES2019、ES10
2020ECMAScript2020ECMAScript11ES2020、ES11
2021ECMAScript2021ECMAScript12ES2021、ES12

下面就来看看ECMAScript各版本有哪些使用技巧吧。 

ECMAScript.png

一、ES6 新特性(2015)

ES6的更新主要是体现在以下方面:

  • 表达式:变量声明,解构赋值
  • 内置对象:字符串拓展、数值拓展、对象拓展、数组拓展、函数拓展、正则拓展、Symbol、Set、Map、Proxy、Reflect
  • 语句与运算:Class、Module、Iterator
  • 异步编程:Promise、Generator、Async。

这里主要介绍一些常用的新特性。还有一些特性,在之前文章中已经介绍过了,这里不在多说,直接上链接:

1. let和const

在ES6中,新增了let和const关键字,其中 let 主要用来声明变量,而 const 通常用来声明常量。let、const相对于var关键字有以下特点:

特性varletconst
变量提升✔️××
全局变量✔️××
重复声明✔️××
重新赋值✔️✔️×
暂时性死区×✔️✔️
块作用域×✔️✔️
只声明不初始化✔️✔️×

这里主要介绍其中的四点:

(1)重新赋值

const 关键字声明的变量是“不可修改”的。其实,const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。但对于引用类型的数据(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是不变的,至于它指向的数据结构就不可控制了。

(2)块级作用域

在引入let和const之前是不存在块级作用域的说法的,这也就导致了很多问题,比如内层变量会覆盖外层的同名变量:

var a = 1;
if (true) {
  var a = 2;
}

console.log(a);   // 输出结果:2 

循环变量会泄漏为全局变量:

var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i]);  // 输出结果:1  2  3
}

console.log(i); // 输出结果:3

而通过let和const定义的变量存在块级作用域,就不会产生上述问题:

let a = 1;
if (true) {
  let a = 2;
}

console.log(a); // 输出结果:1

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);  // 输出结果:1  2  3
}

console.log(i); // Uncaught ReferenceError: i is not defined

(3)变量提升

我们知道,在ES6之前是存在变量提升的,所谓的变量提升就是变量可以在声明之前使用:

console.log(a); // 输出结果:undefined
var a = 1;

变量提升的本质是JavaScript引擎在执行代码之前会对代码进行编译分析,这个阶段会将检测到的变量和函数声明添加到 JavaScript 引擎中名为 Lexical Environment 的内存中,并赋予一个初始化值 undefined。然后再进入代码执行阶段。所以在代码执行之前,JS 引擎就已经知道声明的变量和函数。​

这种现象就不太符合我们的直觉,所以在ES6中,let和const关键字限制了变量提升,let 定义的变量添加到 Lexical Environment 后不再进行初始化为 undefined 操作,JS 引擎只会在执行到词法声明和赋值时才进行初始化。而在变量创建到真正初始化之间的时间跨度内,它们无法访问或使用,ES6 将其称之为暂时性死区:

// 暂时性死区 开始
a = "hello";     //  Uncaught ReferenceError: Cannot access 'a' before initialization

let a;   
//  暂时性死区 结束
console.log(a);  // undefined

(4)重复声明

在ES6之前,var关键字声明的变量对于一个作用域内变量的重复声明是没有限制的,甚至可以声明与参数同名变量,以下两个函数都不会报错:

function funcA() {
  var a = 1;
  var a = 2;
}

function funcB(args) {
  var args = 1; 
}

而let修复了这种不严谨的设计:

function funcA() {
  let a = 1;
  let a = 2;  // Uncaught SyntaxError: Identifier 'a' has already been declared
}

function funcB(args) {
  let args = 1;  // Uncaught SyntaxError: Identifier 'args' has already been declared
}

现在我们项目中已经完全放弃了var,而使用let来定义变量,使用const来定义常量。在ESlint开启了如下规则:

"no-var": 0;

2. 解构赋值

ES6中还引入了解构赋值的概念,解构赋值遵循“模式匹配”,即只要等号两边的模式相等,左边的变量就会被赋予对应的值。不同类型数据的解构方式不同,下面就分别来看看不同类型数据的解构方式。​

平时在开发中,我主要会用到对象的解构赋值,比如在React中解构porps值等,使用解构赋值来获取父组件传来的值;在React Hooks中的useState使用到了数组的解构赋值;

(1)数组解构

具有 Iterator 接口的数据结构,都可以采用数组形式的解构赋值。

const [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo, bar, baz) // 输出结果:1  2  3

这里,ES6实现了对数组的结构,并依次赋值变量foo、bar、baz。数组的解构赋值按照位置将值与变量对应。​

数组还可以实现不完全解构,只解构部分内容:

const [x, y] = [1, 2, 3];   // 提取前两个值
const [, y, z] = [1, 2, 3]  // 提取后两个值
const [x, , z] = [1, 2, 3]  // 提取第一三个值

如果解构时对应的位置没有值就会将变量赋值为undefined:

const [x, y, z] = [1, 2]; 
console.log(z)  // 输出结果:undefined
复制代码

数组解构赋值可以使用rest操作符来捕获剩余项:

const [x, ...y] = [1, 2, 3];   
console.log(x);  // 输出结果:1
console.log(y);  // 输出结果:[2, 3]

在解构时还支持使用默认值,当对应的值为undefined时才会使用默认值:

const [x, y, z = 3] = [1, 2]; 
console.log(z)  // 输出结果:3

(2)对象解构

对象的解构赋值的本质其实是先找到同名的属性,在赋值给对应的变量:

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
console.log(foo, bar); // 输出结果:aaa  bbb

需要注意的是,在JavaScript中,对象的属性是没有顺序的。所以,在解构赋值时,变量必须与属性同名才能去取到值。​

对象的解构赋值也是支持默认值的,当定义的变量在对象中不存在时,其默认值才会生效:

let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb', baz: null };
console.log(foo, bar, baz); // 输出结果:aaa  bbb  null

let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb' };
console.log(foo, bar, baz); // 输出结果:aaa  bbb  ccc

可以看到,只有定义的变量是严格的===undefined时,它的默认值才会生效。

除此之外,我们还需要注意,不能给已声明的变量进行赋值,因为当缺少 let、const、var 关键词时,将会把 {baz} 理解为代码块从而导致语法错误,所以下面代码会报错:

let baz;
{ baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' };

可以使用括号包裹整个解构赋值语句来解决上述问题:

let baz;
({ baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' });
console.log(baz)

在对象的解构赋值中,可以将现有对象的方法赋值给某个变量,比如:

let { log, sin, cos } = Math;
log(12)  // 输出结果:2.4849066497880004
sin(1)   // 输出结果:0.8414709848078965
cos(1)   // 输出结果:0.5403023058681398

(3)其他解构赋值

剩下的几种解构赋值,目前我在项目中应用的较少,来简单看一下。​

  • 字符串解构

字符串解构规则:只要等号右边的值不是对象或数组,就先将其转为类数组对象,在进行解构:

const [a, b, c, d, e] = 'hello';
console.log(a, b, c, d, e)  // 输出结果:h e l l o

类数组对象有 length 属性,因此可以给这个属性进行解构赋值:

let {length} = 'hello';    // 输出结果: 5

由于字符串都是一个常量,所以我们通常是知道它的值是什么的,所以很少会使用变量的解构赋值。

  • 数值和布尔值解构赋值

对数值和布尔值进行解构时,它们将会先被转为对象,然后再应用解构语法:

let {toString: s} = 123;
s === Number.prototype.toString // 输出结果:true

let {toString: s} = true;
s === Boolean.prototype.toString // 输出结果:true

注意null和undefined不能转换为对象,所以如果右边是这两个值,就会报错。​

  • 函数参数解构赋值

函数参数表面上是一个数组,在传入参数的那一刻,就会被解构为x和y。

function add([x, y]){
  return x + y;
}
add([1, 2]);   // 3

除此之外,我们还可以解构函数的返回值:

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

3. 模板字符串

传统的JavaScript语言中,输出模板经常使用的是字符串拼接的形式,这样写相当繁琐,在ES6中引入了模板字符串的概念来解决以上问题。

模板字符串是增强版的字符串,用反引号``来标识,他可以用来定义单行字符串,也可以定义多行字符串,或者在字符串中嵌入变量。

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

// 字符串中调用函数
` ${fn()} 

在平时的开发中,除了上面代码中的应用,很多地方会用到模板字符串,比如拼接一个DOM串,在Emotion/styled中定义DOM结构等,都会用到模板字符串。不过在模板字符串中定义DOM元素就不会有代码提示了。​

在使用模板字符串时,需要注意以下几点:

  • 如果在字符串中使用反引号,需要使用\来转义;
  • 如果在多行字符串中有空格和缩进,那么它们都会被保留在输出中;
  • 模板字符串中嵌入变量,需要将变量名写在${}之中;
  • 模板字符串中可以放任意的表达式,也可以进行运算,以及引用对象的属性,甚至可以调用函数;
  • 如果模板字符中的变量没有声明,会报错。

4. 函数默认参数

在ES6之前,函数是不支持默认参数的,ES6实现了对此的支持,并且只有不传入参数时才会触发默认值:

function getPoint(x = 0, y = 0) {
  console.log(x, y);
}

getPoint(1, 2);   // 1  2
getPoint()        // 0  0 
getPoint(1)       // 1  0

当使用函数默认值时,需要注意以下几点:

(1)函数length属性值

函数length属性通常用来表示函数参数的个数,当引入函数默认值之后,length表示的就是第一个有默认值参数之前的普通参数个数:

const funcA = function(x, y) {};
console.log(funcA.length);  // 输出结果:2 

const funcB = function(x, y = 1) {};
console.log(funcB.length);  // 输出结果:1

const funcC = function(x = 1, y) {};
console.log(funcC.length);  // 输出结果 0 

(2)参数作用域

当给函数的参数设置了默认值之后,参数在被初始化时将形成一个独立作用域,初始化完成后作用域消解:

let x = 1;

function func(x, y = x) {
  console.log(y);
}

func(2);  

这里最终会打印出2。在函数调用时,参数 x, y 将形成一个独立的作用域,所以参数中的y会等于第一个参数中的x,而不是上面定义的1。

5. 箭头函数

ES6中引入了箭头函数,用来简化函数的定义:

const counter = (x, y) => x + y;

相对于普通函数,箭头函数有以下特点:

(1)更加简洁

  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号
  • 如果有多个参数,用逗号分割
  • 如果函数体的返回值只有一句,可以省略大括号
// 1. 不传入参数
const funcA = () => console.log('funcA');
// 等价于
const funcA = function() {
  console.log('funcA');
} 

// 2. 传入参数
const funcB = (x, y) => x + y;
// 等价于
const funcB = function(x, y) {
  return x + y;
} 

// 3. 单个参数的简化
const funcC = (x) => x;
// 对于单个参数,可以去掉 (),简化为
const funcC = x => x;
// 等价于
const funcC = function(x) {
  return x;
}

// 4. 上述代码函数体只有单条语句,如果有多条,需要使用 {}
const funcD = (x, y) => { console.log(x, y); return x + y; }
// 等价于
const funcD = function(x, y) {
  console.log(x, y);
  return x + y;
}

(2)不绑定 this

箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

同样,使用call()、apply()、bind()等方法也不能改变箭头函数中this的指向:

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

(3)不可作为构造函数

构造函数 new 操作符的执行步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(4)不绑定 arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

6. 扩展运算符

扩展运算符:... 就像是rest参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。

spread 扩展运算符有以下用途

(1)将数组转化为用逗号分隔的参数序列:

function  test(a,b,c){
    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3
}

var arr = [1, 2, 3];
test(...arr);

(2)将一个数组拼接到另一个数组:

var arr1 = [1, 2, 3,4];
var arr2 = [...arr1, 4, 5, 6];
console.log(arr2);  // [1, 2, 3, 4, 4, 5, 6]

(3)将字符串转为逗号分隔的数组:

var str='JavaScript';
var arr= [...str];
console.log(arr); // ["J", "a", "v", "a", "S", "c", "r", "i", "p", "t"]

7. Symbol

ES6中引入了一个新的基本数据类型Symbol,表示独一无二的值。它是一种类似于字符串的数据类型,它的特点如下:

  • Symbol的值是唯一的,用来解决命名冲突的问题
  • Symbol值不能与其他类型数据进行运算
  • Symbol定义的对象属性不能使用for...in遍历循环,但是可以使用Reflect.ownKeys 来获取对象的所有键名
let s1 = Symbol();
console.log(typeof s1); // "symbol"

let s2 = Symbol('hello');
let s3 = Symbol('hello');
console.log(s2 === s3); // false

基于以上特性,Symbol 属性类型比较适合用于两类场景中:常量值和对象属性

(1)避免常量值重复

getValue 函数会根据传入字符串参数 key 执行对应代码逻辑:

function getValue(key) {
  switch(key){
    case 'A':
      ...
    case 'B':
      ...
  }
}
getValue('B');

这段代码对调用者而言非常不友好,因为代码中使用了魔术字符串(Magic string,指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值),导致调用 getValue 函数时需要查看函数代码才能找到参数 key 的可选值。所以可以将参数 key 的值以常量的方式声明:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
}
function getValue(key) {
  switch(key){
    case KEY.alibaba:
      ...
    case KEY.baidu:
      ...
  }
}
getValue(KEY.baidu);

但这样也并非完美,假设现在要在 KEY 常量中加入一个 key,根据对应的规则,很有可能会出现值重复的情况:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
  tencent: 'B'
}

这就会出现问题:

getValue(KEY.baidu) // 等同于 getValue(KEY.tencent)

所以在这种场景下更适合使用 Symbol,不需要关心值本身,只关心值的唯一性:

const KEY = {
  alibaba: Symbol(),
  baidu: Symbol(),
  tencent: Symbol()
}

(2)避免对象属性覆盖

函数 fn 需要对传入的对象参数添加一个临时属性 user,但可能该对象参数中已经有这个属性了,如果直接赋值就会覆盖之前的值。此时就可以使用 Symbol 来避免这个问题。创建一个 Symbol 数据类型的变量,然后将该变量作为对象参数的属性进行赋值和读取,这样就能避免覆盖的情况:

function fn(o) { // {user: {id: xx, name: yy}}
  const s = Symbol()
  o[s] = 'zzz'
}

8. 集合 Set

ES6提供了新的数据结构Set(集合)。它类似于数组,但是成员的值都是唯一的,集合实现了iterator接口,所以可以使用扩展运算符和 for…of 进行遍历。

Set的属性和方法:

属性和方法概述
size返回集合的元素个数
add增加一个新的元素,返回当前的集合
delete删除元素,返回布尔值
has检查集合中是否包含某元素,返回布尔值
clear清空集合,返回undefined
//创建一个空集合
let s = new Set();
//创建一个非空集合
let s1 = new Set([1,2,3,1,2,3]);
//返回集合的元素个数
console.log(s1.size);       // 3
//添加新元素
console.log(s1.add(4));     // {1,2,3,4}
//删除元素
console.log(s1.delete(1));  //true
//检测是否存在某个值
console.log(s1.has(2));     // true
//清空集合
console.log(s1.clear());    //undefined

由于集合中元素的唯一性,所以在实际应用中,可以使用set来实现数组去重:

let arr = [1,2,3,2,1]
Array.from(new Set(arr))  // {1, 2, 3}

这里使用了Array.form()方法来将数组集合转化为数组。​

可以通过set来求两个数组的交集和并集:

// 模拟求交集 
let intersection = new Set([...set1].filter(x => set2.has(x)));

// 模拟求差集
let difference = new Set([...set1].filter(x => !set2.has(x)));

用以下方法可以进行数组与集合的相互转化:

// Set集合转化为数组
const arr = [...mySet]
const arr = Array.from(mySet)

// 数组转化为Set集合
const mySet = new Set(arr)

9. Map

ES6提供了Map数据结构,它类似于对象,也是键值队的集合,但是它的键值的范围不限于字符串,可以是任何类型(包括对象)的值,也就是说, Object 结构提供了“ 字符串—值” 的对应, Map 结构提供了“ 值—值” 的对应, 是一种更完善的 Hash 结构实现。 如果需要“ 键值对” 的数据结构, Map 比 Object 更合适。Map也实现了iterator接口,所以可以使用扩展运算符和 for…of 进行遍历。

Map的属性和方法:

属性和方法概述
size返回Map的元素个数
set增加一个新的元素,返回当前的Map
get返回键名对象的键值
has检查Map中是否包含某元素,返回布尔值
clear清空Map,返回undefined
//创建一个空 map
let m = new Map();
//创建一个非空 map
let m2 = new Map([
 ['name', 'hello'],
]);
//获取映射元素的个数
console.log(m2.size);          // 1
//添加映射值
console.log(m2.set('age', 6)); // {"name" => "hello", "age" => 6}
//获取映射值
console.log(m2.get('age'));    // 6
//检测是否有该映射
console.log(m2.has('age'));    // true
//清除
console.log(m2.clear());       // undefined

需要注意, 只有对同一个对象的引用, Map 结构才将其视为同一个键:

let map = new Map(); 
map.set(['a'], 555); 
map.get(['a']) // undefined

上面代码的set和get方法, 表面是针对同一个键, 但实际上这是两个值, 内存地址是不一样的, 因此get方法无法读取该键, 所以会返回undefined。​

由上可知, Map 的键实际上是跟内存地址绑定的, 只要内存地址不一样, 就视为两个键。 这就解决了同名属性碰撞( clash) 的问题,在扩展库时, 如果使用对象作为键名, 就不用担心自己的属性与原来的属性同名。

如果 Map 的键是一个简单类型的值( 数字、 字符串、 布尔值), 则只要两个值严格相等, Map 将其视为一个键, 包括0和 - 0。 另外, 虽然NaN不严格相等于自身, 但 Map 将其视为同一个键。

let map = new Map(); 
map.set(NaN, 123); 
map.get(NaN) // 123 
map.set(-0, 123); 
map.get(+0) // 123 

10. 模块化

ES6中首次引入模块化开发规范ES Module,让Javascript首次支持原生模块化开发。ES Module把一个文件当作一个模块,每个模块有自己的独立作用域,那如何把每个模块联系起来呢?核心点就是模块的导入与导出。

(1)export 导出模块

  • 正常导出:
// 方式一
export var first = 'test';
export function func() {
    return true;
}

// 方式二
var first = 'test';
var second = 'test';
function func() {
    return true;
}
export {first, second, func};
  • as关键字:
var first = 'test';
export {first as second};

as关键字可以重命名暴露出的变量或方法,经过重命名后同一变量可以多次暴露出去。

  • export default

export default会导出默认输出,即用户不需要知道模块中输出的名字,在导入的时候为其指定任意名字。

// 导出
export default function () {
  console.log('foo');
}
// 导入
import customName from './export-default';

注意: 导入默认模块时不需要大括号,导出默认的变量或方法可以有名字,但是对外无效。export default只能使用一次。

(2)import 导入模块

  • 正常导入:
import {firstName, lastName, year} from './profile';

导入模块位置可以是相对路径也可以是绝对路径,.js可以省略,如果不带路径只是模块名,则需要通过配置文件告诉引擎查找的位置。

  • as关键字:
import { lastName as surname } from './profile';

import 命令会被提升到模块头部,所以写的位置不是那么重要,但是不能使用表达式和变量来进行导入。

  • 加载整个模块(无输出)
import 'lodash'; //仅仅是加载而已,无法使用
  • 加载整个模块(有输出)
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意: import * 会忽略default输出

(3)导入导出复合用法

  • 先导入后导出
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
  • 整体先导入再输出以及default
// 整体输出
export * from 'my_module';
// 导出default,正如前面所说,export default 其实导出的是default变量
export { default } from 'foo';
// 具名接口改default
export { es6 as default } from './someModule';

(4)模块的继承

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

注意: export * 会忽略default。

二、ES7 新特性(2016)

1. Array.prototype.includes

includes() 方法用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。该方法不会改变原数组。其语法如下:

arr.includes(searchElement, fromIndex)

该方法有两个参数:

  • searchElement:必须,需要查找的元素值。
  • fromIndex:可选,从fromIndex 索引处开始查找目标值。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。
[1, 2, 3].includes(2);  //  true
[1, 2, 3].includes(4);  //  false
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

在 ES7 之前,通常使用 indexOf 来判断数组中是否包含某个指定值。但 indexOf 在语义上不够明确直观,同时 indexOf 内部使用 === 来判等,所以存在对 NaN 的误判,includes 则修复了这个问题:

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

注意:使用 includes()比较字符串和字符时是区分大小写。

2. 指数操作符

ES7 还引入了指数操作符 ,用来更为方便的进行指数计算,它与 Math.pow() 等效:

Math.pow(2, 10));  // 1024
2**10;           // 1024

三、ES8 新特性(2017)

ES8中引入了异步函数的解决方案async/await,这里不在多介绍,参考文章:《万字长文,重学JavaScript异步编程》

1. padStart()和padEnd()

padStart()和padEnd()方法用于补齐字符串的长度。如果某个字符串不够指定长度,会在头部或尾部补全。

(1)padStart()

padStart()用于头部补全。该方法有两个参数,其中第一个参数是一个数字,表示字符串补齐之后的长度;第二个参数是用来补全的字符串。​

如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串:

'x'.padStart(1, 'ab') // 'x'

如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串:

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

如果省略第二个参数,默认使用空格补全长度:

'x'.padStart(4, 'ab') // 'a   '

padStart()的常见用途是为数值补全指定位数,笔者最近做的一个需求就是将返回的页数补齐为三位,比如第1页就显示为001,就可以使用该方法来操作:

"1".padStart(3, '0')   // 输出结果: '001'
"15".padStart(3, '0')  // 输出结果: '015'

(2)padEnd()

padEnd()用于尾部补全。该方法也是接收两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串:

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

2. Object.values()和Object.entries()

在ES5中就引入了Object.keys方法,在ES8中引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for...of循环使用。它们都用来遍历对象,它会返回一个由给定对象的自身可枚举属性(不含继承的和Symbol属性)组成的数组,数组元素的排列顺序和正常循环遍历该对象时返回的顺序一致,这个三个元素返回的值分别如下:

  • Object.keys():返回包含对象键名的数组;
  • Object.values():返回包含对象键值的数组;
  • Object.entries():返回包含对象键名和键值的数组。
let obj = { 
  id: 1, 
  name: 'hello', 
  age: 18 
};
console.log(Object.keys(obj));   // 输出结果: ['id', 'name', 'age']
console.log(Object.values(obj)); // 输出结果: [1, 'hello', 18]
console.log(Object.entries(obj));   // 输出结果: [['id', 1], ['name', 'hello'], ['age', 18]

注意

  • Object.keys()方法返回的数组中的值都是字符串,也就是说不是字符串的key值会转化为字符串。
  • 结果数组中的属性值都是对象本身可枚举的属性,不包括继承来的属性。

3. 函数扩展

ES2017 规定函数的参数列表的结尾可以为逗号:

function person( name, age, sex, ) {}

该特性的主要作用是方便使用git进行多人协作开发时修改同一个函数减少不必要的行变更。

四、ES9 新特性(2018)

1. for await…of

for await...of方法被称为异步迭代器,该方法是主要用来遍历异步对象。

for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 String,Array,类数组,Map, Set和自定义的异步或者同步可迭代对象。这个语句只能在 async function内使用:

function Gen (time) {
  return new Promise((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(),item)
   }
}
test()

输出结果: 

image.png

2. Promise.prototype.finally()

ES2018 为 Promise 添加了 finally() 方法,表示无论 Promise 实例最终成功或失败都会执行的方法:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const one = '1';
    reject(one);
  }, 1000);
});

promise
  .then(() => console.log('success'))
  .catch(() => console.log('fail'))
  .finally(() => console.log('finally'))

finally() 函数不接受参数,finally() 内部通常不知道 promise 实例的执行结果,所以通常在 finally() 方法内执行的是与 promise 状态无关的操作。

3. 对象的扩展运算符

在ES6中就引入了扩展运算符,但是它只能作用于数组,ES2018中的扩展运算符可以作用于对象:​

(1)将元素组织成对象

const obj = {a: 1, b: 2, c: 3};
const {a, ...rest} = obj;
console.log(rest);    // 输出 {b: 2, c: 3}

(function({a, ...obj}) {
  console.log(obj);    // 输出 {b: 2, c: 3}
}({a: 1, b: 2, c: 3}));

(2)将对象扩展为元素

const obj = {a: 1, b: 2, c: 3};
const newObj ={...obj, d: 4};
console.log(newObj);  // 输出 {a: 1, b: 2, c: 3, d: 4}

(3)可以用来合并对象

const obj1 = {a: 1, b:2};
const obj2 = {c: 3, d:4};
const mergedObj = {...obj1, ...obj2};
console.log(mergedObj);  // 输出 {a: 1, b: 2, c: 3, d: 4}

五、ES10 新特性(2019)

1. trimStart() 和 trimEnd()

在ES10之前,JavaScript提供了trim()方法,用于移除字符串首尾空白符。在ES9中提出了trimStart()和trimEnd() 方法用于移除字符串首尾的头尾空白符,空白符包括:空格、制表符 tab、换行符等其他空白符等。

(1)trimStart()

trimStart() 方法的的行为与trim()一致,不过会返回一个从原始字符串的开头删除了空白的新字符串,不会修改原始字符串:

const s = '  abc  ';

s.trimStart()   // "abc  "

(2)trimEnd()

trimEnd() 方法的的行为与trim()一致,不过会返回一个从原始字符串的结尾删除了空白的新字符串,不会修改原始字符串:

const s = '  abc  ';

s.trimEnd()   // "  abc"

注意,这两个方法都不适用于null、undefined、Number类型。

2. flat()和flatMap()

(1)flat()

在ES2019中,flat()方法用于创建并返回一个新数组,这个新数组包含与它调用flat()的数组相同的元素,只不过其中任何本身也是数组的元素会被打平填充到返回的数组中:

[1, [2, 3]].flat()        // [1, 2, 3]
[1, [2, [3, 4]]].flat()   // [1, 2, [3, 4]]

在不传参数时,flat()默认只会打平一级嵌套,如果想要打平更多的层级,就需要传给flat()一个数值参数,这个参数表示要打平的层级数:

[1, [2, [3, 4]]].flat(2)  // [1, 2, 3, 4]

如果数组中存在空项,会直接跳过:

[1, [2, , 3]].flat());    //  [1, 2, 3]

如果传入的参数小于等于0,就会返回原数组:

[1, [2, [3, [4, 5]]]].flat(0);    //  [1, [2, [3, [4, 5]]]]
[1, [2, [3, [4, 5]]]].flat(-10);  //  [1, [2, [3, [4, 5]]]]

(2)flatMap()

flatMap()方法使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 和连着深度值为1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。该方法会返回一个新的数组,其中每个元素都是回调函数的结果,并且结构深度 depth 值为1。

[1, 2, 3, 4].flatMap(x => x * 2);      //  [2, 4, 6, 8]
[1, 2, 3, 4].flatMap(x => [x * 2]);    //  [2, 4, 6, 8]

[1, 2, 3, 4].flatMap(x => [[x * 2]]);  //  [[2], [4], [6], [8]]
[1, 2, 3, 4].map(x => [x * 2]);        //  [[2], [4], [6], [8]]

3. Object.fromEntries()

Object.fromEntries()方法可以把键值对列表转换为一个对象。该方法相当于 Object.entries() 方法的逆过程。 Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,而Object.fromEntries() 方法把键值对列表转换为一个对象。

const object = { key1: 'value1', key2: 'value2' }
 
const array = Object.entries(object)  // [ ["key1", "value1"], ["key2", "value2"] ]
 
 
Object.fromEntries(array)             // { key1: 'value1', key2: 'value2' }

使用该方法主要有以下两个用途:

(1)将数组转成对象

const entries = [
  ['foo', 'bar'],
  ['baz', 42]
]
Object.fromEntries(entries)  //  { foo: "bar", baz: 42 }

(2)将 Map 转成对象

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])
Object.fromEntries(entries)  //  { foo: "bar", baz: 42 }

4. Symbol描述

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

let dog = Symbol("dog");  // dog 为描述 

在 ES2019 之前,获取一个 Symbol 值的描述需要通过 String 方法 或 toString 方法:

String(dog);              // "Symbol(dog)" 
dog.toString();           // "Symbol(dog)" 

ES2019 补充了属性 description,用来直接访问描述

dog.description;  // dog

5. toString()

ES2019 对函数的 toString() 方法进行了扩展,以前这个方法只会输出函数代码,但会省略注释和空格。ES2019 的 toString()则会保留注释、空格等,即输出的是原始代码:

function sayHi() {
  /* dog */
  console.log('wangwang');
}

sayHi.toString();  // 将输出和上面一样的原始代码

6. catch

在 ES2019 以前,catch 会带有参数,但是很多时候 catch 块是多余的。而现在可以不带参数:

// ES2019 之前
try {
   ...
} catch(error) {
   ...
}

// ES2019 之后
try {
   ...
} catch {
   ...
}

六、ES11 新特性(2020)

1. BigInt

在 JavaScript 中,数值类型 Number 是 64 位浮点数**,所以计算精度和表示范围都有一定限制。ES2020 新增了 BigInt 数据类型,这也是 JavaScript 引入的**第八种基本类型。BigInt 可以表示任意大的整数。其语法如下:

BigInt(value);

其中 value 是创建对象的数值。可以是字符串或者整数。

在 JavaScript 中,Number 基本类型可以精确表示的最大整数是253。因此早期会有这样的问题:

let max = Number.MAX_SAFE_INTEGER;    // 最大安全整数

let max1 = max + 1
let max2 = max + 2

max1 === max2   // true

有了BigInt之后,这个问题就不复存在了:

let max = BigInt(Number.MAX_SAFE_INTEGER);

let max1 = max + 1n
let max2 = max + 2n

max1 === max2   // false

可以通过typeof操作符来判断变量是否为BigInt类型(返回字符串"bigint"):

typeof 1n === 'bigint'; // true 
typeof BigInt('1') === 'bigint'; // true 

还可以通过Object.prototype.toString方法来判断变量是否为BigInt类型(返回字符串"[object BigInt]"):

Object.prototype.toString.call(10n) === '[object BigInt]';    // true

注意,BigInt 和 Number 不是严格相等的,但是宽松相等:

10n === 10 // false 
10n == 10  // true 

Number 和 BigInt 可以进行比较:

1n < 2;    // true 
2n > 1;    // true 
2 > 2;     // false 
2n > 2;    // false 
2n >= 2;   // true

2. 空值合并运算符(??)

在编写代码时,如果某个属性不为 null 和 undefined,那么就获取该属性,如果该属性为 null 或 undefined,则取一个默认值:

const name = dogName ? dogName : 'default'; 

可以通过 || 来简化:

const name =  dogName || 'default'; 

但是 || 的写法存在一定的缺陷,当 dogName 为 0 或 false 的时候也会走到 default 的逻辑。所以 ES2020 引入了 ?? 运算符。只有 ?? 左边为 null 或 undefined时才返回右边的值:

const dogName = false; 
const name =  dogName ?? 'default';  // name = false;

3. 可选链操作符(?.)

在开发过程中,我们经常需要获取深层次属性,例如 system.user.addr.province.name。但在获取 name 这个属性前需要一步步的判断前面的属性是否存在,否则并会报错:

const name = (system && system.user && system.user.addr && system.user.addr.province && system.user.addr.province.name) || 'default';

为了简化上述过程,ES2020 引入了「链判断运算符」?.,可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为null 或 undefined 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。

const name = system?.user?.addr?.province?.name || 'default';

当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。

可选链有以下三种形式:

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()

在使用TypeScript开发时,这个操作符可以解决很多问题。

七、ES12 新特性(2021)

1. String.prototype.replaceAll()

replaceAll()方法会返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉,替换规则可以是字符串或者正则表达式。

let string = 'hello world, hello ES12'
string.replace(/hello/g,'hi')    // hi world, hi ES12
string.replaceAll('hello','hi')  // hi world, hi ES12

注意的是,replaceAll 在使用正则表达式的时候,如果非全局匹配(/g),会抛出异常:

let string = 'hello world, hello ES12'
string.replaceAll(/hello/,'hi') 
// Uncaught TypeError: String.prototype.replaceAll called with a non-global

2. 数字分隔符

数字分隔符可以在数字之间创建可视化分隔符,通过 _ 下划线来分割数字,使数字更具可读性,可以放在数字内的任何地方:

const money = 1_000_000_000
//等价于
const money = 1000000000

该新特性同样支持在八进制数中使用:

const number = 0o123_456
//等价于
const number = 0o123456

3. Promise.any

Promise.any是是 ES2021 新增的特性,它接收一个 Promise 可迭代对象(例如数组),只要其中的一个 promise 成功,就返回那个已经成功的 promise 如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和 AggregateError 类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
]

Promise.any(promises).then((value) => {
  console.log('value: ', value)
}).catch((err) => {
  console.log('err: ', err)
})

// 输出结果:value:  result

如果所有传入的 promises 都失败:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
]

Promise.any(promises).then((value) => {
  console.log('value:', value)
}).catch((err) => {
  console.log('err:', err)
  console.log(err.message)
  console.log(err.name)
  console.log(err.errors)
})

输出结果:

err:AggregateError: All promises were rejected
All promises were rejected
AggregateError
["ERROR A", "ERROR B", "ERROR C"]