JavaScript
フロントエンド
ES2018

ES2018で追加される機能まとめ

はじめに

ECMAScript 2018に最終的に入るProposalが決定しました。
ECMAScriptは毎年6月ごろにリリースされるため、これから新しい機能(Proposal)を入れると間に合わなくなります。
そのため、1月ごろに機能(Proposal)の追加はフリーズされ、この時期からStage 4となったProposalはECMAScript仕様本体へマージされます。

2018-01-29のJS: Firefox 58、Safari 11.1(β)、Parcel 1.5.0、webpack 4(β)、ES 2018の機能が決定 - JSer.info
https://jser.info/2018/01/30/firefox-58-safari-11.1b-parcel-1.5.0-webpack-4b-es-2018/

というわけで、ES2018のProposalがまとまった模様です。
さっそく追ってみます。

テンプレートリテラルのエスケープシーケンス見直し

ES6で追加されたテンプレートリテラルの、バックスラッシュに関するエラーの見直しが図られています。
普通に使っているとそこまでエラーには遭遇しないかもしれませんが、以下のようなケースでエラーになっているみたいです。

const texCommand = `\usepackage[utf8]{inputenc}`;
// Uncaught SyntaxError: Invalid Unicode escape sequence

\uの部分がUnicodeエスケープシーケンス(例:\u0020)の開始と解釈されますが、不正なエスケープシーケンスとしてエラーを吐いてしまいます。今後はこれをエラーにせず、undefinedとして扱うみたいです。

Promisefinally()が追加

Promiseの必ず最後に実行されるfinally()メソッドが追加されました。
try-catch-finallyのようにresolverejectの状態に関わらず、必ず最後に実行されるものとして使用可能です。  
たとえばajax時のローダーを非表示にする処理などはこちらに書くのが適しているといえます。

new Promise((resolve, reject) => {
  window.setTimeout(resolve, 100)
}).then(() => {
  console.log('then')
}).catch(e => {
  console.log('catch')
}).finally(() => {
  console.log('finally')
})
// then
// finally
new Promise((resolve, reject) => {
  window.setTimeout(reject, 100)
}).then(() => {
  console.log('then')
}).catch(e => {
  console.log('catch')
}).finally(() => {
  console.log('finally')
})
// catch
// finally

スプレッド演算子がオブジェクトにも使用できるように

ES6で追加されたスプレッド演算子ですが、配列だけでなくオブジェクトにも使用できるようになります。

/* Rest Properties */
const { a, b, ...c } = { a: 1, b: 2, x: 3, y: 4, z: 5}
console.log(a)  // 1
console.log(b)  // 2
console.log(c)  // {x: 3, y: 4, z: 5}

/* Spread Properties */
console.log({ a, b, ...c })
// {a: 1, b: 2, x: 3, y: 4, z: 5}

非同期に対応したイテレーターの追加

ES6で実装されたIteratorは同期データの取得に対してとても役に立つ機能でした。  
ES2018では非同期データにも対応できるようになりました。
それに伴い、プロパティを操作するfor-ofとは別にfor-await-ofが実装されました。

for await (const line of readLines(filePath)) {
  console.log(line);
}

たとえば以下のコードでは、
a()の中でfor-ofを使用してURLを走査し、fetch()してStringを返します。
b()の中でfor-await-ofを使用して、fetch()した内容を出力するようにしています。

const urls = [
  'https://github.com/tc39/proposal-object-rest-spread',
  'https://github.com/tc39/proposal-regexp-lookbehind',
  'https://github.com/tc39/proposal-regexp-unicode-property-escapes',
  'https://github.com/tc39/proposal-promise-finally',
  'https://github.com/tc39/proposal-async-iteration'
]

async function * a () {
  for (const url of urls) {
    console.log(`Fetching: ${url}`)
    const response = await fetch(url)
    const iterable = response.text()
    yield iterable
  }
}

async function b () {
  for await (const i of a()) {
    const title = i.match(/<title>(.+)<\/title>/)[1]
    console.log(`Title: ${title}`)
  }
}

b();

// Fetching: https://github.com/tc39/proposal-object-rest-spread
// Title: GitHub - tc39/proposal-object-rest-spread: Rest/Spread Properties for ECMAScript
// Fetching: https://github.com/tc39/proposal-regexp-lookbehind
// Title: GitHub - tc39/proposal-regexp-lookbehind: RegExp lookbehind assertions
// Fetching: https://github.com/tc39/proposal-regexp-unicode-property-escapes
// Title: GitHub - tc39/proposal-regexp-unicode-property-escapes: Proposal to add Unicode property escapes `\p{…}` and `\P{…}` to regular expressions in ECMAScript.
// Fetching: https://github.com/tc39/proposal-promise-finally
// Title: GitHub - tc39/proposal-promise-finally: ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
// Fetching: https://github.com/tc39/proposal-async-iteration
// Title: GitHub - tc39/proposal-async-iteration: Asynchronous iteration for JavaScript

