ES6 到 ES12 特性总结

ES6

ES6(es2015)是一次比较重大的革新,比起过去的版本,改动比较大,仅对常用 API 及语法糖进行讲解

1. let 和 const

ES6之前,js只有var一种声明方式,要实现块级作用域,常规操作是用闭包来防止变量泄露。但是在ES6之后,多了letconst两种方式。用var声明的变量没有块级作用域,而letconst都是块级作用域,这三个关键字的区别主要如下:

{
  var a = 10;
  let b = 20;
  const c = 30;
}

a; // 10
b; // UncaughtReferenceError: b is not defined
c; // UncaughtReferenceError: c is not defined

let d = 40;
const e = 50;
d = 60;
d; // 60
e = 70; // Uncaught TypeError: Assignment to constant variable
功能说明 var let const
变量提升
全局变量
重复声明
重新赋值
暂时性死区
块级作用域
只声明不初始化

引申: let 编译成 ES5 之后是如何保持块级作用域的?

首先是常用的 ES6 写法:

const result = [];
(function() {
  for (let i = 0; i < 5; i++) {
    result.push(function() {
      console.log(i);
    });
  }
})();
result.forEach(function(item) {
  item();
}); // => 0,1,2,3,4

经过babel转义后,这里提供一个在线转化地址:

"use strict";

var result = [];
(function() {
  var _loop = function _loop(i) {
    result.push(function() {
      console.log(i);
    });
  };

  for (var i = 0; i < 5; i++) {
    _loop(i);
  }
})();
result.forEach(function(item) {
  item();
});

从上面的代码我们就可以看出,let 创建作用域的方式,其实就是创建了一个函数,在函数内定义一个同名变量并于外部将这个变量传入其中,以此达到创建作用域的目的。

引申: let 变量无法声明提升是如何实现的?

首先是一段熟悉的 ES6 代码:

console.log(a); // undefined
var a = 1;

console.log(b); // VM233:4 Uncaught ReferenceError: b is not defined
let b = 2;

经过 babel 编译后的代码:

"use strict";

console.log(a); // undefined
var a = 1;

console.log(b); // undefined
var b = 2;

按照babel编译后理论上 b 的输出值应该和 a 一样是一个undefined,但是实际上它并不会编译通过,这个逻辑不是由 babel 来控制的,目前看来是浏览器内部 JS 执行引擎支持和实现的。

引申: 上述说到的暂时性死区是个什么东西?

只要块级作用域内存在let / const命令,它所声明的变量 / 常量就“绑定”(binding)这个区域,不再受外部的影响。ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域,凡是在声明之前就使用这些变量,就会报错。这种特性也被成为暂时性死区

var tmp = 123;

if (true) {
  tmp = "abc"; // ReferenceError: tmp is not defined
  let tmp;
}

再来看看babel编译后的代码:

"use strict";

var tmp = 123;

if (true) {
  _tmp = "abc";
  var _tmp = undefined;
}

同样,这个特性也是被浏览器内部的 JS 执行引擎支持和实现的,babel 无法支持这种特性的编译,只能简单的将 let 编译成 var

但是有意思的是,由于 letif 块中是可以构建自己的独立作用域的,babeltmp 这个变量换了个名字来模拟实现块级作用域的创建。

2. 类(Class)

在 ES6 之前构造类的方式都是与原型链相关的,在 ES6 出现了class关键字用来构造一个类。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  information() {
    return `my name is ${this.name}, I am ${this.age} years old`;
  }
}

3. 箭头函数

es6 之前的函数中this的指向都是跟函数运行时的执行环境有关的,使用箭头函数的时候 this 指向跟函数定义时的执行环境有关(this是继承自父执行上下文)。并且箭头函数语法更简洁,没有自己的thisargumentssuper等。

// es5
var list = [1, 2, 3, 4, 5, 6, 7];
var newList = list.map(function(item) {
  return item * item;
});
// es6
const list = [1, 2, 3, 4, 5, 6, 7];
const newList = list.map((item) => item * item);

