JavaScript (ECMAScript). 版本的频繁更新,让我们很难去跟进又新增了哪些新特性。就不要说,找一些实用的代码示例。但在这篇文章里,我将介绍18个有关 ECMAScript 2016, 2017, 和 2018 提议目前被通过的新特性。
这是一篇很长的文章,但其实很容易理解。读到结尾,你将会收获大量有关 ES 的新知识。文中部分图片需要翻墙看。
1. Array.prototype.includes
includes 是数组里很简单的一个实例方法,帮助我们找到数组里是否存在某值。(indexOf 不支持 NaN , 但 includes 支持)
Tips: 最开始,JavaScript 想命名为 contains , 但由于被 Mootools 占用了,所以使用了 includes。
2. Exponentiation infix operator
类似于像 +, - 这种操作符,新增了一个指数操作符。 在 ECMAScript 2016 中, 使用 **
代替了 Math.pow
。
1. Object.values()
Object.values() 这个函数和 Object.keys() 功能上是类似的,但是返回的属性值是对象自身的,不包括原型链上的任何值。
ECMAScript 2017 (ES8)— Object.values()
2. Object.entries()
Object.keys 只是返回数值, 但 Oject.entries() 以数组的形式,返回了key 和 value。使得将对象变为 Map
更容易。
示例1:
ECMAScript 2017 (ES8) — Using Object.entries() in loops
示例2:
ECMAScript 2017 (ES8) — Using Object.entries() to convert Object to Map
3. String padding
String 新增了两个字符串方法, 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=*=*=*=*='
Tips: 这个方法在我们想让console.log 或者终端输出整齐的时候,特别有用。
3.1 padStart example:
在下面这个例子里,我们有多个长度不一的数字, 当我们想让他们都展示10位数的时候,我们可以使用 padStart(10, '0')
ECMAScript 2017 — padStart example
3.2 padEnd example:
当我们输出多个 item ,希望属性值右对齐的时候,可以使用 padEnd
。
下面这个例子里用到了 padStart, padEnd, Object.entries
ECMAScript 2017 — padEnd, padStart and Object.Entries example
const cars = {
'?BMW': '10',
'?Tesla': '5',
'?Lamborghini': '0'
}
Object.entries(cars).map(([name, count]) => {
//padEnd appends ' -' until the name becomes 20 characters
//padStart prepends '0' until the count becomes 3 characters.
console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});
//Prints..
// ?BMW - - - - - - - Count: 010
// ?Tesla - - - - - - Count: 005
// ?Lamborghini - - - Count: 000
3.3 ⚠️ padStart and padEnd on Emojis and other double-byte chars
表情符号和其他多字节的字符编码,是使用多字节的 unicode 编码方式的,所以 padStart 和 padEnd 可能不适用。
比如我们想要写十个?的表情符号。结果如下:
//Notice that instead of 5 hearts, there are only 2 hearts and 1 heart that looks odd!
'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'
因为 ? 这个字符,2个字的字节数是 '\u2764\uFE0F'
, 它本身就是10个字节长度。最后一个字节使用了 ❤️ 的第一个字节 \u2764
产生的。
所以结果是 ❤️❤️❤heart
PS: 你可以使用 这个链接
地址unicode 字节数转换。
4. Object.getOwnPropertyDescriptors
这个方法返回一个对象的所有属性,包括 getter 里的 get 和 setter 里的 set 。之所以添加这个属性是为了让对象可以允许复制和克隆一个对象到另一个对象,包括 getter
和 setter
函数。和 Object.assign
相对。
Object.assign
是浅拷贝所有属性和值,不包括 getter
和 setter
函数。
下面这个例子就是两者的一个区别。将原对象 Car
拷贝到 ElectricCar
,你就会发现 Object.getOwnPropertyDescriptors
拷贝了 getter
和 setter
, 而
Object.assign
没有。
before
Before — Using Object.assign
after
ECMAScript 2017 (ES8) — Object.getOwnPropertyDescriptors
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
//Print details of Car object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
//prints..
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
//Copy Car's properties to ElectricCar using Object.assign
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
//prints..
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
//⚠️Notice that getters and setters are missing in ElectricCar object for 'discount' property !??
//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
// }
// Notice that getters and setters are present in the ElectricCar2 object for 'discount' property!
5. 允许在参数尾部添加逗号
在函数参数尾部,现在可以添加逗号了!为什么要这样做呢?为了帮助 git , 只有新的改动才发生更改, 详细见图。
ECMAScript 2017 (ES 8) — Trailing comma in function paramameter
Tips: 函数尾部也可以添加逗号
6. Async/Await
到目前为止,这个是最重要的一个特性。它帮助我们摆脱异步函数,地狱回调的现状,让代码更加简洁。
async
关键字会让 JavaScript 以不同的方式去编译执行代码。编译器在遇到函数内的 await 之后,会暂停。会假定 await
之后,会返回一个 promise
,等待 promise
有返回结果之后, 再有进一步的运行。
在下面这个例子里,getAmount
方法有两个异步的方法, getUser
和 getBankBalance
。我们用 promise
去实现这个功能,通过使用 async await
让代码更加优雅整洁。
6.1 Async 方法返回一个 Promise.
如果你在等待一个异步方法返回结果,你可以使用 Promise
的 then
方法去捕获结果。
在下面这个例子里,我们想在 doubleAndAdd
外部,用 console.log
输出 doubleAndAdd
的结果。所以我们用 then
语法把结果值传给 console.log
。
ECMAScript 2017 (ES 8) — Async Await themselves returns Promise
让 async/await 同时返回
上面一个例子里,我们调用了两次 await, 所以每调用一次,我们需要等一秒钟(总共两秒)。我们可以使用 Promise.all
让 a 和 b 都有返回值之后,同时处理。
ECMAScript 2017 (ES 8) — Using Promise.all to parallelize async/await
async/await 方法的错误处理
当使用 async/await 的时候,我们有很多种处理错误的方式。
方法一:使用 try 和 catch
ECMAScript 2017 — Use try catch within the async/await function
//Option 1 - Use try catch within the function
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
} catch (e) {
return NaN; //return something
}
return a + b;
}
//?Usage:
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
//Option 2 - *Catch* errors on every await line
//as each await expression is a Promise in itself
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;
}
//?Usage:
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 函数添加 catch
ECMAScript 2017 — Catch the entire async/await function at the end
//Option 3 - Dont do anything but handle outside the function
//since async / await returns a promise, we can catch the whole function's error
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
//?Usage:
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);
});
}
到目前为止, ECMAScript 2018 的最终草案,将会在 2018 年6月或者7月推出。以下所有的特性是 Stage-4
中的,会是 ES2018 的一部分。
Shared memory and atomics
这个是对 JS 引擎的核心增强性功能。(This is a huge, pretty advanced feature and is a core enhancement to JS engines.)
带入多线程的概念到 JavaScript 中, 是为了让 JS 开发者能够通过管理内存,书写高性能,并发程序。(The main idea is to bring some sort of multi-threading feature to JavaScript so that JS developers can write high-performance, concurrent programs in the future by allowing to manage memory by themselves instead of letting JS engine manage memory.)
新增了一个全局对象 SharedArrayBuffer
在共享内存空间存储数据。所以这个数据将会在 JS 线程和 web-worker
线程中分享。
在这之前,如果我们想在主进程和 web-worker 之间分享数据,我们只能通过复制数据, 使用 postMessage 发送给另一个。
现在的话,就不需要那么麻烦了,只需要使用 SharedArrayBuffer
, 数据就可以在两个线程中都可用。
共享内存就会引起一个 Race Condition
的问题。为了避免这种情况发生,所以有了全局对象 Atomics
。Atomics
提供了各种方法,可以实现在使用一个数据时,对数据做锁定。也有一些方法,让修改数据变得更加安全。
这个特性推荐使用一些库去实现,但是目前还没有现成的库。
如果你对这个有兴趣,推荐您几篇文章。
1. From Workers to Shared Memory — lucasfcosta
2. A cartoon intro to SharedArrayBuffers — Lin Clark
3. Shared memory and atomics — Dr. Axel Rauschmayer
2. 删除 Tagged Template literal 限制
首页,我们需要理解一下什么是 Tagged Template literal
。
在 ES2015+, 有个特性是允许开发者在字符串中插入字符变量。如图:
在标签文字中, 可以写一个函数来接收字符串字面的硬编码部分, 例如[‘Hello’,’!’] 以及替换变量, 例如, [ Raja’] , 作为自定义函数的参数(例如 greet) , 并从自定义函数返回任何你想要的。
下面的例子表明, 我们的自定义”标签”函数问候应用一天的时间, 比如”早上好!” “下午好”等等, 这取决于一天中的时间和字符串的字面值, 然后返回一个自定义字符串。
Tag function example that shows custom string interpolation
//A "Tag" function returns a custom string literal.
//In this example, greet calls timeGreet() to append Good //Morning/Afternoon/Evening depending on the time of the day.
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()}`; //<-- append Good morning/afternoon/evening here
}
});
return str;
}
//?Usage:
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!';
}
现在我们讨论了什么是”标记”函数, 许多人希望在不同的领域使用这个功能, 比如在终端中使用命令和 HTTP 请求来编写 uri 等等。
⚠️存在的问题
问题在于, ES2015和 ES2016规范不允许使用像”\u”(unicode) ,”\x”(hexadecimal)这样的逃逸字符, 除非它们看起来完全像 \u00A9
或 \u {2F804}
或 xA9
。
因此, 如果你有一个内部使用其他域规则的标记函数(如终端规则) , 那么可能需要使用不像 \u0049
或 \u {@f804}
的标记函数, 那么您将会得到一个语法错误。
在 ES2018, 只要标签函数返回具有”cooked”属性(无效字符是”未定义的”)的对象中返回值, 则允许这些看似无效的转义字符返回值(无效字符是”未定义的”) , 然后是一个”原始”属性(不管你想要什么)。
function myTagFunc(str) {
return { "cooked": "undefined", "raw": str.raw[0] }
}
var str = myTagFunc `hi \ubla123abla`; //call myTagFunc
str // { cooked: "undefined", raw: "hi \\unicode" }
3. 正则表达式中的 .
在正则表达式里,虽然 .
表示一个字符,但目前它不表示像 \n \r \f
这类字符。
例如:
//Before
/first.second/.test('first\nsecond'); //false
这个新增功能,可以让 . 匹配任意一个字符。但为了兼容旧版本, 我们需要加一个 \s
标识,让正则表达式起作用。
//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true Notice: /s ??
下面是文档API中的内容:
ECMAScript 2018 — Regex dotAll feature allows matching even \n via “.” via /s flag
4. RegExp Named Group Captures
这个特性的增强是从其他语言 Python, Java等学来的,所以称为命名组。“Named Groups.”。
这个方法允许开发者在正则表达式中(?…) 给不同的组起名称。使得我们可以轻易的通过名称获取到某个组。
4.1 Named group 的基础用法
在下面的例子中,我们给日期命名了三个不同的组(?) (?) and (?year)。结果对象将包含一致的属性 group , 包括 year, month。并且
year h会有相对应的值。
ECMAScript 2018 — Regex named groups example
4.2 在 regex 内使用 Named groups
我们可以使用这样的格式 \k<group name>
去匹配它自己。以下是示例:
ECMAScript 2018 — Regex named groups back referencing via \k
4.3 在 String.prototype.replace 中使用 named groups
在 String.prototype.replace 方法中使用 named groups。所以我们能更快捷的交换词。
例如,把 “firstName, lastName” 改成 “lastName, firstName”.
ECMAScript 2018 — Using RegEx’s named groups feature in replace function
5. 对象的 Rest 属性
Rest 操作符(…)三个点允许我们获取 extract Object properties that are not already extracted.
5.1 通过 Rest 解构你需要的属性
ECMAScript 2018 — Object destructuring via rest
5.2 更重要的是,你还可以删除你不需要的选项
ECMAScript 2018 — Object destructuring via rest
6. 对象的 Spread 属性
Spread 和 Rest 的 三个点是一样的,但是不同的是你可以用 Spread 去新建或者组合一个新对象。
Tips: Spread 是对齐赋值的右运算符, 而 Rest 是左运算符。
ECMAScript 2018 — Object restructuring via rest
7. RegExp Lookbehind Assertions
这个是对正则表达式的增强,确保在一些字符串存在之前,另一些字符串存在。before
This is an enhancement to the RegEx that allows us to ensure some string exists immediately before some other string.
你可以使用组(?<=…)
去正向断定,也可以用 (?<!…)
去取反。
Essentially this will match as long as the -ve assertion passes.
正向断定: 我们想确保 # 在 winning 之前。(就是#winning),想正则匹配返回 winning。下面是写法:
反向断定:匹配一个数字,有 € 字符而没有 $ 字符在前面的数字。
ECMAScript 2018 — (?<!…)
for negative assertions
8. RegExp Unicode Property Escapes
用正则去匹配 Unicode 字符是很不容易的。像 \w , \W , \d 这种只能匹配英文字符和数字。但是其他语言的字符怎么办呢,比如印度语,希腊语?
有了 Unicode Property Escapes 之后,它为每一个字符添加一个 metadata 属性,用它去分组或者表达各式各样的符号。
例如 Unicode 数据库组里把所有的印度语字符,标识为 Script = Devanagari。还有一个属性 Script_Extensions, 值也为 Devanagari。
所以我们可以通过搜索 Script=Devanagari,得到所有的印度语。
Devanagari 可以用于印度的各种语言,如Marathi, Hindi, Sanskrit。
在 ECMAScript 2018 里, 我们可以使用 \p
和 {Script=Devanagari}
匹配那些所有的印度语字符。也就是说 \p{Script=Devanagari}
这样就可以匹配。
ECMAScript 2018 — showing \p
//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}
匹配所有的希腊语。
ECMAScript 2018 — showing \p
//The following matches a single Greek character
/\p{Script_Extensions=Greek}/u.test('π'); // true
进一步说,Unicode 表情库里存了各式各样的布尔值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base
的值,都等于 true。所以我们想搜 Emoji 等于 ture,就能搜到所有的表情。
我们用 \p{Emoji} ,\Emoji_Modifier
匹配所有的表情。
举个更加详尽的例子
ECMAScript 2018 — showing how \p can be used for various emojis
//The following matches an Emoji character
/\p{Emoji}/u.test('❤️'); //true
//The following fails because yellow emojis don't need/have Emoji_Modifier!
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌️'); //false
//The following matches an emoji character\p{Emoji} followed by \p{Emoji_Modifier}
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌?'); //true
//Explaination:
//By default the victory emoji is yellow color.
//If we use a brown, black or other variations of the same emoji, they are considered
//as variations of the original Emoji and are represented using two unicode characters.
//One for the original emoji, followed by another unicode character for the color.
//
//So in the below example, although we only see a single brown victory emoji,
//it actually uses two unicode characters, one for the emoji and another
// for the brown color.
//
//In Unicode database, these colors have Emoji_Modifier property.
//So we need to use both \p{Emoji} and \p{Emoji_Modifier} to properly and
//completely match the brown emoji.
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌?'); //true
我们用大写的 P 替代 小写 的 p 去取反。(Lastly, we can use capital “P”(\P ) escape character instead of small p (\p ), to negate the matches.)
参考文献:
1. ECMAScript 2018 Proposal
2. https://mathiasbynens.be/notes/es-unicode-property-escapes
8. Promise.prototype.finally()
finally()
是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally
没有返回值,始终会被执行。
来看案例:
ECMAScript 2018 — finally() in resolve case
ECMAScript 2018 — finally() in reject case
ECMASCript 2018 — finally() in Error thrown from Promise case
ECMAScript 2018 — Error thrown from within catch case
9. Asynchronous Iteration
这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。
新特性新增了一个 for-await-of
循环,允许我们在循环过程中,调用异步方法,返回 Promise, 或者 支持 promise 的 Array。
最炫酷的是,循环会等待每一个 Promise resolve 之后,进入下次循环。
ECMAScript 2018 — Async Iterator via for-await-of