在看本篇文章以前,能夠先看一下以前的文章 深刻理解JavaScript 執行上下文 和 深刻理解JavaScript做用域,理解執行上下文和做用域對理解閉包有很大的幫助。javascript
須要回憶的一些知識點:html
JavaScript
是基於詞法做用域的語言,經過變量定義的位置就能知道變量的做用域。[[Scope]]
。而後,使用 this
、arguments
和其餘命名參數的值來初始化函數的活動對象。但在做用域中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,...直至做用做用域鏈終點的全局執行環境。閉包差很少是面試必問的一個知識點了,記得幾年前剛出來找實習的時候問的是這個,如今出去面試仍是一直在問這個。頗有必要好好學習一下,不只僅是由於面試,更是由於它在代碼中也很是常見。前端
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行的。java
function foo() { var a = 1; // a 是一個被 foo 建立的局部變量 function bar() { // bar 是一個內部函數,是一個閉包 console.log(a); // 使用了父函數中聲明的變量 } return bar(); } foo(); // 1
foo() 函數中聲明瞭一個內部變量 a , 在函數外部是沒法訪問的,bar() 函數是 foo() 函數內部的函數,此時 foo 內部的全部局部變量,對 bar 都是可見的,反過來就不行,bar 內部的局部變量,對 foo 就是不可見的。這就是javaScript特有的」做用域鏈「。面試
function foo() { var a = 1; // a 是一個被 foo 建立的局部變量 function bar() { // bar 是一個內部函數,是一個閉包 console.log(a); // 使用了父函數中聲明的變量 } return bar; } const myFoo = foo(); myFoo();
這段代碼和上面的代碼運行結果徹底一致,其中不一樣的地方就是在於內部函數 bar 在執行前,從外部函數返回。foo()
執行後,將其返回值(也就是內部的 bar
函數)賦值給變量 myFoo
並調用 myFoo()
, 實際上只是經過不一樣的標識符引用調用了內部的函數 bar()
。閉包
foo()
函數執行後,正常狀況下 foo()
的整個內部做用域被銷燬,佔用的內存被回收。可是如今的 foo
的內部做用域 bar()
還在使用,因此不會對其進行回收。bar() 依然持有對改做用域的引用,這個引用就叫作閉包。這個函數在定義的詞法做用域之外的地方被調用。閉包使得函數能夠繼續訪問定義時的詞法做用域。app
用一句話描述:閉包是指有權訪問另外一個函數做用域中變量的函數。建立閉包最多見的方式就是,在一個函數內部建立另外一個函數。異步
function foo(a) { setTimeout(function timer(){ console.log(a) }, 1000) } foo(2);
foo
執行1000ms
後,它的內部做用域不會消失,timer
函數依然保有 foo
做用域的引用。timer函數就是一個閉包。函數
定時器,事件監聽器,Ajax
請求,跨窗口通訊,Web Workers
或者其餘異步或同步任務中,只要使用回調函數,實際上就是閉包。post
for(var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, i * 1000); }
上面的這段代碼,預期是每隔一秒,分別輸出 0, 1, 2, 3, 4
, 但實際上依次輸出的是 5, 5, 5, 5, 5
。首先解釋5是從哪裏來的,這個循環的終止條件是 i
再也不 < 5
,條件首次成立時 i
的值是5
,所以,輸出顯示的是循環結束時 i 的最終值。
延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即便每一個迭代中執行的都是 setTimeout(.., 0)
,全部的回調函數依然是在循環結束後纔會被執行。所以每次輸出一個 5來。
咱們的預期是每一個迭代在運行時都會給本身 "捕獲" 一個 i
的副本。可是實際上,根據做用域的原理,儘管循環中的五個函數都是在各自迭代中分別定義的,可是他們都封閉在一個共享的全局做用域中,所以實際上只有一個 i
。即全部函數共享一個 i
的引用。
for(var i = 0; i < 5; i++) { (function(j){ setTimeout(() => { console.log(j); }, j * 1000); })(i) }
代碼改爲上面這樣,就能夠按照咱們指望的方式進行工做了。這樣修改以後,在每次迭代內使用 IIFE(當即執行函數)會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代內部都會含有一個具備正確值的變量能夠訪問。
for(let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, i * 1000); }
使用 ES6 塊級做用域的 let 替換 var 也能夠達到咱們的目的。
爲何老是 JavaScript 中閉包的應用都有着關鍵詞 「return」, javaScript 祕密花園 中有一段話解釋到:閉包是JavaScript 一個很是重要的特性,這意味着當前做用域老是可以訪問外部做用域的變量。 由於函數是 JavaScript 中惟一擁有自身做用域的結構,所以閉包的建立依賴於函數。
容易致使內存泄漏。
閉包會攜帶包含它的函數做用域,所以會比其餘函數佔用更多的內存。過分使用閉包會致使內存佔用過多,因此要謹慎使用閉包。
在閉包中使用 this
對象。
this對象是運行時基於函數的執行環境綁定的。全局函數中,this指向 window,當函數被做用某個對象的方法調用時,this指向這個對象,不過匿名函數的執行環境具備全局性,所以其this對象一般指向window。以前這篇 一文理解this、call、apply、bind文章中也專門講了this。
var name = 'The window'; var object = { name: 'my Object', getName: function() { return function() { return this.name; } } } console.log(object.getName()()); // The window 非嚴格模式下
getName()
,它返回一個匿名函數,而匿名函數又返回 this.name
。getName
返回一個函數,所以調用 object.getName()()
會當即調用它返回的函數。結果就是返回字符串 「The window 」,即全局 name 變量的值。爲何匿名函數沒有取得包含做用域的this對象呢?每一個函數在被調用時會自動獲取兩個特殊的變量: this
, arguments
。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數的這兩個變量。
不過把外部做用域中的 this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。
var name = 'The window'; var object = { name: 'my Object', getName: function() { var that = this; // 把this對象賦值給了 that變量 return function() { return that.name; } } } console.log(object.getName()()); // my Object
上面代碼中把this對象賦值給了 that
變量,that
變量時包含在函數中的,即時函數返回以後,that也仍然引用這 object,因此調用 object.getName()()
返回 「my Object」
arguments 和 this存在相同的問題,若是想訪問做用域中的 arguments 對象,必須將對該對象的引用保存到另外一個閉包可以訪問的變量中。
有幾種特殊狀況下,this的值可能會意外地發生改變。好比下面的代碼是修改其前面例子的結果。
var name = 'The window'; var object = { name: 'my Object', getName: function() { return this.name } } console.log(object.getName()); // my Object console.log((object.getName)()); // my Object console.log((object.getName = object.getName)()); // The window 非嚴格模式下
「my Object」
"my Object"
this
指向的是 window
,打印的是 "The window"
關於什麼是閉包就大概說到這裏,下一篇文章會講一下閉包的應用場景。