JavaScript 変数の昇格についての簡単な説明

JavaScript 変数の昇格についての簡単な説明

序文

ECMAScript6 では、変数を宣言するために let および const キーワードが追加されました。フロントエンドの面接では、let、const、var の違いについてよく質問されますが、これには変数の昇格や一時的なデッドゾーンなどの知識ポイントが関係します。変数プロモーションと一時的なデッドゾーンとは何かを見てみましょう。

1. どのような変数が促進されますか?

まず、MDN での変数の昇格の説明を見てみましょう。

ホイスティングは、JavaScript で実行コンテキスト (具体的には作成フェーズと実行フェーズ) がどのように機能するかを理解することだと考えられます。 ECMAScript® 2015 言語仕様より前の JavaScript ドキュメントでは、hoisting という用語は見つかりませんでした。
概念の文字通りの意味では、「変数の昇格」とは、変数と関数の宣言が物理レベルでコードの先頭に移動されることを意味しますが、これは正確ではありません。実際、コード内の変数と関数の宣言の場所は移動せず、コンパイルフェーズ中にメモリに配置されます。

簡単に言えば、変数巻き上げとは、JavaScript コードの実行中に JavaScript エンジンが変数宣言部分と関数宣言部分をコードの先頭に持ち上げる動作を指します。変数がホイストされると、そのデフォルト値は undefined に設定されます。 多くのコードがあまり直感的ではないのは、まさに JavaScript の変数昇格機能のせいであり、これは JavaScript の設計上の欠陥でもあります。 ECMAScript6 では、ブロックレベルのスコープを導入し、let キーワードと const キーワードを使用することでこの設計上の欠陥を回避しましたが、JavaScript は下位互換性を保つ必要があるため、変数の昇格は今後も長い間存在し続けるでしょう。

ECMAScript6 より前では、JS エンジンは var キーワードを使用して変数を宣言していました。 var の時代では、変数宣言がどこに書かれていても、最終的にはスコープの先頭に持って来られることになります。 以下は、グローバル スコープで num 変数を宣言し、宣言する前にそれを出力します。

コンソール.log(数値) 
変数番号 = 1

変数宣言が巻き上げられているため、これは undefined を出力します。これは次と同等です。

変数番号
コンソール.log(数値)
数値 = 1

ご覧のとおり、グローバル変数としての num は、グローバル スコープの先頭に昇格されます。

さらに、関数スコープ内で変数の昇格も行われます。

関数 getNum() {
  コンソール.log(数値) 
  変数番号 = 1  
}
取得数()

関数内の変数宣言は関数スコープの先頭に引き上げられるため、これも undefined を出力します。これは次と同等です:

関数 getNum() {
  変数番号 
  コンソール.log(数値) 
  数値 = 1  
}
取得数()

変数の昇格に加えて、関数も昇格されます。 JavaScript には、名前付き関数の宣言に 2 つの形式があります。

//関数宣言:
関数 foo() {}
//変数宣言: 
var fn = 関数 () {}

変数形式で関数を宣言すると、通常の変数と同じように巻き上げられますが、関数宣言はスコープの先頭に巻き上げられ、宣言内容も先頭に巻き上げられます。次のように:

関数()
var fn = 関数() {
	コンソール.log(1)  
}
// 出力: Uncaught TypeError: fn は関数ではありません

関数foo()
関数foo(){
	コンソール.log(2)
}
// 出力: 2

ご覧のとおり、fn が変数形式で宣言され、その前に実行されると、fn は現時点では単なる変数であり、関数に割り当てられていないため、fn メソッドを実行できないというエラー メッセージが報告されます。

2. 可変プロモーションがあるのはなぜですか?

変数の巻き上げは、JavaScript のコンパイル プロセスと密接に関連しています。JavaScript は、他の言語と同様に、コンパイル フェーズと実行フェーズを経ます。この短いコンパイルフェーズ中に、JS エンジンはすべての変数宣言を収集し、事前に有効にします。残りのステートメントは、実行フェーズまたは特定のステートメントが実行されるまで有効になりません。これが可変ホイストの背後にあるメカニズムです。

では、なぜ JavaScript に変数の昇格が存在するのでしょうか?

