34–1. You Don’t Know JS: ES6 & Beyond Chapter 2: Syntax — 1/3
【所要時間】
9時間18分(2018年8月6,7,8日)
【概要】
ES6の構文について
【要約・学んだこと】
Block-Scoped Declarations
JS のvariable scopingの基本単位はfunctionだ。block scopeを作る必要があるなら、通常function declaration以外で最も流行っている方法は、ただちに呼び出されるfunction expression(IIFE)だ。
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
let
Declarations
しかし、block scopeとよばれる、いかなるblockにも結ばれるdeclarationをつくるすることができる。つまりscopeをつくるのに {..} のペアがあるだけでよい。囲われたfunction(もしくはトップレベルならglobal) scopeにつけられたvariableを常に宣言するvarを使う代わりに、letを使う。
var a = 2;
{
let a = 3;
console.log( a ); // 3
}
console.log( a ); // 2
JSで独立した{ .. } blockを使うのはあまり一般的ではないが、いつも有効である。block scopingを持つ言語からきたディベロッパは、そのパターンを認識できる。
{ .. } 専用のblockはblock-scoped variableを作るのに最善の方法だと思う。さらに、blockの最上部には常にlet declarationを置くべきだ。もし1つ以上宣言する場合でも、let1つだけを宣言することをすすめる。
文体的に、{ を開いている行にletを置くのが好ましい。これはこのblockがそれらのvariableを宣言すると言う目的だけというのをわかりやすくする。
{ let a = 2, b, c;
// ..
}
見た目はおかしい。そして他のES6に関する文献で推奨されているのとは一致しないだろう。しかし、これには理由がある。
下記のようなlet-block と呼ばれるlet 宣言の実験的な(標準化されてはいない)フォームが他にもある。
let (a = 2, b, c) {
// ..
}
それはexplicit block scopingと呼ばれるフォームで、{ .. }ペアがみつかるハイジャックのようなものなので、 varをミラーリングするlet declaration formより暗黙的だ。一般的にディベロッパーはimplicit メカニズムよりも、explicit メカニズムを見つけることを好む傾向にある。そして、これはその一例だ。
もし前述の2つのsnippet formを比べるなら、それらはとても似ていて、どちらもexplicit block scopingとしては、スタイル的には的確だ。不運にも、 let (..){..} フォームは最もexplicitなオプションで、ES6には適応されていない。おそらくES6以降で修正されるだろうが、今の所、最初のsnippetが最善の選択だと思う。
letの性質をimplicitに強化する宣言はこれらの使い方を考える。
let a = 2;if (a > 1) {
let b = a * 3;
console.log( b ); // 6
for (let i = a; i <= b; i++) {
let j = i + 10;
console.log( j );
}
// 12 13 14 15 16
let c = a + b;
console.log( c ); // 8
}
if statementの中にのみ存在するvariable、そしてfor loop の中にのみ存在するvariableはどれか。
こたえは、if statement は b と c block-scoped variableをもち、for loopはiとj block-scoped variableを持つ。
let c = .. 宣言がscopeの下の方に現れた。var-declared variablesとは異なり、どこに現れるかに関わらず、囲まれたfunction scope全体につく。そしてlet declarationはblock scopeにつくが、blockに現れるまで初期化されない。
let-declared variableがlet declaration/initializationより前にあるのにアクセスすると、エラーを起こす。var declarationでは順序は関係ない。(様式を除く)
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
}
Warning:
let-declared referenceにアクセスするのが早すぎることからおこるこのReferenceErrorは、Temporal Dead Zone(TDZ)エラーと呼ばれる。すでに宣言されたvariableにアクセスしているが、まだ初期化されていない。これはTDZエラーを見るときだけに起こるのではない。ES6ではいくつかの場所に現れる。また、初期化はコードに明示的にvalueを割り当てる必要はないことに注意。 let b; のように、完全に有効だ。declaration timeに割り当てられていないvalriableは、undefined valueが割り当てられたと推測され、 let b; は let b = undefined;と同じだ。explicit assignment であろうがなかろうが、 let b statement が実行されるまで、bにはアクセスできない。
typeof の挙動は、宣言されていない(または宣言された) variableとは違い、TDZ variableとは異なる動作をする。
{
// `a` is not declared
if (typeof a === "undefined") {
console.log( "cool" );
}
// `b` is declared, but in its TDZ
if (typeof b === "undefined") { // ReferenceError!
// ..
}
// ..
let b;
}
このaは宣言されていない。そのためtypeofだけがそれが存在しているかどうかをチェックする安全な方法だ。しかし、typeof bはTDZエラーをなげる。なぜならコード内の最も遠いところにlet bがあるからだ。
なぜlet 宣言がscopeのトップにあるべきかと言うのを明らかにする。これはアクセスするのが早すぎるという偶発的なエラーを完全に回避する。variableを含むいかなるblockの最初に見ることができると、より明示的だ。
block(if statement, while loopなど)がオリジナルの挙動とscopeの挙動をシェアする必要はない。
規律を維持するかどうかは自分次第であるこの解明者は、リファクタなどの困難を減らすことができる。
Note:
let やblock scopingについてさらに詳しくはchcapter 3 of the Scope & Closures参照。
let
+ for
let declaration blockingのexplicit formの優先の唯一の例外は、for loopのヘッダーに現れるletだ。理由は微妙に感じるかもしれないが、ES6の機能よりも重要だと思われることの1つだ。
var funcs = [];
for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 3
for ヘッダーのlet i は、iはfor loop自身だけではなく、loopの反復ごとに新しいiを宣言する。つまり、loop反復内に作られたclosureは、反復 variableごとに閉じることができる。
for loopのヘッダーにvar i を同じsnippetで宣言しようとしたなら、3の代わりに5を得ただろう。なぜならそれぞれの反復の機能を閉じるためのあたらしい i の代わりに、閉じられた外側のscope1つしかないからだ。
少し冗長だがこれでもできる。
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
ここでは、反復ごとに強制的に新しい j を作成して、closureも同様に動作する。前者の方を好む理由は、for (let…)形式を支持しているからだ。
let はfor ..inや for..of loopでも同様の働きをする。
const
Declarations
考慮すべきblock-scoped declarationにもう1つ別のフォームがある。constantを作るconstだ。
constantは最初にvalueをセットしたらあとは読むだけのvariableだ。
{
const a = 2;
console.log( a ); // 2
a = 3; // TypeError!
}
declaration timeでvariableが1度セットした後に、保持しているvalueを変えることは許可されない。const declarationはexplicit initializationを持たなければいけない。constantにundefined valueが欲しいなら、const a = undefined と宣言すればよい。
constantはvalue自体を制限するわけではないが、そのvalueのvariableを代入する。言い換えると、valueは凍らされるのではなく、不変でもない。なぜならconstはただの割り当てだからだ。もしvalueがobjectやarrayのように複雑ならば、valueのconstantは修正できる。
{
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]
a = 42; // TypeError!
}
a variableは実際はconstant arrayを持つわけではない。むしろarrayへconstant referenceを保持している。このarray自身は、自由に可変する。
Warning:
constantとしてのobjectかarrayの割り当ては、constantのlexical scopeがなくなるまで、valueが集められることができないという意味で、valueへのreferenceとしては、解除することができない。望ましいかもしれないが、意図していないならば注意が必要。
const declarationは長年コードに書式を使ってシグナルを出すことを矯正している。ここでは全ての大文字のvariable nameを宣言し、変更しないように注意したliteral valueを割り当てた。
var assignmentに強制はないが、意図しない変更をキャッチするのに役立つconst assignmentがある。
constはfor, for..in, for..of loopのvariable declarationに使われることができる。しかし、もしfor loopのi++ のように再割り当てを試みると、エラーをなげられる。
const
Or Not
constが、あるシナリオではJSエンジンがletやvarよりも最適だという噂がいくらかある。理論的に、エンジンはvariableのvalueやtypeが変わらない方が簡単に知ることができる。そのため、追跡の可能性をなくすことができる。
constが本当にここで役にたつのか幻想なのかよりも、constant 動作をとろうとしているのかどうかの方がより重要だ。ソースコードの最も重要な役割の1つは、はっきりとしたコミュニケーションをとることだ。自分自身のためだけではなく、将来の自分や他の仲間に、自分の意図を伝えることだ。
あるディベロッパーは全てのvariable declaration をconstで始めて、コード内でそのvalueを変更する必要があるときにlet declarationをし直すのを好む。これは興味深い観点だが、コードの読みやすさや、コードの性能の理由を向上させるとしてははっきりしない。
const のvalueを変更したい後のディベロッパーがconstからletに盲目的に宣言することができるので、実際には保護されているとはいえない。せいぜい偶然変えてしまうことを保護する。しかし、直感と感性以上に、客観的には見えず、何が事故を構成しているのか、防止しているタイプなのかをわかりやすくするのではない。type enforcementと似た考えがある。
アドバイスとして、混乱を招きそうなコードには、意図的に、輝会にシグナルが変更されないvariableにのみconstを使う。言い換えると、コードの動作をconstに頼るのではなく、意図が明確に伝えられた時に、意図のシグナルとなるツールとしてconstを使う。
Block-scoped Functions
ES6からblock内で発生するのfunction declarationは、そのblockにscope指定されるようになった。ES6以前は、この仕様はこれを要求していなかったが、多くの実装がやっていた。そのため、この仕様は現実的なものだ。
{
foo(); // works!
function foo() {
// ..
}
}
foo(); // ReferenceError
このfoo() functionは{..} block内で宣言されており、ES6からblock-scopeがある。そのため、blockの外側では利用できない。しかし、block内でのhoistedに気をつける必要もまたある。前述したTDZ エラートラップを被るlet declarationとは対照的だ。
function declarationのblock-scopingは、以前に同様のコードを書いているなら、問題になる。そして、古いnon-block-scope動作に依存していた。
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}
foo(); // ??
ES6以前の環境では、somethingのvalueに関わらずfoo() は2をプリントした。なぜならどちらのfunction declarationもblockの外からhoistedされ、2番目がいつも優先されたからだ。
ES6では最後の行はReferenceErrorを投げる。
Spread/Rest
ES6は使われる場所や使い方次第で、通常はspred, rest operatorと参照される … operatorを新たに導入した。
function foo(x,y,z) {
console.log( x, y, z );
}
foo( ...[1,2,3] );
… がarrayの前(実際はiterable)で使われる時、個々のvalueにそれをspreadするという働きをする。
function の呼び出しargumentsのセットとしてarrayを広げる時に、このsnippetに示されているような仕様方法をする。この使用法で、 … はapply(..)メソッドのsyntatic replacementをシンプルにしてくれる。これは通常ES6以前で使われていた。
foo.apply( null, [1,2,3] ); // 1 2 3
しかし、… は他のarray declaration内のように、他のcontextのvalueを広げたりするのに使うことができる。
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
この使い方では、 … は基本的にcontact(..)の交換となり、[1].contact( a, [5] )のような動作をする。
他の… のよくある使用法は、本質的に反対だと見ることができる。valueを広げる代わりに、 … はvalueのセットをまとめてarrayにする。
function foo(x, y, ...z) {
console.log( x, y, z );
}
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
このsnippetの…zは本質的に “残りのargumentをzと呼ばれるarrayに集める ” なぜなら x には1が割り当てられ、yには2が割り当てられたので、残りのargument 3, 4, 5がzに集められた。
もちろん、もしparameterに名前がなければ、 … は全てのargumentを集める。
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
Note:
foo(..)の…args declarationは、通常残りのparameterを呼ぶ。なぜなら残りのparameterを集めているからだ。 それが何を含むかよりも、それが何かをより具体的に表現するので、集めることを好む。
この使用法の最も重要な部分は、長期間使用されていないargumentを使用するために、非常に堅実な代替手段を提供することだ。実際は、本当のarrayではなく、array-like objectだ。
なぜならargs(もしくはあなたが読んだもの。多くの人はrやrestを好む)は実際のarrayであるからで、arrayとして扱えるものにargumentを作るために飛び込むというES6以前のトリックの多くを取り除くことができる。
// doing things the new ES6 wayfunction foo(...args) {
// `args` is already a real array
// discard first element in `args`
args.shift();
// pass along all of `args` as arguments
// to `console.log(..)`
console.log( ...args );
}
// doing things the old-school pre-ES6 way
function bar() {
// turn `arguments` into a real array
var args = Array.prototype.slice.call( arguments );
// add some elements on the end
args.push( 4, 5 );
// filter out odd numbers
args = args.filter( function(v){
return v % 2 == 0;
} );
// pass along all of `args` as arguments
// to `foo(..)`
foo.apply( null, args );
}
bar( 0, 1, 2, 3 ); // 2 4
このfoo(..) の…args function declarationは、argumentを集める。そしてconsole.log(..)の… argsはそれらを広げるようコールする。 これは… operatorの使い方の反対だが、対称の例としてよい。
なぜならfunction declarationでの…の使い方に加えて、valueを集めるために…が使われる他のケースがある。このセクションで後ほど見る。
Default Parameter Values
おそらくJSで最も一般的なイディオムの1つは、function parameterのデフォルトvalueを設定することだ。長年して来た方法はよく知られているはず。
function foo(x,y) {
x = x || 11;
y = y || 31;
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 5 ); // 36
foo( null, 6 ); // 17
もしこのパターンを以前に使っていたなら、役立つとともに危険なことを知っているだろう。たとえばparameterの1つにfalsy valueと思われるものにも関わらず、渡せる必要があることだ。
function foo(x,y) {
x = x || 11;
y = y || 31; console.log( x + y );
}
foo( 0, 42 ); // 53 <-- Oops, not 42
0はfalsyのため、 x || 11の結果は11となる。直接0を渡さない。
これを修正するために、以下のようにチェックを書く人もいる。
function foo(x,y) {
x = (x !== undefined) ? x : 11;
y = (y !== undefined) ? y : 31;
console.log( x + y );
}
foo( 0, 42 ); // 42
foo( undefined, 6 ); // 17
しかし、どうやって最初のx argumentを”このargumentを除外する”というシグナルをもったいずれかのvalue(undefinedでさえない)に渡す能力なしで除外するのか。
foo(,5)を使いたくなるが、無効なsyntaxだ。foo.apply(null, [,5]) はトリックとして動きそうだが、apply(..)の変異は、ここでは取り除かれない[undefined,5]として扱われるargumentを意味する。
もしさらに調べるなら、予想していたよりも少ないargumentを単に渡すことで、最後(右側)のargumentのみ除くことができることに気がつく。しかし、argumentリストの最初と真ん中のargumentは取り除くことができない。不可能だ。
JSのデザインに適用されている覚えるべき重要な原則がある。それはundefinedとmissingは、少なくともfunction argumentがある限り違わないということだ。
Note:
紛らわしいことに、この特定の設計原則が適用されない他の場所がJSにある。例えばarrayとempty slotなどだ。詳しくは Types & Grammar title of this series を参照。
これら全ての考えを頭に置いて、欠落しているargumentsにデフォルトvalueを割り当てる作業を合理化するために、ES6で追加された便利な構文を調べることができる。
function foo(x = 11, y = 31) {
console.log( x + y );
}foo(); // 42
foo( 5, 6 ); // 11
foo( 0, 42 ); // 42foo( 5 ); // 36
foo( 5, undefined ); // 36 <-- `undefined` is missing
foo( 5, null ); // 5 <-- null coerces to `0`foo( undefined, 6 ); // 17 <-- `undefined` is missing
foo( null, 6 ); // 6 <-- null coerces to `0`
結果と、それが以前のアプローチとの微妙な違いと類似点の両方をどのように示唆しているかに注目する。
function declaration の x = 11 は x || 11 というとてもよく使われるイディオムよりも、 x ! == undefined ? x : 11に近い。そのため、ES6以前のコードをES6のデフォルトparameter value syntaxに変換する際は気をつけなければいけない。
Note:
rest/gather parameterはデフォルトvalueを持つことができない。そのため、function foo(… vals=[1,2,3]) { は興味深い機能を持つように見えるかもしれないが、有効なsyntaxでない。必要ならば、手動でその種のロジックを適用し続ける必要がある。
Default Value Expressions
function default valueは、単に31といったsimple value以上のvalueである。いかなるexpressionであったり、function callさえにもなれる。
function bar(val) {
console.log( "bar called!" );
return y + val;
}
function foo(x = y + 3, z = bar( x )) {
console.log( x, z );
}
var y = 5;
foo(); // "bar called"
// 8 13foo( 10 ); // "bar called"
// 10 15y = 6;
foo( undefined, 10 ); // 9 10
デフォルトvalue expressionは遅延評価される。つまり、必要な場合のみ実行される。それはparameterのargumentが省略されているか、undefinedの時のみ実行される。
微妙な詳細になるが、function declarationのformal parameterは自身のscope(function declarationの(..)だけを囲んだscope bubbleと考えて良い)にあり、function bodyのscopeではない。これはデフォルトexpressionのidentifierの参照が、最初に外側のスコープを調べる前に、formal parameterのscopeと一致することを意味する。詳しくはScope & Closures title参照。
var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}
foo(); // ReferenceError
w + 1 のデフォルトvalue expressionは、formal parameterのscopeのwを探すが見つけない、そのため外側のscopeのwが使われる。
次に、x + 1 のdefault value expressionのxは、formal parameter scopeのxを探し、幸運なことにxはすでに初期化さている。そのため、y が正常に動作するように割り当てる。
しかし、 z + 1のzは、その時点でまだ初期化されていないparameter vaiableとしてzを見つけるので、外側のscopeからzを見つけようとしない。
このチャプターの前半の”let Declaration”で説明したように、ES6はTDZがあり、それはvariableが初期化されていない状態でアクセスされることを防ぐ。そのため、z +1のデフォルトvalue expressionはTDZ Reference Errorを投げる。
コードのわかりやすさのためにいいアイデアが必ずしも必要ではないが、デフォルトvalue expressionはinline function expression callであってもよく、一般的にimmediately invoked function expression(IIFE)と呼ばれる。
IIFE(または他の実行されたinline function expression)が、デフォルトvalue expressionにふさわしいケースはほとんどない。
Warning:
もしIIFEがx identifierにアクセスを試みて、自身のxを宣言していない場合、前述のようにTDZエラーになる。
前述のsnippetのデフォルトvalue expressionは、(31)を介して右のinlineで実行されるfunctionという点で、IIFEである。その部分を残すなら、デフォルトのコールバックのようにxに割り当てられたデフォルトのvalueは、function reference自体になる。おそらくそのパターンが非常に有効な場合がある。例えば
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
この場合、もし明記されていなければ、デフォルトでcbをno-op empty function callにしたい。function expressionはただのfunction referenceで、目的を達成するfunction call自身ではない。(最後の呼び出しに()を呼ばない)
初期のJSから、少ししか知られていない便利な特色がある。Function.prototypeはからの no-op function 自体である。そのため、declarationはcb = Function.prototypeであり、inline function expressionの作成を保存した。
Destructuring
ES6はdestructuringと呼ばれる新しい機能を導入した。それはstructured assignmentの代わりとして考えると、少し混乱するだろう。
function foo() {
return [1,2,3];
}
var tmp = foo(),
a = tmp[0], b = tmp[1], c = tmp[2];
console.log( a, b, c ); // 1 2 3
foo()が個々のvariable a, b, cに返すvalueをarrayに手動で代入すう。それにはtmp variableが必要だ。
同様にobjectで次のことができる。
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}
var tmp = bar(),
x = tmp.x, y = tmp.y, z = tmp.z;
console.log( x, y, z ); // 4 5 6
tmp.x property valueはx variableに割り当てられ、同様にtmp.yはyに、tmp.zはzに割り当てられる。
objectのarrayまたはpropertyからindexed valueを手動で割り当てることは、structured assignmentと考えられる。ES6はdestructuring専用のsyntax、特にarray destructuringとobject destructuringを追加した。
このsyntax は先ほどのsnippetのtmp variableを必要としなくなり、よりわかりやすくした。
var [ a, b, c ] = foo();
var { x: x, y: y, z: z } = bar();
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
= assignmentの右辺の[a, b, c]のようなsyntaxを見ることにもっと慣れているだろう。valueが割り当てられているからだ。
対照的にdestructuringするとそのパターンが反転する。= assignmentの左辺の[a, b, c]が右辺のarray valueを別々のvariable assignmentに分解するある種のパターンとして扱われる。
同様に{ x: x, y: y, z: z }がobject valueをbar()から別々のvariable assignmentに分解するパターンを指定する。
Object Property Assignment Pattern
前述のsnippetを掘り下げてみよう。property nameが宣言したいvariableと同じだった場合、syntaxを短くできる。
var { x: x, y: y, z: z } = bar(); //これがvar { x, y, z } = bar(); //これになる
console.log( x, y, z ); // 4 5 6
しかし、ここではx: が離れたのか、それとも :x がはなれたのかどちらだろう。 x: が離れている。重要でないように思えるが、理解することが重要だ。
shorter formが書けるなら、なぜlonger formを書くのか。なぜならlonger formは異なるvariable nameにpropertyを割り当てられるからだ。それは時々とても有用だ。
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
これは些細なことに思えるが、このdestructuring formのvariableを理解するのにとても重要だ。normal object literalが指定されるpatternについて考えよう。
var X = 10, Y = 20;
var o = { a: X, b: Y };
console.log( o.a, o.b ); // 10 20
{ a: X, b: Y }で、a はobject propertyで、 X はその割り当てを受け取るsource valueだ。言い換えると、syntactic pattern は target: source で、property-alias: valueだ。target = source である = assignment と同じなので、直感的に理解できる。
しかし、object destructuring assignmentを使用するとき、つまり = operatorの左辺に { .. } object literal-looking syntax を置くと、target: sour patternを元に戻すことができる。
var { x: bam, y: baz, z: bap } = bar();
ここでのsyntax patternは source: target (もしくはvalue: variable-alias)。 x: bam は x propertyが source valueで、 bam は代入先の variableであることを意味する。言い換えると、 object literalは target ← source、そしてobject destructuring assignmentは source → targetとなる。
このsyntaxについて考える別の方法があり、混乱を解消する助けとなるだろう。
var aa = 10, bb = 20;
var o = { x: aa, y: bb };
var { x: AA, y: BB } = o;
console.log( AA, BB ); // 10 20
{ x: aa, y: bb } で、 xとyはobject propertyを表す。{ x: AA, y: BB }は、xとyのobject propertyをまた表す。
{ x, .. }が x: を離れるといったことを思い出そう。実際は異なり、コンセプトとして、これら2つの行で、x: と y: 部分を消去すると、 aa, bb と AA, BBだけが残る。aa から Aに、 bb から BBの割り当てとなる。
そのため、この対称性は、このES6の機能で意図的にsytacticパターンが反転された理由を説明するのに役立つ。
Note:
私はdestructruing assignmentのために、syntax を { AA: x , BB: y } とするのを好む。なぜなら、これは両方の使い方により使い慣れたtarget: source の一貫性を保持していたからだ。
Not Just Declarations
これまでに、var declarationでdestructuring assignmentを使ってきた。(もちろんletやconstでも使える)しかし、destructuringはgeneral assignment operationであって、declarationではない。
var a, b, c, x, y, z;[a,b,c] = foo();
( { x, y, z } = bar() );console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
variableはすでに宣言することができて、destructuringはassignmentをするだけだ。
Note:
object destructuring formでは特に、var/let/const declaratorを離すときに、() の中のassignment expression全体を囲う必要があった。なぜなら、statementの最初のelementとしての左辺の{ .. } が、objectの代わりにblock statementに取られるからだ。
実際に、assignment expression(a, yなど)はただのvariable identifierである必要はない。有効なassignment expressionであればなんでも許可される。
var o = {};[o.a, o.b, o.c] = foo();
( { x: o.x, y: o.y, z: o.z } = bar() );console.log( o.a, o.b, o.c ); // 1 2 3
console.log( o.x, o.y, o.z ); // 4 5 6
destructuringではcomputed property expressionさえもつかうことができる。
var which = "x",
o = {};( { [which]: o[which] } = bar() );console.log( o.x ); // 4
この [which]:
は、computed propertyで、assignmentのsourceとしての質問のobjectからdestructureするためのpropertyだ。o[which]は、ただのnormal object key referenceで、o.x はassignmentのtargetになる。
object mapping/transformationを作るgeneral assignmentを使うことができる。
var o1 = { a: 1, b: 2, c: 3 },
o2 = {};( { a: o2.x, b: o2.y, c: o2.z } = o1 );console.log( o2.x, o2.y, o2.z ); // 1 2 3
arrayにobjectをmapすることもできる。
var o1 = { a: 1, b: 2, c: 3 },
a2 = [];( { a: a2[0], b: a2[1], c: a2[2] } = o1 );console.log( a2 ); // [1,2,3]
他にも
var a1 = [ 1, 2, 3 ],
o2 = {};[ o2.a, o2.b, o2.c ] = a1;console.log( o2.a, o2.b, o2.c ); // 1 2 3
もしくはあるarrayを他のarrayに並べ替えることもできる。
var a1 = [ 1, 2, 3 ],
a2 = [];[ a2[2], a2[0], a2[1] ] = a1;console.log( a2 ); // [2,3,1]
temporary variableを持たない”2つのvariableを交換する”タスクを解決することすらできる
var x = 10, y = 20;[ y, x ] = [ x, y ];console.log( x, y ); // 20 10
Warning:
全てのassignment expressionをdeclarationとして扱われたいのでなければ、assignmentとdeclarationを混ぜないようにするべきだ。
そうしなければ、syntax errorとなる。なので、2つ前の例では var a2 = [] を[ a2[0], ..] = .. のdestructuring assignmentから分けなければいけなかった。var [ a2[0], .. ] = .. を試す意味は何もない。なぜなら、a2[0]は有効なvalid declaration identifierではないからだ。a = var a2 = [] declarationを暗黙的に作成することもできなかった。
【わからなかったこと】
var funcs = [];
for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 3
このsnippetは下記のように動いている。
わからなかったのは、push()によってarrayに入る要素がvalueだと思っていたが、valueではなくてfunctionが入る。
下記の場合、配列の中身は [10, f(), f(), f(), f(), f()]となる。
そして、この f() は i が数値になって展開されたものなので、
function() { console.log(1); }, function() { console.log(2) }… function() { console.log(4) } までが入っている。
funcs[1]で取得されるのはfunction(){console.log(0);}となり、実行すると0が表示される。
funcs[0]では配列のindex 0 に最初から存在している要素 10が表示されるが、funcs[0]() でfunctionを実行しようとすると、エラーになる。
【感想】
理解していたつもりでも、別の場面で使おうとするとちゃんとわかっていなかったことが発覚した。