【译文】ES2016--ES2019你需要知道的一切

768 阅读14分钟

原文:Everything you need to know from ES2016 to ES2019
作者:Alberto Montalesi

JavaScript是一种不断发展的语言,在过去的几年里,ECMAScript 规范中增加了许多新的特性。 本文是我的书《现代 JavaScript 完整指南》的节选,它涵盖了 ES2016ES2017ES2018ES2019的新增内容。

ES2016 中的新事物

ES2016只引入了两个新特性:

  • Array.prototype.includes()
  • 指数运算符

Array.prototype.includes()

如果数组包含某个元素,includes ()方法将返回 true; 如果数组不包含某个元素,则返回 false

let array = [1, 2, 4, 5];

array.includes(2);
// true
array.includes(3);
// false

带索引值的 includes()

我们可以用.includes()搜索从索引值开始以后的元素。 默认值是 0,但也可以传递负值。传入的第一个值是要搜索的元素,第二个是索引:

let array = [1, 3, 5, 7, 9, 11];

array.includes(3, 1);
// find the number 3 starting from array index 1
// true
array.includes(5, 4);
//false
array.includes(1, -1);
// find the number 1 starting from the ending of the array going backwards
// false
array.includes(11, -3);
// true

array.includes(5,4)返回false,因为尽管数组实际上包含数字 5,但是它是在索引 2 中找到的,而索引 4 开始的查找中没有 5,所以结果返回falsearray.includes(1,-1)返回false,数组从索引-1 开始向后查找,也就是从 11 查找,找不到结果。 array.includes(11,-3)返回true,数组从索引-3 向后查找也就是从 7 开始查找,可以找到 11,所以返回true

指数运算符

ES2016之前,我们这样做:

Math.pow(2, 2);
// 4
Math.pow(2, 3);
// 8

现在有了新的指数运算符,我们可以做如下操作:

2 ** 2;
// 4
2 ** 3;
// 8

当组合多个操作时,它会变得非常有用,比如下面这个例子:

2 ** (2 ** 2);
// 16
Math.pow(Math.pow(2, 2), 2);
// 16

使用 Math.pow () ,您需要不断地连接它们,这可能会使代码变得非常长和混乱。 指数运算符提供了一种更快、更干净的方法来做同样的事情。

ES2017 字符串填充, Object.entries(), Object.values()等

ES2017引入了许多很酷的新功能,我们将在这里看到。

字符串填充(.padStart() 及 .padEnd())

我们可以在字符串中添加一些填充,可以在结尾(.padEnd())或在开头(.padstart ())。padEnd,padstart第一个入参为字符串长度,第二个入参为需要填充的值,为空默认为空格。

"hello".padStart(6);
// " hello"
"hello".padEnd(6);
// "hello "

我们指定 6 作为填充,那么为什么在这两种情况下我们只有 1 个空格呢? 这是因为 padStartpadEnd 会填充空白。 在我们的示例中,"hello"是 5 个字母,填充是 6,这样只剩下 1 个空格。

看看这个例子:

"hi".padStart(10);
// 10 - 2 = 8 empty spaces
// "        hi"
"welcome".padStart(10);
// 10 - 6 = 4 empty spaces
// "   welcome"

使用 padStart 右对齐

如果我们想要对齐,可以使用 padStart

const strings = ["short", "medium length", "very long string"];

const longestString = strings.sort(str => str.length).map(str => str.length)[0];

strings.forEach(str => console.log(str.padStart(longestString)));

// very long string
//    medium length
//            short

首先,我们找出最长字符串,算出它的长度。 然后我们根据最长的字符串的长度对所有的字符串应用 padStart,这样就可以使所有的字符串完美地向右对齐。

向填充区域添加自定义值

不仅仅添加空白作为填充,还可以同时填充字符串和数字。

"hello".padEnd(13, " Alberto");
// "hello Alberto"
"1".padStart(3, 0);
// "001"
"99".padStart(3, 0);
// "099"

Object.entries()、Object.values()

首先创建一个 Object。

const family = {
  father: "Jonathan Kent",
  mother: "Martha Kent",
  son: "Clark Kent"
};

在以前的 JavaScript 版本中,我们会像这样访问对象内部的值:

Object.keys(family);
// ["father", "mother", "son"]
family.father;
("Jonathan Kent");

Object.keys ()只返回对象的键,然后必须使用这些键来访问这些值。

现在我们有了另外两种访问对象的方法:

Object.values(family);
// ["Jonathan Kent", "Martha Kent", "Clark Kent"]

Object.entries(family);
// ["father", "Jonathan Kent"]
// ["mother", "Martha Kent"]
// ["son", "Clark Kent"]

Object.values ()返回所有值的数组,而 Object.entry ()返回包含键和值的数组。

Object.getOwnPropertyDescriptors()

此方法返回对象自己的所有属性描述符。 它可以返回valuewritablegetsetenumerableconfigurable的属性。

const myObj = {
  name: "Alberto",
  age: 25,
  greet() {
    console.log("hello");
  }
};
Object.getOwnPropertyDescriptors(myObj);
// age:{value: 25, writable: true, enumerable: true, configurable: true}

// greet:{value: ƒ, writable: true, enumerable: true, configurable: true}

// name:{value: "Alberto", writable: true, enumerable: true, configurable: true}

在函数参数列表和调用中尾随逗号

这只是语法上的一个小小的改变。 现在,在编写对象时,我们可以在每个参数后面留一个逗号,不管它是不是最后一个参数。

// from this
const object = {
  prop1: "prop",
  prop2: "propop"
};

// to this
const object = {
  prop1: "prop",
  prop2: "propop"
};

注意我是如何在第二个属性的末尾写一个逗号的。 如果你不把它写下来,它不会抛出任何错误,但它是一个更好的实践,因为它使你的同事或团队成员更好的维护。

// 我写的
const object = {
  prop1: "prop",
  prop2: "propop"
}

// 同事新增了属性
const object = {
  prop1: "prop",
  prop2: "propop"
  prop3: "propopop"
}
// 忘记写逗号会报错。

共享内存和 Atomics 原子

来自 MDN:

当内存被共享时,多个线程可以在内存中读写相同的数据。 原子操作确保可预测的值被写入和读取,操作在下一个操作开始之前完成,操作不被中断。

Atomics 不是一个构造函数,它的所有属性和方法都是静态的(就像 Math 一样) ,因此我们不能将它与一个新的运算符一起使用,也不能将 Atomics 对象作为一个函数调用。

其方法的例子有:

  • add / sub
  • and / or / xor
  • load / store

AtomicsSharedArrayBuffer (通用的固定长度的二进制数据缓冲区)对象一起使用,这些对象表示通用的、固定长度的原始二进制数据缓冲区。

让我们来看一些原子方法的例子:

Atomics.add(), Atomics.sub(), Atomics.load() 及 Atomics.store()

Atomics.add()将接受三个参数,一个数组、一个索引和一个值,并在执行加法之前在该索引处返回先前的值。

// 创建一个 `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// 第一个赋值
uint8[0] = 10;

console.log(Atomics.add(uint8, 0, 5));
// 10

// 10 + 5 = 15
console.log(uint8[0]);
// 15
console.log(Atomics.load(uint8, 0));
// 15

正如您所看到的,调用 Atomics.add ()将在我们要定位的数组位置返回前一个值。 当我们再次调用 uint8[0]时,我们看到加法已经完成,得到了 15。

要从数组中检索特定值,我们可以使用 Atomics.load 并传递两个参数,一个数组和一个索引。

Atomics.sub ()的工作方式与 Atomics.add ()相同,但它会减去一个值。

// 创建一个 `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// 第一个赋值
uint8[0] = 10;

console.log(Atomics.sub(uint8, 0, 5));
// 10

// 10 - 5 = 5
console.log(uint8[0]);
// 5
console.log(Atomics.store(uint8, 0, 3));
// 3
console.log(Atomics.load(uint8, 0));
// 3

在这里,我们使用 Atomics.sub ()uint8[0]位置的值减去 5,这个值相当于 10-5。 与 Atomics.add ()相同,该方法将在该索引处返回先前的值,在本例中为 10。