まず、スコープから始めましょう。スコープとは、変数が定義されているプログラム内の領域を指し、変数のライフ サイクルを決定します。簡単に言えば、スコープとは変数と関数のアクセス可能な範囲のことです。つまり、スコープは変数と関数の可視性とライフサイクルを制御します。

ES6 より前は、2 種類のスコープがありました。

  • グローバル スコープ内のオブジェクトはコード内のどこからでもアクセス可能であり、そのライフサイクルはページのライフサイクルに付随します。
  • 関数スコープとは、関数内で定義された変数または関数を指し、定義された変数または関数には関数内でのみアクセスできます。関数が実行されると、関数内で定義された変数は破棄されます。

対照的に、他の言語では一般的にブロックレベルのスコープがサポートされています。ブロック スコープは、関数、判定ステートメント、ループ ステートメントなど、一対の中括弧で囲まれたコードの一部です。また、単一の {} もブロック スコープと見なすことができます (オブジェクト宣言内の {} はブロック スコープではないことに注意してください)。簡単に言えば、言語がブロックレベルのスコープをサポートしている場合、コードブロック内で定義された変数はコードブロックの外部からアクセスできず、コードブロック内で定義された変数はコードブロック内のコードが実行された後に破棄されます。

ES6 より前は、ブロック レベルのスコープはサポートされていませんでした。ブロック レベルのスコープがない場合、最も速くてシンプルな設計は、スコープ内で変数を均一に昇格することです。ただし、これは関数内のどこで変数が宣言されていても、コンパイル フェーズ中に実行コンテキストの変数環境に抽出されるという事実に直接つながります。したがって、これらの変数は関数本体全体のどこからでもアクセスできます。これが JavaScript における変数の昇格です。

変数プロモーションを使用すると、次の 2 つの利点があります。

(1)パフォーマンスの向上

JS コードが実行される前に、構文チェックとプリコンパイルが実行され、この操作は 1 回だけ実行されます。これはパフォーマンスを向上させるために行われます。このステップがなければ、各コード実行の前に変数 (関数) を再度解析する必要があります。変数 (関数) のコードは変更されないため、これは不要であり、一度解析すれば十分です。

解析プロセス中に、関数のプリコンパイルされたコードも生成されます。プリコンパイル中に、宣言された変数と作成された関数がカウントされ、関数コードが圧縮されてコメントや不要なスペースなどが削除されます。この利点は、関数が実行されるたびに、関数にスタック スペースを直接割り当てることができる (コード内でどの変数が宣言され、どの関数が作成されるかを取得するために再度解析する必要がない) ことと、コードが圧縮されるため、コードの実行も高速になることです。

(2)フォールトトレランスの向上

変数の昇格により、JS のフォールト トレランスがある程度向上します。次のコードを参照してください。

1 = 1;
var a;
コンソールログ(a); // 1

変数の昇格がない場合、これらの 2 行のコードはエラーを報告しますが、変数の昇格があるため、このコードは正常に実行できます。

開発中にこれを回避することはできますが、コードが複雑で、不注意な使用により変数が最初に使用され、後で定義される場合があり、変数の昇格の存在によりコードは正常に実行されます。もちろん、開発プロセス中は、最初に変数を使用してから宣言することは避けてください。

要約:

  • 解析およびプリコンパイル中の宣言の巻き上げにより、関数が実行時に変数のスタック領域を事前に割り当てることができるため、パフォーマンスが向上します。
  • 宣言の巻き上げにより、JS コードのフォールト トレランスも向上し、一部の非標準コードも正常に実行できるようになります。

3. 可変プロモーションによって生じる問題

変数の昇格が存在するため、JavaScript を使用して他の言語と同じロジックのコードを記述すると、実行結果が異なる場合があります。主に2つの状況があります。

(1)変数が上書きされる

次のコードを見てみましょう。

var 名 = "JavaScript"
関数 showName(){
  console.log(名前);
  もし(0){
   var 名 = "CSS"
  }
}
名前を表示()

ここでは、「JavaScript」の代わりに undefined が出力されます。なぜでしょうか?

まず、showName 関数呼び出しが実行されると、showName 関数の実行コンテキストが作成されます。その後、JavaScript エンジンは showName 関数内のコードの実行を開始します。最初に実行されるのは次の通りです。

console.log(名前);

