Edited at

try-using文を用いるJavaScriptの超モダンな“リソース管理”

リソース管理というのはプログラミングにおける頻出課題のひとつです。そもそもリソースとは何かというのは人によって思い浮かべるものが違うかもしれませんが、ここでいうリソースは「使ったらちゃんと後始末(解放)をしないといけないもの」だと思ってください。今時はピンと来ない方もいるかもしれませんが、「ファイルをopenしたらちゃんとcloseする」とかおおよそそういう話です。

このようなリソースは、一度使おうものならその後何が起ころうとも必ず後始末をしないといけません。たとえ使っている途中でエラーが起こったとしても、適切なエラーハンドリングを行なって忘れずにリソースの後始末をする責任があります。リソース管理を誤ると、メモリリーク等の原因になりかねません。

この記事では、このような「リソース管理」を補助してくれるJavaScriptの新しい言語機能を紹介します。今回紹介するECMAScript Explicit Resource Managementは、つい先日Stage 2になったばかりのプロポーザルです。Stageとは何かみたいな話はこの記事では語りませんが、ざっくり言うと方向性は定まってきたという感じです。もちろん、まだ仕様策定途中であり確定したものではなく、当然ながらブラウザやnode.js等には実装されておらず今試すことはできません。記事タイトルの「超モダン」というのは新しすぎて今はまだ利用不可という意味なのであしからずご了承ください。1〜2年もすれば最新鋭の言語機能になっているかもしれませんから、期待して待ちましょう。

なお、知っている方向けに一言でいうとこれはC#のusingPythonのwith, そしてJavaのtry-with-resources文に相当する言語機能です。機能的にはJavaが一番近いですね。


try-using文を用いるリソース管理の概要

早速中身に入っていきましょう。今回紹介する機能はだいたいこんな感じの構文です。

try using (const resource = makeResource()) {

// この中ではresourceが使用可能
resource.write(/* ... */);
}
// try文を出ると同時にresourceが解放される

見ると分かるようにtry using (...) { ... }という形のtry-usingが追加されています。後でより詳しい説明がありますが、このように()の中で変数宣言をすることができ、try-using文のブロックの中でこの変数を使用可能です。

この変数に入っているものがリソースであり、try文から出るときに自動的にこのリソースに対して“後始末”が行なわれます。後始末というのは具体的にはリソースが持つ[Symbol.dispose]メソッドの呼び出しです。


新しい構文の詳細

上では一例を紹介しましたが、このプロポーザルで追加されるtry-using構文には主に2種類があります。

一つは上で紹介したように()の中で変数を宣言する構文です。この構文では、constによる変数宣言のみ可能です。すなわち、varletによる変数宣言は構文エラーとなりできません1

ここで宣言された変数はtry-using文の{ ... }の中でのみ使用可能です。そして先ほどの述べたとおり、ブロックから出るときに[Symbol.dispose]メソッドの呼び出しによってリソースが解放されます。

try using (const resource = makeResource()) {

// この中ではresourceが使用可能
resource.write(/* ... */);
// ブロックの終わりで resource[Symbol.dispose]() が呼び出される
}

もう一種類の構文は、変数宣言を行わずに()の中に何らかの式を書くものです。

try using (何らかの式) {

// ...
}

この場合も{ ... }の終了時に何らかの式の結果得られたオブジェクトに対して[Symbol.dispose]メソッドが呼び出されます。つまり、この構文は以下と同じということになります。

try using (const _unused = 何らかの式) {

// ...
}

すなわち、変数に入れてもいいけど別に使わないという場合の糖衣構文ということになります。こちらの構文は、主に「try-using文に入ると同時にリソースを作成するのではなく、既存のリソースをtry-using文に当てはめたい」という場合に有用でしょう。


エラー発生時の挙動

最初に述べた通り、リソース管理において重要なことは「いかなる場合でも確実にリソースの後始末を行う」ことです。このtry-using文はそのための機能を持っています。

一言で言うと、「try-using文のブロックから途中で脱出する場合でも脱出直前にリソースの[Symbol.dispose]メソッドが呼ばれる」という挙動になります。

try using (const resource = makeResource()) {

// ブロックの中でエラー発生!!!!!!
throw new Error("OMG");
// ↓当然これは実行されない
console.log("Hi");
}

