【JavaScript】 ECMAScript2020から変数宣言を読み解く
更新日:2020/12/23
変数宣言の定義
letまたはconstによる変数宣言は、13.3.1 LetとConst宣言で次のように定義されています。
構文抜粋
LexicalDeclaration : LetOrConst BindingList ; LetOrConst : let const BindingList: LexicalBinding BindingList ,LexicalBinding LexicalBinding : BindingIdentifier Initializer(option) BindingPattern Initializer
実際の定義はもう少し複雑ですが、ここでは単純化してあります。
定義中の茶色文字は定義名で、リンク先の内容と置換されます。
黒文字は、コード上に入力されるのテキストです。
とはいえ、これでは何が何だかわからないですね。
ECMAScriptはブラウザなどにJavaScriptエンジンを組み込む実装者が、コードに落とし込みやすいように設計されています。
JavaScriptプログラマがこれまで自分で身に着けた知識を元に解析しようとしても、非常に苦労するようにできています。
ということで、苦労しながら解析してみましょう。
最初に定義されているのが、次の構文です。
LexicalDeclaration :LetOrConst BindingList;
訳文中では各文字列にリンクがセットされています。
そしてリンク先の内容を置き換えることで、展開していきます。
LetOrConstは、文字列「let」 または 「const」 に展開されます。
BindingListは、「LexicalBinding」 または 「BindingList , LexicalBinding」に展開されます。
後者についてはLexicalBinding,LexicalBinding,…と繰り返しをあらわすものなので、ここでは前者のみを追うことにします。
「LexicalBinding」は、次の二つに展開されます。
- BindingIdentifier Initializer(オプション)
- BindingPattern Initializer
1のBindingIdentifierのリンク先は次のようになっています。
BindingIdentifierの定義
BindingIdentifier : Identifier yield await
Identifierは、この先もいくつかの定義にリンクしていますが、最終的に予約語でない変数名をあらわしています。
「yield」および「await」は予約語ですが、変数名として使用できるケースもあるので、Identifierとは別に指定されています。
後に続くInitializer(オプション)はオプションです。
リンクを追っていくと、「=」のあとに初期値を指定する構文だということがわかります。
2は、「 [変数,変数…] = 配列値 」 や 「 {変数,変数…} = オブジェクト値 」などの分割代入を想定したものです。
こちらのInitializer(オプション)は必須です。
以上のことから、LexicalDeclarationは次の八つに展開できます。
- let 変数名;
- let 変数名 = 初期値;
- const 変数名;
- const 変数名 = 初期値;
- let [変数,変数…] = 配列値;
- const [変数,変数…] = 配列値;
- let {変数,変数…} = オブジェクト値;
- const {変数,変数…} = オブジェクト値;
全ては確認しきれないので、ここでは上の四つ(分割代入以外)を追ってみます。
構文エラーチェック
次に構文エラーの定義( 静的セマンティクス:早期エラー)を確認してみます。
- 「BindingList の BoundNames が "let" を含む場合、構文エラーです」
これは、let a,let,b; というように変数名としてletを使用するとエラーになるという意味。
- 「BindingList の BoundNames に重複するエントリが含まれている場合、構文エラーです」
これは、let a,b,a; というように同じ変数名を重複使用するとエラーになるという意味。
- 「Initializerが存在せず、LexicalBinding を含む LexicalDeclaration の IsConstantDeclaration が true の場合、構文エラーです」
「LexicalDeclaration の IsConstantDeclaration」は、13.3.1.3を参照すると、「let」のときfalse、「const」のときtrueと定義されています。
このことから、「constで変数を定義するとき、同時に初期化をしないとエラー」という意味になります。
初期化なし時の構文評価
初期化なしのパターンは、次の二つです。
- let 変数名;
- const 変数名;
しかしエラーチェックにより、初期化なしの構文は「let」に限定されます。
このことを念頭に置いて、処理を追っていきます。
JavaScriptエンジンがソースコードを評価していき、「let 変数名;」があらわれたとき13.3.1.4 ランタイムセマンティクス:評価の処理が実行されます。
ここでは、次のような定義が最初に書かれています。
LexicalDeclaration : LetOrConst BindingList ;
これは、ソースコードがそのパターンに一致するとき続くアルゴリズムが実行されるという意味です。
アルゴリズムは次のようになっています。
- BindingList の評価結果を next とする
- ReturnIfAbrupt(next)
- NormalCompletion(empty) を返す
1のBindingList の評価結果は、「BindingList : BindingList ,LexicalBinding」の処理結果です。
2のReturnIfAbrupt関数はエラーチェックです。
3のNormalCompletion関数は{ [[Type]]: normal, [[Value]]: 引数(ここではempty), [[Target]]: empty }を返します。
1についてもう少し詳しく見ていきます。
「BindingList : BindingList ,LexicalBinding」のアルゴリズムは次のようになっています。
- BindingList の評価結果を next とする
- ReturnIfAbrupt(next)
- LexicalBinding の 評価結果を返す
1はlet 変数,変数…を想定したものです。
2のReturnIfAbrupt関数はエラーチェックです。
3は次のような「LexicalBinding : BindingIdentifier」のアルゴリズムが実行されます。
- ResolveBinding(BindingIdentifier の StringValue )を lhs とする
- InitializeReferencedBinding(lhs, undefined) を返す
「BindingIdentifier の StringValue」は、11.6.1.2に行き着き、コード上に入力されている変数名であることがわかります。
ResolveBinding関数は変数名を環境レコードから検索して、その情報を返します。
環境レコードはブロックごとに設定されていて、変数名を保持しています。
この関数を追ってみると変数がすでに環境レコードに存在することが前提であることがわかります。
InitializeReferencedBinding関数は、変数と値undefinedを結びつけます。
これらのことから、「let 変数名;」というコードは、既に作成されている変数にundefinedをセットしているだけだということがわかります。
変数定義ではないですね…
初期化あり時の構文評価
初期化ありのパターンは、次の二つです。
- let 変数名 = 初期値;
- const 変数名 = 初期値;
処理的には変数の値に初期値をセットする以外は、初期化なしと同じです。
つまり「let 変数名;」または「const 変数名;」というコードは、既に作成されている変数に初期値をセットしているだけだということがわかります。
ブロックで変数を作成(定義)している
letやconstなどの変数は、13.2 ブロック(Block)で作成されています。
ブロックは次のように定義されています。
Block : { StatementList }
StatementListは、各ステートメントを集めたもの。
つまりプログラムコードです。
{ }で囲まれたコードがブロックということですね。
ではブロックの評価部分(13.2.13 ランタイムセマンティクス:評価)を見ていきます。
最初の「Block : {}」は空のコードなので省略して、次を見ます。
Block : {StatementList }
- 実行中の実行コンテキスト の LexicalEnvironment を oldEnv とする
- NewDeclarativeEnvironment(oldEnv) を blockEnv とする
- BlockDeclarationInstantiation(StatementList , blockEnv) を実行する
- blockEnv を 実行中の実行コンテキスト の LexicalEnvironment にセットする
- StatementList の評価結果を blockValue とする
- oldEnv を 実行中の実行コンテキスト の LexicalEnvironment にセットする
- blockValue を返す
実行中の実行コンテキストはこれから評価する外側のコードです。
LexicalEnvironmentは、変数名などが格納されているレコードです。
つまり、これから評価するコードの外側にある変数を格納したレコードをoldEnvとしています。
NewDeclarativeEnvironment関数は新規のLexicalEnvironmentを作成して、blockEnvとします。そしてblockEnvの外部LexicalEnvironmentとしてoldEnvをセットしています。
これにより内側から外側に変数名を検索する仕組みが構築されます。
BlockDeclarationInstantiationはブロック内のコードから変数を探してきて、blockEnvに変数を作成しています。
あとは現在のLexicalEnvironmentをこれから実行するブロックのもの(blockEnv)に変更してコードを実行。
終わったら、現在のLexicalEnvironmentを以前のもの(oldEnv)に戻して終了です。
NewDeclarativeEnvironment関数で変数を探す仕組みが少しわかりにくいので、簡単に解説します。
ポイントはステップ3「code の LexicallyScopedDeclarations を declarations とする」です。
これは13.2.6 静的セマンティクス(Static Semantics): LexicallyScopedDeclarationsを参照します。
ここでは次のような構文のアルゴリズムが最初に書かれています。
StatementList : StatementList StatementListItem
ここでStatementListItemは、「StatementListItem : Declaration」に展開できます。
Declarationには、LexicalDeclarationが含まれています。
LexicalDeclarationは最初に解説したletとconst定義です。
つまりコード内にletとconstがあったら、その変数をリストアップしています。
その情報を元に、LexicalEnvironmentに変数を作成しています。
更新日:2020/12/23
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。