34–3. You Don’t Know JS: ES6 & Beyond Chapter 2: Syntax 3/3

Tatsuya Asami
51 min readAug 15, 2018

--

【所要時間】

7時間13分(2018年8月12,13,14,15日)

【概要】

ES6の構文について

【要約・学んだこと】

Arrow Functions

functionを使うthis binding の複雑さに触れてきた。this-based プログラムでnormal functionが持つ不満を理解することは重要だ。なぜならES6の新しい=> arrow function 機能のモチベーションとなっているからだ。

function foo(x,y) {
return x + y;
}

// versus

var foo = (x,y) => x + y;

arrow function の定義は、function bodyの後ろに=>、その後ろにparameterを含むことだ。

このsnippetでは、 arrow functionは (x,y) => x + y だけだ。そしてそのfunction referenceはvariable fooの割り当てが発生する。

もし1つ以上のexpressionがあるなら、もしくはbodyがnon-expression statementを含むなら、bodyは{ .. } で囲まれることのみが必要。

このsnippetのようにexpressionが1つだけなら、{ .. }で囲わなくてもよく、expressionの前のreturnを意味する。

他のarrow functionのバリーエションは

var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
var z = x * 2 + y;
y++;
x *= 3;
return (x + y + z) / 2;
};

arrow functionは常にfunction expressionである。arrow functionのdeclarationは存在しない。また、anonymous function expressionである。
再帰やイベントのbinding/unbindingのためのnamed referenceはない。chapter 7の”Function Names”で詳しく説明。

Note:
全てのnormal function parameterはarrow functionで使える。それにはdefault value、destructuring, rest parameterなども含む。

arrow functionは記述が短く、書きやすいコードだ。実際、ES6に関するほとんどの文献は、new functionとしてarrow functionを即座に独占的に採用しているようだ。

arrow functionの議論のほとんど全ての例は、短い単一のstatementでの使い方だ。たとえば様々なユーティリティのコールバックとして渡されるものだ。

var a = [1,2,3,4,5];

a = a.map( v => v * 2 );

console.log( a ); // [2,4,6,8,10]

これらの場合では、inline function expressionを持ち、1つのstatementで結果も返しており、冗長なfunction keywordとsyntaxの代わりに、軽い動作が魅力だ。

しかし、通常のmultistatement functionを持つarrow functionを使うことの誤用に気をつける必要がある。特にfunction declarationとして自然に表現されていたものだ。

dollabillsyall(..) string literal tag functionを思い出し、 => syntaxを使うように変更しよう。

var dollabillsyall = (strings, ...values) =>
strings.reduce( (s,v,idx) => {
if (idx > 0) {
if (typeof values[idx-1] == "number") {
// look, also using interpolated
// string literals!
s += `$${values[idx-1].toFixed( 2 )}`;
}
else {
s += values[idx-1];
}
}

return s + v;
}, "" );

この例では、function, return, { .. } を取り除き、 =>と var を挿入という修正をしただけだ。これは可読性を大幅に上げるだろうか。

returnの不足と外側の{ .. }はreduce(..) callは、dollabillsyall(..) functionにある唯一のstatementであるという事実を部分的に曖昧にし、その結果は呼び出しの意図した結果となる。
また、scopeの境界を探すためのコード内のfunction の言葉を探す目は、=> を見つけるために今や必要だ。そしてそれは困難だ。

確実なルールではないが、=> arrow function 変換から得られる可読性の利益は、変換されるfunctionの長さに反比例すると言える。長いfunctionは=>の助けが少なく、短いfunctionは=>がより輝く。

短いinline function expressionが必要なコードでは=>を採用するのがおそらく合理的だが、通常の長さの main function はそのままにしておく。

Not Just Shorter Syntax, But this

最も有名な=> の注意点は、コードからfunction, return, { .. } を落とすことで、貴重なキーストロークを保存することだ。

しかし、これまで重要な詳細を飛ばしてきている。=> functionはthis binding の動作に深く関わると先ほど説明した。実際に、=> arrow functionは特殊な方法でのthisの動作の代案として、主に設計されており、this-awareコーディングで、特定の一般的な欠点の解決をする。

キーストロークをセーブすることは、よく誤解を招く。

var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};

var self = this ハックを使い、self.makeRequest(..)を参照した。なぜならaddEventListener(..)に渡したcallback functionの中だからで、this bindingはmakeRequest(..)自体にあるのとは同じでない。言い換えると、this bindingはdynamicなので、self variableを通じてlexical scopeの予測可能性に戻るからだ。

ここで主な=> arrow functionの特徴的なデザインを見ることができる。arrow functionの中では、 this bindingはダイナミックではない代わりに、lexicalだ。前のsnippetで、call backにarrow functionを使ったなら、thisは予測可能なものとなる。

var controller = {
makeRequest: function(..){
btn.addEventListener( "click", () => {
// ..
this.makeRequest(..);
}, false );
}
};

前のsnippedでarrow function callbackにあったlexical thisは、囲われたmakeRequest(..) functionと同じvalueを指す。言い換えると、=> は var self = thisのsyntactic stand-inだ。