この例の場合、このtry-using文はcatch(後述)を持たないので、中で発生したエラーは普通に上に伝播していきます。ただ、エラーがこのtry-using文から脱出するタイミングでしっかりとresource[Symbol.dispose]()が呼ばれることになります。

より具体的な例を示すとこんな感じです。今回のリソースは解放されるとconsole.logするリソースにしましょう。

function makeResource() {

return {
[Symbol.dispose]() {
console.log("disposed");
}
}
}

// エラーをキャッチするための外側のtry文
try {
console.log("1");
try using (const resource = makeResource()) {
console.log("2");
throw new Error("OMG");
console.log("ここは通らない");
}
} catch (err) {
console.log("3");
}
console.log("4");

この例を実行すると1234の順にログが表示されることは言うまでもありません。では、resourceが解放されたことを示すdisposedはどのタイミングで表示されるでしょうか。

答えは23の間です。2の後にエラーが投げられますが、それが内側のtry-using文を出る瞬間にresource[Symbol.dispose]()が呼ばれます。その後エラーが外側のtry文にキャッチされることになります。

また、この例のようにエラーが投げられた場合に限らず、どのような理由でtry-using文を脱出する場合であっても[Symbol.dispose]は呼ばれます。ブロックの中からreturncontinueなどで脱出する場合なども例外ではありません。そのような挙動は既存のtry-finally文でも可能ですが、今回の新構文はそれをより便利に行えるものとなっています。


catchfinallyとの組み合わせ

この記事で紹介しているtry-using文は、実は既存のtry文と同様にcatchfinallyと組み合わせることができます。もちろん、全部ではなくtry usingcatchのみ、あるいはtry usingfinallyのみということも可能です。全部使う場合はこんな感じの構文になります。

try using (const resource = makeResource()) {

// 1. resource を使う処理
} catch (error) {
// 2. エラー処理
} finally {
// 3. 最後の処理
}

お察しの通り、この新しいtry-using文もやはりエラーをキャッチできる機能を持っています。これまでの例ではcatchが無いので内部で発生したエラーは普通に上に伝播していきましたが、catch文がある場合は内部(上の例の1の処理)で発生したエラーをキャッチすることができます(2の処理)。

この構文は、従来のエラーキャッチ機能に加えてさらにリソース解放機能も併せ持っています。リソースが解放されるタイミングは1の処理の直後となります。ポイントは、catchfinallyに入るよりも前に解放されるということです。

処理の流れを確認してみましょう。1でエラーが発生しなかった場合は以下の挙動となります。



  • 1の処理を実行する。


  • resource[Symbol.dispose]()を呼ぶ。


  • 3の処理を実行する。

一方、1でエラーが発生した場合は以下の挙動となります。



  • 1の処理を実行する(エラーで中断)。


  • resource[Symbol.dispose]()を呼ぶ。


  • 2の処理を実行する。


  • 3の処理を実行する。

23の中ではリソースが使えないという点は間違いやすいので注意が必要です。try-using文でconst宣言した変数が1の中でしか使えないと言う事実がこれとうまく噛み合っていますね。


分割代入

try-using文の初期化でconstを用いる場合、constにおける分割代入もサポートされています。具体的にはこういう感じの用法です。

try using (const {read, write} = makeResource()) {

// ...
}

ただ、一つたいへん注意しなければいけないのは、この場合何に対して[Symbol.dispose]が呼ばれるのかということです。実は、分割代入によって作られた変数ひとつひとつに対して[Symbol.dispose]が呼ばれます

ということは、このブロックから脱出した際にはread[Symbol.dispose]()write[Symbol.dispose]()が呼ばれるということです。

さらに言えば、下のように書くのとは意味が違うということです。下のコードではリソースの解放はresource[Symbol.dispose]()ですから、うっかり下のコードを上のコードにリファクタリングしてしまうと意味が変わります。

try using (const resource = makeResource()) {

const {read, write} = resource;
// ...
}

ちょっと非直感的な気もしますが、この挙動についてはTC39ミーティングでも議論になっています。経緯が気になる方は議事録を探してみましょう。


()内で複数の変数を宣言する

実は、try-using文の中のconstで複数の変数(変数でなく分割代入でも構いませんが)を宣言することもできます。

try using (const a = makeResource(), b = makeResource()) {

// ...
}

これはおおよそ以下と同じ意味です2

