JavaScript
ECMAScript

JavaScriptの { } を理解する

いきなりですが、自分の好きなREPLで以下を入力してみてください1

{a: 10}

結果はどうなったでしょうか。

自分が今使っているGoogle Chromeだとこうなりました。

Screenshot from Gyazo

結果は{a: 10}というオブジェクトです。まあ、これは当然ですね。3 + 5と入力すれば実行されて8が返ってくるのですから、{a: 10}というオブジェクトリテラルを書けば{a: 10}というオブジェクトが作られるのは当然です。

……。

ここで、一部の人は「おいふざけんなよ」と思っているかもしれません。というのも、この例は環境によっては違う結果になるのです。具体的には、Chrome以外2のブラウザのREPL(FirefoxやEdgeなど)が該当します。あと、ts-nodeのREPLも該当するらしいです。これらの環境では、結果は{a: 10}ではなく次のようになります。

10

オブジェクトを作ったはずなのに結果が10とか意味不明ですね。そもそも、こんな簡単なプログラムで結果が全然違うとか、JavaScriptは欠陥言語もいいところだし巷のブラウザはバグだらけですね。(うそです)

そこでこの記事では、なぜこのような挙動になるのかについて解説します。そんな説明聞かなくても分かってるよという方は既にこの記事の内容をマスターしているので大丈夫です。

以降、この記事で仕様書という場合はECMAScript® 2018 Language Specificationを指すものとします。

2種類の結果の説明

まず、上で見た2種類の結果の解釈をそれぞれ説明します。

{a: 10}

{a: 10}は言うまでもなくオブジェクトリテラル、すなわち新しいオブジェクトを作る式です。式をREPLに入力したら、その式が評価されて結果が表示されるというのは皆さんご存知の通りです。

ですので、{a: 10}を評価した結果のオブジェクトである{a: 10}が表示されたことになります。これは簡単ですね。

10

では、同じ{a: 10}という入力で10が表示されたのは何が起こったのでしょうか。実は、この場合は{ }がオブジェクトリテラルではなくブロックとして解釈されています。

ブロック

ブロックは文の1種であり、{ 文; ...; 文; } という構文で複数の文をまとめるものです。ブロックの1つの用途はブロックスコープを導入することです。

ブロックスコープの例
let a = 3;
{
  // このブロックスコープ内でaを宣言
  let a = 100;
  console.log(a); // 100
}
// ブロック内で宣言されたaは外に影響を及ぼさない
console.log(a) // 3

また、if文などにも{ ... }の形が出てきますが、あれも実はブロックです。というのも、JavaScriptのif文の構文はif (式) 文 という形であり、のところに文の一種であるブロックを当てはめることでよく見知ったif (式) { 文; ... }という形が出てくることになります。

ラベル付き文

{a: 10}に話を戻すと、{ }がブロックだったので、その中のa: 10が文であるということになります。そもそも文の終わりを示すセミコロン;が無いじゃんと思うかもしれませんが、JavaScriptにはかの悪名高き自動セミコロン挿入機能がありますので、実はこれは自動的にセミコロンが補われて{ a: 10; }になっています。

さて、このa: 10;という文はラベル付き文 (Labelled Statement)です。ラベル付き文はラベル: 文という形をした文であり3、この例ではaがラベルです。すなわち、10;という文にa:というラベル部分がついた文なのです。

ラベル付き文は、continue文やbreak文と組み合わせて使います。continue文やbreak文にラベルを指定することで、ネストしたループ内で外側のループを抜けるようなことができます。

ラベル付き文の例
/**
 * xsの要素が全部ysにも含まれるかどうか調べる関数
 * (計算量に関するツッコミは受け付けていません)
 */
function arrSubsetAsSet(xs, ys) {
  outerLoop:
    for (const x of xs) {
      for (const y of ys) {
        if (x === y) {
          continue outerLoop;
        }
      }
      return false;
    }
  return true;
}

console.log(arrSubsetAsSet([1, 3], [5, 3, 2, 1])); // true

この例では、for文にラベルを付けることで、continue文で外側のfor文を制御しています。ラベルを使うのはcontinuebreakだけなのでループ以外の文にラベルを付ける意味はありませんが、文法上は可能になっています。

式文

