ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript のクロージャーを理解する

JavaScript のクロージャーを理解する

高洛峰
高洛峰オリジナル
2017-01-20 11:41:471007ブラウズ

目次を読む

クロージャーとは何ですか?

クロージャの特徴

クロージャの機能:

クロージャのコード例

メモ

概要

クロージャはJavaScriptの重要な概念であり、日常業務や技術でよく使用されます。ちょっとまとめてみましょう

クロージャとは何ですか?

公式声明:

クロージャとは、別の関数のスコープ内の変数にアクセスできる関数を指します。クロージャを作成する一般的な方法は、関数内に別の関数を作成し、別の関数を通じてこの関数のローカル変数にアクセスすることです。これは単純なクロージャです:

function A(){
 var text="hello world";
 function B(){
 console.log(text);
 }
 return B;
}
var c=A();
c(); // hello world 

文字通りの意味は次のとおりです: 関数 B は変数にアクセスします。 (text) を関数 A のスコープ内に配置し、別の関数 C を通じてこの関数のローカル変数 text にアクセスします。したがって、関数 B はクロージャーを形成します。 C は実際に関数 B を実行するため、C はクロージャであるとも言えます。

A(); を直接実行すると反応がないことに注意してください。 return Bはreturn B()でなければ実行されないので;

クロージャの特徴

クロージャには3つの特徴があります

1. 関数の入れ子関数

2. 関数内で外部パラメータや変数を参照できる

3 . パラメーターと変数はガベージ コレクション メカニズムによってリサイクルされません

ポイント 3、クロージャーのパラメーターと変数がガベージ コレクション メカニズムによってリサイクルされない理由を説明します。

まず、JavaScript のガベージ コレクションの原理を理解しましょう:

(1)、JavaScript では、オブジェクトが参照されなくなった場合、そのオブジェクトは GC (ガベージ コレクション) によってリサイクルされます

(2) )、2 つのオブジェクトが相互に参照し、サードパーティによって参照されなくなった場合、相互に参照している 2 つのオブジェクトもリサイクルされます。

上記のコード例では、A は B の親関数であり、B はグローバル変数 C に割り当てられます (グローバル変数のライフサイクルは、ブラウザーがページをアンロードするまで終了しません)。これにより、B は常にA の存在は A に依存するため、A は常にメモリ内にあり、呼び出しの完了後にガベージ コレクション メカニズム (ガベージ コレクション) によってリサイクルされません。

クロージャの役割:

実際、クロージャの役割もクロージャの特性によって決まります。 上記のクロージャの特性によれば、クロージャの役割は次のとおりです:

1. 内部の変数を読み取ることができます。環境の汚染を避けるために、グローバル変数を一緒に定義する代わりに関数を使用します

2. これらの変数の値をメモリに保持します。

クロージャのコード例

以下は主にいくつかの一般的なクロージャを紹介し、それらを分析します:

demo1 ローカル変数の蓄積。

function countFn(){
 var count=1;
 return function(){  //函数嵌套函数
 count++;
 console.log(count);
 }
}
var y = countFn(); //外部函数赋给变量y;
y(); //2 //y函数调用一次,结果为2,相当于countFn()()
y(); //3 //y函数调用第二次,结果为3,因为上一次调用的count还保存在内存中,没有被销毁,所以实现了累加
y=null; //垃圾回收,释放内存
y(); // y is not a function

最初の実行が完了した後、変数 count はまだメモリに保存されているため、再利用されず、2 回目の実行中に最後の値が累積されます。 y=null が導入されると、参照が破棄され、メモリが解放されます

demo2 ループ内でクロージャーを使用します

コードは次のとおりです (以下の 3 つのコード例): 私たちの目的は、各ループでループ シーケンス番号を呼び出すことです。

demo2 -1

for (var i = 0; i < 10; i++) {
 var a = function(){
 console.log(i)
 }
 a() //依次为0--9
}

この例の結果は問題ありません。0-9を出力しました

匿名関数と変数の各層はクロージャを形成していますが、ループ内には問題がないためです。ループ本体では関数がすぐに実行されます

demo2-2

が、setTimeoutでは異なります

for(var i = 0; i < 10; i++) {
 setTimeout(function() {
 console.log(i); //10次10
 }, 1000);
}

0--10を順番に出力することを想定していますが、実際には 10 を10回出力することになります。 setTimeout 時間を 0 に変更しても、10 10 が出力されます。どうしてこれなの?

これは setTimeout のメカニズムによるもので、setTimeout はタスク キューが終了するときに計測を開始します。前に終了していないプロセスがある場合、そのプロセスが終了するまで待ってから計測を開始します。ここで、タスクキューはそれ自体のループです。

