跟踪 JavaScript (ECMAScript)中的新内容是很困难的,而且更难找到有用的代码示例。
因此,在本文中将介绍 TC39(最终草案) 在 ES2016、ES2017 和 ES2018 中添加的已完成提案中列出的所有 18 个特性,并给出有用的示例。
1.Array.prototype.includes
include 是数组上的一个简单实例方法,可以轻松查找数组中是否有指定内容(包括 NaN)。
2. 求幂操作符
像加法和减法这样的数学运算分别有像 + 和 – 这样运算符。与它们类似,** 运算符通常用于指数运算。在 ECMAScript 2016 中,引入了 ** 代替 Math.pow。
1.Object.values()
Object.values()是一个类似于 Object.keys()的新函数,但返回对象自身属性的所有值,不包括原型链中的任何值。
2.Object.entries()
Object.entries()与 Object.keys 类似,但它不是仅返回键,而是以数组方式返回键和值。这使得在循环中使用对象或将对象转换为映射等操作变得非常简单。
例一:
例二:
3. 字符串填充
在 String.prototype 中添加了两个实例方法:String.prototype.padStart 和 String.prototype.padEnd,允许在初始字符串的开头或末尾追加 / 前置空字符串或其他字符串。
‘someString’.padStart(numberOfCharcters [,stringForPadding]);
‘5’.padStart(10) // ‘ 5’
‘5’.padStart(10, ‘=*’) //’=*=*=*=*=5′
‘5’.padEnd(10) // ‘5 ‘
‘5’.padEnd(10, ‘=*’) //’5=*=*=*=*=’
当我们想要在漂亮的打印显示或终端打印进行对齐时,这非常有用。
3.1 padStart 例子:
在下面的例子中,有一个不同长度的数字列表。我们希望在“0”为追加符让所有项长度都为 10 位,以便显示,我们可以使用 padStart(10,‘0’)轻松实现这一点。
3.2 padEnd 例子:
当我们打印多个不同长度的项目并想要右对齐它们时,padEnd 非常有用。
下面的示例是关于 padEnd、padStart 和 Object.entries 的一个很好的实际示例:
const cars = {
‘????BMW’: ’10’,
‘????Tesla’: ‘5’,
‘????Lamborghini’: ‘0’
}
Object.entries(cars).map(([name, count]) => {
console.log(`${name.padEnd(20, ‘ -‘)} Count: ${count.padStart(3, ‘0’)}`)
})
// 打印
// ????BMW – – – – – – – Count: 010
// ????Tesla – – – – – – Count: 005
// ????Lamborghini – – – Count: 000
3.3 ⚠️ 注意 padStart 和 padEnd 在 Emojis 和其他双字节字符上的使用
Emojis 和其他双字节字符使用多个 unicode 字节表示。所以 padStart padEnd 可能不会像预期的那样工作!⚠️
例如: 假设我们要垫达到 10 个字符的字符串的心❤️emoji。结果如下:
‘heart’.padStart(10, “❤️”); // prints.. ‘❤️❤️❤heart’
这是因为 ❤️ 长 2 个字节(’ u2764 uFE0F’)!单词 heart 是 5 个字符,所以我们只剩下 5 个字符来填充。所以 JS 使用 (‘u2764uFE0F’) 填充两颗心并生成 ❤️❤️。对于最后一个,它只使用 (‘u2764uFE0F’) 的第一个字节(u2764)来生成,所以是 ❤;
4.Object.getOwnPropertyDescriptors
此方法返回给定对象的所有属性的所有属性(包括 getter setter set 方法), 添加这个的主要目的是允许浅 拷贝 / 克隆到另一个对象中的对象,类似 bject.assign。
Object.assign 浅拷贝除原始对象的 getter 和 setter 方法之外的所有属性。
下面的示例显示了 Object.assign 和 Object.getOwnPropertyDescriptors 以及 Object.defineProperties 之间的区别,以将原始对象 Car 复制到新对象 ElectricCar 中。可以看到使用 Object.getOwnPropertyDescriptors,discount 的 getter 和 setter 函数也被复制到目标对象中。
使用 Object.defineProperties
var Car = {
name: ‘BMW’,
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
console.log(Object.getOwnPropertyDescriptor(Car, ‘discount’));
// 打印
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
// 使用 Object.assign 拷贝对象
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object’s ‘discount’ property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, ‘discount’));
// 打印
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
//
//⚠️请注意,“discount”属性的 ElectricCar 对象中缺少 getter 和 setter!????????
//Copy Car’s properties to ElectricCar2 using Object.defineProperties
//and extract Car’s properties using Object.getOwnPropertyDescriptors
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
//Print details of ElectricCar2 object’s ‘discount’ property
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, ‘discount’));
//prints..
// {get: [Function: get], ????????????????????????
// set: [Function: set], ????????????????????????
// enumerable: true,
// configurable: true
// }
// 请注意,在 ElectricCar2 对象中存在“discount”属性的 getter 和 setter !
5. 函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma), 此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。这一变化将鼓励开发人员停止丑陋的“行以逗号开头”的习惯。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。
6.Async/Await
到目前为止,个人感受是这是最重要和最有用的功能。async 函数允许我们不处理回调地狱,并使整个代码看起来很简单。
async 关键字告诉 JavaScript 编译器以不同的方式对待函数。每当编译器到达函数中的 await 关键字时,它就会暂停。它假定 wait 之后的表达式返回一个 promise,并在进一步移动之前等待该 promise 被 resolved 或 rejected。
在下面的示例中,getAmount 函数调用两个异步函数 getUser 和 getBankBalance。使用 async await 更加优雅和简单达到有有序的调用 getUser 与 getBankBalance。
6.1.async 函数默认返回一个 promise
如果您正在等待 async 函数的结果,则需要使用 Promise 的 then 语法来捕获其结果。
在以下示例中,我们希望使用 console.log 来打印结果但是不在 doubleAndAdd 函数里面操作。因为 async 返回是一个 promise 对象,所以可以在 then 里面执行我们一些打印操作。
6.2 并行调用 async/await
在前面的例子中,我们调用 doubleAfterlSec,但每次我们等待一秒钟(总共 2 秒)。相反,我们可以使用 Promise.all 将它并行化为一个并且互不依赖于。
6.3 async/await 函数对错误的处理
在使用 async/wait 时,有多种方法可以处理错误。
方法一:在函数内使用 try catch
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
} catch (e) {
return NaN; //return something
}
return a + b;
}
doubleAndAdd(‘one’, 2).then(console.log); // NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方法二:在 await 后使用 catch 捕获错误
// 方法二:在 await 后使用 catch 获取错误
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a).catch(e => console.log(‘”a” is NaN’)); // ????
b = await doubleAfter1Sec(b).catch(e => console.log(‘”b” is NaN’)); // ????
if (!a || !b) {
return NaN;
}
return a + b;
}
doubleAndAdd(‘one’, 2).then(console.log); // NaN and logs: “a” is NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方法三:在整个的 async-await 函数捕获错误
// 方法三:在整个的 async-await 函数捕获错误
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
doubleAndAdd(‘one’, 2)
.then(console.log)
.catch(console.log); // ????????????<——- use “catch”
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
1. 共享内存 和 Atomics
这是一个巨大的、相当高级的特性,是 JS 引擎的核心增强。
其主要原理是在 JavaScript 中引入某种多线程特性,以便 JS 开发人员将来可以通过允许自己管理内存而不是让 JS 引擎管理内存来编写高性能的并发程序。
这是通过一种名为 SharedArrayBuffer (即 共享数组缓冲区) 的新类型的全局对象实现的,该对象本质上是将数据存储在共享内存空间中。因此,这些数据可以在主 JS 线程和 web 工作线程之间共享。
到目前为止,如果我们想在主 JS 线程和 web 工作者之间共享数据,我们必须复制数据并使用 postMessage 将其发送到另一个线程。
你只需使用 SharedArrayBuffer,数据就可以立即被主线程和多个 web 工作线程访问。workers 之间的协调变得更简单和更快(与 postMessage() 相比)。
但是在线程之间共享内存会导致竞争条件。为了帮助避免竞争条件,引入了“Atomics”全局对象。Atomics 提供了各种方法来在线程使用其数据时锁定共享内存。它还提供了安全地更新该共享内存中的搜索数据的方法。
如果你这对个感兴趣,可以阅读以下文章:
ES2017 新特性:共享内存 和 Atomics
From Workers to Shared Memory — lucasfcosta
A cartoon intro to SharedArrayBuffers — Lin Clark
Shared memory and atomics — Dr. Axel Rauschmayer
2. Tagged Template literal restriction removed
首先,我们需要知道的什么是 Template literals(“标记的模板文字”),以便更好地理解这个特性。Template literals 是一个 ES2015 特性,它使用反引号包含一个字符串字面量,并且支持嵌入表达式和换行,如:
下面的例子显示,我们的自定义“Tag”函数 greet 添加了一天中的时间,比如“Good Morning!”“Good afternoon”等等,取决于一天中的时间字符串的文字和返回自定义字符串。
function greet(hardCodedPartsArray, …replacementPartsArray) {
console.log(hardCodedPartsArray); //[‘Hello ‘, ‘!’]
console.log(replacementPartsArray); //[‘Raja’]
let str = ”;
hardCodedPartsArray.forEach((string, i) => {
if (i < replacementPartsArray.length) {
str += `${string} ${replacementPartsArray[i] || ”}`;
} else {
str += `${string} ${timeGreet()}`; //<– 追加 Good morning/afternoon/evening here
}
});
return str;
}
const firstName = ‘Raja’;
const greetings = greet`Hello ${firstName}!`; //????????<– Tagged literal
console.log(greetings); //’Hello Raja! Good Morning!’ ????
function timeGreet() {
const hr = new Date().getHours();
return hr < 12
? ‘Good Morning!’
: hr < 18 ? ‘Good Afternoon!’ : ‘Good Evening!’;
}
现在我们讨论了什么是“标记”函数,许多人希望在不同的领域中使用这个特性,比如在 Terminal 中用于命令,在组成 uri 的 HTTP 请求中,等等。
⚠️ 带标记字符串文字的问题
问题是 ES2015 和 ES2016 规范不允许使用像“u”(unicode)、“x”(十六进制)这样的转义字符,除非它们看起来完全像“u00A9”或 u{2F804}或 xA9。
因此,如果你有一个内部使用其他域规则(如终端规则)的标记函数,可能需要使用看起来不像 u0049 或 u {@ F804}的 ubla123abla,那么你会得到一个语法错误,
function myTagFunc(str) {
return {“cooked”: “undefined”, “raw”: str.raw[0] }
}
var str = myTagFunc `hi \ubla123abla`; //call myTagFunc
str // {cooked: “undefined”, raw: “hi \\unicode”}
3. 用于正则表达式的“dotall”标志
目前在正则表达式中,虽然点 (“.”) 应该匹配单个字符,但它不匹配像 n r f 等新行字符。
例如:
//Before
/first.second/.test(‘first\nsecond’); //false
这种增强使 点 运算符能够匹配任何单个字符。为了确保它不会破坏任何东西,我们需要在创建正则表达式时使用 s 标志。
//ECMAScript 2018
/first.second/s.test(‘first\nsecond’); //true Notice: /s ????????
更多的方法,请看这里:
4.RegExp Named Group Captures
这种增强 RegExp 特性借鉴于像 Python、Java 等其他语言,因此称为“命名组”。这个特性允许编写开发人员以 (<name>…) 格式为 RegExp 中的组的不同部分提供名称(标识符),使用可以用这个名称轻松地获取他们需要的任何组。
4.1 Named group 的基础用法
在下面的示例中,我们使用 (?<year>) (?<month>) 和 (?<day>) 名称对日期正则表达式的不同部分进行分组。结果对象现在将包含一个 groups 属性,该属性具有 year、month 和 day 的相应值。
let re1 = /(\d{4})-(\d{2})-(\d{2})/;
let result1 = re1.exec(‘2015-01-02’);
console.log(result1);
// [‘2015-01-02’, ‘2015’, ’01’, ’02’, index: 0, input: ‘2015-01-02’]
// ECMAScript 2018
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result2 = re2.exec(‘2015-01-02’);
console.log(result2);
// [“2015-01-02”, “2015”, “01”, “02”, index: 0, input: “2015-01-02”,
// groups: {year: “2015”, month: “01”, day: “02”}
// ]
console.log(result2.groups.year); // 2015
4.2 在 regex 内使用 Named groups
使用 \k< 组名 > 格式来反向引用正则表达式本身中的组, 例如:
// 在下面的例子中,我们有一个包合的“水果”组。
// 它既可以配“苹果”,也可以配“橘子”,
// 我们可以使用“\k<group name>”(\k<fruit>) 来反向引用这个组的结果,
// 所以它可以匹配“=”相同的单词
let sameWords = /(?<fruit>apple|orange)=\k<fruit>/u;
sameWords.test(‘apple=apple’) // true
sameWords.test(‘orange=orange’) // true
sameWords.test(‘apple=orange’) // false
4.3 在 String.prototype.replace 中使用 named groups
在 String.prototype.replace 方法中使用 named groups。所以我们能更快捷的交换词。
例如,把“firstName, lastName”改成“lastName, firstName”。
let re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;
‘Hello World’.replace(re, `$<lastName>, $<firstName>`) // “World, Hello”
5. 对象的 Rest 属性
rest 操作 …(三个点)允许挑练我们需要的属性。
5.1 通过 Rest 解构你需要的属性
let {firstName, age, …remaining} = {
firstName: ‘ 王 ’,
lastName: ‘ 智艺 ’,
age: 27,
height: ‘1.78’,
race: ‘ 黄 ’
}
firstName; // 王
age; // 27
remaining; // {lastName: “ 智艺 ”, height: “1.78”, race: “ 黄 ”}
6. 对象的扩展属性
扩展 和 解析 的 三个点是一样的,但是不同的是你可以用 扩展 去新建或者组合一个新对象。
扩展 是对齐赋值的右运算符,而 解构 是左运算符。
const person = {fName: ‘ 小明 ’, age: 20};
const account = {name: ‘ 小智 ’, amount: ‘$1000’};
const personAndAccount = {…person, …account};
personAndAccount; // {fName: “ 小明 ”, age: 20, name: “ 小智 ”, amount: “$1000”}
7. 正则表达式反向 (lookbehind) 断言
断言 (Assertion) 是一个对当前匹配位置之前或之后的字符的测试,它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。
正则表达式的断言一共有 4 种形式:
(?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)
(?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)
(?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)
(?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)
你可以使用组(?<=…) 去正向断言,也可以用 (?<!…) 去取反。
正向断言:我们想确保 # 在 winning 之前。(就是 #winning),想正则匹配返回 winning。下面是写法:
反向断言:匹配一个数字,有 € 字符而没有 $ 字符在前面的数字。
更多内容可以参考:S2018 新特征之:正则表达式反向 (lookbehind) 断言
8. RegExp Unicode Property Escapes
用正则去匹配 Unicode 字符是很不容易的。像 w , W , d 这种只能匹配英文字符和数字。但是其他语言的字符怎么办呢,比如印度语,希腊语?
例如 Unicode 数据库组里把所有的印度语字符,标识为 Script = Devanagari。还有一个属性 Script_Extensions,值也为 Devanagari。所以我们可以通过搜索 Script=Devanagari,得到所有的印度语。
Devanagari 可以用于印度的各种语言,如 Marathi, Hindi, Sanskrit。
在 ECMAScript 2018 里,我们可以使用 p 和 {Script=Devanagari} 匹配那些所有的印度语字符。也就是说 p{Script=Devanagari} 这样就可以匹配。
//The following matches multiple hindi character
/^\p{Script=Devanagari}+$/u.test(‘हिन्दी’); //true
//PS:there are 3 hindi characters h
同理,希腊语的语言是 Script_Extensions 和 Script 的值等于 Greek。也就是用 Script_Extensions=Greek or Script=Greek 这样就可以匹配所有的希腊语,也就是说,我们用 p{Script=Greek} 匹配所有的希腊语。
进一步说,Unicode 表情库里存了各式各样的布尔值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base 的值,都等于 true。所以我们想搜 Emoji 等于 ture,就能搜到所有的表情。
我们用 \p{Emoji} ,\Emoji_Modifier 匹配所有的表情。
参考文献:
ECMAScript 2018 Proposal
https://mathiasbynens.be/note…
8. Promise.prototype.finally()
finally() 是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally 没有返回值,始终会被执行。
让我们看看各种情况。
9. 异步迭代(Asynchronous Iteration)
这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。
原文: Here are examples of everything new in ECMAScript 2016, 2017, and 2018
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》