跟踪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 使用 ('\u2764\uFE0F' ) 填充两颗心并生成 ❤️❤️。 对于最后一个,它只使用 ('\u2764\uFE0F' ) 的第一个字节(\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); }); }
7.共享内存 和 Atomics
这是一个巨大的、相当高级的特性,是JS引擎的核心增强。
其主要原理是在 JavaScript 中引入某种多线程特性,以便JS开发人员将来可以通过允许自己管理内存而不是让 JS 引擎管理内存来编写高性能的并发程序。
这是通过一种名为 SharedArrayBuffer (即 共享数组缓冲区) 的新类型的全局对象实现的,该对象本质上是将数据存储在共享内存空间中。因此,这些数据可以在主JS线程和web工作线程之间共享。
到目前为止,如果我们想在主 JS 线程和 web 工作者之间共享数据,我们必须复制数据并使用postMessage 将其发送到另一个线程。
你只需使用SharedArrayBuffer,数据就可以立即被主线程和多个web工作线程访问。workers 之间的协调变得更简单和更快(与 postMessage() 相比)。
但是在线程之间共享内存会导致竞争条件。为了帮助避免竞争条件,引入了 “Atomics” 全局对象。 Atomics 提供了各种方法来在线程使用其数据时锁定共享内存。 它还提供了安全地更新该共享内存中的搜索数据的方法。
如果你这对个感兴趣,可以阅读以下文章:
8. 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" }
9.用于正则表达式的“dotall”标志
目前在正则表达式中,虽然点(“.”)应该匹配单个字符,但它不匹配像 \n \r \f 等新行字符。
例如:
//Before /first.second/.test('first\nsecond'); //false
这种增强使 点 运算符能够匹配任何单个字符。为了确保它不会破坏任何东西,我们需要在创建正则表达式时使用\s标志。
//ECMAScript 2018 /first.second/s.test('first\nsecond'); //true Notice: /s ????????
更多的方法,请看这里:
10.RegExp Named Group Captures
这种增强 RegExp特性借鉴于像Python、Java等其他语言,因此称为“命名组”。这个特性允许编写开发人员以(…)格式为 RegExp 中的组的不同部分提供名称(标识符),使用可以用这个名称轻松地获取他们需要的任何组。
10.1 Named group 的基础用法
在下面的示例中,我们使用 (?) (?) 和 (?) 名称对日期正则表达式的不同部分进行分组。结果对象现在将包含一个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
10.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
10.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"
11.对象的 Rest 属性
rest操作 …(三个点)允许挑练我们需要的属性。
11.1 通过 Rest 解构你需要的属性
let { firstName, age, ...remaining } = { firstName: '王', lastName: '智艺', age: 27, height: '1.78', race: '黄' } firstName; // 王 age; // 27 remaining; // { lastName: "智艺", height: "1.78", race: "黄" }
12.对象的扩展属性
扩展 和 解析 的 三个点是一样的,但是不同的是你可以用 扩展
去新建或者组合一个新对象。
扩展
是对齐赋值的右运算符, 而解构
是左运算符。
const person = { fName: '小明', age: 20 }; const account = { name: '小智', amount: '$1000'}; const personAndAccount = { ...person, ...account }; personAndAccount; // {fName: "小明", age: 20, name: "小智", amount: "$1000"}
13.正则表达式反向(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)断言
14. 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
匹配所有的表情。
参考文献:
15. Promise.prototype.finally()
finally()
是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally 没有返回值,始终会被执行。
让我们看看各种情况。
16.异步迭代(Asynchronous Iteration)
这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。
原文: Here are examples of everything new in ECMAScript 2016, 2017, and 2018