JavaScript:閉包

  本文和你們聊聊閉包,閉包與變量對象和做用域鏈有着比較多的聯繫,在閱讀本文前,你們須要理解執行上下文、變量對象以及做用域鏈等內容,這些內容對理解閉包的本質有很大的幫助,前面的兩篇文章已經梳理過了,不清楚的同窗能夠先閱讀以前的文章。java

自由變量

  上篇文章沒有提到自由變量這個概念,如今須要理解這個概念。編程

  在一個做用域中使用了一個變量,可是這個變量沒有在這個做用域中聲明(在其餘做用域中聲明),對於該做用域而言,這個變量就是一個自由變量。閉包

let a = 10;
function foo() {
  let b = 20;
  console.log(a + b); // 10 在foo函數做用域中,a就是一個自由變量
}
foo();
複製代碼

  從上面的實例來看,調用foo函數時,a的取值是來自全局做用域,因此變量a相對foo函數做用域而言變量a是一個自由變量,而b的取值是來自foo做用域,因此變量b對於foo做用域變量b不是自由變量。模塊化

定義

  閉包是函數和聲明該函數的詞法環境的組合。函數式編程

  其實閉包的概念很差解釋,彷佛解釋不清楚,目前業界對閉包的概念解釋有兩種,可是不論是哪一種解釋,思想是一致的,只是包含的範圍不一樣而已,咱們看下面的實例,再來講說閉包這個東西。函數

function foo() {
  let a = 10;
  function bar() {
    console.log(a); // 10
  }
  return bar;
}
let baz = foo();
baz();
複製代碼

  上面是一個很簡單的實例,這就產生了閉包,爲啥產生了閉包???工具

  函數foo中建立了函數bar,並返回了函數bar,並在函數foo做用域外執行了函數bar,當函數bar執行時,訪問了foo做用域中的變量a,這就產生了閉包。性能

  也就是說當一個函數有權訪問另外一個函數做用域中的變量,而且該函數在另外一個函數的詞法做用域外執行就會產生閉包。ui

  從上面的實例來看,也就有人會理解函數foo是閉包,也有人理解函數bar是閉包,Chrome開發者工具中會以函數foo代指閉包。其實不用管閉包是指哪一個,咱們須要理解什麼狀況下會產生閉包,閉包產生是在一個什麼樣的場景。下面從底層原理上分析閉包產生的緣由。this

原理

  咱們先看一個實例:

function foo() {
  let a = 10;
  function bar() {
    console.log(a); // 10
  }
  return bar;
}
let baz = foo();
baz();
複製代碼

  這個實例和上面的舉例是同一個,產生了閉包,咱們分析下這個實例在代碼執行過程當中,執行上下文棧的狀況:

// 建立執行上下文棧
ECStack = [];

// 最早進入全局環境,全局執行上下文被建立被壓入棧
ECStack.push(globalContext);

// foo() 建立該函數執行上下文並壓入棧中
ECStack.push(<foo> functionContext);

// foo()執行完畢彈出
ECStack.pop();

// baz被調用,建立baz執行上下文並壓入棧中
ECStack.push(<baz> functionContext);

// baz執行完畢彈出
ECStack.pop();

// 代碼全局執行完畢,全局執行上下文彈出
ECStack.pop();
複製代碼

  在來看看bar函數執行上下文的內容:

bar.[[scope]] = [fooContext.VO, globalContext.VO];

