ECMAScript 2018 (ES9) まとめ

0 件のコメント

今回は「ECMAScript2018 (ES9)」についてまとめます。

今回の大きな修正は「正規表現」です。 正規表現に新たなフラグや機能が追加されました。

文字列

テンプレートリテラル

テンプレートリテラルが抱えていた制約を緩和する機能が追加されました。

何が問題だったか

LaTeXコマンドには「¥unicode」や「¥xerxes」といったコマンドがあるそうです。 一方で、ECMAScriptでは「¥u」はユニコードを示す特殊文字、「¥x」は16進数を示す特殊文字となっています。 その結果、「¥unicode」や「¥xerxes」をテンプレートリテラルで利用すると、後に続く文字列が想定とは異なるためシンタックスエラーとなって落ちてしまいます。

1
var txt = `\unicode`    // SyntaxError: Invalid Unicode escape sequence

どのように解決されたのか

ECMAScript2018 からは「"タグ付き" テンプレートリテラル」の場合に限りシンタックスエラーにはせず、undefined が戻るようになります。 もともとの文字列はタグ関数において .raw というプロパティにアクセスすることで取り出せます。 タグを付けない通常通りのテンプレートリテラルを使う場合、引き続きシンタックスエラーになります。

1
2
3
4
5
6
7
8
9
10
function tag(strs) {
  console.dir(strs);
}
 
tag`\unicode`;
// 0: undefined
// raw: ["\unicode"]
 
`\unicode`;
// SyntaxError: Invalid Unicode escape sequence

「タグ付きテンプレートリテラル」とは、「"タグ関数"を利用したテンプレートリテラル」です。 この「タグ関数」を利用すると、テンプレートリテラルの出力内容を変更することができます。 参考までにサンプルを以下に記載します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function tag(strings, ...values) {
  console.dir(strings);
  // Array(3)
  //   0: "Hello "
  //   1: " World "
  //   2: ""
  //   length: 3
  //   raw: (3) ["Hello ", " World ", ""]
 
  console.dir(values);
  // Array(2)
  //   0: 8
  //   1: 15
  //   length: 2
 
  return "foo bar !";
}
 
var a = 5;
var b = 3;
tag`Hello ${a + b} World ${a * b}`;
// "foo bar !"

正規表現

s フラグ

「単一行モード(single line mode)」での検索が行えるようになります。 単一行モードで検索すると "."(ピリオド) の意味が「改行文字を含めたすべての文字に一致する」よう変更されます。

1
2
3
4
5
var re1 = /foo.bar/s;
var re2 = /foo.bar/;
 
re1.test("foo\rbar");    // true
re2.test("foo\rbar");    // false

u フラグ

「Unicode文字」には「Unicode文字プロパティ」という属性情報が存在します。 この「Unicode文字プロパティ」にはその文字がそもそも「文字」なのか「記号」なのかといった情報(General Category)や、「ラテン文字」「ひらがな」「カタカタ」「漢字」であるかといった情報(Script)などを持っています。

ECMAScript2018 で追加された u フラグ を利用すると、この「Unicode文字プロパティ」に対してマッチングをかけることができるようになります。 指定は \p{<Unicode文字プロパティ>} (肯定ケース) または \P{<Unicode文字プロパティ>} (否定ケース) で行います。 以下に簡単なサンプルを記載します。

1
2
3
4
5
6
7
8
9
// 肯定ケース
/^\p{scx=Hiragana}+$/u.test("ひらがな")   // true
/^\p{scx=Katakana}+$/u.test("カタカナ")   // true
/^\p{scx=Han}+$/u.test("漢字")            // true
 
// 否定ケース
/^\P{scx=Hiragana}+$/u.test("ひらがな")   // false
/^\P{scx=Katakana}+$/u.test("カタカナ")   // false
/^\P{scx=Han}+$/u.test("漢字")            // false

参考記事

名前付きキャプチャ

