還擔憂面試官問閉包?

網上總結閉包的文章已經爛大街了,不敢說筆者這篇文章多麼多麼xxx,只是我的理解總結。各位看官瞅瞅就好,大神還但願多多指正。此篇文章總結與《JavaScript忍者祕籍》 《你不知道的JavaScript上卷》javascript

系列博客地址:https://github.com/Nealyang/YOU-SHOULD-KNOW-JS前端

安利我的react技術棧+express+mongoose實戰我的博客教程 React-Express-Blog-Demojava

前言

爲何咱們須要理解而且掌握閉包,且不說大道理,就問你要不要成爲JavaScript高手?不要?那你要不要面試找工做嘛。。。react

再者,對於任何一個前端er或者JavaScript開發者來講,理解閉包能夠看作是另外一種意義上的重生。閉包是純函數編程語言的一個特性,由於他大大簡化複雜的操做,因此很容易在一些JavaScript庫以及其餘高級代碼中找到閉包的使用。git

一言以蔽之,閉包,你就得掌握。es6

談談閉包以前,咱們先說說做用域

這裏咱們要說的做用域值得是詞法做用域。詞法做用域即爲定義在詞法階段的做用域。換句話說,就是你寫代碼時將變量和塊做用域寫在哪裏所決定的。所以在詞法解析的時會保持做用域不變。(JavaScript引擎在運行JavaScript代碼的時候大體通過分詞/詞法分析、解析/語法分析、代碼生成三個步驟)。github

老規矩,看代碼(就是代碼多~~)面試

function foo(a) {
  var b = a*2;
  function bar(c) {
    console.log(a,b,c);
  }
  bar(b*3);
}
foo(2);//2 4 12
複製代碼

這個例子中有三個逐級嵌套的做用域,如圖:express

截圖來自《你不知道的JavaScript》

部分一包含整個做用域也就是全局做用域。其中包含標識符:foo編程

部分二包含foo所建立的做用域,其中包含:a,bar和b

部分三包含bar所建立的做用域,其中包含:c

這些做用域氣泡的包含關係給引擎提供了足夠多的位置信息。在上面的代碼中,引擎執行console.log的時候,並查找a,b,c。他首先在最裏面的做用域,也就是bar(...)函數的做用域。引擎沒法在這一層做用域中找到變量a,所以引擎會去上一級嵌套做用域foo(...)中查找,若是找到了,則即便用。

若是a,c 都存在做用域bar(...),foo(...)做用域中,console.log(...)即不須要到foo的外部做用域中去查找變量。

不管函數在哪裏被調用,且不管他們如何被調用,他的詞法做用域都只由函數被聲明的位置決定的。詞法做用域查找只會查找一級標識符,好比a,b和c。

簡單理解詞法做用域的概念,其實也就是咱們常說的做用域,關於JavaScript中欺騙詞法以及更多關於詞法做用域的介紹,請翻閱《你不知道的JavaScript》。

閉包的概念

說到閉包的概念,這裏還真的比較模糊,咱們且看下各類經典書籍給出的概念

《JavaScript權威指南》中的概念

函數對象能夠經過做用域鏈互相關聯起來,函數體內部的變量均可以保存在函數做用域內,這種特性在計算機科學中成爲閉包

《JavaScript權威指南》中的概念

閉包是指有權訪問另外一個函數做用域中的變量的函數。

《JavaScript忍者祕籍》中的概念

閉包是一個函數在建立時容許該自身函數訪問並操做該自身函數之外的變量時所建立的做用域。

《你不知道的JavaScript》中的概念

閉包是基於詞法做用域書寫代碼時所產生的天然結果。當函數記住並訪問所在的詞法做用域,閉包就產生了。

我的理解

閉包就是一個函數,一個能夠訪問並操做其餘函數內部變量的函數。也能夠說是一個定義在函數內部的函數。由於JavaScript沒有動態做用域,而閉包的本質是靜態做用域(靜態做用域規則查找一個變量聲明時依賴的是源程序中塊之間的靜態關係),因此函數訪問的都是咱們定義時候的做用域,也就是詞法做用域。因此閉包纔會得以實現。

咱們常見的閉包形式就是a 函數套 b 函數,而後 a 函數返回 b 函數,這樣 b 函數在 a 函數之外的地方執行時,依然能訪問 a 函數的做用域。其中「b 函數在 a 函數之外的地方執行時」這一點,才體現了閉包的真正的強大之處。

實質性的問題

function outer() {
  var a = 2;
  function inner() {
    console.log(a);//2
  }
  inner();
}
outer();
複製代碼

基於詞法做用域和查找規則,inner函數是能夠訪問到outer內部定義的變量a的。從技術上講,這就是閉包。可是也能夠說不是,由於用來解釋inner對a的引用方法是詞法做用域的查找規則,而這些規則只是閉包中的一部分而已。

下面咱們將上面的代碼修改下,讓咱們可以清晰的看到閉包

function outer() {
  var a = 2;
  function inner() {
    console.log(a);
  }
  return inner;
}
var neal = outer();
neal();//2
複製代碼

多是全部講解閉包的博客中都用爛了的例子了。這裏inner函數被正常調用執行,而且能夠訪問到outer函數裏定義的變量a。講道理,在outer函數運行後,一般函數整個內部做用域都會被銷燬。

