理解函數做用域與閉包

前言

但凡讀書,或者學一門技術,都要問本身如下幾個問題。javascript

  • 它是什麼?
  • 它有什麼用?/發明它是爲了解決什麼問題?
  • 它有什麼弊端?

我下面就試着從這幾個方向來闡述閉包這個概念。html

概念

在瞭解閉包以前,咱們須要瞭解幾個概念。本文在這裏只作簡單介紹,如須要進一步瞭解,請參考文章末尾的連接。java

做用域

變量和函數的可做用範圍,分爲局部做用域和全局做用域。Javascript不具備塊級做用域,而具備函數做用域。node

執行環境(execuation context)

變量和函數有權訪問的其餘數據。git

執行環境棧(execuation context stack)

每一個函數在執行的時候,會把它的執行環境推入一個棧中,在函數執行完畢後執行環境出棧並被銷燬。保存在其中的全部函數和比變量定義隨之銷燬,控制權返回到以前的執行環境中。全局的執行環境在應用程序退出(瀏覽器關閉)纔會被銷燬。github

做用域鏈(scope chain)

做用域鏈用於保證對執行環境有權訪問的變量和函數的有序訪問。面試

什麼是閉包?

閉包這個概念,在函數式編程裏很常見,簡單的說,就是使內部函數能夠訪問定義在外部函數中的變量。嚴格一點的定義是編程

在函數內聲明另外一個函數,而且返回這個函數。這個返回的函數和它的執行環境總體叫作閉包。

讓咱們來看一個例子:瀏覽器

function f1(){
  var val = 10;
}
console.log(val);    //Uncaught ReferenceError: val is not defined(…)

因爲從函數外部沒法訪問函數內部的變量,因此報出了錯誤。那麼如何可以訪問到局部做用域的變量呢?閉包

function f1(){
  var val = 10;
  function f2(){
    console.log(val);
  }
  return f2;
}

var f2 = f1();
f2();           // 10

在這段代碼中,f2 函數和其執行環境構成了一整個閉包。對於常規的 f1() 方法, 在其內部的變量 val 應該在 f1() 方法執行完畢之後就被垃圾回收。可是 f1() 返回了一個新的方法 f2()。因爲 f2() 訪問了其外部函數的變量 val,val就構成了f2函數的執行環境。val 存在於f2的做用域鏈中,只要f2()方法沒有被銷燬,其做用域鏈中的變量和函數就不會被銷燬, val 也就會一直存在。

閉包有什麼用?

for循環變量沒法保持的問題

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 5);
}

上面這個代碼塊會打印五個 5 出來,而咱們預想的結果是打印 1 2 3 4 5。

之因此會這樣,是由於 setTimeout 中的 i 是對外層 i 的引用。當 setTimeout 的代碼被解析的時候,運行時只是記錄了 i 的引用,而不是值。而當 setTimeout 被觸發時,五個 setTimeout 中的 i 同時被取值,因爲它們都指向了外層的同一個 i,而那個 i 的值在迭代完成時爲 5,因此打印了五次 5

爲了獲得咱們預想的結果,咱們能夠把 i 賦值成一個局部的變量,從而擺脫外層迭代的影響。

for (var i = 0; i < 5; i++) {
  (function (idx) {
    setTimeout(function () {
      console.log(idx);
    }, 5);
  })(i);
}

製造函數構造器

假如咱們要實現一系列的函數:add10,add20。咱們爲此構造了一個名爲 adder 的構造器,以下:

var adder = function (x) {
  var base = x;
  return function (n) {
    return n + base;
  };
};

var add10 = adder(10);
console.log(add10(5));

var add20 = adder(20);
console.log(add20(5));

每次調用 adder 時,adder 都會返回一個函數給咱們。咱們傳給 adder 的值,會保存在一個名爲 base 的變量中。因爲返回的函數在其中引用了 base 的值,因而 base 的引用計數被 +1。當返回函數不被垃圾回收時,則 base 也會一直存在。

閉包有什麼弊端?

因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。

面試題

面試題一

請定義這樣一個函數

function repeat (func, times, wait) {
}
//  這個函數能返回一個新函數,好比這樣用
//  var repeatedFun = repeat(alert, 10, 5000)
//  調用這個 repeatedFun ("helloworld")
//  會alert十次 helloworld, 每次間隔5秒

代碼參見:JS bin 閉包面試題一

面試題二

寫一個函數stringconcat, 要求能

var result1 = stringconcat("a", "b")  result1 = "a+b"
var stringconcatWithPrefix = stringconcat.prefix("helloworld");
var result2 = stringconcatWithPrefix("a", "b")  result2 = "helloworld+a+b"

代碼參見:JS bin 閉包面試題二

參考:
學習Javascript閉包(Closure)

node-lessons/lesson11 at master · alsotang/node-lessons · GitHub

JavaScript做用域鏈
相關文章
相關標籤/搜索