JS中的做用域、閉包、this機制和原型每每是最難理解的概念之一。筆者將經過幾篇文章和你們談談本身的理解,但願對你們的學習有一些幫助。javascript
上一篇中咱們說了做用域,這一篇咱們談一談閉包。筆者避免了使用JS中一些複雜的概念,僅僅闡述一些必要的概念和原理,避免由於複雜的概念使得閉包讓你們望而生畏。java
閉包是什麼?看似一個晦澀難懂的名詞。MDN上給對閉包的解釋是:面試
A closure is the combination of a function and the lexical environment within which that function was declared.編程
這個說法實在是太不明確了,組合?只要組合了就是閉包麼?若是不是的話?那怎麼組合才能造成閉包呢?數組
咱們再來看看百度百科上對於閉包的定義。閉包
閉包就是可以讀取其餘函數內部變量的函數。框架
彷彿各不相同的說法,那麼,咱們究竟應該如何理解閉包呢?編程語言
首先拋出一個概念,自由變量,就是下文代碼中的foo1()函數內部的變量a,爲何叫它自由變量呢, 由於它既不是它所在函數內的參數,也不是在所在函數的內部建立的。 做爲人咱們可能很好理解a表明着什麼,可是JS引擎怎麼理解這個a呢,確定有一套相應的規則幫助JS引擎理解這個a。JS中的詞法做用域,就是幫助JS引擎理解這個a的一套規則。ide
function foo() {
var a = 1;
function foo1() {
console.log(a);
}
foo1();
}
foo(); // 1
複製代碼
咱們經過咱們掌握的詞法做用域的知識分析一下上面的代碼,全局做用域中定義了foo,foo的做用域中定義了a和foo1,foo1做用域中什麼都沒定義,可是有一個輸出語句,當foo1執行時console.log(a);語句時,會按照做用域從裏到外查找,找到它上層的a的值並打印出來。理論上來講,這個時候,其實已經建立了一個閉包(實際上,在JS中,任何一個擁有了自由變量的函數都是閉包)。因此咱們如今再來看看MDN和百度百科上的定義,其實兩個說的都對,MDN的說法相對抽象一些,百度百科說的也對,由於在JavaScript中,因爲詞法做用域的規則存在,能訪問函數內部的局部變量的只有定義在該函數內部的子函數,因此一些人理解JS中的閉包必定是存在嵌套的函數的,這樣的理解也沒有什麼錯誤。函數
一個值得注意的地方是,閉包是在代碼建立時候產生的,而不是在代碼運行時產生的, 不少人會在這個地方產生誤解。不過毫無疑問的是,因爲閉包的存在致使了JS代碼在運行時能夠產生一些獨特的表現。
用過Vue和React等框架的同窗,對於組件生命週期確定不陌生,其實在代碼的執行過程當中,一段段的代碼也和組件同樣,存在的屬於本身的生命週期。JS代碼的生命週期大體分爲三個階段,內存分配階段、內存使用階段、內存回收階段。其中,明白內存回收機制(垃圾回收機制)對於透徹的理解和使用閉包具備重要的做用。
垃圾回收機制是編程語言中必備的一個機制,代碼運行在內存中,定義的變量毫無疑問的會佔用必定的內存。學習JS的同窗應該能夠直觀的感覺到,JS相較於C/JAVA系列的語言而言,自由了太多(例如在C/JAVA中,若是定義數組的話,初始化的時候須要指定爲這個數組分配的內存大小,它們對於內存的控制相對嚴格的多)。
如此寬鬆的內存分配,若是尚未垃圾回收機制的話,佔用的內存會隨着代碼的增多而越多越多,最後耗盡系統的內存,形成系統的崩潰。JS引擎對於再也不須要的變量佔用的內存資源進行回收,能夠儘量的減小代碼運行時候的內存佔用。
不一樣語言中實現垃圾回收機制的作法各不相同,大致上的實現策略有兩種,一種是標記清除,一種是引用計數。可是對於理解閉包而言,理解兩種垃圾清除策略十分的重要。
標記清除的策略是這樣的,對於進入執行環境的變量,標記爲進入執行環境的狀態,當離開當前執行環境的時候,標記爲離開的狀態,下次垃圾回收器來的時候對離開的狀態的變量釋放內存,即垃圾回收。
引用計數的策略則不一樣,MDN中對於引用計數的描述:
The main notion garbage collection algorithms rely on is the notion of reference. Within the context of memory management, an object is said to reference another object if the former has an access to the latter (either implicitly or explicitly). For instance, a JavaScript object has a reference to its prototype (implicit reference) and to its properties values (explicit reference). In this context, the notion of "object" is extended to something broader than regular JavaScript objects and also contains function scopes (or the global lexical scope).
例如一段代碼中定義了var a={c:1};,而後定義var b=a;,最後令b=1;a=1;。這個時候{c:1}的引用爲0,能夠吧{c:1}進行回收了。事實上,只要一個對象不管是a仍是b ,只要任何一個有訪問另外一個對象{c:1}的權限,就始終不能回收{c:1}對象。這裏的對象不只僅是javascript對象,也包括了做用域。 明白這一點相當重要。
JS中代碼的執行通常分紅兩個環境,一個是建立時環境,即詞法做用域;還有一個就是運行時環境,咱們一般叫它執行環境(執行上下文和執行上下文棧)。JS是單線程的,每進行一個函數調用時就會建立一個執行上下文對象, 將這個對象壓入執行上下文棧中,函數調用結束時則將該執行上下文對象從執行上下文棧中彈出。 這樣確保了單線程的JS在同一時間只在同一個執行上下文中。
首先JS代碼進入全局,建立全局執行上下文對象並壓入棧中。
每當發生一個函數調用,就建立一個執行上下文對象並將這個對象壓棧。
當函數調用完成時,將該函數調用時建立的執行上下文對象出棧。
最後整個代碼執行完畢,彈出全局上下文執行對象。
執行上下文是一個相對抽象的概念,用於標記函數調用的過程。
明白了JS中的垃圾回收機制和執行上下文機制,咱們再來分析一下第一段代碼:
function foo() {
var a = 1;
function foo1() {
console.log(a);
}
foo1();
}
foo(); // 1
複製代碼
詞法做用域:
全局(global)=> foo => foo1
執行上下文棧的操做順序:
Global Execution Context(push) => foo Execution Context(push) => foo1 Exection Context(push)
=> foo1 Execution Context(pop) => foo Execution Context(pop) => Global Execution Context
內存管理:
給foo分配內存 => foo 函數調用 => 給foo1,a分配內存 => foo1 函數調用 => foo1 調用結束 => 釋放foo1的內存 => foo調用結束 => 釋放a的內存 => 全局調用結束 => 釋放foo內存
怎麼樣,沒什麼特別的吧,咱們再來看看第二段代碼:
function foo() {
var a = 1;
function foo1() {
console.log(a);
}
return foo1;
}
var b = foo();
b();
複製代碼
第二段代碼的詞法做用域和執行上下文棧的操做順序都是同樣的,可是在內存管理上略有不一樣。
內存管理
給foo,b分配內存 => foo 函數調用 => 給foo1,a分配內存 => 將foo1的引用返回給b => foo 函數調用結束 => b(foo1) 函數調用 => foo1調用結束 => 釋放foo1, a的內存 => 全局調用結束 => 釋放foo, b的內存
這裏說明一下,JS中的函數使用是引用方式傳遞的,return foo1; 是將指向foo1函數的指針返回給b,因此b()其實是foo1();
看出來區別了麼,由於foo 中的return將foo1返回到全局變量b中,因此b始終經過foo1的做用域保持着對局部變量a引用(參考上面垃圾回收引用計數策略的定義),在b執行結束前都不會釋放a的內存。
除了return以外,還有一種常見的寫法會致使局部變量的內存沒法釋放,那就是將函數做爲參數,這種寫法經常在回調函數中見到。
function foo () {
var a = 1;
setTimeout(function foo1 () {
console.log(a);
},1000);
setTimeout(function () {
console.log(a);
},1000);
}
foo();
複製代碼
說明一下,具名函數和匿名函數的差異僅僅在是否能夠調用自身上已是否方便定位錯誤,除此以外並沒什麼區別。這裏foo1內部存在自由變量a,因此是一個閉包。setTimeout是一個全局的方法,因此實際上,foo1被做爲參數傳到了全局方法中,在setTimeout方法執行完成前,始終保持着對foo內部做用域的引用,a的內存也不會被釋放。
第二段和第三段寫法的閉包都具共同的特徵,就是局部函數的變量和參數不會被垃圾回收,是常駐內存的!咱們暫且稱呼它們爲特殊的閉包。
普通的閉包的存在看似毫無用處,特殊的閉包看似有百害無一益,實際上,只要運用獲得,特殊的閉包也能夠很強大。特殊的閉包可讓局部變量常駐內存,同時避免局部變量污染了全局變量,使得設計私有的方法和變量成爲可能!
特殊閉包的主要用法一:函數工廠
function foo(value1) {
return function v2(value2) {
console.log(value1 * value2);
}
}
var a = foo(2);
var b = foo(3);
a(2); // 4
b(2); // 6
複製代碼
特殊閉包的主要用法二:設計私有變量和方法
function foo() {
var value = 0;
function addOne() {
value += 1;
}
return {
addOne: addOne,
getValue: function getValue() {
console.log(value);
}
}
}
var b = foo();
var c = foo();
b.addOne();
b.getValue(); // 1
c.getValue(); // 0
複製代碼
特殊閉包的主要用法三:函數柯里化
函數柯里化和閉包的結合並不簡單,涉及到求值策略和編程思想的轉換,筆者將在後續的文章中單獨介紹這一部分的用法。
特殊的閉包還有不少其餘的用法,這裏就不一一列舉了,有興趣的讀者能夠自行百度。
在ES6出現以前,閉包的做用就是模擬塊級做用域,說到模擬塊級做用域,則和當即執行函數分不開。
什麼是當即執行函數?
(function foo() {
...
})()
!function foo() {
...
}()
+function foo() {
...
}
複製代碼
首先把函數聲明或者匿名函數加上一些特殊運算符,如加上()、+、!等,將其變成函數表達式,再在後面加上()表示當即執行,就建立了一個當即執行函數。當即執行函數內部定義的方法和變量不會污染全局變量。
for(var i=0;i<10;i++) {
setTimeout(function(){
console.log(i);
},1000);
}
複製代碼
上面是很常見的一道面試題,延時函數中的方法在循環結束以後纔會執行,因爲沒有塊級做用域,全部的setTimeout中的回調函數在閉包的做用下共享一個全局變量i,這個i的值是10。因此打印出來的結果是10個10。
這個時候就須要用一個東西捕獲每一個循環中i的值,放在本身的做用域中。當即執行函數恰好派上了用場。
for(var i=0;i<10;i++) {
(function (i) {
setTimeout(function() {
console.log(i)
},1000);
})(i);
}
複製代碼
上面代碼中回調函數打印的i其實是每一次循環中當即執行函數捕獲的i的副本。
有同窗會說,我把setTimeout的延時改爲0是否是就能夠了?
for(var i=0;i<10;i++) {
setTimeout(function(){
console.log(i);
},0);
}
複製代碼
事實上並不行,仍然打印出來的仍是10個10,爲何呢?由於JS是單線程的,只能爲setTimeout維護了一個單獨的隊列,當前任務處理完了纔會處理setTimeout隊列中的內容,因此setTimeout的時間參數並非相對於調用該函數的時間差,仍是相對於開始執行setTimeout隊列時的時間差。
碼字不易,若是喜歡就點個贊吧~