正規表現は "()"(丸括弧) を利用するとキャプチャと呼ばれる機能が使えます。 キャプチャは "()"(丸括弧) で囲まれた条件にマッチした文字列を後から再利用することができる機能です。 通常は "("(開き丸括弧) の現れる順に番号が振られ、再利用する際はその番号を利用して一致する文字列を呼び出します。

1
2
3
4
5
6
7
8
9
10
/(\d{4})-(\d{2})-(\d{2})/.exec("2018-10-21");
// (4) ["2018-10-21", "2018", "10", "21", index: 0, input: "2018-10-21", groups: undefined]
//   0: "2018-10-21"    ←マッチした全量
//   1: "2018"          ←1番目のキャプチャ
//   2: "10"            ←2番目のキャプチャ
//   3: "21"            ←3番目のキャプチャ
//   groups: undefined
//   index: 0
//   input: "2018-10-21"
//   length: 4

現れた順に番号を振る既存の方法でもやりたいことは実現できるのですが、実装するうえでは該当する番号が何を示しているのかわかりづらい状態になってしまいます。 そこで、最近の正規表現には「名前付きキャプチャグループ」というものが追加されているので、JavaScriptにも取り込もうというのが今回の追加内容です。

基本

名前付きキャプチャグループは「(?<name> ...)」で表現します。 一致した結果は .groups 配下にオブジェクトが生成されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec("2018-10-21");
// (4) ["2018-10-21", "2018", "10", "21", index: 0, input: "2018-10-21", groups: {…}]
//   0: "2018-10-21"
//   1: "2018"
//   2: "10"
//   3: "21"
//   groups:
//     day: "21"
//     month: "10"
//     year: "2018"
//   index: 0
//   input: "2018-10-21"
//   length: 4

名前付きキャプチャを使っていきなりマッチする文字列を取得するには以下のような記述もできます。

1
2
var {groups : {year, month, day}} = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec("2018-10-21");
console.log(`${year}/${month}/${day}`);   // 2018/10/21

後方参照

名前付きキャプチャで一致した文字列をその正規表現中で再利用させることができます。 同一正規表現内で再利用する場合、 ¥k<name> で指定します。

1
2
3
4
5
var re = /(?<key1>\d{2})-(?<key2>\d{2})-\k<key1>-\k<key2>/;
 
re.test("00-11-00-11");  // true
re.test("00-11-00-12");  // false
re.test("00-11-01-11");  // false

置換

名前付きキャプチャが利用できるようになることで、名前付きキャプチャを使った文字列置換( String.prototype.replace() )もできるようになります。 文字列置換で再利用する場合、 $<name> で指定します。

1
2
var name = "Tanaka Minoru";
name.replace(/(?<last>\w+)\s(?<first>\w+)/, "$<first> $<last>");    // "Minoru Tanaka"

後読み

正規表現には「先読み」と「後読み」(あわせて「先後読み」)と呼ばれる機能があります。 「先読み」は「文字列の先を確認して正規表現がマッチするか調べる」もので、「後読み」は「文字列の手前を確認して正規表現がマッチするか調べる」ものです。 これに加えて「先後読み」には「肯定」と「否定」があるので、以下の4パターンが存在します。

種類 指定方法
肯定先読み (?=pattern)
否定先読み (?!pattern)
肯定後読み (?<=pattern)
否定後読み (?<!pattern)

JavaScriptの正規表現にはもともと「先読み」しか存在していませんでした。 これに対して、ECMAScript2018で「後読み」が追加されました。

1
2
3
4
5
6
7
// 肯定後読み
/(?<=Tanaka)Minoru/.exec("TanakaMinoru");       // ["Minoru", index: 6, input: "TanakaMinoru", groups: undefined]
/(?<=Tanaka)Minoru/.exec("SuzukiMinoru");       // null
 