// es5 function
var a = 11;
var obj = {
  a: 22,
  say: function() {
    console.log(this.a);
  },
};
obj.say(); // 22 this指向运行时的obj对象

// 箭头函数
var a = 11;
var obj = {
  a: 22,
  say: () => {
    console.log(this.a);
  },
};
obj.say(); // 11 箭头函数的this指向obj所在的环境
var a = 11;
function test1() {
  this.a = 22;
  let b = function() {
    console.log(this.a);
  };
  b();
}
var x = new test1(); // 11

var a = 11;
function test2() {
  this.a = 22;
  let b = () => {
    console.log(this.a);
  };
  b();
}
var x = new test2(); // 22

TIP

简单来说就是,箭头函数的 this 指向定义时的上下文环境(继承自父执行上下文),普通函数的 this 指向运行的上下文环境。

另外,箭头函数是不可以当构造函数的,也就是不能通过 new 操作符进行操作,否则会报错。

因为箭头函数本身没有自己的 this,也没有arguments对象,因此call()apply()bind()这些方法去改变 this 指向堆兼有函数也是无效的。

4. 函数默认参数

在 es6 之前,如果我们需要定义函数的初始参数,需要这么写:

// es5
function config(data) {
  data = data || "data is empty";
  // 如果参数的布尔值为fasle的时候就有问题  config(0)
}
// es6
const config = (data = "data is empty") => {};

5. 模板字符串

在 ES6 之前,拼接字符串的话都需要+

var name = "kirs";
var age = 24;
var info = "my name is" + name + ", I am " + age + " years old";

// es6
const name = "kirs";
const age = 24;
const info = `my name is ${name}, I am ${age} years old`;

6. 解构赋值

我们通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量

// es5
var a = 10;
var b = 20;
var temp = a;
a = b;
b = temp;
// es6
let a = 10;
let b = ((20)[(a, b)] = [b, a]);

7. 模块化

在 ES6 之前,js 并没有模块化的该你啊。也只有社区定制的类似 Commonjs 和 AMD 之类的规则

// circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;

// index.js
const circle = require("./circle.js");
console.log(`半径为2的圆面积是${circle.area(2)}`);

// circle.js
const { PI } = Math;
export const area = (r) => PI * r ** 2;
export const circumference = (r) => 2 * PI * r;

// index.js
import { area } from "./circle.js";
console.log(`半径为2的圆面积是${area(2)}`);

8.扩展操作符(Spread operator)

扩展操作符可以在调用函数/数组构造时,将表达式或者字符串在语法层面展开;还可以在构造字面量对象时,将对象表达式按照key-value方式展开。

function sum(x, y, z) {
  return x + y + z;
}
var list = [5, 6, 7];
var total = sum.apply(null, list);

// es6
const sum = (x, y, z) => x + y + z;
const list = [5, 6, 7];
const total = sum(...list);

WARNING

扩展运算符只能适用于那鞋布置了迭代器的对象(字符串,数组等)

const obj = {
  id: 112233,
};
const array = [...obj]; // TypeError: obj is not iterable

9. 对象简写属性

在 ES6 之前对某个同名元素进行变量赋值需要重写一遍,而在 ES6 之后可以简写

var cat = "Tom";
var mouse = "Jerry";

var obj = {
  cat: cat,
  mouse: mouse,
};
// es6

const cat = "Tom";
const mouse = "Jerry";

const obj = {
  cat,
  mouse,
};

10. Promise

Promise是 ES6 提供的一种异步解决方案,比回调函数更加清晰明了。

Promise总共有 3 种状态:

  1. 等待中(pending)
  2. 完成了(resolved)
  3. 拒绝了(rejected)

WARNING

Promise一旦从等待状态变成了其他状态就永远不能改变状态了

new Promise((resolve, reject) => {
  resolve("success");
  // 无效
  reject("reject");
});

TIP

当我们在构造Promise的时候,构造函数内部的代码是立即执行的。

new Promise((resolve, reject) => {
  console.log("new promise");
  resolve("success");
});
console.log("finish");
// new promise -> finish