barContext = {
  VO: {xxx}, // 變量對象
  this: xxx,
  scopeChain: [barContext.VO].concat(bar.[[scope]]) // [barContext.VO, fooContext.VO, globalContext.VO]
}
複製代碼

  從上面的執行上下文棧的執行狀況來看,baz函數執行的時候,foo函數的執行上下文已經出棧了,按照JavaScript垃圾回收機制,foo函數執行上下文的變量對象失去引用後會被垃圾回收機制回收。

  可是上面的實例特殊,bar函數在foo函數中建立,foo函數最終是返回了bar函數,並經過變量baz,在foo函數做用域外執行了,以及訪問了foo函數做用域中的a變量。

  函數bar執行上下文中的做用域鏈包含了函數foo執行上下文中的變量對象fooContext.VO,因此函數foo執行上下文的變量對象不會被垃圾回收機制回收,函數bar訪問了函數foo中的變量,阻止了函數foo執行上下文的變量對象被垃圾回收機制回收,正所以函數bar在函數foo的詞法做用域外執行,同時也能夠訪問foo做用域中的變量a,這也就是產生閉包的緣由。

  咱們來概括下閉包本質是什麼:

  閉包是一個函數,上面的實例來看,不論是foo函數仍是bar函數,歸根結底仍是一個函數,可是和普通函數不同,其擁有特殊能力。

  歸納的講,咱們能夠把閉包看做是一個場景,若是一個函數B在函數A中建立,當函數A的執行上下文已經出棧了,可是函數B在函數A的詞法做用域外執行並仍然能訪問函數A中的變量對象,咱們就能夠說這產生了閉包。咱們能夠不用在乎函數A是閉包仍是函數B是閉包,但咱們要清楚什麼場景下會產生閉包。

  概括下閉包的特色:

  • 函數A的執行上下文已經出棧
  • 函數B能訪問函數A執行上下文的變量對象
  • 函數B在函數A的詞法做用域外執行

  最後總結性的說,函數A調用完成後,函數A的執行上下文已經出棧,其變量對象會失去引用等待被垃圾回收機制回收,然而閉包,阻止這一過程,由於函數B的做用域鏈包含了函數A的執行上下文的變量對象。

  下面咱們看一個實例,熟悉下閉包,加強對閉包的理解。

function foo() {
  let a = 'Hello world';
  function bar() {
    a += ' 6';
    console.log(a);
  }
  return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
複製代碼

  函數foo調用完成後,此時函數foo執行上下文的變量對象內容以下:

fooContext.VO = {
  bar:  <reference to FunctionDeclaration 'bar'>,
  a: 'Hello world'
}
複製代碼

  當函數foo調用完成後,其執行上下文出棧後,它的變量對象沒有被垃圾回收機制回收,由於baz函數調用,函數bar的做用域鏈保存了函數foo執行上下文的變量對象,其變量對象一直在內存中,沒有被銷燬。

  在函數baz第一次調用後,訪問了函數foo做用域中的變量a,並對變量a作相關的操做,使得變量a的值發生了變化,值爲Hello world 6,此時函數foo執行上下文的變量對象內容以下:

fooContext.VO = {
  bar:  <reference to FunctionDeclaration 'bar'>,
  a: 'Hello world 6'
}
複製代碼

  第一次調用baz後,函數foo中的變量a值爲Hello world 6,沒有被銷燬,因此第二次調用baz時,函數foo中的變量a值爲Hello world 6 6

  也正由於閉包會阻止垃圾回收機制對變量進行回收,變量會永久存在內存中,至關於全局變量同樣會佔用着內存,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。如上面實例,咱們能夠將變量設置爲null

function foo() {
  let a = 'Hello world';
  function bar() {
    a += ' 6';
    console.log(a);
  }
  return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
baz = null; //若是baz再也不使用,將其指向的對象釋放
複製代碼

閉包應用

  在JavaScript中,由於閉包獨有的特性,其應用場景不少。

  • 用於保存私有屬性,將不須要對外暴露的屬性、函數保存在閉包函數的父函數裏,避免外部操做對值的干擾
  • 避免局部屬性污染全局變量空間致使的命名空間混亂
  • 模塊化封裝,將對立的功能模塊經過閉包進去封裝,只暴露較少的API供外部應用使用
  • 柯里化,在函數式編程中,利用閉包可以實現不少炫酷的功能,柯里化即是其中很重要的一種

  關於閉包的應用,在這裏先不作展開,由於裏面也有不少本身不太清楚的東西,例如函數式編程,目前本身也不太熟悉,裏面還涉及不少其餘的知識,關於閉包的應用這塊內容暫時不作詳細的輸出,避免不懂裝懂,在這裏先梳理閉包有哪些應用,後期對柯里化、模塊化封裝等內容另外作文字輸出。

相關文章
相關標籤/搜索