知られてそうで知られてない少し知られてるECMAScript 2015 (ES6)の新機能2016年05月12日 15時38分

Kyoto.js #10で「知られてそうで知られてない少し知られてるECMAScript 2015 (ES6)の新機能」というライトニングトークをしたので、そのスライドに当日しゃべった内容や補足を追記して以下に掲載します。


自己紹介

ECMAScript 2015 (ECMA-262 6th Edtition)

  • クラス構文
  • アロー関数
  • ブロックスコープ
  • Promise
  • etc...

2015年にECMAScriptが大幅に改定されました。クラス構文やアロー関数の追加はあちこちで取り上げられているので皆さんご存知でしょうが、ここではあまり取り上げられていない地味な新機能を紹介します。

Unicodeの符号位置

// 🍣

'\uD83C\uDF63';  // ES5
'\u{1F363}';     // ES6

String.fromCharCode(0xD83C, 0xDF63);  // ES5
String.fromCodePoint(0x1F363);        // ES6

JavaScriptの文字列はUTF-16符号化形式で表現されます。絵文字などBMP(基本多言語面)外の文字はサロゲートペアと呼ばれるふたつの符号単位の組み合わせで表現されるため、これまでエスケープシーケンスを使うときなどには符号単位(16ビット符号なし整数)ごとに指定する必要がありました。ES2015ではUnicodeの符号位置(U+0U+10FFFFまでの値)を直接指定できるようになります。

Unicode正規化

'㍿ 100パーセント'.normalize('NFKC');
// => '株式会社 100パーセント'

'神 森鷗外'.normalize('NFC')
// => '神 森鷗外'

Unicodeには結合文字などの表現形式をそろえる「正規化」という手順が定義されています。ES2015では文字列にnormalizeメソッドが追加され、NFCやNFKCといった複数種類の正規化を適用できるようになりました。

NFKCで文字列を正規化すると、組文字が個々の文字に展開されたり、全角英数字が半角に、半角カタカナが全角にそろえられたりします。正規化を行うことで「神」が「神」になるなど字体が変わってしまうこともあるので注意してください。

正規表現の/uフラグ(unicodeオプション)

/^..$/.test('🍣');  // => true
/^.$/u.test('🍣');  // => true

/\p/.test('p');   // => true
/\p/u.test('p');  // => SyntaxError

これまで正規表現は符号単位ごとにマッチしていましたが、/uフラグを指定することで符号位置ごとにマッチするようになります。

また、未定義のメタ文字があったとき、これまでは\が無視されていましたが、/uフラグ下では構文エラーになります。これにより、将来新たなメタ文字を導入しても、既存の正規表現パターンの挙動が変わってしまうということがなくなります。将来の拡張性確保のためには重要な変更です。

正規表現の/yフラグ(stickyオプション)

let re = /bar/y;

re.test('foobar');  // => false

re.lastIndex = 3;
re.test('foobar');  // => true

通常正規表現マッチは部分一致ですが、/yフラグを付けると前方一致になります。/bar/.test('foobar')はマッチ成功となりますが/bar/y.test('foobar')barが先頭にないのでマッチ失敗となります。

前方一致といいましたが、より正確に言うと正規表現オブジェクトのlastIndexプロパティで指定した位置からの一致になります。上の例ではlastIndex3を指定したので、fooの直後からマッチを試みることになり、マッチが成功しています。

これが役に立つ場面として字句解析があげられます。文字列からトークンを切り出した後、次のトークンを探すときには前回のトークンの終了位置直後から探索できると便利でしょう。

正規表現の挙動の上書き

let regExpLike = {
    [Symbol.match]()   { return 42; },
    [Symbol.replace]() { return 23; },
    [Symbol.search]()  { return 12; },
};
'foo'.match(regExpLike);           // => 42
'foo'.replace(regExpLike, 'bar');  // => 23
'foo'.search(regExpLike);          // => 12

文字列のmatchメソッドなどは、引数として渡されたオブジェクトに処理を委譲するようになりました。Symbol.matchメソッドを持つオブジェクトを引数に渡すことで、そのメソッドの返り値がそのまま文字列のmatchメソッドの返り値となります。

RegExpを継承するオブジェクトなら、execメソッドを上書きするだけでも大丈夫です。