Promise实现了链式调用,也就是说每次调用then之后返回的都是一个Promise,并且是一个全新的Promise,原因是因为状态不可变。如果你在then中使用了return,那么return的值会被Promise.resolve()包裹。

Promise.resolve(1)
  .then((res) => {
    console.log(res); // 1
    return 2; // Promise.resolve(2)
  })
  .then((res) => {
    console.log(res); // 2
  });

11. for...of 语句

for...of语句在可迭代对象(array, Map, Set, String, TypedArray, arguments对象等)上创建一个迭代循环,调用自定义迭代钩子并为每个不同属性的值执行语句。

const array1 = ["a", "b", "c"];

for (const element of array1) {
  console.log(element);
  // a b c
}

12. Symbol

Symbol是 ES6 出现的一种基本数据类型,symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法暴露全局的 symbol 注册,且类似于内建对象类。

每个Symbol()返回的symbol值都是唯一的。当参数为对象时,将调用对象的 toString()方法。

TIP

一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol("foo");
const symbol4 = Symbol("foo");
const symbol5 = Symbol({ name: "Lucy" }); // Symbol([object Object])

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol('foo')"
console.log(symbol3 === sumbol4); // false

如果我们想创造两个相等的 Symbol 变量,可以使用Symbol.for(key)

Symbol.for(key)使用给定的 key 搜索现有的 symbol,如果找到则返回该 symbol。否则将使用给定的 key 在全局 symbol 注册表中创建一个新的 symbol。

const symbol1 = Symbol.for("foo");
const symbol2 = Symbol.for("foo");

console.log(symbol1 === symbol2);

WARNING

这里需要注意一点,我们需要使用 Symbol()函数创建 symbol 变量,并非使用构造函数,使用 new 操作符会直接报错。

new Symbol(); // Uncaught TypeError: Symbol is not a constructor

当使用 Symbol 作为对象属性时,可以保证对象不会出现重名属性,调用for...in不能将其枚举出来,另外调用Object.getOwnPropertyNamesObject.keys()也不能获取 Symbol 属性。

var obj = {
  name: "Lucy",
  [Symbol("name")]: "jack",
};
Object.getOwnPropertyNames(obj); // ["name"]
Object.keys(obj); // ["name"]
for (var i in obj) {
  console.log(i); // name
}
Object.getOwnPropertySymbols(obj); // [Symbol(name)]

Symbol 的应用场景:

  • 防止 XSS:

ReactReactElement对象中,有一个$$typeof属性,它是一个 Symbol 类型的变量:

const REACT_ELEMENT_TYPE =
  (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) ||
  0xeac7;

ReactElement.isValidElement函数用来判断一个 React 组件是否是有效的,下面是它的具体实现。