話を戻すと、a: 10;という文は10;という文にラベルが付いたものでした。では10;とはなんでしょうか。実はこれは式文 (Expression Statement)であり、式;という形をした文です。式文は、ただその式を評価するだけの文です。式の評価結果は捨てられます。この例だと、10;という文は式10を評価するだけです。式10を評価しても何も起こりませんから、この文は実質何もしないようなものです。

そんなの何の意味があるんだと思うかもしれませんが、実際のところ、皆さんも式文は非常によく使っているはずです。例えば、以下の文は式文です。

console.log(123);

この文は、console.log(123)という式に;がついた文になっています。よくよく考えれば関数呼び出しは式ですから、関数を呼び出す文は式文であるというわけです。他の例としては、x = 3;のような代入文も式文です。この2つが代表的な例でしたが、最近はawait式の式文を書くことが多くなったかもしれません。

以上で{a: 10}の2つ目の解釈の説明は終わりです。結局のところ、10;という文にラベルを付けてブロックで囲ったものとして解釈されていたというわけです。

文の結果

しかし、まだ1つ疑問が残っていますね。{a: 10}を実行すると10と表示された、あの10は何なのかということです。

そもそも、式というのは計算(評価)されて結果が得られるものです。例えば1 + 2という式は評価されて3という値になります。関数呼び出しの式を評価するときは、その関数が実行されます。

その一方で、文は結果を返さないはずです。だいたい、10;の結果が10という話ならまだ納得できるかもしれませんが、var x, y;の結果とか、continue;の結果、あるいはfor文の結果は何かみたいな話をされても困るわけで、文の結果という概念を考えること自体がナンセンスに思えます。

しかしながら、実はJavaScriptは文の結果などという意味不明な概念を持っています。実際のところ文の結果をプログラムから利用する方法はほぼありませんが4REPLで文を実行したらその結果が表示されるのです。{a: 10}を入力して10が表示されたのは、{a: 10}という文の結果として10が表示されたのです。

このことは、以下の規則から説明できます。

  • 式文式;の結果はの評価結果です。
  • ラベル付き文ラベル: 文の結果は右のの結果です。
  • ブロックの評価結果は最後の文の結果です5

これにより、{a: 10}は文1つだけからなるブロックなので、その結果はa: 10;の結果となります。ラベル付き文の結果は元の(ラベルの付いていない)文の結果と同じなので、今回は10;の結果となります。式10の評価結果は10なので、文10;の結果は10となります。

以上から、{a: 10}の文の結果が10となったのです。

注意点

たった今説明した{a: 10}の結果が10となるのは、あくまで{a: 10}を文として解釈した場合です。以下の文を実行した場合は、xに入るのは10ではなく{a: 10}です。

const x = {a: 10};

なぜなら、変数宣言は変数 = 式という形であり6=の右辺に来るのは文ではなく式ではなければならないため、{a: 10}が文として解釈されることはないからです(そもそも、文の結果というのは先にも述べたとおり「意味不明な概念」であり、文の結果を変数に代入するなどという意味不明なことは考えるべきではありません)。{a: 10}を式として解釈する場合はオブジェクトリテラルとして解釈するしかないため、xに入るのは{a: 10}というオブジェクトになります。

そもそも、ブロックは式ではないという点は、JavaScriptにあまり慣れていない人にとっては注意すべき点かもしれません。実際のところ、ブロックが式であるような言語も存在します(Rustなど)。

Rustのブロックの例
fn main() {
  //      ↓式の位置にブロックを書くことができる
  let x = {
    // ブロック内で変数宣言も可能
    let foo = 1i64;
    // ブロックの最後に書いた値がブロックの値となる
    foo * 10
  };
  // 10が表示される
  println!("{}", x);
}

JavaScriptはこのような言語とは異なり、ブロックは文であり式ではありません。

どちらが正しいのか

ここまでで、2種類の結果がそれぞれどのような解釈に基づいたものなのかを説明しました。では、どちらが正しくてどちらが間違っているのでしょうか。

一応述べておくと、最初に紹介した解釈では、{a: 10}{a: 10}という式に;をつけた式文(ただし;が省略されている)と解釈されることになります。この解釈では、まず{a: 10}という式が評価され(結果は{a: 10}というオブジェクト)、その評価結果が式文の結果となったことから、{a: 10}という文を実行すると文の結果として{a: 10}というオブジェクトが表示されたことになります。

どちらが正しいのか確かめる一つの方法としては、evalを用いる方法があります。evalは、REPL以外で上で説明した「文の結果」を利用することができる唯一の方法です。