try using (const a = makeResource()) {

try using (const b = makeResource()) {
// ...
}
}

注目すべき点は、この場合上の書き方でも下の書き方でも必ずbaの順にリソースが解放されるということです。try-usingで複数のリソースを同時に宣言する場合は宣言と逆順にリソースが解放されるのです。これは次の話とも関わっています。


リソース初期化中のエラー処理

先ほど、この新しいtry-using文はエラー処理もできるということを紹介しました。実は、今回エラー処理に指して考えないといけないことが少し増えています。それは、{ ... }の中だけでなく(...)の中でエラーが発生することがあるということです。

try using (const resource = makeResource()) { // ←ここでエラーが発生したら?

console.log("1");
} catch (error) {
console.log("2");
}

この例で、makeResource()でエラーが発生したらどうなるでしょうか。まず、console.log("1");は実行されません。リソース初期化中にエラーが発生した場合はtry-usingのブロックの中身には入らないのです。

そして、ちゃんとcatchブロックは実行されます。すなわち、実行結果としては2だけ表示されるということになります。

なお、resource[Symbol.dispose]()は実行されません。というか、リソースが生成される(resourceにリソースが代入される)より前にエラーが発生したのでそもそも解放すべきリソースがありませんね。

では、次の例はどうでしょうか。

try using (const resource1 = makeResource(), resouce2 = makeResource()) {

console.log("1");
} catch (error) {
console.log("2");
}

先ほど紹介した機能を用いて、2つのリソースを一緒に初期化しました。ここで、2回目のmakeResourceでエラーが発生した場合を考えましょう。ポイントは、resource1は既に初期化が完了している(=解放する責任がある)ということです。

この場合、まずリソース初期化中にエラーが発生したということで、tryのブロックの中身は実行されません("1"は表示されません)。

そして、catchブロックに入るresource1が解放される、すなわちresource1[Symbol.dispose]()が呼び出されます。あとは先ほどと同じです。

このようなレアケースでもちゃんとリソースが解放できるようになっていてとても偉いですね。

ちなみに、リソース初期化中のエラーにはもうひとつバリエーションがあります。それは、リソースが[Symbol.dispose]メソッドを持っていなかった場合です。リソースが[Symbol.dispose]メソッドを持っているかどうかはリソースの初期化時にチェックされ、持っていなかった場合は即座にエラーとなります。


[Symbol.dispose]メソッド取得のタイミング

これまでいくつかtry-using文の注意点を説明しましたが、説明すべきことがもう1つあります(まだ実装もされていないのに時期尚早だとか言わないでくださいね)。

それは、[Symbol.dispose]メソッドが取得されるのはtryブロックに入る前であるということです。すなわち、tryブロックの中でリソースの[Symbol.dispose]メソッドを書き換えても反映されません。下の例で考えてみましょう。

function makeResource() {

return {
[Symbol.dispose]() {
console.log("disposed!");
}
};
}

try using (const resource = makeResource()) {
// tryブロックの中でリソースの[Symbol.dispose]メソッドを書き換えている
resource[Symbol.dispose] = () => {
console.log("Hello, world!");
};
}

この例でtry-using文から出たときには何が表示されるでしょうか。

答えは"disposed!"です。try-usingにさしかかった瞬間にresource[Symbol.dispose]が裏で保存されており、それがリソース解放時に呼び出されます。よって、[Symbol.dispose]メソッドをあとで書き換えても反映されないのです。


リソース解放中のエラー処理とAggregateError

実は、この記事で扱っているプロポーザルは、try-using文の登場にあわせて新しいエラーオブジェクトをひとつ定義しています。それがAggregateErrorです。これは端的に言えば複数のエラーをひとつにまとめたもので、try-using文の処理が始まってからcatchに到達するまでに複数のエラーが発生するシチュエーションに対応するために導入されたものです。

そのようなシチュエーションが実際に発生するのはリソースの解放中にエラーが発生した場合、言い方を変えれば[Symbol.dispose]メソッドがエラーを発生させた場合です。

具体例を見てみましょう。

// 開放時にエラーが発生する変なリソースを作る

function makeResource() {
return {
[Symbol.dispose]() {
throw new Error("dispose error");
}
};
}

try using (const resource = makeResource()) {
throw new Error("error in try");
} catch(error) {
console.log(error); // ←このerrorは何?
}

