「クロージャ」とは何ですか?
-
09-06-2019 - |
質問
カリー化について質問したところ、クロージャについて言及されました。閉鎖とは何ですか?カレーとどう関係するの?
解決
変数のスコープ
ローカル変数を宣言すると、その変数にはスコープが設定されます。一般に、ローカル変数は、それを宣言したブロックまたは関数内にのみ存在します。
function() {
var a = 1;
console.log(a); // works
}
console.log(a); // fails
ローカル変数にアクセスしようとすると、ほとんどの言語は現在のスコープでその変数を検索し、ルート スコープに到達するまで親スコープをたどっていきます。
var a = 1;
function() {
console.log(a); // works
}
console.log(a); // works
ブロックまたは関数が終了すると、そのローカル変数は不要になり、通常はメモリから消去されます。
これが私たちが通常物事がうまくいくことを期待する方法です。
クロージャは永続的なローカル変数スコープです
クロージャは、コードの実行がそのブロックの外に移動した後でもローカル変数を保持する永続スコープです。クロージャをサポートする言語 (JavaScript、Swift、Ruby など) では、参照を保持していれば、変数が宣言されたブロックの実行が終了した後でも、スコープ (その親スコープを含む) への参照を保持できます。どこかのブロックまたは関数に。
スコープ オブジェクトとそのすべてのローカル変数は関数に関連付けられており、その関数が存続する限り存続します。
これにより、関数の移植性が得られます。関数が最初に定義されたときにスコープ内にあった変数は、完全に異なるコンテキストで関数を呼び出した場合でも、後で関数を呼び出したときにもスコープ内にあることが期待できます。
例えば
この点を説明するための JavaScript の非常に簡単な例を次に示します。
outer = function() {
var a = 1;
var inner = function() {
console.log(a);
}
return inner; // this returns a function
}
var fnc = outer(); // execute outer to get inner
fnc();
ここでは関数内に関数を定義しました。内部関数は、外部関数のすべてのローカル変数にアクセスできるようになります。 a
. 。変数 a
内部関数のスコープ内にあります。
通常、関数が終了すると、そのローカル変数はすべて吹き飛ばされます。ただし、内部関数を返して変数に代入すると、 fnc
それが後も持続するように outer
終了しました、 スコープ内にあったすべての変数 inner
定義された場合も永続化されます. 。変数 a
閉じられています -- 閉じられている内にあります。
変数に注意してください。 a
完全にプライベートです fnc
. 。これは、JavaScript などの関数型プログラミング言語でプライベート変数を作成する方法です。
ご想像のとおり、電話をかけると fnc()
の値を出力します a
, 、つまり「1」です。
クロージャのない言語では、変数は a
関数の実行時にガベージ コレクションが行われて破棄されるはずです outer
出ました。fnc を呼び出すとエラーが発生します。 a
もはや存在しない。
JavaScript では、変数 a
関数が最初に宣言されたときに変数スコープが作成され、関数が存在し続ける限り存続するため、永続化されます。
a
の範囲に属します outer
. 。の範囲 inner
のスコープへの親ポインタがあります outer
. fnc
を指す変数です inner
. a
限り持続する fnc
持続します。 a
閉鎖の範囲内にあります。
他のヒント
(JavaScript で) 例を示します。
function makeCounter () {
var count = 0;
return function () {
count += 1;
return count;
}
}
var x = makeCounter();
x(); returns 1
x(); returns 2
...etc...
この関数 makeCounter が行うことは、呼び出されるたびに 1 ずつカウントアップする関数 (x と呼びます) を返すことです。x にパラメータを提供していないため、x は何らかの方法でカウントを記憶する必要があります。いわゆる字句スコープに基づいて、どこでそれを見つけるべきかを知っています。値を見つけるには、定義されている場所を探す必要があります。この「隠された」値はクロージャと呼ばれるものです。
これが私のカリー化の例です。
function add (a) {
return function (b) {
return a + b;
}
}
var add3 = add(3);
add3(4); returns 7
見てわかるのは、パラメータ a (3) を指定して add を呼び出すと、その値が、add3 として定義している返された関数のクロージャに含まれていることです。そうすることで、add3 を呼び出すと、加算を実行するための値をどこで見つければよいかがわかります。
カイルの答え かなり良いです。唯一の追加の明確化は、クロージャは基本的にラムダ関数が作成された時点のスタックのスナップショットであるということだと思います。その後、関数が再実行されると、スタックは関数を実行する前の状態に復元されます。したがって、カイルが述べているように、その隠された価値 (count
) は、ラムダ関数の実行時に使用できます。
まず第一に、ここにいるほとんどの人が言うことに反して、 閉鎖は ない 機能!だから何 は それ?
それは セット 関数の「周囲のコンテキスト」(関数の「周囲のコンテキスト」として知られる) で定義されたシンボルの 環境) これにより、それが CLOSED 式 (つまり、すべてのシンボルが定義され、値を持つため、評価できる式) になります。
たとえば、JavaScript 関数がある場合:
function closed(x) {
return x + 3;
}
それは 閉じた式 なぜなら、その中に出現するすべてのシンボルがその中で定義されている(その意味が明確である)ため、それを評価できるからです。つまり、それは、 自己完結型.
しかし、次のような関数がある場合:
function open(x) {
return x*y + 3;
}
それは オープンな表現 なぜなら、その中に定義されていないシンボルが含まれているからです。つまり、 y
. 。この関数を見ても、何が何だかわかりません。 y
は何か、そしてそれが何を意味するのか、その値がわからないので、この式を評価できません。つまり、内容がわかるまでこの関数を呼び出すことはできません y
という意味が込められているはずです。これ y
と呼ばれます 自由変数.
これ y
定義を要求しますが、この定義は関数の一部ではありません。関数は別の場所、「周囲のコンテキスト」(コンテキストとも呼ばれます) で定義されます。 環境)。少なくともそれが私たちが望んでいることです:P
たとえば、次のようにグローバルに定義できます。
var y = 7;
function open(x) {
return x*y + 3;
}
または、それをラップする関数で定義することもできます。
var global = 2;
function wrapper(y) {
var w = "unused";
return function(x) {
return x*y + 3;
}
}
式内の自由変数に意味を与える環境の部分は、 閉鎖. 。回転するのでこのように呼ばれます。 開ける 式を 閉まっている 1 つは、これらの欠落している定義をすべてのオブジェクトに提供することによって、 自由変数, 、それを評価できるようにしました。
上の例では、内部関数 (必要がないため名前を付けませんでした) は オープンな表現 なぜなら変数は y
その中にあります 無料 – その定義は関数の外側、つまりそれをラップする関数内にあります。の 環境 その無名関数の場合、変数のセットは次のようになります。
{
global: 2,
w: "unused",
y: [whatever has been passed to that wrapper function as its parameter `y`]
}
さて、 閉鎖 それはこの環境の一部です 閉まる 内部関数のすべての定義を指定することにより、 自由変数. 。私たちの場合、内部関数内の唯一の自由変数は y
, したがって、その関数のクロージャはその環境のこのサブセットになります。
{
y: [whatever has been passed to that wrapper function as its parameter `y`]
}
環境内で定義されている他の 2 つのシンボルは次のとおりです。 ない の一部 閉鎖 その関数を実行する必要がないためです。それらは必要ありません 近い それ。
その背後にある理論の詳細については、こちらをご覧ください。https://stackoverflow.com/a/36878651/434562
上記の例では、ラッパー関数がその内部関数を値として返すことに注意してください。この関数を呼び出す瞬間は、関数が定義された (または作成された) 瞬間から時間的に遠い場合があります。特に、そのラッピング関数はもう実行されておらず、コールスタック上にあったパラメータももう存在しません:P これは問題になります。なぜなら、内部関数は y
呼ばれたらそこにいるように!言い換えれば、クロージャから何らかの方法で変数を取得する必要があります。 長生きする ラッパー関数を使用し、必要なときにそこに存在します。したがって、内部関数は次のことを行う必要があります。 スナップショット これらの変数をクロージャにして、後で使用できるように安全な場所に保存します。(コールスタックの外側のどこか。)
これが、人々がこの用語をよく混同する理由です 閉鎖 使用する外部変数のスナップショットを作成できる特別なタイプの関数、または後で使用するためにこれらの変数を保存するために使用されるデータ構造です。しかし、今ではそれらが事実であることを理解していただければ幸いです ない クロージャー自体 – それらは単なる手段です 埋め込む プログラミング言語のクロージャ、または必要なときに関数のクロージャからの変数をそこに存在できるようにする言語メカニズム。クロージャに関しては多くの誤解があり、それが (不必要に) この主題を実際よりもはるかに混乱させ、複雑にしています。
クロージャは、別の関数の状態を参照できる関数です。たとえば、Python では、これはクロージャ「inner」を使用します。
def outer (a):
b = "variable in outer()"
def inner (c):
print a, b, c
return inner
# Now the return value from outer() can be saved for later
func = outer ("test")
func (1) # prints "test variable in outer() 1
クロージャの理解を容易にするために、クロージャが手続き型言語でどのように実装されるかを検討すると役立つ場合があります。この説明は、Scheme でのクロージャの単純化された実装に従います。
まず、名前空間の概念を導入する必要があります。Scheme インタプリタにコマンドを入力すると、式内のさまざまなシンボルが評価され、その値が取得される必要があります。例:
(define x 3)
(define y 4)
(+ x y) returns 7
定義式は、x のスポットに値 3 を格納し、y のスポットに値 4 を格納します。次に、(+ x y) を呼び出すと、インタープリターは名前空間内の値を検索し、操作を実行して 7 を返すことができます。
ただし、Scheme には、シンボルの値を一時的にオーバーライドできる式があります。以下に例を示します。
(define x 3)
(define y 4)
(let ((x 5))
(+ x y)) returns 9
x returns 3
let キーワードは、x を値 5 とする新しい名前空間を導入します。y が 4 であることがわかり、合計が 9 に返されることがわかります。また、式が終了すると x が 3 に戻ることもわかります。この意味で、x はローカル値によって一時的にマスクされています。
手続き型言語とオブジェクト指向言語には同様の概念があります。関数内でグローバル変数と同じ名前の変数を宣言すると、常に同じ効果が得られます。
これをどのように実装すればよいでしょうか?簡単な方法はリンク リストを使用することです。先頭には新しい値が含まれ、末尾には古い名前空間が含まれます。シンボルを検索する必要がある場合は、先頭から始めて末尾に向かって作業します。
ここで、当面はファーストクラス関数の実装に進みましょう。多かれ少なかれ、関数は、関数が呼び出されて戻り値が得られるときに実行される一連の命令です。関数を読み込むと、これらの命令をバックグラウンドで保存し、関数が呼び出されたときに実行できます。
(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns ?
x を 3 と定義し、plus-x をそのパラメータ y に x の値を加えたものと定義します。最後に、x が新しい x でマスクされている環境で plus-x を呼び出します。この x の値は 5 です。関数 plus-x の演算 (+ x y) を単に保存すると、x が 5 というコンテキストにあるため、返される結果は 9 になります。これは動的スコープと呼ばれるものです。
ただし、Scheme、Common Lisp、および他の多くの言語には、いわゆる字句スコープがあり、操作 (+ x y) を保存することに加えて、その特定の時点での名前空間も保存します。そうすることで、値を検索するときに、このコンテキストでは x が実際には 3 であることがわかります。これで閉会です。
(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns 7
要約すると、リンク リストを使用して、関数定義時の名前空間の状態を保存することができます。これにより、外側のスコープから変数にアクセスできるようになり、残りの部分に影響を与えることなく変数をローカルにマスクする機能が提供されます。プログラム。
以下に、クロージャーがなぜひどいのかを示す実際の例を示します。これは私の Javascript コードからそのまま抜粋したものです。説明しましょう。
Function.prototype.delay = function(ms /*[, arg...]*/) {
var fn = this,
args = Array.prototype.slice.call(arguments, 1);
return window.setTimeout(function() {
return fn.apply(fn, args);
}, ms);
};
そして、それをどのように使用するかは次のとおりです。
var startPlayback = function(track) {
Player.play(track);
};
startPlayback(someTrack);
ここで、このコード スニペットの実行後、たとえば 5 秒後などに再生を遅延して開始したいと想像してください。まあ、それは簡単です delay
そしてそれは終わりです:
startPlayback.delay(5000, someTrack);
// Keep going, do other things
電話をかけるとき delay
と 5000
ミリ秒経過すると、最初のスニペットが実行され、渡された引数をそのクロージャに保存します。そして5秒後、 setTimeout
コールバックが発生しても、クロージャはそれらの変数を保持しているため、元のパラメータを使用して元の関数を呼び出すことができます。
これはカリー化、つまり機能装飾の一種です。
クロージャがなければ、何らかの方法で関数の外部でこれらの変数の状態を維持する必要があるため、論理的に内部に属するコードが関数の外部に散在します。クロージャを使用すると、コードの品質と読みやすさが大幅に向上します。
先生
クロージャは関数とそのスコープであり、変数に割り当てられます (または変数として使用されます)。したがって、名前クロージャは次のようになります。スコープと関数は囲まれており、他のエンティティと同様に使用されます。
Wikipedia スタイルの詳細な説明
第一級関数を備えた言語で字句スコープの名前バインディングを実装するための手法。
それはどういう意味ですか?いくつかの定義を見てみましょう。
この例を使用して、クロージャとその他の関連定義について説明します。
function startAt(x) {
return function (y) {
return x + y;
}
}
var closure1 = startAt(1);
var closure2 = startAt(5);
console.log(closure1(3)); // 4 (x == 1, y == 3)
console.log(closure2(3)); // 8 (x == 5, y == 3)
最高級の関数
基本的にそれは意味します 他のエンティティと同じように関数を使用できます. 。それらを変更したり、引数として渡したり、関数から返したり、変数に代入したりできます。技術的に言えば、それらは 第一級国民, したがって、名前は次のとおりです。最高級の関数。
上の例では、 startAt
(を返します匿名) 関数が割り当てられる関数 closure1
そして closure2
. 。ご覧のとおり、JavaScript は関数を他のエンティティ (第一級市民) と同じように扱います。
名前のバインディング
名前のバインディング それは見つけることです 変数のデータ (識別子) 参考文献. 。スコープはバインディングの解決方法を決定するものであるため、ここでは非常に重要です。
上の例では:
- 内部の匿名関数のスコープでは、
y
に縛られています3
. - で
startAt
のスコープ、x
に縛られています1
または5
(閉店状況により異なります)。
匿名関数のスコープ内では、 x
はどの値にもバインドされていないため、上位 (startAt
の)スコープ。
語彙のスコープ指定
として ウィキペディアによると, 、 スコープ:
バインディングが有効なコンピューター プログラムの領域は次のとおりです。 ここで、名前はエンティティを参照するために使用できます。.
次の 2 つのテクニックがあります。
- 字句 (静的) スコープ:変数の定義は、その変数を含むブロックまたは関数を検索することによって解決され、それが失敗した場合は、外側の含まれるブロックを検索する、というように続きます。
- 動的スコープ:呼び出し元の関数が検索され、次にその呼び出し元の関数が検索され、というように呼び出しスタックが上に進みます。
さらに詳しい説明については、 この質問をチェックしてください そして ウィキペディアを見てください.
上の例では、JavaScript が字句的にスコープされていることがわかります。 x
が解決されると、バインディングは上位 (startAt
の) スコープ、ソース コードに基づく (x を検索する匿名関数が内部で定義されている) startAt
) コールスタック、つまり関数が呼び出された方法 (スコープ) には基づいていません。
ラッピング(締め)
この例では、呼び出すと、 startAt
, 、に割り当てられる(ファーストクラスの)関数を返します。 closure1
そして closure2
したがって、変数が渡されるため、クロージャが作成されます。 1
そして 5
以内に保存されます startAt
のスコープ。返された匿名関数で囲まれます。この匿名関数を呼び出すと、 closure1
そして closure2
同じ引数(3
)、の値 y
(それがその関数のパラメータであるため) すぐに見つかりますが、 x
は無名関数のスコープにバインドされていないため、解決は (語彙的に) 上位の関数スコープ (クロージャに保存されたもの) で続行されます。 x
いずれかに結合していることがわかる 1
または 5
. 。これで合計に関するすべてがわかったので、結果を返して出力できるようになりました。
ここで、JavaScript の基本的な部分であるクロージャとその動作について理解する必要があります。
カリー化
ああ、あなたはまた何を学びましたか カリー化 についてです:複数のパラメータを持つ 1 つの関数を使用する代わりに、関数 (クロージャ) を使用して操作の各引数を渡します。
自由変数を含まない関数は純粋関数と呼ばれます。
1 つ以上の自由変数を含む関数はクロージャと呼ばれます。
var pure = function pure(x){
return x
// only own environment is used
}
var foo = "bar"
var closure = function closure(){
return foo
// foo is a free variable from the outer environment
}
通常の状況では、変数はスコープ ルールによってバインドされます。ローカル変数は、定義された関数内でのみ機能します。クロージャは、便宜上、このルールを一時的に破る方法です。
def n_times(a_thing)
return lambda{|n| a_thing * n}
end
上記のコードでは、 lambda(|n| a_thing * n}
クロージャです。 a_thing
ラムダ (匿名関数作成者) によって参照されます。
ここで、結果の無名関数を関数変数に入れるとします。
foo = n_times(4)
foo は通常のスコープ規則を破り、内部で 4 を使用し始めます。
foo.call(3)
12を返します。
つまり、関数ポインタは、プログラム コード ベース内の位置への単なるポインタです (プログラム カウンタと同様)。一方 クロージャ = 関数ポインタ + スタック フレーム.
.
閉鎖 これは、関数が独自のスコープ変数、外部関数変数、およびグローバル変数にアクセスできる JavaScript の機能です。
クロージャは、外部関数が戻った後でも、その外部関数スコープにアクセスできます。これは、関数が終了した後でも、クロージャは外部関数の変数と引数を記憶し、アクセスできることを意味します。
内部関数は、それ自体のスコープ、外部関数のスコープ、およびグローバル スコープで定義された変数にアクセスできます。そして、外部関数は、独自のスコープとグローバル スコープで定義された変数にアクセスできます。
******************
Example of Closure
******************
var globalValue = 5;
function functOuter()
{
var outerFunctionValue = 10;
//Inner function has access to the outer function value
//and the global variables
function functInner()
{
var innerFunctionValue = 5;
alert(globalValue+outerFunctionValue + innerFunctionValue);
}
functInner();
}
functOuter();
出力は内部関数自身の変数、外部関数変数、グローバル変数の値を合計した20となります。
これはもう 1 つの実際の例で、ゲームで人気のあるスクリプト言語である Lua を使用しています。標準入力が利用できないという問題を避けるために、ライブラリ関数の動作方法を少し変更する必要がありました。
local old_dofile = dofile
function dofile( filename )
if filename == nil then
error( 'Can not use default of stdin.' )
end
old_dofile( filename )
end
old_dofile の値は、このコード ブロックがそのスコープを終了すると (ローカルであるため) 消えますが、値はクロージャで囲まれているため、新しく再定義された dofile 関数はアクセスできます。つまり、コピーとして関数とともに保存されています。 「高級品」。
から Lua.org:
関数が別の関数に囲まれて書かれている場合、その関数は囲んでいる関数からローカル変数に完全にアクセスできます。この機能は字句スコープと呼ばれます。それは当然のように聞こえるかもしれませんが、そうではありません。字句スコープとファーストクラス関数はプログラミング言語における強力な概念ですが、その概念をサポートしている言語はほとんどありません。
Java を使用している場合は、クロージャをクラスのメンバー関数と比較できます。この例を見てください
var f=function(){
var a=7;
var g=function(){
return a;
}
return g;
}
関数 g
はクロージャです: g
閉まる a
で。それで g
メンバー関数と比較できます。 a
クラスフィールドおよび関数と比較できます。 f
クラス付き。
閉鎖別の関数内に定義された関数がある場合はいつでも、内側の関数は外側関数で宣言された変数にアクセスできます。クロージャについては、例を挙げて説明するのが最も効果的です。リスト2-18では、内側の関数が外側のスコープから変数(可変炎の機能)にアクセスできることがわかります。外側の関数の変数は内側の関数によって閉じられています (または内側の関数にバインドされています)。したがって、閉鎖という用語。コンセプト自体は非常にシンプルで、かなり直感的です。
Listing 2-18:
function outerFunction(arg) {
var variableInOuterFunction = arg;
function bar() {
console.log(variableInOuterFunction); // Access a variable from the outer scope
}
// Call the local function to demonstrate that it has access to arg
bar();
}
outerFunction('hello closure!'); // logs hello closure!
ソース: http://index-of.es/Varios/Basarat%20Ali%20Syed%20(auth.)-Beginning%20Node.js-Apress%20(2014).pdf
クロージャをより深く理解するには、以下のコードをご覧ください。
for(var i=0; i< 5; i++){
setTimeout(function(){
console.log(i);
}, 1000);
}
ここで何が出力されるのでしょうか? 0,1,2,3,4
そうはならない 5,5,5,5,5
閉店のため
それで、それはどうやって解決するのでしょうか?答えは以下の通りです。
for(var i=0; i< 5; i++){
(function(j){ //using IIFE
setTimeout(function(){
console.log(j);
},1000);
})(i);
}
簡単に説明しますと、関数が作成されたとき、最初のコードの for ループが 5 回呼び出されるまで呼び出されるまで何も起こりませんが、すぐには呼び出されないため、呼び出されたとき、つまり 1 秒後、またこれは非同期なので、この for ループが終了する前に値 5 が格納されます。 var i に入力し、最後に実行します setTimeout
5 回関数を実行して印刷する 5,5,5,5,5
IIFE、つまり関数式の即時呼び出しを使用してどのように解決するかは次のとおりです。
(function(j){ //i is passed here
setTimeout(function(){
console.log(j);
},1000);
})(i); //look here it called immediate that is store i=0 for 1st loop, i=1 for 2nd loop, and so on and print 0,1,2,3,4
さらに詳しくは、クロージャを理解するために実行コンテキストを理解してください。
let (ES6 機能) を使用してこれを解決するもう 1 つの解決策がありますが、内部では上記の関数が機能します。
for(let i=0; i< 5; i++){ setTimeout(function(){ console.log(i); },1000); } Output: 0,1,2,3,4
=> 詳しい説明:
メモリ内で、for ループが実行されると、次のようにピクチャ make が実行されます。
ループ1)
setTimeout(function(){
console.log(i);
},1000);
ループ2)
setTimeout(function(){
console.log(i);
},1000);
ループ3)
setTimeout(function(){
console.log(i);
},1000);
ループ4)
setTimeout(function(){
console.log(i);
},1000);
ループ5)
setTimeout(function(){
console.log(i);
},1000);
ここでは i は実行されず、ループが完了した後、var i は値 5 をメモリに保存しましたが、そのスコープは子関数で常に表示されるため、関数が内部で実行されると setTimeout
5回印刷したらアウト 5,5,5,5,5
したがって、これを解決するには、上で説明したように IIFE を使用します。
カリー化 :引数のサブセットを渡すだけで関数を部分的に評価できます。このことを考慮:
function multiply (x, y) {
return x * y;
}
const double = multiply.bind(null, 2);
const eight = double(4);
eight == 8;
閉鎖:クロージャは、関数のスコープ外の変数にアクセスすることに他なりません。関数内の関数または入れ子関数はクロージャではないことに留意することが重要です。クロージャは、関数スコープ外の変数にアクセスする必要がある場合に常に使用されます。
function apple(x){
function google(y,z) {
console.log(x*y);
}
google(7,2);
}
apple(3);
// the answer here will be 21
閉めるのはとても簡単です。次のように考えることができます。クロージャ = 関数 + その語彙環境
次の関数を考えてみましょう。
function init() {
var name = “Mozilla”;
}
上記の場合、クロージャはどうなるでしょうか?関数 init() とその語彙環境内の変数 (名前)。閉鎖 = init() + 名前
別の関数を考えてみましょう。
function init() {
var name = “Mozilla”;
function displayName(){
alert(name);
}
displayName();
}
ここでの閉鎖はどうなるでしょうか?内部関数は外部関数の変数にアクセスできます。displayName() は、親関数 init() で宣言された変数名にアクセスできます。ただし、displayName() に同じローカル変数が存在する場合は、それが使用されます。
クロージャ 1 : init 関数 + ( name 変数 + displayName() 関数) --> 字句スコープ
クロージャー 2 : displayName 関数 + ( name 変数 ) --> 字句スコープ