ReactElement.isValidElement = function(object) {
  return (
    typeof object === "object" &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
};

React渲染时会把没有$$typeof标识,以及规则校验不通过的组件过滤掉。

如果你的服务器有一个漏洞,允许用户存储任意 JSON 对象, 而客户端代码需要一个字符串,这可能会成为一个问题:

// JSON
let expectedTextButGotJSON = {
  type: "div",
  props: {
    dangerouslySetInnerHTML: {
      __html: "/* put your exploit here */",
    },
  },
};
let message = { text: expectedTextButGotJSON };
<p>{message.text}</p>;

JSON中不能存储Symbol类型的变量,这就是防止XSS的一种手段。

  • 私有属性

借助Symbol类型的不可枚举,我们可以在类中模拟私有属性,控制变量读写:

const privateField = Symbol();
class myClass {
  constructor() {
    this[privateField] = "ConardLi";
  }
  getField() {
    return this[privateField];
  }
  setField(val) {
    this[privateField] = val;
  }
}
  • 防止属性污染

在某些情况下,我们可能要为对象添加一个属性,此时就有可能造成属性覆盖,用 Symbol 作为对象属性可以保证永远不会出现同名属性。

例如我们模拟实现一个 call 方法:

Function.prototype.myCall = function(context) {
  if (typeof this !== "function") {
    return undefined; // 用于防止 Function.prototype.myCall() 直接调用
  }
  context = context || window;
  const fn = Symbol();
  context[fn] = this;
  const args = [...arguments].slice(1);
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

我们需要在某个对象上临时调用一个方法,又不能造成属性污染,Symbol 是一个很好的选择。

13. 迭代器与生成器

迭代器(Iterator)是一种迭代的机制,为各种不同的数据结构提供一种统一的访问方式。任何数据结构只要内部有 Iterator 接口,就可以完成依次迭代的操作。

一旦创建,迭代器对象可以勇敢重复调用next()显示地迭代,从而获取该对象每一级的值,直到迭代完,返回{value: undefined, done:true}

生成器函数使用function*语法编写,最初调用的时候,生成器不执行任何的代码,而是返回一种成为Generator的迭代器。通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。

TIP

可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次

function* makeRangeInterator(start = 0, end = Infinity, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

const a = makeRangeInterator(1, 10, 2);
a.next(); // {value: 1, done: false}
a.next(); // {value: 3, done: false}
a.next(); // {value: 5, done: false}
a.next(); // {value: 7, done: false}
a.next(); // {value: 9, done: false}
a.next(); // {value: undefined, done: true}

14. Set 与 WeakSet

Set对象允许你存储任何类型的唯一值,无论是原始值还是对象引用。

可以通过 Set 进行数组去重.

const arr = [1, 1, 2, 3, 4, 5, 6, 4];
const newArr = [...new Set(arr)]; // 1,2,3,4,5,6

WeakSet结构与Set结构类似,但是有如下两点区别:

  1. WeakSet对象只能存放对象引用,不能存放值,而Set结构都可以.
  2. WeakSet对象中存储的对象值都是被弱引用的,如果没有其他的变量或属性引用这个对象值,则这个对象值会被当成垃圾回收掉。正因为这样,WeakSet对象是无法被枚举的,没有办法拿到它所包含的所有元素。
var ws = new WeakSet();
var obj = {};
var foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo); // false 对象foo没有被添加

ws.delete(window);
ws.has(window); // false

wx.clear(); // 清空整个WeakSet对象

15. Map 与 WeakMap

Map对象保存的是建/值对的集合,任何值(对象或者原始值)都可以作为一个键或者一个值。

var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN); // not a number

var otherNaN = Number("foo");
myMap.get(otherNaN); // not a number

WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值是可以任意的。

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap(),

var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "aa");
wm2.set(o1, o2);
wm2.set(o3, undefined);
wm2.set(wm1, wm2);
wm1.get(o2) // aa
wm2.get(o2) // undefined
wm2.get(o3) // undefined

wm1.has(o2) // true
wm2.has(o2) // false
wm2.has(o3) // true

wm3.set(o1, 37);
wm3.get(o1) // 37
wm3.clear()
wm3.get(o1) // undefined,w3为空

wm1.has(o1) // true
wm1.delete(o1)
wm1.has(o1) // false

16. Proxy 与 Reflect

Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

Reflect是一个内置的对象,它提供拦截javascript操作的方法。这些方法与Proxy的方法相同。Reflect不是一个函数对象,因此它是不可构造的。ProxyReflect是非常完美的配合。

const observe = (data, callback) => {
  return new Proxy(data, {
    get(target, key) {
      return Reflect.get(target, key);
    },
    set(target, key, value, proxy) {
      callback(key, value);
      target[key] = value;
      return Reflect.set(target, key, value, proxy);
    },
  });
};
const FooBar = {
  open: false,
};
const FooBarObserver = observe(FooBar, (property, value) => {
  property === "open" && value
    ? console.log("FooBar is open")
    : console.log("FooBar is closed");
});

console.log(FooBarObserver.open); // false
FooBarObserver.open = true; // true

WARNING

如果对象带有configurable:false 或者writable: false属性,则代理失效。

17. Regex 对象的扩展