var self = this(もしくはfunction .bind(this)callの代わり)が通常通り役に立つ場合、=> arrow functionは同じ原則のよりよい代わりのoperatingだ。

もし=<がvar self = thisか、 .bind(this)を置き換えると、それは役に立つ。=>をvar self = thisが動作する必要がない this-aware functionで使うと何が起こるのか。

var controller = {
makeRequest: (..) => {
// ..
this.helper(..);
},
helper: (..) => {
// ..
}
};
controller.makeRequest(..);

controller.makeRequest(..)として呼び起こしていないにも関わらず、this.helper referenceは失敗する。なぜならここでの this はcontrollerを通常通り指さないからだ。それは囲われたscopeから構文的にthisを継承する。この前のsnippetではそれがglobal scopeで、this がglobal objectを指す。

lexicalなthisに加えて、functionが、自身のarguments arrayを持たない代わりに、parentを継承するlexical argumentsを持ち、またlexical superとnew.targetを持つ。chapter 3 の”Classes”参照。

=>を適切に使えるかどうかの一連のルールはよりニュアンス的なものを含む。

  • 短い単一のstatement inline function expressionを持つ場合は、唯一のstatementはcomputed valueのreturnだ。そしてfunctionはその中のthis referenceで、self-reference(recursion, event binding/unbinding)はなく、合理的なfunctionは期待できないが、おそらく=> arrow functionを安全にリファクタできる。
  • var self = this hackもしくはthis bindingが正しく行われるように囲われたfunctionで .bind(this) function expressionに頼っているinner function expressionを持つなら、そのinner function expressionはおそらく安全に => arrow functionになることができる。
  • argumentsのlexicalコピーを作るための囲われたfunction var argus = Array.prototype.slice.call(arguments)のinner function expressionを持つなら、そのinner function expressionはおそらく安全に=> arrow functionになれる。
  • それ以外の全て。normal function declarations, longer multistatement function expressions, functions that need a lexical name identifier self-reference (recursion, etc.),や、以前の文字にフィットしない他のfunctionなどでは、=> function syntaxを使うのは避けたほうがいい。

結論:
=> はthis, arguments, super の lexical bindingだ。よくある問題を修正するために設計された意図的な機能であって、ES6のバグやミスではない。

キーストロークがほとんどないという誇大宣伝を信じてはいけない。キーストロークをセーブしようが捨てようが、タイプしたキャラクター全てで何をしようとしているのかを正確に知る必要がある。

もし=> arrow functionがこれらの理由にマッチしないfunctionを持っているならば、object literalの一部として宣言されるべきで、”Concise Methods”というfunction syntaxを短くするほかのオプションがあることを覚えておこう。

for..of Loops

ES6には、forとfor .. in loop に加えて for.. of loopが追加された。iterator(反復子)によって作られたvalueのセットをループする。

for .. of で繰り返すvalueは、iterable(繰り返し可能)、もしくはiterableなobject()にcoerced/boxedされることが可能なvalueでなければならない。iterableは単にループが使うiteratorを作成することができるobjectだ。

for .. of とfor .. inを比べてみよう。

var a = ["a","b","c","d","e"];

for (var idx in a) {
console.log( idx );
}
// 0 1 2 3 4

for (var val of a) {
console.log( val );
}
// "a" "b" "c" "d" "e"

for .. in ループはarrayのkey/indexをループし、for .. of はaのvalueをループする。

ES6以前のバージョンでの for .. ofはこうだった。

var a = ["a","b","c","d","e"],
k = Object.keys( a );

for (var val, i = 0; i < k.length; i++) {
val = a[ k[i] ];
console.log( val );
}
// "a" "b" "c" "d" "e"

ES6だが、non-for. of と等価ではない。これはinteratorを手動で反復することも垣間見ることができる。(チャプター3の“Iterators”参照。)

var a = ["a","b","c","d","e"];

for (var val, ret, it = a[Symbol.iterator]();
(ret = it.next()) && !ret.done;
) {
val = ret.value;
console.log( val );
}
// "a" "b" "c" "d" "e"

for .. of ループはiteratorを反復可能にするように要求する。(built-in Symbol.iteratorを使う) Chapter7の”Well-Known Symbols”参照。
そして、iteratorを繰り返し呼び、作られたvalueをloop iteration variableに代入する。

JSのstandard built-in valuesは、デフォルトiterable(もしくは与えられた)を含む。

  • Arrays
  • Strings
  • Generators (see Chapter 3)
  • Collections / TypedArrays (see Chapter 5)

Warning:
plain objectはデフォルトループのfor. of には適さない。なぜならデフォルトiteratorを持たないからだ。それは意図的で、ミスではない。しかし、ここでこれ以上深入りはしない。

chapter3の”Iterators”で、自身のobjectでのiteratorの定義の仕方を学ぶ。

primitive stringの文字をループするには

for (var c of "hello") {
console.log( c );
}
// "h" "e" "l" "l" "o"

“hello” primitive string valueはString object wrapperに相当するものにcoerced/boxedされていて、これはデフォルトでiterableだ。

for (XYZ of ABC).. では、XYZ clauseはassignment expressionかdeclarationで、forやfor .. in ループの同じclauseと同じだ。だから下記のようにできる。