このコードを実行するには、変数 name が必要です。コードには 2 つの name 変数があります。1 つはグローバル実行コンテキストにあり、その値は JavaScript です。もう 1 つは showName 関数の実行コンテキストにあります。if(0) が true になることはないので、name の値は CSS です。では、どれを使えばいいのでしょうか?関数実行コンテキスト内の変数を最初に使用する必要があります。関数実行プロセス中に、JavaScript はまず現在の実行コンテキストで変数を検索します。変数巻き上げが存在するため、現在の実行コンテキストには if(0) の変数 name が含まれており、その値は未定義であるため、取得された name の値は未定義になります。
ここでの出力は、ブロックレベルスコープをサポートする他の言語とは異なります。例えば、C言語はグローバル変数を出力するため、ここで誤解を招きやすいです。

(2)変数は破壊されない

関数foo(){
  (var i = 0; i < 5; i++) の場合 {
  }
  コンソールにログ出力します。 
}
関数foo()

他のほとんどの言語で同様のコードを実装すると、 for ループの終了後に i が破棄されますが、JavaScript コードでは i の値は破棄されないため、最終的な出力は 5 になります。これも変数の昇格によって発生します。実行コンテキストを作成するときに、変数 i が昇格されているため、for ループが終了しても変数 i は破棄されません。

4. 変数の巻き上げを無効にする

上記の問題を解決するために、ES6 では let キーワードと const キーワードが導入され、JavaScript が他の言語と同様にブロックレベルのスコープを持つことができるようになりました。 let と const には変数の昇格はありません。変数を宣言するにはletを使いましょう:

コンソール.log(数値) 
数値を1とします

// 出力: キャッチされない ReferenceError: num が定義されていません

これを const 宣言に変更すると、結果は同じになります。let および const で宣言された変数は、特定のコードが実行されると同時に宣言が有効になります。

変数の昇格メカニズムは多くの誤操作につながる可能性があります。宣言し忘れた変数は開発フェーズ中に明確に検出されず、コード内で未定義として隠されます。実行時エラーを減らし、 undefined が予期しない問題を引き起こすのを防ぐために、ES6 では宣言前に使用できないという強い制約が特に課されています。ただし、let と const には違いがあります。let キーワードを使用して宣言された変数は変更できますが、const を使用して宣言された変数の値は変更できません。

ES6 がブロックレベルのスコープを通じて上記の問題をどのように解決するかを見てみましょう。

関数fn() {
  var 数値 = 1;
  (真)の場合{
    var 数値 = 2;  
    console.log(数値); // 2
  }
  console.log(数値); // 2
}
関数()

このコードでは、変数 num は関数ブロックの先頭と if 内の 2 か所で定義されています。var のスコープは関数全体であるため、コンパイル フェーズ中に次の実行コンテキストが生成されます。

実行コンテキストの変数環境から、最終的に num という変数が 1 つだけ生成され、関数本体での num へのすべての代入操作によって変数環境の num の値が直接変更されることがわかります。したがって、上記コードの最終出力は 2 になります。同じロジックのコードの場合、他の言語の最終出力値は 1 になるはずです。これは、if 内の宣言がブロック外の変数に影響を与えてはならないためです。

var キーワードを let キーワードに置き換えて、その効果を確認してみましょう。

関数fn() {
  num = 1 とします。
  (真)の場合{
    num = 2 とします。  
    console.log(数値); // 2
  }
  console.log(数値); // 1
}
関数()

このコードを実行すると、期待どおりの出力が生成されます。これは、let キーワードがブロックレベルのスコープをサポートしているため、JavaScript エンジンはコンパイル段階で let in if によって宣言された変数を変数環境に保存しないからです。つまり、let in if によって宣言されたキーワードは関数全体に表示されるようには昇格されません。したがって、if ブロック内に印刷された値は 2 であり、ブロックから飛び出した後に印刷された値は 1 です。これは私たちの習慣と一致しています。ブロック内で宣言された変数は、ブロック外の変数に影響を与えません。

5. JS はブロックレベルのスコープをどのようにサポートしますか?

そこで疑問になるのが、ES6 はどのようにして変数の昇格機能とブロックレベルのスコープの両方をサポートするのかということです。実行コンテキストの観点から理由を見てみましょう。

JavaScript エンジンは、変数環境を通じて関数レベルのスコープを実装します。では、ES6 は関数レベルのスコープに基づいてブロックレベルのスコープをどのようにサポートするのでしょうか?まず次のコードを見てみましょう。