class MyRegExp extends RegExp {
   exec(str) { return [42]; }
}
let re = new MyRegExp;
'foo'.match(re); // => [42]

この挙動の変更は、XRegExpのような正規表現を拡張するライブラリの作成に有用だと思われます。

数学関数の追加

Math.sinh(1);   // => 1.1752011936438014
Math.expm1(1);  // => 1.718281828459045

双曲線関数や常用対数・二進対数関数など、多くの数学関数が追加されています。

Math.expm1(x)Math.exp(x) - 1と同じですが、xが0に近い値のときにより精度の高い結果を返します。

数値に限定した判別関数

isNaN('foo');         // => true
Number.isNaN('foo');  // => false

isFinite('42');         // => true
Number.isFinite('42');  // => false

従来のグローバル関数isNaN / isFiniteは引数を数値に変換してから非数・有限値かどうかを判定していました。ES2015で追加されたNumber.isNaN / Number.isFiniteは引数が数値でなければその時点で偽を返します。

整数の扱い

Math.trunc(3.1);   // => 3
Math.trunc(-3.1);  // => -3

Math.imul(0xFFFFFFFF, 0xFFFFFFFF);  // => 1

Number.isInteger(Math.pow(2, 53));      // => true
Number.isSafeInteger(Math.pow(2, 53));  // => false

Number.MAX_SAFE_INTEGER;
// => 9007199254740991 (2 ** 53 - 1)
Number.MIN_SAFE_INTEGER;
// => -9007199254740991 (- (2 ** 53 - 1))

Math.truncは引数が正の数なら切り下げ(Math.floor)を、負の数なら切り上げ(Math.ceil)を行います。

Math.imulはふたつの32ビット符号なし整数値を乗算し、その結果の下位32ビットの値を返します。単に*演算子を使うと64ビット浮動小数点数の有効桁数を超えたときに不正確な値になってしまいます。

整数かどうかの判定メソッドNumber.isIntegerも追加されました。Number.isSafeIntegerは整数であるかどうかに加えて、64ビット浮動小数点数で一の位まで正確に表せる範囲内かどうかも見ます。新たな定数を使って言うと、Number.MIN_SAFE_INTEGER以上Number.MAX_SAFE_INTEGER以下の整数に対してNumber.isSafeIntegerは真を返します。

completion valueの変化

eval('42; if (true) {}');  // ES5 => 42
eval('42; if (true) {}');  // ES6 => undefined

ECMAScript仕様では、式や文の評価結果(返り値や制御フロー)を表すcompletion recordという値が定義されています。これは概念的なものであり、各JavaScriptエンジンがcompletion recordという値を実装しているとは限りません。completion recordに基づく文の評価結果、いわば「文の返り値」は通常触れることはできませんが、eval関数の結果としてその値を確かめられます。

ES2015ではif文やfor文といった制御構文におけるcompletion recordの扱いが少し変更されました。ES5までは制御構文の本体が空だったとき、プログラム全体の返り値はその制御構文の直前の文の返り値を引き継いでいました。ES2015からは制御構文の本体が空ならundefinedが返るようになります。

この変更は主にJavaScriptエンジンの実装の都合によるものです。これまでは制御構文の評価に移ってもそれ以前の文の返り値を保持しておく必要がありました。ES2015に沿った実装なら制御構文の評価に移った時点でそれ以前の文の返り値を破棄してもよく、実装に最適化の余地が生まれると考えられます。

HTMLのようなコメント (付属書B: Webブラウザのための追加機能)

<!--
doSomething();
-->

HTMLのscript要素内にJavaScriptを書くときなど、要素の内容全体を<!-- -->で囲むことがあります。このとき<!---->も単一行コメントとみなされますが、この挙動はECMAScriptで定義されたものではなく、あくまで各Webブラウザの独自拡張という扱いでした。

ES2015では(仕様本文ではなく)付属書において<!-- -->をコメントとして扱う構文を定義しています。ただし、あくまでWebブラウザが後方互換性を確保するために実装するものであり、ブラウザと直接関係ない実装(Node.jsなど)がこの構文に対応することは推奨されていません。プログラマもこうしたレガシーな機能を使うべきでないとされています。