首頁  >  文章  >  web前端  >  理解javascript中的閉包

理解javascript中的閉包

高洛峰
高洛峰原創
2017-01-20 11:41:47972瀏覽

閱讀目錄

什麼是閉包?

閉包的特性

閉包的作用:

閉包的程式碼範例

注意事項

總結

閉包在javascript來說是比較重要的概念,平時工作中也是用的比較多的一項技術。下來對其進行一個小小的總結

什麼是閉包?

官方說法:

閉包是指有權存取另一個函數作用域中的變數的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數,透過另一個函數存取這個函數的局部變數

下面就是一個簡單的閉包:

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

按照字面量的意思是:函數B有權存取函數A作用域中的變數(text),透過另一個函數C來存取這個函數的局部變數text。因此函數B形成了一個閉包。也可以說C是閉包,因為C執行的實際上是函數B。

這個要注意的是,直接執行A();是沒有任何反應的。因為return B沒有執行,除非是return B();

閉包的特性

閉包有三個特性:

 1.函數巢狀函數

 2.函數內部可以引用外部的參數和變數

 2.函數內部可以引用外部的參數和變數

3.參數和變數不會被垃圾回收機制回收

解釋第3點,為什麼閉包的參數和變數不會被垃圾回收機制回收呢?

首先我們先了解javascript的垃圾回收原理:

(1)、在javascript中,如果一個物件不再被引用,那麼這個物件就會被GC(garbage collection)回收;

(2)、如果兩個物件互相引用,而不再被第3者所引用,那麼這兩個互相引用的物件也會被回收。

上面的範例程式碼中A是B的父函數,而B被賦給了一個全域變數C(全域變數的生命週期直到瀏覽器卸載頁面才會結束),這導致B總是在記憶體中,而B的存在依賴A,因此A也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

閉包的作用:

其實閉包的作用也是有閉包的特性決定的,根據上面的閉包特性,閉包的作用如下:

  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還保存在記憶體中,所以不會被回收,以致於第二次執行的時候可以對上次的值就行累加。當引入y=null時,銷毀引用,釋放記憶體

demo2 循環中使用閉包

程式碼如下(下面的三個程式碼範例):我們的目的是想在每次循環中呼叫循環序號:

demo2 -1

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

這個例子的結果是沒有題的,我們依次印出了0-9

每一層匿名函數和變數i都組成了一個閉包,但是這樣在循環中並沒有問題,因為函數在循環體中立即被執行了

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 作為參數傳進去,透過形參e 快取了i,也就是說e變數相當於是i 的一個拷貝,並帶進返回的函數裡面。

當 setTimeout 的執行時,它就擁有了對 e 的引用,而這個值是不會被循環改變的。

也可以用下面的寫法,和上面類似:

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

demo3 循環中添加事件

看下面的一個典型的demo.

我們希望每次點擊出li的時候,alert好讓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),也就是都是alert循環結束之後的索引值。這是為什麼呢? 🎜🎜這是因為循環中為不同的元素綁定事件,事件回調函數裡如果調用了跟循環相關的變量,則這個變量取循環的最後一個值。 🎜

由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是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