漫談閉包

什麼是閉包?

紅寶書定義: 閉包是指有權訪問另外一個函數作用域中變量的函數

MDN閉包定義: 閉包是指那些能夠訪問自由變量的函數。

閉包產生的原因?

理解閉包,首先要明白作用域的概念,在ES5中只存在兩種作用域-全局作用域和函數作用域,當訪問一個變量時,解釋器會首先在當前作用查找標識符,如果沒有找到,就去父作用域找,直到找到該變量的標示符不在父作用域中,這就是作用域鏈,每個子函數會拷貝上級的作用域,形成一個作用域鏈條。
如:

var a = 1;
function f1() {
  var a = 2;
  function f2() {
    var a = 3;
    console.log(a); //3
  }
}

這段代碼中,f1的作用域指向有全局作用域和它本身,而f2的作用域指向全局作用域,f1和它本身。作用域是從最底層向上找,直到找到全局作用域爲止,如果全局還沒有的話就報錯。

閉包產生的本質就是當前環境中存在指向父級作用域的引用。如:

function f1(){
  var a = 2;
  function f2() {
    console.log(a); // 2   
 }
 return f2;
}
var x = f1();
x(); // 2

這裏x會拿到父級作用域中的變量,輸出2,因爲在當前環境中,含有對f2的引用,f2恰恰引用了window,f1,和f2的作用域。因此f2可以訪問到f1的作用域的變量。

白話解讀閉包

很多書籍上閉包的概念很抽象,很難理解,簡單說閉包就是可以讀取其他函數內部變量的函數。由於JS語言中,只有函數內部的子函數纔可以讀取外部的局部變量,因此可以把閉包簡單理解爲“定義在一個函數內部的函數”

閉包的用途
  • 讀取函數內部的變量
  • 保存變量的引用,讓這些變量的值始終保持在內存中
function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

這段代碼中,result實際上是一個閉包函數,一共運行了兩次,第一次的值是999,第二次是1000,說明函數f1中的局部變量一直保存在內存中,並沒有在f1調用後被自動清除。

爲什麼會這樣呢?原因就在於f1是f2的父函數,而f2被賦給了一個全局變量,這導致f2始終在內存中,而f2的存在依賴於f1,因此f1也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。

這段代碼中,“nAdd=function(){n+=1}”這一行,nAdd前面沒有使用var關鍵字,因此nAdd是一個全局變量,而不是局部變量。其次nAdd的值是一個匿名函數,而這個匿名函數本身也是一個閉包,所以nAdd相當於一個setter,可以在函數外部對函數內部的局部變量進行操作。

如何解決下面的循環輸出問題?
for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}

爲什麼會全部輸出6?
setTimeout爲宏任務,由於JS中單線程eventLoop機制,在主線程同步任務執行完後纔去執行宏任務,因此循環結束後setTimeout中的回調才依次執行,但輸出i的時候當前作用域沒有,往上一級再找,發現了i,此時循環已經結束,i變成了6。因此會全部輸出6。

  • 解法1:利用IIFE(立即執行函數表達式)當每次for循環時,把此時的i變量傳遞到定時器中
 for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
  • 解法2:給定時器傳入第三個參數, 作爲timer函數的第一個函數參數
for(var i=1;i<=5;i++){
  setTimeout(function timer(j){
    console.log(j)
  }, 0, i)
}
  • 解法3
for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i)
  },0)
}
使用閉包的注意點
  1. 由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
  2. 閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章