今回makeResource()で作られるのは、開放するとエラーが発生するという変なリソースです。上の例を実行するとどうなるか考えてみましょう。

まず、リソースの初期化は問題なく完了してtryブロックの中が実行されます。そこにはthrow文が待ち構えており、error in tryというエラーが投げられます。

エラーが発生したので次はcatchブロックに移らないといけませんが、その前にresourceを解放しなければいけません。そのためresource[Symbol.dispose]()が呼び出されます。そうなると、ここで2つ目のエラーdispose errorが発生することになります。

ということは、tryブロック(およびリソース)の処理でエラーが2つ発生してしまいました。このときcatchブロックが受け取るエラーは2つのうちどちらなのでしょうか。

答えは両方です。より正確には、両方の情報を持ったAggregateErrorオブジェクトが作られそれがcatchブロックに渡されます。AggregateErrorオブジェクトはerrorsプロパティを持ち、これがその中に含まれるエラーの配列となっています。

つまり、上の例のerrorはおおよそ次のようなオブジェクトになっています。

AggregateError {

errors: [Error("error in try"), Error("dispose error")]
}

try文の場合は配列のエラーは発生した順番に入っています。

いずれにせよ、発生したエラーの情報を漏らすこと無く得られるのはいいデザインですね。時代によっては「最後のエラーオブジェクトのみcatchブロックに渡される」みたいな言語仕様になっていたかもしれません。

余談ですが、複数のエラーをまとめたオブジェクトの需要はPromise.anyという別のプロポーザルでも発生しており、AggregateErrorはそちらのプロポーザルにも登場しています。

これからのエラーハンドリングはAggregateErrorの考慮が求められると言えるでしょう。


try-using-await

実はtry-using文にはasync関数の中でのみ使える亜種があります。それはtry-using-await文です。

try-using-await文は通常のtry-using文と同様の挙動をしますが、一つ違いがあります。それは、リソースの解放時に呼ばれるメソッドが[Symbol.dispose]ではなく[Symbol.asyncDispose]であるということです。

[Symbol.asyncDispose]はリソースの解放を非同期的に行うことができます。[Symbol.asyncDispose]が返り値としてPromiseを返した場合、try-using-await文は自動的にそのPromiseawaitします。

なお、非同期的な解放のサポートはオプショナルです。つまり、try-using-await文の場合でも、リソースが[Symbol.asyncDispose]メソッドを持っていなかった場合は通常の[Symbol.dispose]が代わりに使用されます。

試しに、解放に1秒かかる変なリソースを作ってみましょう。

function makeResource() {

return {
async [Symbol.asyncDispose]() {
await sleep(1000); // sleep関数は予めいい感じに定義しておく
console.log("disposed!");
}
}
}

async function main() {
try using await (const resource = makeResource()) {
console.log("1");
}
console.log("2");
}

main();

main関数の中に注目してください。try-using-awaitでリソースを使っていますが、このプログラムの挙動はどうなるでしょうか。

結果は、まず1が表示され、1秒後にdisposed!2が表示されます。この1秒という待ち時間がresource[Symbol.asyncDispose]()の呼び出しに由来するものです。try-using-await文により自動的にresource[Symbol.asyncDispose]()が呼び出されますが、その返り値は上の定義の通り、1秒後に解決されるPromiseです。try-using-await文はこのPromiseを自動的にawaitするため、tryブロックから出るときに1秒待つという挙動になります。

先述のように、リソースが[Symbol.asyncDispose]を持たない場合は[Symbol.dispose]にフォールバックされます。解放に非同期処理が必要ないリソースはフォールバックさせるとよいでしょう。逆に必ず非同期的に解放しないといけない場合は、[Symbol.dispose]をそもそも用意しないか、呼ぶとエラーになるようにするのがよいでしょう。TypeScriptサポートのことまで考えると、そのような場合は上の例のように[Symbol.dispose]をそもそも持たないようにするのがベストプラクティスとなりそうです。

それにしても、try using awaitというキーワード3連続は威圧感がありますね。まあ中で暗黙的に何かをawaitする構文は構文中にawaitと明示する方針のようなので仕方ありません。既存のfor-await-of文も非同期イテレータのnext()の返り値を暗黙のうちにawaitしています。


イテレータと[Symbol.dispose]

