原文:Everything you need to know from ES2016 to ES2019
作者:Alberto Montalesi
JavaScript是一种不断发展的语言,在过去的几年里,ECMAScript 规范中增加了许多新的特性。
本文是我的书《现代 JavaScript 完整指南》的节选,它涵盖了 ES2016,ES2017,ES2018,ES2019的新增内容。
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,所以结果返回false。
array.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 个空格呢? 这是因为 padStart 和 padEnd 会填充空白。 在我们的示例中,"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()
此方法返回对象自己的所有属性描述符。 它可以返回value、writable、 get、 set、enumerable和configurable的属性。
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
Atomics与 SharedArrayBuffer (通用的固定长度的二进制数据缓冲区)对象一起使用,这些对象表示通用的、固定长度的原始二进制数据缓冲区。
让我们来看一些原子方法的例子:
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()
这三个方法都在数组的给定位置执行位 AND、 OR 和 XOR 操作。 你可以在维基百科的 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
正如你可以看到的那样,then在finally仍然返回了值,这个值不是由finally返回的,而是由第一个then创建的。
RegExp
4 个新的与 RegExp 相关的特性出现在了 ECMAScript 的新版本中。 它们是:
- s(dotAll) flag for regular expressions
- RegExp named capture groups)
- RegExp Lookbehind Assertions)
- RegExp Unicode Property Escapes
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 ()的参数传递,无论是 Array、 Map 还是实现迭代协议的其他对象。
你可以在这里阅读更多关于迭代协议的内容。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