関数fn(){
    変数a = 1
    b = 2とする
    {
      b = 3とする
      var c = 4
      d = 5とする
      コンソールログ(a)
      コンソールログ(b)
      コンソールログ(d)
    }
    コンソールログ(b) 
    コンソール.log(c)
}   
関数()

このコードが実行されると、JavaScript エンジンはそれをコンパイルし、実行コンテキストを作成してから、コードを順番に実行します。 let キーワードはブロックスコープを作成しますが、let キーワードは実行コンテキストにどのように影響するのでしょうか?

(1)実行コンテキストを作成する

作成された実行コンテキストを図に示します。

上の図から、次のことがわかります。

  • var で宣言された変数は、コンパイルフェーズ中に変数環境に格納されます。
  • let で宣言された変数は、コンパイルフェーズ中にレキシカル環境に格納されます。
  • 関数スコープ内では、let で宣言された変数はレキシカル環境に格納されません。

(2)実行コード

コード ブロックが実行されると、変数環境の a の値は 1 に設定され、字句環境の b の値は 2 に設定されます。関数の実行コンテキストは、図のようになります。

関数のスコープブロックに入ると、スコープブロック内でletによって宣言された変数がレキシカル環境の別の領域に格納されることがわかります。この領域の変数は、スコープブロック外の変数に影響を与えません。たとえば、変数bがスコープ外で宣言され、変数bがスコープブロック内でも宣言されている場合、実行がスコープに入ると、それらはすべて独立して存在します。

実際、レキシカル環境内ではスタック構造が維持されています。スタックの一番下は関数の最も外側の変数です。スコープ ブロックに入ると、スコープ ブロック内の変数はスタックの一番上にプッシュされます。スコープの実行が完了すると、スコープの情報がスタックの一番上からポップされます。これがレキシカル環境の構造です。ここでの変数は、let または const によって宣言された変数を指します。

次に、スコープブロック内の console.log(a) を実行すると、変数 a の値がレキシカル環境と変数環境で検索される必要があります。検索方法は、レキシカル環境スタックの先頭に沿って下方向に検索します。レキシカル環境のブロックで見つかった場合は、JavaScript エンジンに直接返されます。見つからない場合は、変数環境での検索を続けます。変数の検索は次のように行われます。

スコープ ブロックが実行されると、その中で定義された変数がレキシカル環境スタックの先頭からポップされ、最終的な実行コンテキストは図のようになります。

ブロックレベル スコープは、字句環境のスタック構造を通じて実装され、変数の巻き上げは変数環境を通じて実装されます。この 2 つを組み合わせることで、JavaScript エンジンは変数の巻き上げとブロックレベル スコープの両方をサポートします。

6. 一時的なデッドゾーン

最後に、一時的なデッドゾーンの概念を見てみましょう。

var name = 'JavaScript';
{
	名前 = 'CSS';
	名前を付けます。
}

// 出力: 捕捉されない ReferenceError: 初期化前に 'name' にアクセスできません

ES6 では、ブロック内に let と const が存在する場合、このブロック内でこれら 2 つのキーワードによって宣言された変数は、最初から閉じたスコープを形成します。このような変数を宣言する前に使用しようとすると、エラーが発生します。エラーが報告されるこの領域は一時的なデッドゾーンです。上記のコードの 4 行目の上の領域は一時的なデッド ゾーンです。

グローバル name 変数を正常に参照したい場合は、let 宣言を削除する必要があります。

var name = 'JavaScript';
{
	名前 = 'CSS';
}

この時点でプログラムは正常に実行されます。実際、これはエンジンが name 変数の存在を認識していないことを意味するものではありません。逆に、エンジンはそれを認識しており、let を使用して現在のブロックで name が宣言されていることを明確に認識しています。そのため、この変数に一時的なデッドゾーン制限が追加されます。 let キーワードを削除すると、効果はなくなります。

実際、これが一時的なデッドゾーンの本質です。プログラムの制御フローが新しいスコープでインスタンス化されると、このスコープで let または const で宣言された変数が最初にスコープ内に作成されますが、この時点ではレキシカルにバインドされていないため、アクセスできません。アクセスすると、エラーがスローされます。したがって、実行中のプロセスが変数を作成するためにスコープに入ってから、その変数にアクセスできるようになるまでの時間は、一時的なデッドゾーンと呼ばれます。