18. Math 对象的扩展

  • Number.parseInt() 返回转化值的整数部分
  • Number.parseFloat() 返回转换值的浮点数部分
  • Number.isFinite() 是否为有限数值
  • Number.isNaN() 是否为 NaN
  • Number.isinteger() 是否为整数
  • Math.trunc() 返回数值整数部分
  • Math.sign() 返回数值类型(正数 1、负数-1、零 0)
  • Math.imul(x,y) 返回两个数值相乘

19. Array 对象的扩展

  • Array.from() 转化具有Iterator接口的数据结构为真正的数组,返回新数组
Array.from("foo"); // ["f", "o", "o"]
  • Array.of() 转化一组值为真正的数组,返回新数组
Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]
  • Array.copyWithin(target, start, end) 把指定位置的成员复制到其他位置,返回原数组
const arr1 = [1, 2, 3, 4, 5];
arr1.copyWithin(0, 3, 4);
//  [4, 2, 3, 4, 5]
  • Array.find() 返回第一个符合条件的成员
const arr = [5, 12, 18, 130];
arr.find((item) => item > 10); // 12
  • Array.findIndex() 返回第一个符合条件的成员的索引值
const arr = [5, 12, 18, 130];
arr.findIndex((item) => item > 10); // 1
  • Array.fill(value, start, end) 根据指定的值填充整个数组
const arr = [1, 2, 3, 4];
arr.fill(1); // [1, 1, 1, 1]
  • Array.keys() 返回以索引值为遍历器的对象
const arr = [1, 2, 3, 4];
const iterator = arr.keys();

for (const key of iterator) {
  console.log(key); // 0 1 2 3
}
  • Array.values() 返回以属性值为遍历器的对象
const arr = [1, 2, 3, 4];
const iterator = arr.values();

for (const key of iterator) {
  console.log(key); // 1 2 3 4
}
  • Array.entries() 返回以索引值和属性值为遍历器的对象
const arr = [1, 2, 3, 4];
const iterator = arr.entries();

console.log(iterator.next().value); // [0: 1]
console.log(iterator.next().value); // [1: 2]

引申: const 声明变量的规则是怎么样的?

我们已经知道了const在声明变量的时候就必须要进行赋值,一但 const 变量被定义,后面就不能对其进行修改。

WARNING

这里还需要注意的一点是:

const 实际保证的是常量空间存储的数据不可被改变,而常量对应的值可以被改变。比如说我们定义了一个对象:

const student = {
    name: 'jack',
    age: 24
};

// 我们可以对值进行修改
student.age = 25;
student.gender = 'male';

// 但是我们不可以改变其存储的地址 下面的语法就会报错
student = {
    name: 'jack',
    age: 25,
    gender: 'male',
};

ES7

1.Array.prototype.includes()

用来判断一个数组是否包含一个指定的值。根据情况,如果包含则返回true,否则返回false

const arr = [1, 2, 3];
arr.includes(1); // true
arr.includes(4); // false

2.幂运算符**

幂运算符**具有与Math.pow()一样的功能。

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

ES8

1. asyncawait

asyncawait能够解决promise嵌套过多带来的问题,可以简化代码,让异步代码看起来像同步代码

// promise
fetch("coffee.jpg")
  .then((res) => res.blob())
  .then((myBlob) => {
    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement("img");
    image.src = objectURL;
    document.body.appendChild(image);
  })
  .catch((err) => {
    console.log("error: " + err.message);
  });
// async和await

async function myFetch() {
  let response = await fetch("coffee.jpg");
  let myBlob = await response.blob();

  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement("img");
  image.src = objectURL;
  document.body.appendChild(image);
}

myFetch();

2.Object.values()

返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。

const object1 = {
  a: "somestring",
  b: 42,
  c: false,
};
console.log(Object.values(object1)); // ["somestring", 42, false]

3.Object.entries()

返回一个给定对象自身可枚举属性的键值对数组。

const object1 = {
  a: "somestring",
  b: 42,
};