而閉包的神奇之處正是如此能夠阻止垃圾回收這種事情的發生,事實上,內部做用域已然存在且拿着a變量,因此沒有被回收。inner函數擁有outer函數內部做用域的閉包,使得該做用域可以一直存活,以供inner函數在以後的任什麼時候間能夠訪問。

inner()已然持有對該做用域的引用,而這個引用就被叫作閉包。

函數在定義時的詞法做用域之外的地方被調用,閉包使得函數能夠繼續訪問定義時的詞法做用域。

不管經過何種手段將內部函數傳遞到所在的詞法做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包

var fn;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  fn = baz;
}

function bar() {
  fn();
}
foo();
bar();
複製代碼

上面的代碼不作過多解釋,挺簡單,經過下面的代碼,咱們再說下閉包的三個有趣的概念

var outerValue = 'ninja';
var later;
function outFunction() {
  var innerValue = 'Neal';
  function innerFunction(param){
      console.log(outerValue,innerValue,param,tooLate);
  }
  later = innerFunction;
}
console.log('tooLate is',tooLate);
outFunction();
later('Nealyang');
var tooLate = 'Haha';
later('Neal_yang');

//tooLate is undefined
//ninja Neal Nealyang undefined
//ninja Neal Neal_yang Haha
複製代碼

上面代碼運行結果你們能夠自行嘗試。總之,從上面的代碼中,咱們能夠看到閉包的有趣的三個概念

  • 內部函數的參數包含在閉包中
  • 做用域以外的全部變量、即使是函數聲明以後的那些聲明,也都包含在閉包中.
  • 相同做用域內,還沒有聲明的變量,不能進行提早引用

代碼到處有閉包

function wait(message) {
     setTimeout( function timer() {
         console.log( message ); }, 1000 ); }
wait( "Hello, closure!" );
複製代碼

如上的代碼,一個很常見的定時器,可是timer函數具備涵蓋wait做用域的閉包,由於此還保留對變量Message的引用。

wait執行1s後,他的內部做用域並不會消失,timer函數依然保持有wait做用域的閉包。

深刻到引擎內部原理中,內置的g工具函數setTimeout持有對一個參數的引用,引擎調用這個函數,在例子中就是內部的timer函數,而詞法做用域在這個過程當中保持完整。這就是閉包。

不管什麼時候何地,若是將函數做爲第一級值類型並處處傳遞,你就會看到閉包在這些函數中的使用。在定時器、事件監聽、Ajax請求、跨窗口通訊或者其餘異步任務中,只要使用回調函數,就在使用閉包。

在經典的for循環中使用閉包

for (var i=1; i<=5; i++) {
     setTimeout( function timer() {
         console.log( i ); }, i*1000 ); }
複製代碼

如上for循環,你們都知道輸出6,畢竟這個做用域中,咱們只有一個i,全部的回調函數都是在這個for循環結束之後才執行的。

若是咱們試圖假設循環中的每個迭代在運行時都會給本身捕獲一個i的副本,可是根據做用域的工做原理,儘管循環中五個函數是在各個迭代中分別定義,可是他們都被封閉在共享的做用域中,所以仍是隻有一個i。

因此回到正題,咱們須要使用閉包,在每個循環中每個迭代都讓他產生一個閉包做用域。

因此咱們代碼修改以下:

for (var i=1; i<=5; i++) { (function() {
         setTimeout( function timer() {
             console.log( i ); }, i*1000 ); })(); }
複製代碼

but!!!你也發現了,這樣並不姓,不是IIFE會產生一個閉包的麼?是的沒錯,可是若是這個IIFE產生的閉包做用域是可空的,那麼將它封裝起來又有什麼意義呢?因此它須要點實質性的東西,讓咱們去使用。

for (var i=1; i<=5; i++) { (function(j) {
         setTimeout( function timer() {
console.log( j ); }, j*1000 ); })( i ); }
複製代碼

固然,如上問題咱們可使用es6中的let來解決。可是這裏就不作過多說明了。你們能夠自行Google。

模塊

這個部分比較簡單好理解,由於閉包能夠很好造成塊級做用域,對內部變量有很好的隱藏。因此天然咱們能夠將其做爲模塊開發的手段。撇開現在的export、import不談

直接看例子就好,操做比較常規

function foo() {
    var something = "cool"; 
    var another = [1, 2, 3];
    function doSomething() {
         console.log( something ); 
    } 
    function doAnother() {
         console.log( another.join( " ! " ) ); 
    }
    return {
        doSomething:doSomething,
        doAnother:doAnother
    }
}
複製代碼

簡單說明下,doSomething和doAnother函數具備涵蓋模塊實例內部做用域的閉包。當經過返回一個含有屬性引用的對象的方式來將函數傳遞到詞法做用域外部,咱們已經創造了能夠觀察和實踐的 閉包條件。

  • 必須有外部的封閉函數,該函數必須至少被調用一次
  • 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或修改私有的狀態。

固然,上面的代碼咱們還能夠寫成IIFE的形式。可是畢竟市場上講解閉包的好文是在太多,這裏咱們就點到爲止。

相關文章
相關標籤/搜索