本文和你們聊聊閉包,閉包與變量對象和做用域鏈有着比較多的聯繫,在閱讀本文前,你們須要理解執行上下文、變量對象以及做用域鏈等內容,這些內容對理解閉包的本質有很大的幫助,前面的兩篇文章已經梳理過了,不清楚的同窗能夠先閱讀以前的文章。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
供外部應用使用關於閉包的應用,在這裏先不作展開,由於裏面也有不少本身不太清楚的東西,例如函數式編程,目前本身也不太熟悉,裏面還涉及不少其餘的知識,關於閉包的應用這塊內容暫時不作詳細的輸出,避免不懂裝懂,在這裏先梳理閉包有哪些應用,後期對柯里化、模塊化封裝等內容另外作文字輸出。