このコードはとてもわかりやすかったため、以下のページから抜粋しています。

ECMAScript 2018(ES2018)の新機能まとめ | あるいてっく
https://arui.tech/es2018-new-features/#Lookbehind_Assertions

正規表現

ここから下は全て正規表現の機能です。

sフラグの追加

正規表現で使用される.は改行コードなどを含まれていませんでした。
これまでこの問題を回避するためには[\s\S]のように書く必要がありました。

/foo.bar/.test('foo\nbar');
// → false

/foo[\s\S]bar/.test('foo\nbar');
// → true

少し分かりづらかったこの問題を回避するためにsフラグが追加されました。  
sフラグを使用することで正規表現中に含まれる.が改行コードなどを含めて評価してくれるようになります。

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

正規表現のキャプチャグループに名前がつけられるように変更

今までexec()などでキャプチャした場合、結果として配列が返ってきていました。
この配列はresult[0]のように添え字を渡すことでしか判断できませんでした。

let re = /(\d{4})-(\d{2})-(\d{2})/;
let result = re.exec('2015-01-02'); 
// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

この問題を回避するために、キャプチャする部分に名前を渡すことができるようになりました。
使い方は簡単で(?<name>...)のシンタックスで書けばOKです。

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';

後方参照が追加

この機能には後方参照というものがあり、\k<name>を使うと、前方で使用した名前に再度アクセスすることができます。
言葉にするとわかりづらいため、以下のコードを御覧ください。

let duplicate = /^(?<half>.*).\k<half>$/u;
duplicate.test('a*b'); // false
duplicate.test('a*a'); // true

replace()での使用

replace()でもこの機能は使用することができます。  
例えば以下のコードは日付のフォーマットを入れ替えるような動作をします。

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

後読みの追加

javascriptの正規表現には先読みしか存在しておらず、今まで後読みのアサーションが存在していませんでした。
今回ついに後読みアサーションが実装されることになりました。
先読みと後読みそれぞれを記載いたします。

肯定先読み

まず先読みは(?=***)のシンタックスで書きます。
以下のコードは123456が続くかどうかを判断しています。

'123456'.match(/123(?=456)/)     // -> ["123"]
'123'.match(/123(?=456)/)        // -> null
'1234'.match(/123(?=456)/)       // -> null
'1234567'.match(/123(?=456)/)    // -> ["123"]

否定先読み

否定先読みは(?!***)のシンタックスで書きます。

'123456'.match(/123(?!456)/)     // -> null
'123'.match(/123(?!456)/)        // -> ["123"]
'1234'.match(/123(?!456)/)       // -> ["123"]
'1234567'.match(/123(?!456)/)    // -> null

肯定後読み

今回追加された後読みは(?<=***)のシンタックスを使います。
以下のコードは、上に書いたコードの逆で123を前にもつ456を検出します。

'123456'.match(/(?<=123)456/)     // -> ["456"]
'456'.match(/(?<=123)456/)        // -> null
'3456'.match(/(?<=123)456/)       // -> null
'1234567'.match(/(?<=123)456/)    // -> ["456"]

否定後読み

否定後読みは(?<!***)のシンタックスで表されます。
そのため、以下のコードでは123を持たない456を検出します。

'123456'.match(/(?<!123)456/)     // -> null
'456'.match(/(?<!123)456/)        // -> ["456"]
'3456'.match(/(?<!123)456/)       // -> ["456"]
'1234567'.match(/(?<!123)456/)    // -> null

ユニコード内のプロパティがマッチング対象に追加

Unicodeの文字内にはたくさんのプロパティが指定されています。  
このプロパティを、正規表現の中でマッチングに使うことができるようになります。

今までは同様の機能を実装するのに外部ライブラリなどを使用する必要があったのですが、今回は言語仕様としてサポートされることになりました。

この機能は\p{…} and \P{…}のシンタックスを使って書きます。

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

Unicodeプロパティの調べ方などは以下のページにも詳しくまとまっていました。

ECMAScript 2018(ES2018)の新機能まとめ | あるいてっく
https://arui.tech/es2018-new-features/#Lookbehind_Assertions

おわりに

正規表現に多くの機能が追加され、細かいところが強化されている印象を受けました。
何かと使うことが多い機能なので上手に使いこなしていきたいと思います。