setTimeout はループ終了後にのみ計測を開始するため、何があっても setTimeout の i は最後のループの i になります。このコードでは、最後の i は 10 なので、10 個の 10 が出力されます。

これが、setTimeout のコールバックがループするたびに値を取得せず、最後の値を取得する理由です

demo2-3

解決策上記setTimeout はループを連続して出力できません

for(var i=0;i<10;i++){
 var a=function(e){
 return function(){
  console.log(e); //依次输入0--9
 }
 }
 setTimeout(a(i),0);
}

setTimeout の最初のパラメーターには関数が必要なので、関数が返されるときに、i がパラメーターとして渡され、i は仮パラメーター e を介してキャッシュされます。つまり、変数 e は i のコピーと等価であり、返された関数に取り込まれます。

setTimeout が実行されると、e への参照があり、この値はループによって変更されません。

上記と同様の次の記述方法を使用することもできます:

for(var i = 0; i < 10; i++) {
 (function(e) {
 setTimeout(function() {
  console.log(e); //依次打印出0-9
 }, 0);
 })(i);
}

demo3 ループにイベントを追加します

以下の典型的なデモを見てください

li がクリックされるたびにインデックス値が更新されることを願っています。の li がアラートされるので、次のコードを使用します:

<ul id="test">
 <li>第一个</li>
 <li>第二个</li>
 <li>第三个</li>
 <li>第四个</li>
</ul>
var nodes = document.getElementsByTagName("li");
for(i = 0,len=nodes.length;i<len;i++){
 nodes[i].onclick = function(){
 alert(i); //值全是4
 };
}

予想に反して、どの li をクリックしても、アラート ループ終了後のインデックス値であるalert(4) になります。どうしてこれなの?

これは、イベントがループ内の異なる要素にバインドされているためです。ループに関連する変数がイベント コールバック関数で呼び出された場合、この変数はループの最後の値を取得します。

由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是for里面的作用域),当事件触发的时候,作用域中的变量已经随着循环走到最后了。

还有一点就是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。

要实现点击li,alert出li的索引值,需要将上面的代码进行以下的修改:

<ul id="test">
 <li>第一个</li>
 <li>第二个</li>
 <li>第三个</li>
 <li>第四个</li>
</ul>
var nodes=document.getElementsByTagName("li");
for(var i=0;i<nodes.length;i++){
 (function(e){
 nodes[i].onclick=function(){
  alert(e);
 };
 })(i)
}

解决思路: 增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标)。

当立即执行函数执行的时候,e 值不会被销毁,因为它的里面有个匿名函数(也可以说是因为闭包的存在,所以变量不会被销毁)。执行后,e 值 与全局变量 i 的联系就切断了,

也就是说,执行的时候,传进的 i 是多少,立即执行函数的 e 就是多少,但是 e 值不会消失,因为匿名函数的存在。

也可以用下面的解法,原理是一样的:

<ul id="test">
 <li>第一个</li>
 <li>第二个</li>
 <li>第三个</li>
 <li>第四个</li>
</ul>
var nodes=document.getElementsByTagName(&#39;li&#39;);
for(var i = 0; i<nodes.length;i++){
 (function(){
 var temp = i;
 nodes[i].onclick = function () {
  alert(temp);
 }
 })();
}

注意事项

1、造成内存泄露

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以只有在绝对必要时再考虑使用闭包。

2、在闭包中使用this也可能会导致一些问题。

其实我们的目的是想alert出object里面的name

var name="The Window";
 var object={
 name:"My Object",
 getNameFunc:function(){
  return function(){
  return this.name;
  }
 }
 }
 alert(object.getNameFunc()()); // The Window

因为在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

每个函数在被调用时,都会自动取的两个特殊变量:this和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止。也就是说,里面的return function只会搜索

到全局的this就停止继续搜索了。因为它永远不可能直接访问外部函数中的这两个变量。

稍作修改,把外部作用域中的this对象保存在一个闭包能够访问的变量里。这样就可以让闭包访问该对象了。

var name="The Window";
 var object={
 name:"My Object",
 getNameFunc:function(){
  var that=this;
  return function(){
  return that.name;
  }
 }
 }
 alert(object.getNameFunc()()); // My Object

我们把this对象赋值给了that变量。定义了闭包之后闭包也可以访问这个变量。因此,即使在函数返回之后,that也仍引用这object,所以调用object.getNameFunc()()就返回 “My Object”了。

总结

当在函数内部定义了其他函数,就创建了闭包。闭包有权访问包含函数内部的所有变量。

闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。

当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。

使用闭包必须维护额外的作用域,所有过度使用它们可能会占用大量的内存

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持PHP中文网!

更多理解javascript中的闭包相关文章请关注PHP中文网!

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。