for (let [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}

// "a: somestring"
// "b: 42"

4.padStart()

用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。填充从当前字符串的开始(左侧)应用的。

const str1 = "5";
console.log(str1.padStart(2, "0")); // "05"

const fullNumber = "2034399002125581";
const last4Digits = fullNumber.slice(-4);
const maskedNumber = last4Digits.padStart(fullNumber.length, "*");
console.log(maskedNumber); // "************5581"

5.padEnd()

用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。

const str1 = "Breaded Mushrooms";
console.log(str1.padEnd(25, ".")); // "Breaded Mushrooms........"
const str2 = "200";
console.log(str2.padEnd(5)); // "200  "

Object.getOwnPropertyDescriptors()

用来获取一个对象的所有自身属性的描述符。

const object1 = {
  property1: 42,
};

const descriptors1 = Object.getOwnPropertyDescriptors(object1);

console.log(descriptors1.property1.writable); // true

console.log(descriptors1.property1.value); // 42

// 浅拷贝一个对象
Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

// 创建子类
function superclass() {}
superclass.prototype = {
  // 在这里定义方法和属性
};
function subclass() {}
subclass.prototype = Object.create(
  superclass.prototype,
  Object.getOwnPropertyDescriptors({
    // 在这里定义方法和属性
  })
);

ES9

1.for await...of

for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 StringArrayArray-like 对象(比如arguments 或者NodeList),TypedArrayMapSet和自定义的异步或者同步可迭代对象。其会调用自定义迭代钩子,并为每个不同属性的值执行语句。

async function* asyncGenerator() {
  var i = 0;
  while (i < 3) {
    yield i++;
  }
}

(async function() {
  for await (num of asyncGenerator()) {
    console.log(num);
  }
})();
// 0
// 1
// 2

2.正则表达式反向(lookbehind)断言

在 ES9 之前,JavaScript 正则表达式,只支持正向断言。正向断言的意思是:当前位置后面的字符串应该满足断言,但是并不捕获。例子如下:

"fishHeadfishTail".match(/fish(?=Head)/g); // ["fish"]

3.正则表达式 Unicode 转义

4.正则表达式 s/dotAll 模式

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

6.对象扩展操作符

ES6 中添加了数组的扩展操作符,让我们在操作数组时更加简便,美中不足的是并不支持对象扩展操作符,但是在 ES9 开始,这一功能也得到了支持,例如:

var obj1 = { foo: "bar", x: 42 };
var obj2 = { foo: "baz", y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

7.Promise.prototype.finally()

finally()方法会返回一个Promise,当promise的状态变更,不管是变成rejected或者fulfilled,最终都会执行finally()的回调。

fetch(url)
  .then((res) => {
    console.log(res);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("结束");
  });

ES10

1.Array.prototype.flat() / flatMap()

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

flatMap()map() 方法和深度为 1 的 flat() 几乎相同.,不过它会首先使用映射函数映射每个元素,然后将结果压缩成一个新数组,这样效率会更高。

var arr1 = [1, 2, 3, 4];

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

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

// 深度为1
arr1.flatMap((x) => [[x * 2]]); // [[2], [4], [6], [8]]

flatMap()可以代替reduce()concat(),例子如下:

var arr = [1, 2, 3, 4];
arr.flatMap((x) => [x, x * 2]); // [1, 2, 2, 4, 3, 6, 4, 8]
// 等价于
arr.reduce((acc, x) => acc.concat([x, x * 2]), []); // [1, 2, 2, 4, 3, 6, 4, 8]

但这是非常低效的,在每次迭代中,它创建一个必须被垃圾收集的新临时数组,并且它将元素从当前的累加器数组复制到一个新的数组中,而不是将新的元素添加到现有的数组中。

2.String.prototype.trimStart() / trimLeft() / trimEnd() / trimRight()

在 ES5 中,我们可以通过trim()来去掉字符首尾的空格,但是却无法只去掉单边的,但是在 ES10 之后,我们可以实现这个功能。

const Str = "   Hello world!  ";
console.log(Str); // '   Hello world!  '
console.log(Str.trimStart()); // 'Hello world!  '
console.log(Str.trimLeft()); // 'Hello world!  '
console.log(Str.trimEnd()); // '   Hello world!'
console.log(Str.trimRight()); // '   Hello world!'

WARNING

不过这里有一点要注意的是,trimStart()trimEnd()才是标准方法,trimLeft()trimRight()只是别名。

3.Object.fromEntries()

把键值对列表转换为一个对象,它是Object.entries()的反函数。

const entries = new Map([
  ["foo", "bar"],
  ["baz", 42],
]);

const obj = Object.fromEntries(entries);

console.log(obj); // Object { foo: "bar", baz: 42 }

4.String.prototype.matchAll

matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。并且返回一个不可重启的迭代器。例子如下:

var regexp = /t(e)(st(\d?))/g
var str = 'test1test2'

str.match(regexp) // ['test1', 'test2']
str.matchAll(regexp) // RegExpStringIterator {}
[...str.matchAll(regexp)] // [['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4], ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]]

5.BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。

BigInt 可以表示任意大的整数。可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()。 在以往的版本中,我们有以下的弊端:

// 大于2的53次方的整数,无法保持精度
2 ** 53 === 2 ** 53 + 1;
// 超过2的1024次方的数值,无法表示
2 ** 1024; // Infinity

TIP

BigIntNumber不是严格相等的,但是宽松相等的。

6.globalThis

globalThis属性包含类似于全局对象 this值。所以在全局环境下,我们有:

globalThis === this; // true

ES11

1.Promise.allSettled

在之前使用Promise.all的时候,如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise直接进入 reject 状态。

当我们在一个页面中并发请求 3 块区域的数据的时候,如果其中一个接口挂了,这将导致页面的数据全都无法渲染出来,这是我们无法接受的。

Promise.all([
  Promise.reject({
    code: 500,
    msg: "服务异常",
  }),
  Promise.resolve({
    code: 200,
    list: [],
  }),
  Promise.resolve({
    code: 200,
    list: [],
  }),
])
  .then((res) => {
    // 如果其中一个任务是 reject,则不会执行到这个回调。
    doSomething(res);
  })
  .catch((error) => {
    // 本例中会执行到这个回调
    // error: {code: 500, msg: "服务异常"}
  });

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

Promise.allSettled([
    Promise.reject({code: 500, msg:'服务异常'}),
    Promise.resolve({ code: 200, list: []}),
    Promise.resolve({code: 200, list: []})])
]).then((res) => {
    /*
        0: {status: "rejected", reason: {…}}
        1: {status: "fulfilled", value: {…}}
        2: {status: "fulfilled", value: {…}}
    */
    // 其他业务过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
    RenderContent(res.filter((el) => {
        return el.status !== 'rejected';
    }));
});