以上でtry-using及びtry-using-await文の紹介は終わりです。

このプロポーザルが仕様として導入されれば、既存のオブジェクトやDOM由来のオブジェクトの中にもリソースとしてtry-using文と一緒に利用可能なものが出てくるでしょう。このプロポーザルではその一例としてイテレータに対して[Symbol.dispose]メソッドが定義されています。

イテレータの[Symbol.dispose]メソッドは自身のreturnメソッドを(存在すれば)呼び出すものとして定義されています。ジェネレータ関数により作られたイテレータはちょうどreturnメソッドを持っていますから、ジェネレータ関数との併用が期待されていることが分かります。

ジェネレータ関数によって作られたイテレータのreturnメソッドは、動作中のジェネレータ関数を強制終了させる動作をします3。そうなると何が嬉しいかというと、ジェネレータ関数がtry-using文で止まっていた場合そのリソースを解放できます

ちょっと長いですが例を用いて説明します。

function makeResource() {

return {
[Symbol.dispose]() {
console.log(`resource is disposed!`);
}
};
}

// リソースからひとつずつデータを読みだす関数(のつもり)
function* readFromResource() {
try using (const resource = makeResource()) {
while (true) {
yield 1;
}
}
}

// イテレータをリソースとして使う
try using (const iter = readFromResource()) {
// 3回データを読んで終わり
iter.next();
iter.next();
iter.next();
}
// ここで "resource is disposed!"と表示される

この例ではreadFromResourceというジェネレータ関数を定義しました。このジェネレータ関数は内部でリソースを使用しており、try-using文の内部に留まってyieldでデータを発生させ続けます。

例の最後のところでreadFromResource()を使っています。readFromResourceはジェネレータ関数なので呼び出すとイテレータが返りますが、このプロポーザルではイテレータが[Symbol.dispose]を持っているのでこの例のようにリソースとしてtry-using文で使うことができます。内部では適当にイテレータを使っています。

注目すべきは、イテレータを使い終わった後です。このタイミングで"resource is disposed!"と表示されます。

これはどういうことかというと、まずtry-using文を脱したタイミングでiter[Symbol.dispose]()が呼ばれます。それはiter.return()を呼び出すため、readFromResource()のジェネレータ関数が強制終了します。ということは、readFromResource内のtry-using文から脱出したことになるためresource[Symbol.dispose]()の呼び出しが発生します。これが"resource is disposed!"というログを表示するのです。

結局この例で解決したかった問題は何かというと、ジェネレータ関数により作ったイテレータを放置するといつ使い終わったのか分からないという問題です。

上の例のreadFromResourceジェネレータ関数は、使い続ける限りはずっとtry-using文の中にいます。これは、イテレータが使われ続ける限りはずっとリソースを使い続けていることを意味します。イテレータを使い終わったあと(明示的に終了させることなく)この状態で放置されると、このリソースは解放されることがありません。これを防ぐためにイテレータのreturn()メソッドでジェネレータ関数を明示的に強制終了させなければいけないのです。return()メソッドが呼び出されるとジェネレータ関数の制御が外に脱出しようとするため、try-using文の外に出た扱いとなり無事にリソースが解放されます。

そして、イテレータの[Symbol.dispose]メソッドは自動的にこのreturn()メソッドを呼び出すものとなっています。別の言い方をすれば、イテレータ自体が裏にジェネレータ関数を持っているリソースであり、イテレータを解放することで裏のジェネレータ関数(さらには裏のジェネレータ関数が保持しているリソース)が解放されるということになります。

今後try-using文を使うにあたってはこのような「何がリソースであるか」という考え方が重要になってくるでしょう。イテレータとジェネレータ関数というのは極端な例ですが、何らかのリソースを裏で保持しているオブジェクトは一般に自身もリソースとなり、自分が解放されたら自分が持っているリソースを解放しなければいけません。

そんなに難しくない言語仕様のわりには考えさせられることが多いですね。それだけリソース管理は難しいということでしょう。


仕様の変遷

最後に、少しこのプロポーザルの変遷に触れたいと思います。プロポーザルが現在の形になったのはStage 2に上がると同時なので、この先は比較的安定することが予想されます。逆に言えば、Stage 1の間は結構仕様がダイナミックに変化していました。

まず、最初の草案ではtry-using文ではなく単なるusing文でした。C#と同じ形ですね。