var o = {};

for (o.a of [1,2,3]) {
console.log( o.a );
}
// 1 2 3

for ({x: o.a} of [ {x: 1}, {x: 2}, {x: 3} ]) {
console.log( o.a );
}
// 1 2 3

for.. of loopは他のループのbreak, continue, return(function内なら)と、例外が投げられた時のように途中で止まることがある。これらの場合、iteratorのreturn(..) function が自動的に呼び出され(存在するなら)、必要に応じてiterator がクリーンアップタスクを実行する。

Note:
iterables とiteratorsの詳細については“Iterators” in Chapter 3 参照。

Regular Expressions

JSのregular expressionは長いこと変わっていない。ES6の新しいいくつかのトリックを学ぶ。

Unicode Flag

ここでは新しい u flagというUnicodeをexpressionとマッチさせるのを学ぶ。

JSのstringは16-bit characterの結果として解釈される。それはBasic Multilingual Plane(BMP)のcharacterと一致する。しかし、この範囲にはないUTF-16のキャラクターがたくさんあるので、stringにはこれらのマルチバイト文字が含まれている可能性がある。

ES6以前では、regular expressionはBMPキャラクターでのみマッチした。それは拡張文字はマッチングを目的とした2つの別々のキャラクターとして扱われていた。これはしばしばよくないことがおこった。

ES6からは u flagが、regular expressionにUnicode(UTF-16)characterの解釈をするstringを処理するように指示する。そのような拡張文字が1つの存在(entity)としてマッチングされるようになる。

Warning:
名前の意味に関わらず、UTF-16は厳密には16bitを意味するわけではない。Modern Unicodeは21bitを使い、UTF-8やUTF-16のようなスタンダードは文字の表現で何bit使われているかをおおよそ参照する。

例: (straight from the ES6 specification): 𝄞 (the musical symbol G-clef) is Unicode point U+1D11E (0x1D11E).

もしregular expression pattern(/𝄞/のような)この文字が現れたら、standard BMP解釈は、一致する2つの別々の文字(0xD834 and 0xDD1E)だということになる。しかし、新しいES6 Unicode-aware modeは、/𝄞/u (もしくは the escaped Unicode form /\u{1D11E}/u) が1つのマッチする文字のstringとして"𝄞" とマッチする。

non-Unicode BMP modeでは、このpatternは2つの別々の文字として扱われる。しかし、試して見るとわかるように、stringに"𝄞" が含まれていることがわかる。

/𝄞/.test( "𝄞-clef" );			// true

マッチの長さは重要だ。

/^.-clef/ .test( "𝄞-clef" );		// false
/^.-clef/u.test( "𝄞-clef" ); // true

patternの ^.-clef はnormal “-clef” テキストの前の最初にある1文字とマッチするだけだ。standard BMP modeでは、マッチは失敗する(2文字)しかし、u Unicode modeでフラグが立てられている場合、マッチは成功する。(1文字)

また、+ と * のような数量子をUnicode code point全体に1文字として適用することに注意が必要。キャラクターlower surrogate(下位代理人、またはシンボルの右半分)だけではない。/[💩-💫]/u のような文字クラスに表示されるUnicode文字についても同様。

