JavaScript のスコープとコンテキストは、JavaScript がもたらす柔軟性のおかげで、この言語に固有です。各関数には異なる変数コンテキストとスコープがあります。これらの概念は、JavaScript のいくつかの強力なデザイン パターンの基礎となっています。ただし、これは開発者に大きな混乱をもたらします。以下では、JavaScript におけるコンテキストとスコープの違いと、さまざまなデザイン パターンでそれらがどのように使用されるかを包括的に明らかにします。
コンテキストとスコープ
まず明確にする必要があるのは、コンテキストとスコープは異なる概念であるということです。長年にわたり、多くの開発者がこれら 2 つの用語を混同し、一方をもう一方と誤って説明していることに気づきました。公平を期すために言うと、これらの用語は非常にわかりにくいものになっています。
すべての関数呼び出しにはスコープとそれに関連付けられたコンテキストがあります。基本的に、スコープは関数ベースであり、コンテキストはオブジェクトベースです。言い換えれば、スコープは各関数呼び出しでの変数へのアクセスに関連しており、各呼び出しは独立しています。コンテキストは常にキーワード this の値であり、現在の実行可能コードを呼び出すオブジェクトへの参照です。
変数スコープ
変数はローカル スコープまたはグローバル スコープで定義でき、その結果、異なるスコープからランタイム変数にアクセスできるようになります。グローバル変数は関数本体の外側で宣言する必要があり、実行中のプロセス全体に存在し、どのスコープでもアクセスおよび変更できます。ローカル変数は関数本体内でのみ定義され、関数呼び出しごとに異なるスコープを持ちます。このトピックは呼び出し内のみの値の割り当て、評価、操作であり、スコープ外の値にはアクセスできません。
現在、JavaScript はブロックレベルのスコープをサポートしていません。ブロックレベルのスコープとは、if ステートメント、switch ステートメント、loop ステートメントなどのステートメント ブロック内の変数の定義を指します。これは、ステートメント ブロックの外部では変数にアクセスできないことを意味します。現在、ステートメント ブロック内で定義されている変数は、ステートメント ブロックの外部からアクセスできます。ただし、let キーワードが ES6 仕様に正式に追加されたため、これは間もなく変更されます。ローカル変数をブロックレベルのスコープとして宣言するには、var キーワードの代わりにこれを使用します。
「この」コンテキスト
通常、コンテキストは関数の呼び出し方法によって異なります。関数がオブジェクトのメソッドとして呼び出される場合、これはメソッドが呼び出されるオブジェクトに設定されます。
次のようにコードをコピーします。
var オブジェクト = {
foo:関数(){
アラート(この === オブジェクト);
}
};
object.foo(); // true
new 演算子を使用してオブジェクトのインスタンスを作成する関数を呼び出す場合にも、同じ原則が適用されます。この方法で呼び出すと、this の値は新しく作成されたインスタンスに設定されます。
次のようにコードをコピーします。
関数 foo(){
アラート(これ);
}
foo() // ウィンドウ
new foo() // foo
アンバインド関数を呼び出すと、これはデフォルトでグローバル コンテキストまたはウィンドウ オブジェクト (ブラウザー内の場合) に設定されます。ただし、関数が strict モード (「use strict」) で実行される場合、この値はデフォルトで undefine に設定されます。
実行コンテキストとスコープチェーン
JavaScript はシングルスレッド言語です。つまり、ブラウザーでは一度に 1 つのことしか実行できません。 JavaScript インタープリターが最初にコードを実行するとき、最初はデフォルトでグローバル コンテキストが使用されます。関数を呼び出すたびに、新しい実行コンテキストが作成されます。
ここでよく混乱が生じますが、ここでの「実行コンテキスト」という用語は、上で説明したコンテキストではなく、スコープを意味します。これは不適切な命名ですが、この用語は ECMAScript 仕様によって定義されており、それに従う以外に選択肢はありません。
新しい実行コンテキストが作成されるたびに、スコープ チェーンの先頭に追加され、実行スタックまたは呼び出しスタックになります。ブラウザは常に、スコープ チェーンの最上位にある現在の実行コンテキストで実行されます。完了すると、それ (現在の実行コンテキスト) がスタックの最上位から削除され、制御が前の実行コンテキストに戻ります。例えば:
次のようにコードをコピーします。
関数 first(){
2番目();
関数 Second(){
三番目();
関数 third(){
4番目();
関数 four(){
//何かをする
}
}
}
}
初め();
前のコードを実行すると、ネストされた関数が上から下に 4 番目の関数まで実行されます。このとき、スコープ チェーンは上から下に 4 番目、3 番目、2 番目、1 番目、グローバルになります。 4 番目の関数は、グローバル変数と、独自の変数と同様に、1 番目、2 番目、および 3 番目の関数で定義された変数にアクセスできます。 4 番目の関数の実行が完了すると、4 番目のコンテキストがスコープ チェーンの先頭から削除され、実行は 3 番目の関数に戻ります。このプロセスは、すべてのコードの実行が完了するまで継続されます。
異なる実行コンテキスト間の変数名の競合は、ローカルからグローバルまでスコープ チェーンを登ることによって解決されます。これは、同じ名前のローカル変数がスコープ チェーン内でより高い優先順位を持つことを意味します。
簡単に言えば、関数実行コンテキストで変数にアクセスしようとするたびに、検索プロセスは常に独自の変数オブジェクトから開始されます。探している変数が独自の変数オブジェクト内に見つからない場合は、スコープ チェーンの検索を続けます。スコープ チェーンをたどり、各実行コンテキスト変数オブジェクトを調べて、変数名に一致する値を見つけます。
閉鎖
クロージャは、ネストされた関数がその定義 (スコープ) の外でアクセスされたときに形成され、外側の関数が戻った後に実行できるようにします。これ (クロージャ) は、外部関数のローカル変数、引数、および関数宣言への (内部関数内での) アクセスを維持します。カプセル化により、実行コンテキストを外側のスコープから隠して保護しながら、さらなる操作を実行できるパブリック インターフェイスを公開できます。簡単な例は次のようになります。
次のようにコードをコピーします。
関数 foo(){
var local = 'プライベート変数';
戻り関数 bar(){
ローカルに戻ります。
}
}
var getLocalVariable = foo();
getLocalVariable() // プライベート変数
クロージャの最も一般的なタイプの 1 つは、よく知られたモジュール パターンです。これにより、パブリック、プライベート、特権メンバーをモックすることができます。
次のようにコードをコピーします。
var モジュール = (関数(){
var privateProperty = 'foo';
関数 privateMethod(args){
//何かをする
}
戻る {
パブリックプロパティ: "",
publicメソッド: function(args){
//何かをする
}、
特権メソッド: function(args){
privateMethod(args);
}
}
})();
モジュールは実際にはシングルトンに似ており、最後に一対のかっこを追加し、インタープリターがモジュールの解釈を終了した直後にモジュールを実行します (関数をすぐに実行します)。クロージャ実行コンテキストで使用できる外部メンバーは、返されたオブジェクトのパブリック メソッドとプロパティ (Module.publicMethod など) だけです。ただし、実行コンテキストは保護され (クロージャ)、変数との対話はパブリック メソッドを通じて行われるため、すべてのプライベート プロパティとメソッドはプログラムのライフ サイクルを通じて存在します。
別のタイプのクロージャは、即時呼び出し関数式 IIFE と呼ばれます。これは、ウィンドウ コンテキストで自己呼び出しされる匿名関数にすぎません。
次のようにコードをコピーします。
関数(ウィンドウ){
var a = 'foo'、b = 'bar';
関数プライベート(){
//何かをする
}
ウィンドウ.モジュール = {
パブリック: function(){
//何かをする
}
};
})(これ);
この式は、グローバル名前空間を保護するのに非常に役立ちます。関数本体内で宣言されたすべての変数はローカル変数であり、クロージャを通じてランタイム環境全体にわたって持続します。ソース コードをカプセル化するこの方法は、プログラムとフレームワークの両方で非常に一般的であり、通常は外部と対話するための単一のグローバル インターフェイスを公開します。
電話して応募する
これら 2 つの単純なメソッドはすべての関数に組み込まれており、カスタム コンテキストで関数を実行できます。 call 関数にはパラメータ リストが必要ですが、apply 関数ではパラメータを配列として渡すことができます。
次のようにコードをコピーします。
関数 user(最初、最後、年齢){
//何かをする
}
user.call(window, 'ジョン', 'ドウ', 30);
user.apply(window, ['John', 'Doe', 30]);
実行結果は同じで、ユーザー関数がウィンドウ コンテキストで呼び出され、同じ 3 つのパラメーターが提供されます。
ECMAScript 5 (ES5) では、コンテキストを制御する Function.prototype.bind メソッドが導入されました。このメソッドは、関数の呼び出し方法に関係なく、bind メソッドの最初の引数に永続的にバインドされる新しい関数を返します。クロージャを通じて関数のコンテキストを修正します。これをサポートしていないブラウザの場合の解決策は次のとおりです。
次のようにコードをコピーします。
if(!(Function.prototype の 'bind')){
Function.prototype.bind = function(){
var fn = this、context = argument[0]、args = Array.prototype.slice.call(arguments, 1);
戻り関数(){
fn.apply(context, args) を返します。
}
}
}
これは、オブジェクト指向やイベント処理などのコンテキスト損失でよく使用されます。これが必要なのは、ノードの addEventListener メソッドがイベント ハンドラーがバインドされているノードとして関数の実行コンテキストを常に維持するためであり、これが重要です。ただし、高度なオブジェクト指向手法を使用し、コールバック関数のコンテキストをメソッドのインスタンスとして維持する必要がある場合は、コンテキストを手動で調整する必要があります。これはバインドによってもたらされる利便性です。
次のようにコードをコピーします。
関数 MyClass(){
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);
}
MyClass.prototype.onClick = function(e){
//何かをする
};
バインド関数のソース コードを振り返ると、Array のメソッドを呼び出す次の比較的単純なコード行に気づくかもしれません。
次のようにコードをコピーします。
Array.prototype.slice.call(arguments, 1);
興味深いことに、ここで引数オブジェクトは実際には配列ではないことに注意することが重要ですが、多くの場合、ノードリスト (document.getElementsByTagName() メソッドによって返される結果) とよく似た、配列のようなオブジェクトとして説明されます。これらには長さ属性が含まれており、値にインデックスを付けることができますが、スライスやプッシュなどのネイティブの配列メソッドをサポートしていないため、配列ではありません。ただし、配列と同様に動作するため、配列メソッドを呼び出してハイジャックすることができます。配列のようなコンテキストで配列メソッドを実行する場合は、上記の例に従ってください。
他のオブジェクトのメソッドを呼び出すこの手法は、JavaScript で古典的な継承 (クラス継承) をエミュレートするときにオブジェクト指向にも適用されます。
次のようにコードをコピーします。
MyClass.prototype.init = function(){
// "MyClass" インスタンスのコンテキストでスーパークラスの init メソッドを呼び出します
MySuperClass.prototype.init.apply(this, argument);
}
サブクラス (MyClass) のインスタンスでスーパークラス (MySuperClass) のメソッドを呼び出すことで、この強力な設計パターンを再現できます。
結論は
最新の JavaScript ではスコープとコンテキストが重要かつ基本的な役割を果たすため、高度なデザイン パターンを学習する前にこれらの概念を理解することが非常に重要です。クロージャ、オブジェクト指向、継承、またはさまざまなネイティブ実装について話す場合でも、コンテキストとスコープが重要な役割を果たします。 JavaScript 言語をマスターし、そのコンポーネントを深く理解することが目標の場合、スコープとコンテキストを出発点にする必要があります。
翻訳者の補足
作成者によって実装されたバインド関数は不完全です。バインドによって返される関数を呼び出すときにパラメータを渡すことができません。次のコードはこの問題を修正します。
次のようにコードをコピーします。
if(!(Function.prototype の 'bind')){
Function.prototype.bind = function(){
var fn = this、context = argument[0]、args = Array.prototype.slice.call(arguments, 1);
戻り関数(){
return fn.apply(context, args.concat(arguments));//修正
}
}
}