using文時代の例

using (const resource = makeResource()) {

// ...
}

さらに()の中はconst宣言だけでなく式も可能ですから、using(resource) { ... }のような形も可能となります。

この案の問題点は、なんと言ってもusingがキーワードではない点でしょう。そのため、usingという関数を呼び出しているのと紛らわしいという問題が発生します。これはusing(resource)まで構文解析しても解決できず、そのあとに{が来るかどうかまで調べないとどちらなのか分かりません。また、悪名高き自動セミコロン挿入によって、従来のJavaScriptでも次のようなコードが存在し得ます。

using(resource)

{
// ...
}

これはusing(resource)という関数呼び出しと{ ... }というブロックの2つを逐次実行するというプログラムです。機能追加で既存のプログラムの意味が変わるのはまずいですから、必然的に新しいusing構文ではusing(...) { ... }){に改行が入ってはいけないというルールが必要になります。これはたいへん分かりにくいし不便ですね。

というような事情があり、usingのみを用いる案は頓挫しました。

次に出てきたのがtryだけを用いる案です。この時点で既存のtry文が持つエラーキャッチ機能が今回のプロポーザルの機能の一部として導入されました。これはJavaのtry文が持つ機能と同様ですから、一気にC#寄りからJava寄りに変遷したことになります。

try (const resource = makeResource()) {

// ...
} catch(err) {
// ...
}

tryは既にキーワードなので既存のプログラムにtry(...)という関数呼び出しは存在せず、また既存のtry文はtry { ...という形で始まるので新構文ともぶつかりません。構文上の曖昧性は回避できています。

ならばこの方向性で進むかと思いきや、今度は既存のエラーをキャッチできるtry文がある中でtryとだけ書いてリソース管理ができるのはさすがに分かりにくいという問題が挙がりました。

ということで、分かりやすさと構文上の問題点を考慮した結果、最終的にJavaとC#の折衷とも言えるtry using文が誕生したのです。めでたしめでたし。

ちなみに、ここでは構文の変化を取り上げましたが他にも細かい仕様があっちに行ったりこっちに行ったりしていす。具体的にはtry using文の()の中で分割代入したとき何がリソースとして扱われるのか(現在の仕様では分割代入で作られた変数ひとつひとつがリソースですが、当初の案では分解される前のオブジェクトがリソースとして扱われていました)、またリソースに[Symbol.dispose]メソッドが無かった場合エラーはいつ発生するのか(現在の仕様ではtryブロック開始時ですが、当初の案ではリソース解放時でした)といった違いがあります。細かい話なので興味がある方は調べてみましょう。


まとめ

この記事では、JavaScriptにリソースという概念を持ち込むExplicit Resource Managementというプロポーザルを紹介しました。筆者はC#はやらないのですがC#のusing文は結構いいという話を聞きますから、きっとJavaScriptのtry-using文もいい感じなのだろうと信じています。

このプロポーザルの登場によって、将来的にJavaScriptプログラムにおけるリソース管理というのがどのような形になるのかが見えてきました。いくつかの例を通してご紹介した通り、try-using文という便利な構文があってもなおリソース管理には考えることが結構あります。ぜひこの記事を通して今のうちにリソースの概念を理解しておきましょう。このプロポーザルがStage3くらいになれば、利用例にtry using (const thing = new Something())なんて書いてあるライブラリが出てきてもおかしくありません。いざその時になっても怯まないように備えておきたいものですね。


関連記事

ある意味この記事と関連している筆者の既存記事をひとつご紹介します。

この記事ではWeakRefという別のプロポーザルを紹介しています。こちらが構文上のexplicit(明示的)なリソース管理ならあちらはGCによるimplicit(暗黙的)なリソース管理と言えるかもしれません。





  1. ただし、これらのキーワードを使わずに既存の変数に=を用いて再代入する場合は、この代入文が式と見なせるため、後述のもうひとつの構文(()の中に式を書くもの)に当てはまるため可能となります。すごく紛らわしいので注意しましょう。 



  2. おおよそというのは、スコープの作られ方の違いやエラー処理のされ方(後述)の違いに表れます。 



  3. 正確には、停止中のyield式からreturn completionを発生させます(completionについては筆者の既存記事JavaScriptの{}を理解するで詳しく解説しています)。