Note:
uの動作についての詳細はたくさんあるので、これらを参照。(https://mathiasbynens.be/notes/es6-unicode-regex)

Sticky Flag

ES6のregular expressionに追加された他のflag modeは y だ。 これはよく”sticky mode”と呼ばれる。stickyは本質的に、regular expressionの lastIndex propertyによって、regular expressionがバーチャルアンカーが最初に指定された場所でのみマッチングを根ざしているということを意味する。

説明のために2つのregular expressionについて考えてみよう。最初がsticky modeなしで、2つ目がsticky modeだ。

var re1 = /foo/,
str = "++foo++";
re1.lastIndex; // 0
re1.test( str ); // true
re1.lastIndex; // 0 -- not updated
re1.lastIndex = 4;
re1.test( str ); // true -- ignored `lastIndex`
re1.lastIndex; // 4 -- not updated

このsticky modeなしのsnippetで3点注目すべき点がある:

  • test(..) はlastIndexのvalueに注意を払わない。常にinput stringの先頭からのマッチを実行する。
  • なぜならpatternはstart-of-input アンカーの^ を持たず、”foo” の検索がstring全体を自由にマッチを探すために移動することができる。
  • lastIndex は test(..)によって更新されない。

次にsticky mode regular expressionをみてみよう。

var re2 = /foo/y,		// <-- notice the `y` sticky flag
str = "++foo++";
re2.lastIndex; // 0
re2.test( str ); // false -- "foo" not found at `0`
re2.lastIndex; // 0
re2.lastIndex = 2;
re2.test( str ); // true
re2.lastIndex; // 5 -- updated to after previous match
re2.test( str ); // false
re2.lastIndex; // 0 -- reset after previous match failure

sticky modeでは

  • test(..)lastIndex を、strの正確で唯一の位置としてマッチを探すために使う。マッチを探すために前に移動しない。lastIndexの位置にあるかどうかはわからない。
  • もしマッチしたら、test(..)はlastIndexをマッチのすぐの後ろの文字を指すように更新する。もしマッチしなかったら、test(..)はlastIndexを0にリセットする。

^- rootがstart-of-inputでないnormal non-sticky patternは、マッチを探すためのinput string内を自由に移動できる。

このセッションの最初で提案したように、これを探す別の方法は、y がpatternの先頭にあるバーチャルアンカーを意味する。それはlastIndexの位置に正確に関係(マッチの開始を制限)する。

Warning:
これまでの文献では、この動作はpatternの^(start-of-input)アンカーを暗示しているようなものであると主張している。それは正確ではない。”Anchored Sticky”で後ほど詳細を説明する。

Sticky Positioning

繰り返しのマッチにyを使うのは、不思議な制限のように思えるが、lastIndexが正確に正しいポジションにあることを手動で確かめる必要がある。これはマッチングのための先送り機能を持たないからだ。

ありがちな状況として、気にしているマッチが常に倍数(0,10,20..など)のポジションにあることがわかるなら、気にするものにマッチした限定されたpatternを作ることができるが、それらの固定ポジションにマッチする前毎に、lastIndexを手動でセットする。

var re = /f../y,
str = "foo far fad";
str.match( re ); // ["foo"]re.lastIndex = 10;
str.match( re ); // ["far"]
re.lastIndex = 20;
str.match( re ); // ["fad"]

しかし、そのような定位置でフォーマットされていないstringを解析している場合は、lastIndexをそれぞれのマッチの前に設定することを検討することがおそらく不可能となるでしょう。

yはlastIndexがマッチするための正確なポジションにあることを求める。しかし、lastIndexを手動でセットすることは厳しく求められてはいない。

その代わり、気にしているものの前後に、全てのメインマッチで捕えるような方法でexpressionを構築することができる。

なぜなら、lastIndexはマッチの最後を超えて次の文字に設定されるため、もしそのポイントまで全てマッチさせた場合、lastIndexはy patternが次回から始まる正しいポジションに常にある。

Warning:
このように十分にパターン化された方法でinput stringの構造を予測できないなら、このテクニックは適切でなく、yを使用できないかもしれない。

構造家されたstring inputを持つことは、yがstring全体で繰り返されるマッチを実行できる、最も実用的なシナリオである可能性が高い。

var re = /\d+\.\s(.*?)(?:\s|$)/y
str = "1. foo 2. bar 3. baz";

str.match( re ); // [ "1. foo ", "foo" ]

re.lastIndex; // 7 -- correct position!
str.match( re ); // [ "2. bar ", "bar" ]

re.lastIndex; // 14 -- correct position!
str.match( re ); // ["3. baz", "baz"]

これは動作する。なぜならinput stringの構造について事前に知っていたからだ。望んでいるマッチ(“foo”など)の前には常に”1"のような数字のprefixがあり、その後のスペース、もしくはstring($ anchor)がある。そのため、構築したregular expressionは、それぞれのメインマッチでその全てを捕らえ、マッチするグループ()を使い、本当に気にしているものを便利に分離している。

最初のマッチ(“1. foo”)のあと、lastIndex は 7、それはすでに次のマッチがスタートするのに必要なポジション、”2. bar”などだ。

リピートマッチでy stickyをつかうなら、デモンストレーションだけのために自動で置かれたlastIndexを持つ機会を探しているだろう。

Sticky Versus Global

g global match flag と exec(..) メソッドとlastIndexの相対マッチをエミュレートできる。

var re = /o+./g,		// <-- look, `g`!
str = "foot book more";

re.exec( str ); // ["oot"]
re.lastIndex; // 4

re.exec( str ); // ["ook"]
re.lastIndex; // 9

re.exec( str ); // ["or"]
re.lastIndex; // 13

re.exec( str ); // null -- no more matches!
re.lastIndex; // 0 -- starts over now!

exec() は特定のstringを探す。arrayの結果かnullを返す。

g patternがexec(..)とマッチするのは、lastIndexの現在のvalueからマッチングを開始することだが、それぞれのマッチ(またはfailure)の後にlastIndexを更新する。これはyの動作と同じではない。

このsnippetのposition6に位置する”ook”は、2回目のexec(..) callによってマッチされ、発見された。lastIndexが4(最後のマッチの終わりから)であったにも関わらずだ。なぜなら、再起ほど述べた通り、non-stickyマッチはマッチの中で自由に動くことができるからだ。sticky mode expressionではfailする。なぜなら前方に動くことを許可しないからだ。

yの代わりにgを使うことの他の欠点は、gはstr.match(re)のマッチメソッドの動作を変えることがある。

var re = /o+./g,		// <-- look, `g`!
str = "foot book more";

str.match( re ); // ["oot","ook","or"]

全てのマッチが1回で返された。これを望んでいない場合もある。

y sticky flagはtest(..)やmatch(..)のようなユーティリティと、1回に1つのプログレッシブマッチを行う。lastIndexがマッチ毎に常に正しい位置にあることを確認すること。

Anchored Sticky

sticky modeは ^ で始まるpatternを暗示すると考えるの間違いだ。^ anchorはregular expressionの明確な意味を持ち、sticky modeで代わりになるわけではない。^ は常にinputの最初を参照するアンカーで、lastIndexとは関わりがない。

Firefoxでsticky modeをES6以前で実行すると、^ は lastIndexと関係があり、そのためこの動作が何年も続いている。

ES6はそうしないことを選んだ。^は patternの中では入力開始を絶対的に意味するのみだ。

結果として、もしそこでマッチを許可するなら、/^foo/y のようなpatternは、常に、そして唯一”foo”がstringの先頭でマッチすることを発見する。lastIndexは0ではなく、マッチはfailとなる。

var re = /^foo/y,
str = "foo";

re.test( str ); // true
re.test( str ); // false
re.lastIndex; // 0 -- reset after failure

re.lastIndex = 1;
re.test( str ); // false -- failed for positioning
re.lastIndex; // 0 -- reset after failure

結論: y プラス ^ プラス lastIndex > 0は、常にfailed マッチを引き起こす互換性のない組み合わせだ。

Note:
yが^の代わりにならないが、m lineのmodeは^は改行後の入力開始、またはテキストの開始を意味する。そのため、yとm flagを一緒に使うパターンでは、stringの^ rooted マッチを発見することができる。
なぜならy stickyだからで、lastIndexがその後毎回新しい行の正しい位置(行の終わりにマッチすることで)をさしている、もしくは後のマッチがない必要がある。

Regular Expression flags

ES6以前では、何のflagが適用されているかを知るためのsource propertyの中身からregular expression objectを実験したかったら、それらを解析する必要があった。皮肉にも、おそらく他のregular expressionでもそうだ。

var re = /foo/ig;

re.toString(); // "/foo/ig"

var flags = re.toString().match( /\/([gim]*)$/ )[1];

flags; // "ig"

ES6以降では、これらのvalueを新しいflag propertyで直接得ることができる。

var re = /foo/ig;

re.flags; // "gi"

微妙なニュアンスだが、ES6の仕様では、このexpressionのflagがこの順序でリスト化されるように要求している。
元のpatternがどの順序で指定されているかに関わらず、“gimuy” だ。これは/igと”gi”の違いの理由である。

ES6からの別の調整は、既存のregula expressionを渡すと、RegExp(..)がコンストラクタフラグを意識するようになった。

var re1 = /foo*/y;
re1.source; // "foo*"
re1.flags; // "y"

var re2 = new RegExp( re1 );
re2.source; // "foo*"
re2.flags; // "y"

var re3 = new RegExp( re1, "ig" );
re3.source; // "foo*"
re3.flags; // "gi"

ES6以前では、re3の構造でエラーが発生したが、ES6では複製時にflagを上書きする。

Number Literal Extensions

ES6以前では、number literalは、octal(8進数) formが公式に指定をされておらず、ブラウザが実際に同意した拡張機能としてのみ許可されていた。

var dec = 42,
oct = 052,
hex = 0x2a;

Note:
ことなるベースでnumberを指定するが、numberの mathematic valueは保存されているもので、デフォルト出力解釈はいつもbase-10だ。この前のsnippetでは3つのvariableにはすべて42 valueが格納されている。

052が非標準的なフォーム拡張機能であることを説明する。

Number( "42" );				// 42
Number( "052" ); // 52
Number( "0x2a" ); // 42

ES5はブラウザ拡張機能の8進数を許可し続ける。(このような矛盾を含む) strict modeを除き、8進数literal form(52)は許可されない。この制限は多くのディベロッパーががコード整理の目的でbase-10の数字に0をつけて、無関係に接頭辞をつけるような習慣(他の言語からの)を持っていたから行われていた。そしてnumber valueを完全に変更したという偶然の事実に遭遇した。

ES6はbase-10 numbersの外でのnumber literalがどのように表現されるかについて、change/variationの遺産を続けている。正式な8進数、16進数形式、さらに新しいbinary形式がある。webとの理由から、古い8進数 052 フォームは、non-strict modeでは合法であり続けるが(指定されていないが)、これ以上使われないべきだ。

ここで新いES6 number literal formを見よう。

var dec = 42,
oct = 0o52, // or `0O52` :(
hex = 0x2a, // or `0X2a` :/
bin = 0b101010; // or `0B101010` :/

小数点以下はbase-10でのみ許可される。8進数、16進数、2進数はすべてinteger formだ。

これらのフォームのstring表現は、全て強制的にcoerced/convertedすることができる。

Number( "42" );			// 42
Number( "0o52" ); // 42
Number( "0x2a" ); // 42
Number( "0b101010" ); // 42

厳密にES6に新しいものはないが、逆の変換の方向に進むことができるのはあまり知られていない。

var a = 42;

a.toString(); // "42" -- also `a.toString( 10 )`
a.toString( 8 ); // "52"
a.toString( 16 ); // "2a"
a.toString( 2 ); // "101010"

実際に、2から36の任意のbase フォームでこのように数字を表すことができるが、standard baseの外に出ることはまれだ。(2,8,10,16)

Unicode

ES6で変わったことのみを説明するが、深くは追求しない。詳しくはこれらを参照。

Mathias Bynens (http://twitter.com/mathias) has written/spoken extensively and brilliantly about JS and Unicode (see https://mathiasbynens.be/notes/javascript-unicode and http://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode).

0x0000 から 0xFFFF のレンジのUnicode文字は、見たことのある、またはやりとりしている可能性が高い標準印刷文字(様々な言語)全てが含まれている。この文字のグループは、Basic Multilingual Plane(BMP)と呼ばれる。
BMPは☃ (U+2603). といったシンボルも含む。

0x10FFFまで、BMPセット以外にたくさんの拡張Unicode文字がある。これらのシンボルはastral symbolとしてしばしば参照される。それはBMP文字以外の文字16 plane(layer/grouping)のセットに与えられた名前だ。
astral symbolの例は𝄞 (U+1D11E) and 💩 (U+1F4A9).

ES6以前では、JS stringsは次のようなUnicode escapeを使用して、Unicode 文字を指定できた。

var snowman = "\u2603";
console.log( snowman ); // "☃"

しかし、\uXXXX Unicodeエスケープは4つの16進数文字をサポートしているだけなので、このようなBMP文字セットを表現することしかできない。ES6以前でUnicode escapingを使用してastral characterを表すには、surrogate pairを使う必要がある。基本的に2つの特別に計算されたUnicode-escape文字が並んでいて、JSはこれを1つのastral 文字として解釈する。

var gclef = "\uD834\uDD1E";
console.log( gclef ); // "𝄞"

ES6以降では、Unicode point escapingと呼ばれるUnicode escapingの新しいフォームを持つ。

var gclef = "\u{1D11E}";
console.log( gclef ); // "𝄞"

見ての通り、違いはescape sequenceでの{}の表現だ。それは任意の16進数の文字を含むことができる。可能な限り最高の ポイントvalueをUnicode(0x10FFFF)で表すには、6つしかないので、これで十分だ。

Unicode-Aware String Operations

デフォルトでは、JS string operationとメソッドは、string valueのastral symbolには敏感ではない。そのため、それぞれのBMP文字を個別に扱う。単一のastral文字なしでも、構成する2つのsurrogateの半分も扱う。

var snowman = "☃";
snowman.length; // 1

var gclef = "𝄞";
gclef.length; // 2

このようなstringの長さを正確に測るには、次のトリックが有効だ。

var gclef = "𝄞";

[...gclef].length; // 1
Array.from( gclef ).length; // 1

foo .. of loop はbuilt-in interatorが組み込まれていることをおもいだそう。このiteratorはUnicode-aware、つまり自動的に単一のvalueとしてastral symbolを出力する。stringシンボルのarrayを作成するarray literalでは… spread operatorを使用する。そして結果のarrayの長さを調べる。ES6の Array.from(..)は[…XYZ]と同じことだが、これについての詳細はチャプター6で学ぶ。

Warning:
stringの長さを取得するためにiteratorを構築して使うことは、パフォーマンスはかなり高価だが、理論的に最適化されたnative utility/propertyが行うもの比較する。

不運にも全ての答えは単純ではない。surrogate ペアに加えて、特殊なUnicode code pointがあり、それは他の特別な方法動作する。これの説明ははるかに難しい。例えば、以前の隣り合う文字を変更するCombining Diacritical Marcsという修正するポイントがある。

console.log( s1 );				// "é"
console.log( s2 ); // "é"

見た目は同じだが、s1とs2を作成した。

var s1 = "\xE9",
s2 = "e\u0301";

以前の長さのトリックは、s2では機能しない。

[...s1].length;					// 1
[...s2].length; // 2

この場合、ES6 String#normalize(..)utilityを使用して、その長さを調べる前にvalueのUnicode normalizationを実行できる。

var s1 = "\xE9",
s2 = "e\u0301";

s1.normalize().length; // 1
s2.normalize().length; // 1

s1 === s2; // false
s1 === s2.normalize(); // true

本質的に normalize(..)は”e\u0301”の結果を取り、”\xE9”にnormalizeする。normalizationは結合する適切なUnicode 文字があれば、複数の隣接する結合マークを組み合わせることもできる。

var s1 = "o\u0302\u0300",
s2 = s1.normalize(),
s3 = "ồ";

s1.length; // 3
s2.length; // 1
s3.length; // 1

s2 === s3; // true

不運にも、normalizationは完璧ではない。単一の文字を変更する結合マークを複数持つなら、期待していた長さのカウントをとれないだろう。なぜなら、全てのマークの組み合わせを表す単一のnormalizeされた文字が存在しない可能性があるからだ。

var s1 = "e\u0301\u0330";

console.log( s1 ); // "ḛ́"

s1.normalize().length; // 2

視覚的に1つの文字としてレンダリングされているのは、より正確にgraphemeを呼ぶという意味で、プログラム処理の意味では1つの文字に厳密に関連するとは限らない。

詳しくはこちら check out the “Grapheme Cluster Boundaries” algorithm (http://www.Unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries).

Character Positioning

lengthの弊害と同様に、”ポジション2の文字は何か”と尋ねる本当に意味は何だろうか。ES6以前のastral chharacterの原子数を尊重しないcharAt(..)から来ており、結合マークを考慮しないだろう。

var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";

console.log( s1 ); // "abćd"
console.log( s2 ); // "abćd"
console.log( s3 ); // "ab𝒞d"

s1.charAt( 2 ); // "c"
s2.charAt( 2 ); // "ć"
s3.charAt( 2 ); // "" <-- unprintable surrogate
s3.charAt( 3 ); // "" <-- unprintable surrogate

ES6はcharAt(..)のUnicode対応を与えていない。ポストES6のために考えられているそのようなユーティリティがある。

しかし、前のセクションで学んだ通り、ES6の答えをハックできる。

var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";

[...s1.normalize()][2]; // "ć"
[...s2.normalize()][2]; // "ć"
[...s3.normalize()][2]; // "𝒞"

Warning:
単一の文字を手に入れたいときはiteratorを作り、使い切っている。これは理想的ではない。ES6以降でbuilt-inで最適化されたユーティリティが得られることを願っている。

charCodeAt(..)ユーティリティバージョンとは何か。ES6はcodePointAt(..)を与えてくれた。

var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";

s1.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s2.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s3.normalize().codePointAt( 2 ).toString( 16 );
// "1d49e"

他のディレクションはどうだろう。String.fromCharCode(..) のUnicode-aware versionは、String.fromCodePoint(..)だ。

String.fromCodePoint( 0x107 );		// "ć"

String.fromCodePoint( 0x1d49e ); // "𝒞"

String.fromCodePoint(..)とcodePointAt(..)を組み合わせて、Unicode-aware charAt(..)を得ることができる。

var s1 = "abc\u0301d",
s2 = "ab\u0107d",
s3 = "ab\u{1d49e}d";

String.fromCodePoint( s1.normalize().codePointAt( 2 ) );
// "ć"

String.fromCodePoint( s2.normalize().codePointAt( 2 ) );
// "ć"

String.fromCodePoint( s3.normalize().codePointAt( 2 ) );
// "𝒞"

astral symbolを含むstringsを動作させるときは、注意が必要。

replace(..)やmatch(..)のようなregular expressionを使用するいくつかの文字列メソッドもある。ES6では、”Unicode Flag”で説明したように、regular expressionにUnicodeの認識をもたらす。

JSのUnicode stringサポートは、様々な追加機能を使用して、ES6以前よりもはるかに優れているが、完全ではない。

Unicode Identifier Names

Unicodeはidentifier nameでもまた使われる。ES6以前に、次のようなUnicode-escapeが行えた。

var \u03A9 = 42;

// same as: var Ω = 42;

ES6からは、先に説明したコードポイントescape syntaxを使うこともできる。

var \u{2B400} = 42;

// same as: var 𫐀 = 42;

どのUnicode characterが使えるかは複雑なルールがある。さらにidentifier nameの最初の文字でない場合に許可されるものもある。

Note:
詳しくはこちら参照。Mathias Bynens has a great post (https://mathiasbynens.be/notes/javascript-identifiers-es6) on all the nitty-gritty details.

identifier nameでこのような特殊な文字を使うことは滅多にない。このような難解なコードを記述しては最適なサービスを提供できない。

Symbols

symbolは他のprimitive タイプとは異なり、literalフォームはない。

var sym = Symbol( "some optional description" );

typeof sym; // "symbol"
  • Symbol(..)とnewは一緒に使うべきではない。コンストラクタでもobjectを作るわけでもない。
  • Symbol(..)にパスされたparameterはオプショナルだ。もしパスされたら、symbolの目的にあった説明を与えたstringでなければならない。
  • typeof のアウトプットは、新しいvalue(“symbol”)symbolを識別する主な方法だ。

もし記述が提供されていたら、symbolの文字列表現にのみ使われる。

sym.toString();		// "Symbol(some optional description)"

primitive string valueがstringのインスタンスではないのと同じように、symbolもSymbolのインスタンスではない。symbol valueのボックス化されたwrapper object formを作る場合、次のようにする。

sym instanceof Symbol;		// false

var symObj = Object( sym );
symObj instanceof Symbol; // true

symObj.valueOf() === sym; // true

Note:
このsnippetのsymObjはsymで交換可能だ。いずれのフォームもsymbolが使われる全ての場所で使用可能だ。boxed wrapper object form(symObj)をprimitive form(sym)の代わりに使う理由はあまりない。他のprimitiveにも同様のアドバイスをしつつ、symObjよりもsymを優先させた方がよいだろう。

symbol自身の内部value、つまりnameとして参照されるのは、codeから隠され、手に入れることができない。このsymbole valueは自動で作られた固有の(アプリケーション内の)string valueとして考えることができる。

しかし、valueが隠されていて入手できないなら、symbolが何を持っているのか。

symbolの主なポイントは、他のvalueと衝突できないstringのようなvalueを作成することだ。そのため、たとえばsymbolをevent nameを表現するコンスタントとして使うことを考える。

const EVT_LOGIN = Symbol( "event.login" );

“event.login”のような一般的なstring literalの代わりにEVT_LOGINを使う。

evthub.listen( EVT_LOGIN, function(data){
// ..
} );

ここではEVT_LOGINが持つvalueは他のvalueによってコピーされることはない。

Note:
前のsnippetで想定していたevthub utilityは、EVT_LOGIN argumentのvalueをイベントハンドラを追跡するinternal object(hash)のproperty/keyとして直接使用している。evthubがsymbol valueを実際のstringとして使う必要があるなら、symbolまたはtoString()をexplicitly coereceする必要がある。これはsymbolのimplicit string coercionが許可されていないからだ。

symbolを隠れた、扱いやすいものとして使いたいspecial propertyとして、objectのproperty name/keyとして直接使うことができる。

このsingleton patternの動作をするモジュールについて考える。つまりそれ自体を一度しか作れない。

const INSTANCE = Symbol( "instance" );

function HappyFace() {
if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

function smile() { .. }

return HappyFace[INSTANCE] = {
smile: smile
};
}

var me = HappyFace(),
you = HappyFace();

me === you; // true

このINSTANCE symbol valueは、HappyFace() function objectに静的に格納されている、隠されたメタ属性のような特殊なものだ。

代わりに_instanceのような古いpropertyでも同じ動作をしていただろう。symbolの使い方はメタプログラミングスタイルを改善し、INSTANCE propertyを他の通常propertyと区別して保持する。

Symbol Registry

symbolを使用することはEVT_LOGINやINSTANCE variableを外部scope(おそらくglobal scope)に格納しなければいけないという弱点がある。さもなければ、公共で利用できる場所に格納されているので、symbolを使う必要がある全てのコードの全部分が、アクセスできる。

symbolへのアクセスでコードを整理するのを助けるために、global symbol registryを作成することができる。

const EVT_LOGIN = Symbol.for( "event.login" );

console.log( EVT_LOGIN ); // Symbol(event.login)
function HappyFace() {
const INSTANCE = Symbol.for( "instance" );

if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

// ..

return HappyFace[INSTANCE] = { .. };
}

Symbol.for(..)は、global symbolレジストリを参照して、symbolが提供されたdescription textですでに格納されているかどうかを確認し、もしそうなら返す。そうでなければ、返すために1つつくる。言い換えると、global symbolレジストリはsymbol valueを、description textによって、singleton自体として扱う。

偶発的な衝突をさけるため、symbolの説明を特有にしたいだろう。簡単な方法の1つは、prefix/context/namespacing情報を含めることだ。

function extractValues(str) {
var key = Symbol.for( "extractValues.parse" ),
re = extractValues[key] ||
/[^=&]+?=([^&]+?)(?=&|$)/g,
values = [], match;

while (match = re.exec( str )) {
values.push( match[1] );
}

return values;
}

レギストリの他のsymbolがその記述と衝突する可能性が低いので、string value”extractValues.parse”を使う。

regular expressionを上書きしたいなら、symbol registryを使うこともできる。

extractValues[Symbol.for( "extractValues.parse" )] =
/..some pattern../g;

extractValues( "..some string.." );

symbolレジストリがこれらのvalueをglobalに保存するのに役立つのとは別に、symbolというよりkeyとして、”extractValues.parse”というstringを実際に使用するだけでできた。

レジストリに格納されているsymbol valueをつかって、その中に格納されているdescription text(key)を検索することがある。例えば、symbol value自体を渡すことができないので、アプリケーションを別の部分のレジストリ内のsymboleを見つける方法を伝える必要がある。

var s = Symbol.for( "something cool" );

var desc = Symbol.keyFor( s );
console.log( desc ); // "something cool"

// get the symbol from the registry again
var s2 = Symbol.for( desc );

s2 === s; // true

Symbols as Object Properties

symbolがobjectのproperty/keyとして使われているなら、それは特別な方法で格納され、objectのpropertyの通常列挙型には表示されない。

var o = {
foo: 42,
[ Symbol( "bar" ) ]: "hello world",
baz: true
};

Object.getOwnPropertyNames( o ); // [ "foo","baz" ]

objectのsymbol propertyを取得するには

Object.getOwnPropertySymbols( o );	// [ Symbol(bar) ]

これはproperty symbolが実際に隠れているか、アクセスできないということをObject.getOwnPropertySymbols(..)リストで常に見ることができることを意味する。

Built-In Symbols

ES6にはJS object valueに様々な meta 動作を後悔する、事前に定義されたbuilt-in symbolが多数ある。しかし、これらのsymbolはglobal symbol レジストリに登録されていない。

その代わり、Symbol function objectのpropertyとして保存されている。例えば、”for..of” sectionでは、Symbol.iterator valueを紹介した。

var a = [1,2,3];

a[Symbol.iterator]; // native function

この仕様はbuilt-in symbolsを参照するために@@ prefix notationを使うが、最も一般的なのは@@iterator、@@toStringTag,@@toPrimitiveだ。おそらく頻繁に使用されることはないが、他にもいくつか定義されている。

Note:
これらのbuilt-in symbolがmetaプログラミングの目的でどのように使用されているかの詳細は “Well Known Symbols” in Chapter 7 参照。

【わからなかったこと】

【感想】
ES6で相当な量の新しいsyntax formが追加されたことがわかった。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

Responses (1)