然后,我们使用 Atomics.store ()在数组的特定索引处存储特定值,在本例中为 3,在本例中为第一个位置 0。 Atomics.store ()将返回我们刚刚传递的值,在本例中为 3。 可以看到,当我们在该特定索引上调用 Atomics.load ()时,得到的结果是 3,而不是 5。

Atomics.and(), Atomics.or() 及 Atomics.xor()

这三个方法都在数组的给定位置执行位 ANDORXOR 操作。 你可以在维基百科的 en.Wikipedia.org/wiki/bitwis… 链接上阅读更多关于按位操作的内容

ES2017 Async and Await

ES2017引入了一种处理promise的新方法,称为“ async / await”。

在我们深入研究这个新语法之前,让我们快速回顾一下我们通常如何写一个promise:

// fetch a user from github
fetch("api.github.com/user/AlbertoMontalesi")
  .then(res => {
    // return the data in json format
    return res.json();
  })
  .then(res => {
    // if everything went well, print the data
    console.log(res);
  })
  .catch(err => {
    // or print the error
    console.log(err);
  });

这是一个非常简单的promise: 从 GitHub 获取用户并将其打印到控制台。

让我们看一个不同的例子:

function walk(amount) {
  return new Promise((resolve, reject) => {
    if (amount < 500) {
      reject("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`), amount);
  });
}

walk(1000)
  .then(res => {
    console.log(res);
    return walk(500);
  })
  .then(res => {
    console.log(res);
    return walk(700);
  })
  .then(res => {
    console.log(res);
    return walk(800);
  })
  .then(res => {
    console.log(res);
    return walk(100);
  })
  .then(res => {
    console.log(res);
    return walk(400);
  })
  .then(res => {
    console.log(res);
    return walk(600);
  });

// you walked for 1000ms
// you walked for 500ms
// you walked for 700ms
// you walked for 800ms
// uncaught exception: the value is too small

让我们看看如何使用新的 async / await 语法重写这个 promise

Async and Await

function walk(amount) {
  return new Promise((resolve, reject) => {
    if (amount < 500) {
      reject("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`), amount);
  });
}

// create an async function
async function go() {
  // use the keyword `await` to wait for the response
  const res = await walk(500);
  console.log(res);
  const res2 = await walk(900);
  console.log(res2);
  const res3 = await walk(600);
  console.log(res3);
  const res4 = await walk(700);
  console.log(res4);
  const res5 = await walk(400);
  console.log(res5);
  console.log("finished");
}

go();

// you walked for 500ms
// you walked for 900ms
// you walked for 600ms
// you walked for 700ms
// uncaught exception: the value is too small

让我们来分析一下我们刚刚做了什么:

  • 创建一个带async关键字的函数
  • 关键字会告诉我们函数返回一个promise
  • 如果返回一个非promise结果,则会被包裹promise
  • await关键字只会存在async函数里
  • 顾名思义,await表示会等待结果返回

让我们看看在异步函数之外使用 await 会发生什么:

// use await inside a normal function
function func() {
  let promise = Promise.resolve(1);
  let result = await promise;
}
func();
// SyntaxError: await is only valid in async functions and async generators


// use await in the top-level code
let response = Promise.resolve("hi");
let result = await response;
// SyntaxError: await is only valid in async functions and async generators

记住: 您只能在async函数中使用 await

错误处理

在一个正常的promise中,我们会使用.catch ()来捕捉promise返回的最终错误。 在这里,情况并没有太大的不同:

async function asyncFunc() {
  try {
    let response = await fetch("http:your-url");
  } catch (err) {
    console.log(err);
  }
}

asyncFunc();
// TypeError: failed to fetch

我们使用 try... catch 来捕获错误,但是在没有 try... catch 的情况下,我们仍然可以捕获这样的错误:

async function asyncFunc() {
  let response = await fetch("http:your-url");
}
asyncFunc();
// Uncaught (in promise) TypeError: Failed to fetch

asyncFunc().catch(console.log);
// TypeError: Failed to fetch

ES2018 Async Iteration 等

现在让我们来看看 ES2018引入了什么。

对象的 Rest / Spread

还记得 ES6(ES2015)是如何允许我们这样做的吗?

const veggie = ["tomato", "cucumber", "beans"];
const meat = ["pork", "beef", "chicken"];

const menu = [...veggie, "pasta", ...meat];
console.log(menu);
// Array [ "tomato", "cucumber", "beans", "pasta", "pork", "beef", "chicken" ]

现在我们也可以对对象使用 rest / spread 语法,让我们看看如何:

let myObj = {
  a: 1,
  b: 3,
  c: 5,
  d: 8
};

// we use the rest operator to grab everything else left in the object.
let { a, b, ...z } = myObj;
console.log(a); // 1
console.log(b); // 3
console.log(z); // {c: 5, d: 8}

// using the spread syntax we cloned our Object
let clone = { ...myObj };
console.log(clone);
// {a: 1, b: 3, c: 5, d: 8}
myObj.e = 15;
console.log(clone);
// {a: 1, b: 3, c: 5, d: 8}
console.log(myObj);
// {a: 1, b: 3, c: 5, d: 8, e: 15}

使用 spread 操作符,我们可以轻松地创建 Object 的克隆,这样当我们修改原始 Object 时,克隆就不会被修改,就像我们在讨论数组时看到的那样。

异步迭代 Async Iteration

异步迭代器与迭代器非常相似,只是它的 next ()方法返回{ value,done }键值对promise

为此,我们将使用 for-await-of 循环,它通过将迭代转换为 promise 来工作,除非它们已经是一个。

const iterables = [1, 2, 3];

async function test() {
  for await (const value of iterables) {
    console.log(value);
  }
}
test();
// 1
// 2
// 3

在执行期间,使用[ symboli.asynciterator ]()方法从数据源创建异步迭代器。 每次访问序列中的下一个值时,我们都隐式地等待迭代器方法返回的promse

Promise.prototype.finally()

promise完成后,我们可以调用一个回调。

const myPromise = new Promise((resolve, reject) => {
  resolve();
});
myPromise
  .then(() => {
    console.log("still working");
  })
  .catch(() => {
    console.log("there was an error");
  })
  .finally(() => {
    console.log("Done!");
  });

.finally()会返回一个promise,所以可以继续在后面添加.then().catch(),但是.then()中捕获的值不是由.finally()产生的,而是有最近的一次then产生的。

const myPromise = new Promise((resolve, reject) => {
  resolve();
});
myPromise
  .then(() => {
    console.log("still working");
    return "still working";
  })
  .finally(() => {
    console.log("Done!");
    return "Done!";
  })
  .then(res => {
    console.log(res);
  });
// still working
// Done!
// still working

正如你可以看到的那样,thenfinally仍然返回了值,这个值不是由finally返回的,而是由第一个then创建的。

RegExp

4 个新的与 RegExp 相关的特性出现在了 ECMAScript 的新版本中。 它们是:

s (dotAll) flag for regular expression

ECMAScript为正则表达式引入了一个新的 s 标志,它使得.匹配任何字符,包括行终止符。

/foo.bar/s.test("foo\nbar");
// true

RegExp named capture groups

来自文档:

编号的捕获组允许引用正则表达式匹配的字符串的某些部分。 每个捕获组都被分配一个唯一的编号,并且可以使用该编号进行引用,但是这会使正则表达式难以理解和重构。 例如,给定 / ( d {4})-( d {2})-( d {2}) / 匹配一个日期,如果不检查周围的代码,就无法确定哪个组对应于该月,哪个组对应于该日。 另外,如果要交换月份和日期的顺序,还应该更新组引用。 捕获组可以使用(? Name...)语法,用于任何标识符名称。 日期的正则表达式可以写为 /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u 每个名称都应该是唯一的,并遵循 ECMAScript 的语法。 可以从正则表达式结果的 groups 属性的属性访问命名组。 还创建了对组的编号引用,就像对未命名的组一样。

例如:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec("2015-01-02");
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

let {
  groups: { one, two }
} = /^(?<one>.*):(?<two>.*)$/u.exec("foo:bar");
console.log(`one: ${one}, two: ${two}`);
// one: foo, two: bar

RegExp Lookbehind Assertions

来自文档:

使用后向断言,可以确保一个模式是或不是在另一个模式之前,例如匹配一个美元数额而不捕获美元符号。 积极地查看断言背后的内容表示为(?<=...) 它们确保包含在断言之后的模式之前的模式。 例如,如果想匹配一个美元数额而不想捕获美元符号,/(?<=$)\d+(\.\d*)?/ 可以使用,匹配$10.53和返回10.53。 然而,却无法匹配€10.53。 否定的断言后面的查找表示为(?<!...), 另一方面,确保断言之后的模式不会出现在模式之前。 例如,/(?<!$)\d+(?:\.\d*)/ 不会匹配$10.53,却能匹配€10.53

RegExp Unicode Property Escapes

来自文档:

这带来了形式为 \p{…}\P{…} 的 Unicode 属性转义的添加。 Unicode 属性转义是在设置了 u 标志的正则表达式中可用的一种新型转义序列。 有了这个功能,我们可以写:

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test("π");
// true

ES2019 会带什么新特性?

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

Array.prototype.flat()将递归地压入数组直到我们指定的深度。 如果没有指定深度参数,则默认值为 1。 我们可以使用 Infinity 来展开所有嵌套数组。

const letters = ["a", "b", ["c", "d", ["e", "f"]]];
// default depth of 1
letters.flat();
// ['a', 'b', 'c', 'd', ['e', 'f']]

// depth of 2
letters.flat(2);
// ['a', 'b', 'c', 'd', 'e', 'f']

// which is the same as executing flat with depth of 1 twice
letters.flat().flat();
// ['a', 'b', 'c', 'd', 'e', 'f']

// Flattens recursively until the array contains no nested arrays
letters.flat(Infinity);
// ['a', 'b', 'c', 'd', 'e', 'f']

Array.prototype.flatMap()在处理“深度”参数的方式上与前一个相同,但是我们也可以使用 flatMap()对其进行映射并返回新数组中的结果,而不是简单地将数组展开。

let greeting = ["Greetings from", " ", "Vietnam"];

// let's first try using a normal `map()` function
greeting.map(x => x.split(" "));
// ["Greetings", "from"]
// ["", ""]
// ["Vietnam"]

greeting.flatMap(x => x.split(" "));
// ["Greetings", "from", "", "", "Vietnam"]

如你所见,如果我们使用.map(),我们将得到一个多维数组,这个问题可以通过使用.flatmap () 展开数组。

Object.fromEntries()

Object.fromEntries()可以将键值对的数组转换为Object对象

const keyValueArray = [
  ["key1", "value1"],
  ["key2", "value2"]
];

const obj = Object.fromEntries(keyValueArray);
// {key1: "value1", key2: "value2"}

我们可以将任何迭代类型作为 Object.fromEntries ()的参数传递,无论是 ArrayMap 还是实现迭代协议的其他对象。

你可以在这里阅读更多关于迭代协议的内容。developer.mozilla.org/en-us/docs/…

String.prototype.trimStart() / .trimEnd()

String.prototype.trimStart()从字符串的开头移除空白,而 String.prototype.trimEnd ()从结尾移除空白。

let str = "    this string has a lot of whitespace   ";

str.length;
// 42

str = str.trimStart();
// "this string has a lot of whitespace   "
str.length;
// 38

str = str.trimEnd();
// "this string has a lot of whitespace"
str.length;
// 35

当然了,也可以用 .trimLeft() 替代 .trimStart(), .trimRight() 替代 .trimEnd()

Catch 可选的参数

ES2019 之前,您必须始终在 catch 子句中包含一个异常变量。 E2019 允许你省略它。

// Before
try {
   ...
} catch(error) {
   ...
}

// ES2019
try {
   ...
} catch {
   ...
}

当您想要忽略错误时,这是很有用的。 对于更详细的用例列表,我强烈推荐这篇文章: 2ality.com/2017/08/opt…

Function​.prototype​.toString()

.toString()方法返回一个表示函数源代码的字符串。

function sum(a, b) {
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//    return a + b;
//  }

它还包括注释。

function sum(a, b) {
  // perform a sum
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//   // perform a sum
//   return a + b;
// }

Symbol.prototype.description

.description 返回 Symbol 的描述。

const me = Symbol("Alberto");
me.description;
// "Alberto"

me.toString();
//  "Symbol(Alberto)"

如果你想下载ES2016--ES2019对照表,请点击链接inspiredwebdev.com/cheatsheets