五百字的閉包詳解與一千字的使用場景

相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)javascript

在理解閉包以前,有個重要的概念須要先了解一下,就是 js 執行上下文前端

每當引擎遇到一個函數調用,它會爲該函數建立一個新的執行上下文並壓入棧的頂部。java

在建立階段會發生三件事:

  1. This 綁定
  2. 建立詞法環境組件。
  3. 建立變量環境組件。

當該函數執行結束時,執行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。執行上下文中聲明的全部變量都將被刪除。面試

先看個栗子:閉包

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope(); // local scope
複製代碼

依據詞法做用域邏輯,查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象。函數

所以,內部函數f在執行時,找到定義位置的父級函數checkscope內的scope並將其返回,而後內部函數與外部函數的上下文依次彈出並銷燬。post

再看另外一個栗子:ui

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
const f2 = checkscope();
f2(); // local scope
複製代碼

與上個栗子不一樣,這裏並無在函數內部調用f,而是將f返回賦值給f2,所以,隨着checkscope函數的結束,由於f2持有對函數f的引用,f沒有被銷燬,又由於f持有外部函數的做用域,因此scope也沒有被銷燬。spa

由此得出結論:.net

  • 閉包就是內部函數,咱們能夠經過在一個函數內部或者 {} 塊裏面定義一個函數來建立閉包。閉包能夠訪問外部做用域,即便這個外部做用域已經執行結束。
  • 閉包在沒有被外部使用的狀況下,隨執行結束銷燬
  • 閉包的外部做用域是在其定義的時候已決定,而不是執行的時候。

下面是擴展閱讀(偷懶直接複製的,連接在末尾,罪過~):

閉包與循環

閉包只存儲外部變量的引用,而不會拷貝這些外部變量的值。

function initEvents(){
  for(var i=1; i<=3; i++){
    setTimeout(function showNumber(){
     console.log(i)
    },10);
  }
}
initEvents(); // 4,4,4
複製代碼

這個示例中,咱們建立了3個閉包,皆引用了同一個變量 i,因爲變量 i 隨着循環自增,所以最終輸出的都是一樣的值。

for 語句塊中使用 let 變量聲明,將在每次循環中爲 for 語句塊建立一個新的局部變量。

function initEvents(){
  for(let i=1; i<=3; i++){
    setTimeout(function showNumber(){
     console.log(i)
    },10);
  }
}
initEvents();
複製代碼

函數與私有狀態

經過閉包,咱們能夠建立擁有私有狀態的函數,閉包使得狀態被封裝起來。

自增生成器函數

經過閉包,咱們能夠建立自增生成器函數。一樣,內部狀態是私有的。示例以下:

function createAGenerate(count, increment) {
  return function(){
    count += increment;
    return count;
  }
}
let generateNextNumber = createAGenerate(0, 1);
console.log(generateNextNumber()); //1
console.log(generateNextNumber()); //2
console.log(generateNextNumber()); //3
let generateMultipleOfTen = createAGenerate(0, 10);
console.log(generateMultipleOfTen()); //10
console.log(generateMultipleOfTen()); //20
console.log(generateMultipleOfTen()); //30
複製代碼

對象與私有狀態

以上示例中,咱們能夠建立一個擁有私有狀態的函數。同時,咱們也能夠建立多個擁有同一私有狀態的函數。基於此,咱們還能夠建立一個擁有私有狀態的對象。

function TodoStore(){
  let todos = [];
  
  function add(todo){
    todos.push(todo);
  }
  function get(){
    return todos.map(toTodoViewModel);
  }
  
  function toTodoViewModel(todo) {
     return { id : todo.id, title : todo.title };
  }
  
  return Object.freeze({
    add,
    get
  });
}
複製代碼
複製代碼

TodoStore() 函數返回了一個擁有私有狀態的對象。在外部,咱們沒法訪問私有的 todos 變量,而且 add 和 get 這兩個閉包擁有相同的私有狀態。在這裏,TodoStore() 是一個工廠函數。

閉包 vs 純函數

閉包就是那些引用了外部做用域中變量的函數。

爲了更好的理解,咱們將內部函數拆成閉包和純函數兩個方面:

  • 閉包是那些引用了外部做用域中變量的函數。
  • 純函數是那些沒有引用外部做用域中變量的函數,它們一般返回一個值而且沒有反作用。

在上述例子中,add()get() 函數是閉包,而 isPriorityTodo()toTodoViewModel() 則是純函數。

裝飾器函數也使用了閉包的特性。

垃圾回收

Javascript 中,局部變量會隨着函數的執行完畢而被銷燬,除非還有指向他們的引用。當閉包自己也被垃圾回收以後,這些閉包中的私有狀態隨後也會被垃圾回收。一般咱們能夠經過切斷閉包的引用來達到這一目的。

避免全局變量

Javascript 中,咱們很容易建立出全局變量。任何定義在函數和 {} 塊以外的變量都是全局的,定義在全局做用域中的函數也是全局的。

若是你收穫了新知識,請給做者點個贊吧,在左側邊欄第一個按鈕點一下~

參考文章:

[譯]發現 JavaScript 中閉包的強大威力

相關文章
相關標籤/搜索