クロージャ-この言葉を聞いたことがあるJavaScript開発者の方は多いのではないでしょうか。 私がJavaScriptの世界に入り始めた頃、クロージャによく出会いました。 クロージャは、JavaScriptの中でも最も重要で興味深い概念の1つだと思います。
面白いと思わないのですか? コンセプトを理解していないときによく起こることですが、それは面白いと思わないということです。 (あなたに起こるかどうかはわかりませんが、私の場合はこれです)。
というわけで、この記事ではクロージャを面白くしてみようと思います。
クロージャの世界に入る前に、まず語彙のスコープについて理解しましょう。 すでにご存知の方は、次の部分を読み飛ばしてください。
レキシカル スコープ
皆さんは、ローカル スコープとグローバル スコープは知っているけど、レキシカル スコープって一体何だろうと思うかもしれません。 私もこの言葉を聞いたとき、同じように反応しました。 でも、大丈夫です。 安心してください。
他の2つのスコープと同様にシンプルです。
function greetCustomer() { var customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); // Hi! anchal } greetingMsg();}
上の出力から、内側の関数が外側の関数の変数にアクセスできることがわかります。 これがレキシカル・スコープで、変数のスコープと値は、その変数が定義/作成された場所(つまりコード内の位置)によって決定されます。 分かりましたか?
最後の部分は、あなたを混乱させたかもしれません。 では、もう少し詳しくご説明しましょう。 レキシカル・スコープはスタティック・スコープとも呼ばれていることをご存知ですか? そう、それが別名なのです。
プログラミング言語の中には、動的なスコーピングもあります。 なぜ動的スコーピングの話をしたかというと
いくつかの例を見てみましょう。
function greetingMsg() { console.log(customerName);// ReferenceError: customerName is not defined}function greetCustomer() { var customerName = "anchal"; greetingMsg();}greetCustomer();
この出力に同意しますか? はい、参照エラーが出ます。
別の例を見てみましょう:
function addNumbers(number1) { console.log(number1 + number2);}function addNumbersGenerate() { var number2 = 10; addNumbers(number2);}addNumbersGenerate();
上記の出力は、動的にスコープされた言語では20になります。 辞書的なスコープをサポートする言語では、referenceError: number2 is not defined
となります。
動的スコープでは、検索はまずローカル関数で行われ、次にそのローカル関数を呼び出した関数の中に入ります。 次に、その関数を呼び出した関数の中を検索し、というようにコールスタックをさかのぼっていきます。
「ダイナミック」とは、その名の通り、変化を意味します。 変数のスコープや値は、その関数がどこから呼び出されたかによって異なる可能性があります。 変数の意味は実行時に変わることがあります。
ダイナミック スコーピングの概要はお分かりになりましたか?
レキシカル・スコープでは、検索はまずローカル関数で行われ、次にその関数が定義されている内部の関数を検索します。
レキシカル・スコープでは、検索はまずローカル関数で行われ、次にその関数が定義されている内部の関数に入り、次にその関数が定義されている内部の関数を検索する、という具合です。
つまり、レキシカル・スコープまたはスタティック・スコープとは、変数のスコープと値が、その変数が定義された場所から決定されるということです。 変化しないのです。
もう一度、上の例を見て、自分で出力を考えてみましょう。 ひとつだけひねりを加えて、number2
を先頭に宣言してみましょう。
正しくは、語彙的にスコープされた言語では12です。 これは、まず addNumbers
number2
変数を取得するので、出力は12になります。
なぜここで語彙のスコープにこれほど時間をかけたのか、不思議に思うかもしれません。 これはクロージャの記事であって、レキシカル スコーピングの記事ではありません。 しかし、レキシカル スコーピングについて知らなければ、クロージャを理解することはできません。
なぜでしょうか? その答えは、クロージャの定義を見たときにわかるでしょう。
クロージャとは何ですか?
クロージャの定義を見てみましょう:
クロージャは、内側の関数が外側の関数の変数や引数にアクセスするときに作られます。 内側の関数がアクセスできるのは –
1.
1.自分自身の変数
2.外部関数の変数や引数
3.グローバル変数
ちょっと待って!これはクロージャの定義なのか、レキシカルスコーピングの定義なのか? どちらの定義も同じように見えます。 どう違うのでしょうか?
さて、だからこそ私は上で語彙的スコーピングを定義したわけです。 クロージャはレキシカル/スタティック スコーピングと関係があるからです。
クロージャがどのように違うのか、その別の定義をもう一度見てみましょう。
または
内部の関数は、親関数がすでに実行された後でも、その親のスコープにアクセスすることができます。 まだ要点がつかめていなくても心配ありません。 理解を深めるための例があります。 レキシカル スコーピングの最初の例を修正してみましょう:
function greetCustomer() { const customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); } return greetingMsg;}const callGreetCustomer = greetCustomer();callGreetCustomer(); // output – Hi! anchal
このコードの違いは、内部の関数を返して後で実行していることです。 プログラミング言語の中には、関数の実行中にローカル変数が存在するものがあります。 しかし、関数が実行されると、それらのローカル変数は存在せず、アクセスできなくなります。
しかし、ここでは事情が異なります。 親関数が実行された後も、内側の関数(戻り値の関数)は、親関数の変数にアクセスすることができます。 はい、お察しの通りです。 クロージャがその理由です。
内側の関数は、親関数が実行されているときにその辞書的なスコープを保持しているので、後でその内側の関数がそれらの変数にアクセスすることができます。
もっとよく知るために、コンソールの
callGreetCustomer
のプロパティのリストを調べてみましょう。console.dir(callGreetCustomer);
上記の画像から、内部の関数がどのようにしてプロパティを保持しているかがわかります。 内側の関数は、
greetCustomer()
customerName
)を保持していることがわかります。この例で、上記のクロージャの定義をよりよく理解していただけたでしょうか。 また、クロージャが少し楽しくなったのではないでしょうか。
では、次はどうしましょう?
クロージャの動作例
function counter() { let count = 0; return function() { return count++; };}const countValue = counter();countValue(); // 0countValue(); // 1countValue(); // 2
countValue
を呼び出すたびに、変数countの値が1ずつ増えていきます。いや、それは間違いです。 クロージャは変数の参照を保存します。 そのため、値を更新すると、2回目、3回目の呼び出しに反映され、その後もクロージャが参照を保存するためです。
少しは理解できましたか? 別の例を見てみましょう。
function counter() { let count = 0; return function () { return count++; };}const countValue1 = counter();const countValue2 = counter();countValue1(); // 0countValue1(); // 1countValue2(); // 0countValue2(); // 1
あなたが正しい答えを当てたことを願っています。 もしそうでなければ、その理由は次のとおりです。
countValue1
countValue2
dir()
]
の値をチェックすることができます。3つ目の例を見てみましょう。
const addNumberCall = addNumber(7);addNumberCall(8) // 15addNumberCall(6) // 13
シンプルですね。 新たに得たクロージャの知識を使ってみましょう:
function addNumber(number1) { return function (number2) { return number1 + number2; };}
さて、少しトリッキーな例を見てみましょう:
function countTheNumber() { var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = function () { return x; }; } return arrToStore;}const callInnerFunctions = countTheNumber();callInnerFunctions() // 9callInnerFunctions() // 9
関数を格納するすべての配列要素からは、9という出力が得られますが、正解でしたか? そうだといいのですが、それでも理由をお話ししましょう。 これは、クロージャの動作によるものです。
クロージャは、値ではなく、参照を格納します。 ループが最初に実行されたとき、xの値は0で、2回目にはxは1になり、という具合です。 クロージャは参照を格納しているので、ループが実行されるたびにxの値が変化し、最終的にxの値は9になります。 つまり、
callInnerFunctions()
では9という出力が得られます。でも、0から8までの出力が欲しい場合はどうすればいいのでしょうか? 簡単です。 クロージャを使います。
以下のソリューションを見る前に考えてみてください。
function callTheNumber() { function getAllNumbers(number) { return function() { return number; }; } var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = getAllNumbers(x); } return arrToStore;}const callInnerFunctions = callTheNumber();console.log(callInnerFunctions()); // 0console.log(callInnerFunctions()); // 1
ここでは、各反復に対して個別のスコープを作成しています。
console.dir(arrToStore)
]
内のxの値を異なる配列要素で確認することができます。以上です。
私の他の記事を読みたい方は、こちらのプロフィールをご覧ください。