2.可选链(Optional chaining)

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

// 假设有一个user对象
const name = props && props.user && props.user.info && props.user.info.name;

// 使用可选链
const name = props?.user?.info?.name;

TIP

可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段。

项目中需要支持的话 需要配置babel转换

npm install @babel/plugin-proposal-optional-chaining --dev

3.空值合并运算符(Nullish coalescing Operator)

当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值:

const level = (user.data && user.data.level) || "暂无等级";

如果说用户的等级本身就是 0 级的话,在上述的情况下就会被转化为"暂无等级"。

// 使用空值合并
const level = `${user?.data?.level}` ?? "暂无等级";

ES12

  • 2021.02.20

ES2021 也就是 2021 年的 ECMAScript 版本。ES2021 并没有像 ES2015 那样提出很多新特性,但却合并了一些有用的特性。

以下是 ES2021 的新特性概览:

  • String.prototype.replaceAll
  • Promise.any
  • WeakRef
  • 逻辑赋值运算符
  • 数字分隔符

String.protype.replaceAll

在 ES2021 之前,要替换掉一个字符串中的所有指定字符,我们可以这么做:

const fruits = "🍎+🍐+🍓+";
const fruitsWithBanana = fruits.replace(/\+/g, "🍌");
console.log(fruitsWithBanana); //🍎🍌🍐🍌🍓🍌

ES2021 则提出了replaceAll方法,并将其挂载在 String 的原型上,可以这么用:

const fruits = "🍎+🍐+🍓+";
const fruitsWithBanana = fruits.replaceAll("+", "🍌");
console.log(fruitsWithBanana); //🍎🍌🍐🍌🍓🍌

Promise.any

Promise.any方法和Promise.race类似——只要给定的迭代中的一个 promise 成功,就采用第一个 promise 的值作为它的返回值,但与Promise.race的不同之处在于——它会等到所有 promise 都失败之后,才返回失败的值:

const myFetch = (url) =>
  setTimeout(() => fetch(url), Math.floor(Math.random() * 3000));
const promises = [
  myFetch("/endpoint-1"),
  myFetch("/endpoint-2"),
  myFetch("/endpoint-3"),
];
// 使用 .then .catch
Promise.any(promises) // 任何一个 promise 成功。
  .then(console.log) // 比如 ‘3’
  .catch(console.error); // 所有的 promise 都失败了
// 使用 async-await
try {
  const first = await Promise.any(promises); // 任何一个 promise 成功返回。
  console.log(first);
} catch (error) {
  // 所有的 promise 都失败了
  console.log(error);
}

WeakRef

WeakRef 提案主要包含两个新功能:

  • 可以通过WeakRef类来给某个对象创建一个弱引用
  • 可以通过FinalizationRegistry类,在某个对象被垃圾回收之后,执行一些自定义方法

上述两个新功能可以同时使用,也可以单独使用,取决于你的需求。一个 WeakRef 对象包含一个对于某个对象的弱引用,被称为目标引用

通过弱引用一个对象,可以让该对象在没有其它引用的情况下被垃圾回收机制回收。WeakRef 主要用来缓存映射一些大型对象,当你希望某个对象在不被其它地方引用的情况下及时地被垃圾回收,那么你就可以使用它。

function toogle(element) {
  const weakElement = new WeakRef(element);
  let intervalId = null;

  function toggle() {
    const el = weakElement.deref();
    if (!el) {
      return clearInterval(intervalId);
    }
    const decoration = weakElement.style.textDecoration;
    const style = decoration === "none" ? "underline" : "none";
    decoration = style;
  }
  intervalId = setInterval(toggle, 1000);
}
const element = document.getElementById("link");
toogle(element);
setTimeout(() => element.remove(), 10000);

FinalizationRegistry接收一个注册器回调函数,可以利用该注册器为指定对象注册一个事件监听器,当这个对象被垃圾回收之后,会触发监听的事件,具体步骤如下。首先,创建一个注册器:

const registry = new FinalizationRegistry((heldValue) => {
  // ....
});

接着注册一个指定对象,同时也可以给注册器回调传递一些参数:

registry.register(theObject, "some value");

逻辑赋值运算符

逻辑赋值运算符结合了逻辑运算符和赋值表达式。逻辑赋值运算符有两种:

  • 或等于(||=)
  • 且等于(&&=)
// 或等于
|   a   |   b   | a ||= b | a (运算后) |
| true  | true  |   true  |        true         |
| true  | false |   true  |        true         |
| false | true  |   true  |        true         |
| false | false |   false |        false        |
a ||= b
// 等同于:
a || (a = b);

// 且等于
|   a   |   b   | a &&= b | a (运算后) |
| true  | true  |   true  |        true         |
| true  | false |   false |        false        |
| false | true  |   false |        false        |
| false | false |   false |        false        |
a &&= b
// 等同于:
a && (a = b);

数字分隔符

通过这个功能,我们利用 “(_,U+005F)” 分隔符来将数字分组,提高数字的可读性:

1_000_000_000; // 十亿
101_475_938.38; // 亿万

const amount = 12345_00; // 12,345
const amount = 123_4500; // 123.45 (保留 4 位小数)
const amount = 1_234_500; // 1,234,500

0.000_001; // 百万分之一
1e10_000; // 10^10000

const binary_literals = 0b1010_0001_1000_0101;
const hex_literals = 0xa0_b0_c0;
const bigInt_literals = 1_000_000_000_000n;
const octal_literal = 0o1234_5670;
上次更新时间: 2021-06-25 09:36:00