evalは引数としてJavaScriptプログラムの文字列を受け取り、それを実行する関数です。evalには式だけでなく文を渡すことができ、evalの返り値は最後に実行した文の(上で説明した意味での)結果となります。

evalの例
var x = 10;
var res = eval('if (x > 0) { x * 10; } else { 0; }');
console.log(res); // 100

この例ではevalでif文を実行しました。if文の「文の結果」は、実行されたほうの文の結果となります。

evalで文の結果を取得できることが分かったので、問題の{a: 10}をevalに渡してみましょう。

var res = eval('{a: 10}');
console.log(res); // 10

やってみると、結果は10となるはずです。ChromeなどはREPLで{a: 10}を実行すると{a: 10}になったにも関わらず、evalを使って試すと10になります。これはどうも10が優勢になってきましたね。

では、いよいよ仕様書を見ましょう。仕様書はJavaScript(ECMAScript)を定義する文書ですから、全ての答えが載っています。さらに言えば、JavaScriptの文法は曖昧性が無いことになっていますから、{a: 10}という文の解釈のどちらが正しくてどちらが間違っているか書いてあるはずです。

答えは、式文の定義を見ると載っています。

仕様書13.5節冒頭部分のスクリーンショット

ご丁寧にNOTEの最初の一文に答えが書いてあります。

An ExpressionStatement cannot start with a U+007B (LEFT CURLY BRACKET) because that might make it ambiguous with a Block.