let および const キーワードが登場する前は、typeof 演算子は 100% 安全でした。しかし、今では一時的なデッド ゾーンも発生する可能性があります。import キーワードを使用してパブリック モジュールを導入したり、新しいクラスを使用してクラスを作成したりしても、一時的なデッド ゾーンが発生する可能性があります。その理由は、変数が使用される前に宣言されるためです。

typeof a // キャッチされない ReferenceError: a は定義されていません
a = 1とする

ご覧のとおり、a を宣言する前に typeof キーワードを使用するとエラーが発生します。これは一時的なデッド ゾーンによって発生します。

要約する

JavaScript 変数のプロモーションに関するこの記事はこれで終わりです。JavaScript 変数のプロモーションに関するより関連性の高いコンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • JavaScript変数オブジェクトの詳細な理解
  • JavaScript 変数と変換の詳細
  • JavaScript 変数の型と変数間の変換を理解していますか?
  • JavaScript での変数宣言をご存知ですか?
  • JavaScript の基本変数
  • JavaScript でローカル変数をグローバル変数に変換する方法
  • JS ES6 変数分割代入の詳細な説明
  • 文字列連結と変数の適用に関する Javascript 初心者向けガイド
  • JS変数プロモーションの原理と使用例の簡単な分析
  • JavaScript での変数の使用

<<:  MySQL を使用してポート 3306 を開いたり変更したり、Ubuntu/Linux 環境でアクセス許可を開く

>>:  Msyql トランザクション分離について知っておくべきこと

推薦する

分散監視システムにおけるZabbixのアクティブ、パッシブ、Web監視のプロセスの詳細な説明

前回の記事では、Zabbix のネットワーク検出機能について学習し、アクションと組み合わせてホストの...

Dockerはnextcloudを使用してプライベートBaiduクラウドディスクを構築します

突然、ドキュメントの保存と共同作業のためのプライベート サービスを構築する必要がありました。多くの場...

3分でUbuntu 16.04を初期化し、Java、Maven、Docker環境をデプロイする

Fast-Linux プロジェクト アドレス: https://gitee.com/uitc/Fas...

Ubuntu 20.04 では、隠し録音ノイズ低減機能が有効になります (推奨)

最近、 Ubuntu 20.04でkazamを使用して録音しているときに、問題が見つかりました。シス...

VMwareがwin10ホームバージョンに64ビットオペレーティングシステムをインストールできない問題を解決します

問題の説明VMware Workstationが新しい仮想マシンを作成し、64ビットオペレーティング...

CSS チュートリアル: CSS 属性メディア タイプ

スタイルシートの最も重要な機能の 1 つは、ページ、画面、電子シンセサイザーなどの複数のメディアに適...

Linux 環境に nginx をインストールするチュートリアル

目次1. 必要な環境をインストールする //gccをインストールする yum で gcc-c++ を...

Linux に起動方法を追加する (サービス/スクリプト)

システムの起動時に読み込む必要がある設定ファイル/etc/profile、/root/.bash_p...

Vue ページ印刷で自動ページングを実装する 2 つの方法

この記事では、ページ印刷の自動ページングを実現するためのVueの具体的なコードを例として紹介します。...

Pythonで書かれたWebアプリケーションをDockerでデプロイする実践

目次1. Dockerをインストールする2. コードを書く3. Dockerfileを書く4. 画像...

HTML+CSS3+JSで実装されたドロップダウンメニュー

成果を達成する html <div class="コンテナ"> &l...

JavaScript の知識: コンストラクタも関数である

目次1. コンストラクタの定義と呼び出し2. 新しいキーワードの目的3. コンストラクタの問題: メ...

HTML面接の質問の要約

1. doctypeの役割、厳密モードと混合モードの違い、そしてその重要性1. 構文形式: <...

Webデザインチュートリアル(8):Webページの階層と空間デザイン

<br />前回の記事:Webデザイン講座(7):Webページ制作の効率化1:必要な小言...

CSS スタイルのリセットとクリア (異なるブラウザで同じ効果を表示するため)

異なるブラウザ間でページの表示を一致させるためには、フロントエンド開発において CSS スタイルのク...