ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript クロージャ パート 1: クロージャの概要
スコープ チェーンと変数オブジェクトについては以前に紹介しましたが、クロージャについて話すと簡単に理解できるようになりました。実際、閉店についてはすでに誰もが話し合っています。それにもかかわらず、ここでは理論的な観点からクロージャについて説明し、ECMAScript のクロージャが実際に内部でどのように動作するかを見ていきます。
ECMAScript クロージャについて直接議論する前に、関数型プログラミングの基本的な定義をいくつか確認する必要があります。
ご存知のとおり、関数型言語 (ECMAScript もこのスタイルをサポートしています) では、関数はデータです。たとえば、関数を変数に割り当てたり、パラメータとして他の関数に渡したり、関数から返したりすることができます。このような関数には特別な名前と構造があります。
定義
関数引数 (「Funarg」) — 値が関数である引数です。
関数引数 (「Funarg」) — 値が関数である引数です。
例:
function exampleFunc(funArg) { funArg(); } exampleFunc(function () { alert('funArg'); });
上記の例の funarg の実際のパラメータは、実際には exampleFunc に渡される匿名関数です。
逆に、関数パラメーターを受け入れる関数は、高階関数 (略して HOF) と呼ばれます。関数関数、部分数学理論、または演算子とも呼ばれます。上記の例では、exampleFunc がそのような関数です。
前に述べたように、関数はパラメータとしてだけでなく、戻り値としても使用できます。このような関数を返す関数は、関数値を持つ関数または関数値関数と呼ばれます。
(function functionValued() { return function () { alert('returned function is called'); }; })()();
通常のデータの形式で存在できる関数 (例: パラメーターが渡されるとき、関数パラメーターを受け入れる、または関数値を返す) は、ファーストクラス関数 (通常はファーストクラス オブジェクト) と呼ばれます。 ECMAScript では、すべての関数はファーストクラスのオブジェクトです。
通常のデータとして存在できる関数 (例: パラメーターが渡されるとき、関数パラメーターを受け入れる、または関数値を返す) は、ファーストクラス関数 (通常はファーストクラス オブジェクト) と呼ばれます。
ECMAScript では、すべての関数はファーストクラスのオブジェクトです。
自分自身をパラメータとして受け取る関数は、自動適用関数または自己適用関数と呼ばれます:
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative); })();
自分自身を戻り値として受け取る関数は、自動複製関数または自己複製関数関数と呼ばれます)。多くの場合、「自己複製」という用語は文献で使用されます。
(function selfReplicative() { return selfReplicative; })();
自己複製関数のより興味深いパターンの 1 つは、コレクション自体を受け入れるのではなく、コレクションの 1 つの項目のみを引数として受け入れることです。
// 接受集合的函数 function registerModes(modes) { modes.forEach(registerMode, modes); } // 用法 registerModes(['roster', 'accounts', 'groups']); // 自复制函数的声明 function modes(mode) { registerMode(mode); // 注册一个mode return modes; // 返回函数自身 } // 用法,modes链式调用 modes('roster')('accounts')('groups') //有点类似:jQueryObject.addClass("a").toggle().removClass("b")
しかし、コレクションを直接渡すことは比較的効果的で直感的です。
関数パラメータで定義された変数は、「funarg」がアクティブ化されているときにアクセス可能です(コンテキストデータを格納する変数オブジェクトはコンテキストが入力されるたびに作成されるため):
function testFn(funArg) { // funarg激活时, 局部变量localVar可以访问了 funArg(10); // 20 funArg(20); // 30 } testFn(function (arg) { var localVar = 10; alert(arg + localVar); });
ただし、 ECMAScript では、関数を親にカプセル化できます。関数を作成し、親関数コンテキストの変数を使用できます。この機能により、funarg の問題が発生する可能性があります。
Funarg 問題
スタック指向プログラミング言語では、関数のローカル変数は、関数がアクティブ化されるたびにスタックにプッシュされます。
関数が戻ると、これらのパラメータはスタックから削除されます。このモデルでは、関数を関数値として (たとえば、親関数からの戻り値として) 使用することに重大な制限が設けられています。ほとんどの場合、関数に自由変数がある場合に問題が発生します。
自由変数とは、関数で使用される変数を指しますが、関数パラメーターでも関数のローカル変数でもありません
例:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn; } var someFn = testFn(); someFn(20); // 30
上記の例では、innerFn 関数の場合、localVar は自由変数です。
ローカル変数を保存するためにスタック指向モデルを使用するシステムの場合、これは、testFn 関数呼び出しが終了すると、そのローカル変数がスタックから削除されることを意味します。このように、外部から innerFn への関数呼び出しが行われた場合、エラーが発生します(localVar 変数が存在しなくなるため)。
さらに、上記の例では、スタック指向の実装モデルでは、戻り値として innerFn を返すことは単純に不可能です。これは testFn 関数のローカル変数でもあるため、testFn が戻ったときにも削除されます。
もう 1 つの問題は、システムが動的スコープを使用し、関数が関数パラメーターとして使用される場合です。
次の例 (疑似コード) を見てください:
var z = 10; function foo() { alert(z); } foo(); // 10 – 使用静态和动态作用域的时候 (function () { var z = 20; foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域 })(); // 将foo作为参数的时候是一样的 (function (funArg) { var z = 30; funArg(); // 10 – 静态作用域, 30 – 动态作用域 })(foo);
動的スコープを使用すると、変数システム (識別子) が変数の動的スタックを通じて管理されることがわかります。したがって、自由変数は、関数の作成時に保存された静的スコープ チェーンではなく、現在アクティブな動的チェーンでクエリされます。
这样就会产生冲突。比方说,即使Z仍然存在(与之前从栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?
上述描述的就是两类funarg问题 —— 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。
为了解决上述问题,就引入了 闭包的概念。
闭包
闭包是代码块和创建该代码块的上下文中数据的结合。
让我们来看下面这个例子(伪代码):
var x = 20; function foo() { alert(x); // 自由变量"x" == 20 } // 为foo闭包 fooClosure = { call: foo // 引用到function lexicalEnvironment: {x: 20} // 搜索上下文的上下文 };
上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性——创建该函数上下文的作用域链。
“lexical”通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值总是10。
定义中我们使用的比较广义的词 —— “代码块”,然而,通常(在ECMAScript中)会使用我们经常用到的函数。当然了,并不是所有对闭包的实现都会将闭包和函数绑在一起,比方说,在Ruby语言中,闭包就有可能是: 一个过程对象(procedure object), 一个lambda表达式或者是代码块。
对于要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。
以上就是JavaScript闭包其一:闭包概论的内容,更多相关内容请关注PHP中文网(www.php.cn)!