// 否定後読み
/(?<!Tanaka)Minoru/.exec("TanakaMinoru");       // null
/(?<!Tanaka)Minoru/.exec("SuzukiMinoru");       // ["Minoru", index: 6, input: "SuzukiMinoru", groups: undefined]

式と演算子

残余プロパティ / 分割プロパティ

ECMAScript2015 (ES6) で追加された「スプレッド演算子( ... )」がオブジェクトにも適用できるようになりました。 適用できる箇所はオブジェクトから展開して代入する箇所(残余プロパティ)とオブジェクトへ展開して与える箇所(分割プロパティ)です。

1
2
3
4
5
6
7
8
9
// 残余プロパティ
var {x, y, ...z} = {x: 1, y: 2, a: 3, b: 4, c: 5 };
x   // 1
y   // 2
z   // {a: 3, b: 4, c: 5}
 
// 分割プロパティ
var n = {x, y, ...z};
n   // {x: 1, y: 2, a: 3, b: 4, c: 5}

Promise.prototype.finally()

明示的な finaly() が追加されました。 これで非同期処理 ( Promise ) において try - catch - fainally が実現できるようになります。 ちなみに、 finally() で例外が発生した場合、再度 Promise が生成されるようです。

1
2
3
4
5
6
7
8
9
10
11
(new Promise((resolve, reject) => {
  window.setTimeout(() => { reject(); }, 1000);
})).then((data) => {
  console.log("then");
}).catch((err) => {
  console.log("catch");
}).finally(() => {
  console.log("finally");
});
// catch
// finally

非同期イテレータ

「非同期イテレータ」「非同期ジェネレータ」が利用できるようになり、これに伴って for - await - of 構文が新しくできました。

「非同期イテレータ」は「同期イテレータ」と同じように .next() の戻り値が {value, done} となるようなイテレータです。 ただ、これ単体だと使いづらく…「非同期ジェネレータ」を使った方が分かりやすいです。 「非同期ジェネレータ」は yield の戻り値が「非同期イテレータ」となっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 指定された値を1秒後に戻す
var remoteProcess1 = function (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve(value); }, 1000);
  });
};
 
// 1秒おきに文字を取得してくる
var asyncIterator1 = async function* () {
  yield await remoteProcess1("a");
  yield await remoteProcess1("b");
  yield await remoteProcess1("c");
};
 
// 単純な非同期イテレータのサンプル
var aitr = asyncIterator1();
aitr.next().then(({ value, done }) => {
  console.log(value);
});
// a     ←イテレータを1度しか実行してないので

さて、上記の「非同期イテレータ」「非同期ジェネレータ」をさらに使いやすくできるのが for - await - of 構文です。 非同期処理の応答を待ちながらループを回すことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 指定された値を1秒後に戻す
var remoteProcess1 = function (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve(value); }, 1000);
  });
};
 
// 1秒おきに文字を取得してくる
var asyncIterator1 = async function* () {
  yield await remoteProcess1("a");
  yield await remoteProcess1("b");
  yield await remoteProcess1("c");
};
 
// for-await-of 構文を利用したサンプル
var main = async function () {
  for await (var val of asyncIterator1()) {
    console.log(val);
  }
};
main();
// a
// b
// c

対応状況

2018年10月21日現在。

コンパイラ ブラウザ ランタイム
Babel 7 Closure 2018.09 IE 11 Edge 18 Firefox 62 Chrome 69 Safari 12 Node.js 8
×

Babel は「後方参照」以外使えます。 Closureコンパイラは正規表現周りが全滅で、その他は使えるようです。 ブラウザだとChrome以外はまだ実装中といったステータス。 Nodeもどちらかというと実装中といったステータス。 全体的にまだまだな印象です。

今回は「ECMAScript2018 (ES9)」についてまとめました。 参考になったでしょうか? 本記事がお役に立っていると嬉しいです!!

参考記事

最後に… このブログに興味を持っていただけた方は、 ぜひ 「Facebookページ に いいね!」または 「Twitter の フォロー」 お願いします!!