(要約)ブロックとの曖昧性を避けるため、式文は{で始まることができません。

というわけで、{a: 10}は最初が{なので、式文として解釈するのは間違いでした。この{ }はブロックとして解釈しなければならず、したがって{a: 10}の「文の結果」は10が正解です。

evalで{a: 10}を解釈したときにChromeの結果も10になったのはちゃんとこの仕様に従っているからということになります。

では、冒頭で紹介した、Chromeやnode.jsのREPLのこの挙動は間違いなのでしょうか。

Screenshot from Gyazo

そう問われると、間違いとは言い切れないものがあります。なぜなら、REPLの挙動は仕様で定められていないからです。

先ほど仕様書からECMAScriptの文法の一部を引用しましたが、JavaScriptの文法はあくまでソース全体を一度にパースするときに適用されるものです。ですから、REPLのようにインタラクティブにプログラムが入力され部分部分を実行するような環境においても文法に厳密に従う必要がないという主張も道理が通るかもしれません。(といっても、本来のJavaScriptと乖離した解釈をされると何の役にも立ちませんから加減が必要ですが。)

結局のところ、なるべく仕様に厳密に従おうとしているのがFirefox等で、不必要な厳密さよりも利便性を優先したのがChromeやnode.jsということになります。

余談

曖昧性を無くす

{a: 10}をオブジェクトリテラルとして解釈して欲しい場合は()で囲むのが一番簡単です。({a: 10})とするとこれはブロックではなく式文として解釈されます。逆に{a: 10}をブロックとして解釈して欲しい場合は省略されているセミコロンを補って{a: 10;}とするとよいでしょう。

アロー関数の{ }

{ }の意味が紛らわしいかもしれない場面として、アロー関数があるようです。アロー関数は関数を作るリテラルで、以下のように書きます。{ }の中に関数の本体が書かれていることが分かります。

アロー関数の例
const func = (x, y) => {
  const answer = x + y;
  return answer;
};

console.log(func(3, 5)); // 8

また、アロー関数には=>の直後に返り値の式を書く省略形があります。

const func = (x, y) => x + y;
// これは以下と同じ
// const func = (x, y) => { return x + y; };

では、こう書くとどうなるでしょうか。

const func = (x, y) => { x + y };

console.log(func(3, 5)); // undefined

実は、上のように書くとfuncは返り値を返さない関数となります。なぜなら、アロー関数の形は(引数たち) => 式(引数たち) => { 文たち }の2種類であり、後者の形の場合は中で明示的にreturn文を使わないと値を返せないからです。前者の形ではreturnと書かなくても値を返せますが、その代わり書けるのは式だけです。

繰り返しになりますが、ブロックは式ではなく文なので、=>の後に{ x + y }と書いた場合は前者ではなく後者(中身はx + y;という式文)になります。

一応注意しておきますが、x + y;という式文の「文の結果」はx+yなのだからx+yが返り値として返らないとおかしい、などとは思わないでくださいね。文の結果はevalとREPL以外では無視されるのであり、勝手に関数の返り値になったりはしないのです。

仕様書を読む

さて、今回の記事、REPLにおける{a: 10}の問題とその答えをざっと理解していただけたのではないかと思います。しかし、読者の中には「お前の言っていることなど信用ならん、仕様書が全て!」という方もいるかもしれません。そのような人はQiitaの記事を読むべきではありませんが、もしかしたら居るかもしれないので、仕様書の中に今回の話題がどのように表れているかを最後に解説します。

仕様書を読むにあたってのキーワードはCompletion Recordです。Completion Recordは仕様書の6.2.3節で定義されており、ざっくり言うと「文や式の評価結果を表すオブジェクトのようなもの」です。もちろんこれは仕様書内の用語であり、JavaScriptプログラムに対して露出されることはありません。

Completion Recordは[[Type]]フィールドと[[Value]]フィールドを持ち7、この2つによって計算結果が表現されます。[[Type]]フィールドはnormal, return, break, continue, throwの5種類があり、[[Value]]は何らかのJavaScriptの値です。

普通に計算がされた場合は[[Type]]はnormalとなります。例えば3 + 5という式の計算結果を示すCompletion Recordは、なんやかんやがあった後で{[[Type]]: normal, [[Value]]: 8}となるでしょう。

[[Type]]がreturnであるようなCompletion Recordは、お察しとは思いますがreturn文の結果として現れます。他のタイプも同様です。

注目すべきは、Completion Recordは式の評価結果にも文の評価結果にも使われるという点です。これまたお察しかと思いますが、上で「文の結果という意味不明な概念」と呼んでいたのは、文の評価結果としてのCompletion Recordの[[Value]]フィールドのことです。

if文の仕様

以上を踏まえて、仕様書を読んでみましょう。evalのときにちょっと話に出てきたif文を例にとります。まず、以下の文をREPLで実行してみてください。

if (true) {
  100;
} else {
  10;
}

if文の「文の結果」として100が表示されるはずです。if文は条件部に応じてthen節かelse節のどちらかを実行するものですが、if文の「文の結果」は実行されたほうの文の結果と等しくなります。このことを仕様書で確かめましょう。該当部分の仕様はこのようになっています。

13.6.7 Runtime Semantics: Evaluation
IfStatement: if ( Expression ) Statement else Statement

  1. Let exprRef be the result of evaluating Expression.
  2. Let exprValue be ToBoolean(? GetValue(exprRef)).
  3. If exprValue is true, then     a. Let stmtCompletion be the result of evaluating the first Statement.
  4. Else,     a. Let stmtCompletion be the result of evaluating the second Statement.
  5. Return Completion(UpdateEmpty(stmtCompletion, undefined)).

1は条件部分の式を評価します。ということは、その結果であるexprRefはCompletion Recordです……と言いたいところですが、実はそうとは限りません。式がobj.fooである場合など、Completion RecordではなくReferenceである可能性があります。

2を順番に見ていきます。GetValueは仕様書内でよく使われる便利関数であり、ざっくり言うとReferenceの中身を解決して値を取り出してくれます。例えばobj.fooという式を評価するとobj.fooを指すReferenceになりますが、GetValueを通すことで具体的な値に解決されます。Completion Recordの場合はそのままです8

次の?仕様書で超頻出のマクロです。これは前置マクロであり、「abrupt completionのときはそれをこのアルゴリズム全体の結果とする。それ以外のときはそのまま」という意味です。Abrupt completionというのは、[[Type]]がnormal以外のCompletion Recordのことです。

Abrupt completionの共通する挙動は「プログラムの実行をその場で中断して脱出する」というものです。これは、例外が発生した場合([[Type]]がthrowのCompletion Recordが発生)を考えるとイメージしやすいでしょう。この脱出という挙動が?によって表されています。要するに、2における?が言っていることは、「条件式の評価結果がabrupt completionだった場合は、if文の評価を中断してそれをそのままif文の結果とする」ということです。例外に例えると、条件式の評価中に例外が発生した場合、if文はそれ以上評価されずに例外が上へ伝播していく挙動を表しています。

次のToBooleanは普通に値を真偽値に変換する関数です。1などの値はここでtrueに変換されます。

3と4は、条件式の結果に応じて1番目か2番目の文を実行し、その結果をstmtCompletionという変数に入れています。変数名から分かる通り、文を実行した結果はCompletion Recordです。

5は基本的には「その結果をそのままif文全体の結果とする」ということを言いたいわけですが、何か2つ関数をかませてあります。UpdateEmptyは、「completion recordの[[Value]]がemptyだったらそれをundefinedにする」というものです。このemptyというのは「結果が無い」ということを表す厄介者です(また後で説明します)。その次のCompletionは、よく分かりませんが多分返り値がCompletion Recordであることを明示する以上の意味は無いです。

以上がif文のセマンティクスでした。「まず条件部分を評価し、その結果に応じてthen部分かelse部分かを評価し、その結果をif文全体の結果とする」という流れが読み取れたと思います。

ブロックの仕様

他にも、ブロックの「文の結果」はその中の最後に実行された文の結果になると述べましたが、そのことは以下の部分で書かれています。

13.2.13 Runtime Semantics: Evaluation
StatementList: StatementList StatementListItem
1. Let sl be the result of evaluating StatementList.
2. ReturnIfAbrupt(sl).
3. Let s be the result of evaluating StatementListItem.
4. Return Completion(UpdateEmpty(s, sl)).

ざっくり言うと、StatementListを前から順番に実行していって、abrupt completionが発生したら実行を中断してそれを返すようになっています。結果はCompletion(UpdateEmpty(s, sl))となっていますが、これは結果は基本的にはs(最後の文の結果)としつつも、もし結果がemptyだったらその前の結果(sl)を採用するという意味です。

それ以外にも、while文やfor文は最後に実行したループの結果が採用される(emptyは除く)などなかなか面白いことが書いてあるので、目を通してみるとよいのではないかと思います。尤も、REPLとevalでしか意味がないわけですが。

empty

先ほども説明したとおり、Completion Recordの[[Value]]はemptyである可能性があります。

具体的には、変数宣言(let x = 1;など)に加えて空文(;)、debugger文(debugger;)が該当します。また、ブロックについても、空の場合({ })や中の文の結果が全部emptyの場合は該当します。上で説明した通り、emptyはブロック文の結果に影響しない特性を持ちます。ただし、if文の例で見た通り、emptyが好ましくない場合は基本的にundefinedに変換されます。

特に、変数宣言の「文の結果」がemptyであることを用いると、REPLで変数を定義したときにundefinedと表示される挙動が説明できます。

ChromeのREPLにlet a = 10;と入力したときのスクリーンショット

ここで表示されているのは変数宣言の「文の結果」ですが、emptyだったのでREPLでの表示のためにundefinedに変換されていると考えられます。(正直なところ、emptyだったら表示しなくてもいいんじゃないかと個人的には思いますが。)

emptyとブロックを組み合わせると楽しい挙動を観察できます。例えば、次のブロックの文の結果は3です。

{
  3;
  var a;
}

その理由は、最後の文var a;の結果がemptyなのでその前の文3;の結果である3が採用されているからです。

一方、次のブロックの文の結果はundefinedです。

{
  3;
  if (true) var a;
}

これは、var a;の文の結果はemptyですが、if文によりそれがundefinedに変換されているためif (true) var a;の結果がemptyではなくundefinedとなっていることが理由です。

これにより、文;if (true) 文;が等価ではないことが明らかになりました。(文の結果を考慮に入れるならですが。)

return Completionを返す式

ところで、Completionの種類はnormal, continue, break, throw, returnの5種類でした。このうち、の評価結果として可能なCompletionはどれでしょうか。

まず、普通の式はnormalなのでこれは当然ありえます。また、式に評価中にエラーが発生することは普通にありますから、throwも可能です。

一方で、continuebreakは無理でしょう。これらはcontinue文やbreak文により発生しますが、式の中からこれらの文に到達するには関数呼び出しによって別の関数に入る必要があります。しかし、breakcontinueの影響はその関数の中で完結してしまい、外に飛び出してくることはありません。

では、returnはどうでしょう。これもcontinuebreakと同じ理屈で無理のように思えます。別の関数に入ってその中でreturn文に到達しても、return文の効果はその関数から脱出した時点で終了します。では無理なのか、と思いきや実は可能ですreturn Completionを発生させることができる式が一つだけあります。それはyield式です。

yield式はジェネレータ関数の中でのみ使用可能な式であり、指定された値をジェネレータから発生させて実行を一時中断します。そして、ジェネレータの再開時にジェネレータに与えられた値を返します。

ジェネレータの例
function* gen() {
  // まず1を発生させる
  const x = yield 1;
  // 次に10を発生させる
  const y = yield 10;
  // 最後に100を発生させて終了
  return 100;
}

// ジェネレータを作成
const g = gen();
// 1回目の値を発生させる
const value1 = g.next();
console.log(value1); // {done: false, value: 1}
// 2回目の値を発生させる
const value2 = g.next();
console.log(value2); // {done: false, value: 10}
// 3回目の値を発生させる
const value3 = g.next();
console.log(value3); // {done: true, value: 100}

このように、ジェネレータのnextメソッドを呼ぶことでyieldで止まっていたジェネレータ関数を先に進ませることができます。上の例では変数xyに特に意味はありませんが、yieldが式であることを強調するために書いています。

さて、実はジェネレータにはnextの他にあと2つメソッドがあります。それはreturnthrowです。なんと、returnメソッドを呼び出すことで、関数内部のyieldからreturn Completionが発生するのです。

ジェネレータをreturnさせる例
function* gen() {
  // まず1を発生させる
  const x = yield 1;
  // 次に10を発生させる
  const y = yield 10;
  // 最後に100を発生させて終了
  return 100;
}

// ジェネレータを作成
const g = gen();
// 1回目の値を発生させる
const value1 = g.next();
console.log(value1); // {done: false, value: 1}
// 強制returnさせる
const value2 = g.return(5);
console.log(value2); // {done: true, value: 5}
// もう実行は終了したのでnextを読んでも結果が来ない
const value3 = g.next();
console.log(value3); // {done: true, value: undefined}

この例では、2回目にreturnを呼んでいるので、const y = yield 10;return 5;に書き換わったかのような挙動をします。5はreturnメソッドの引数に与えられたものです。

これが、式からreturn Completionを発生させる唯一の方法です。面白いですね。なお、もうひとつのthrowメソッドというのはもちろん、throw Completionを発生させるメソッドです。

do式

現在、ECMAScriptの新機能としてdo式が提案されています。といっても、アイデア段階のまま長らくStage 1なので難航しているようですが。

do式はdo { 文; ...; 文; }のようにdoの中に文を並べることができるものですが、これ自体が式であるという点がポイントです。do式は、文たちを実行した「文の結果」を式の評価結果として取り出すことができるものなのです。

便利ですが、if (true) 文と同じではないみたいな問題が表面化してくることを考えるとなかなか大変なのもうなずけます。

まとめ

余談が長くなりましたが、この記事では{a: 10}問題を取っ掛かりとして「文の結果」の概念について解説しました。

実用上この「文の結果」の概念はREPLくらいでしか関係してきません(最後に紹介したdo式が導入されればまた話が変わってきますが)。REPLで何か結果らしきものが現れたらそれがこの記事で説明した「文の結果」であることを理解し、正しくその意味を解釈できるようになりましょう。

特に、途中言及したREPLの仕様は規定されていないことは重要です。REPLで「文の結果」とかいう変な概念が表面化することも相まって、REPLの結果だけからJavaScriptについて議論することは危険であることがお分かりになると思います。REPLの挙動を完璧に理解しているのでなければ、正確な結果が必要なときはREPLだけに頼るのではなく独立したJavaScriptプログラムを用意して実行してみるのがよいでしょう。

関連記事


  1. REPLとは、JavaScriptを入力するとその場で結果を返してくれるやつです。ブラウザの開発者ツールが一番身近だと思います。あと、node.jsもREPLがついています。 

  2. Chromium系はChromeと同じ挙動をするのでOperaとかはChromeと同じです。 

  3. 実は関数宣言にもラベルを付けることができますが。 

  4. 後で触れますが、現時点で唯一の方法がevalです。 

  5. あとで説明しますが、厳密には結果の無い文が混ざったときの挙動が少し異なります。 

  6. 分解代入などもあるので実は必ずしもこうではありませんが省略しています。 

  7. breakcontinue文の処理用に[[Target]]フィールドも持ちますが今回はあまり深入りしません。 

  8. 厳密には[[Type]]がnormalのCompletion Recordは取り外されて生の中身が返されますが、仕様書はどうも[[Type]]がnormalのCompletionと生の値をあまり区別していないような気がしますので、ここでもそれに倣って無視できる場面では無視していきます。