變量提升
實際上變量和函數聲明在代碼里的位置是不會變的,而且是在編譯階段被 JAVAScript 引擎放入內存中,一段 JavaScript 代碼在執行之前需要被 JavaScript 引擎編譯,編譯完成之后,才會進入執行階段。大致流程為:JavaScript 代碼片段 ——> 編譯階段 ——> 執行階段—>。
編譯階段,每段執行代碼會分為兩部分,第一部分為變量提升部分的代碼,第二部分為執行部分的代碼。經過編譯后,生成執行上下文(Execution context)和 可執行代碼。
執行上下文 是 JavaScript 執行一段代碼時的運行環境,比如調用一個函數,就會進入函數的執行上下文,從而確定該函數執行期間用到的如 this、變量、對象以及函數等。
執行上下文由 變量環境(Variable Environment) 和 **詞法環境(Lexical Environment)**對象 組成,變量環境保存了代碼中變量提升的內容,包括 var 定義和 function 定義的變量。而詞法環境保存 let 和 const 定義塊級作用域的變量。
塊級作用域就是通過詞法環境的棧結構來實現的,而變量提升是通過變量環境來實現,通過這兩者的結合,JavaScript 引擎也就同時支持了變量提升和塊級作用域了
變量查找過程:沿著詞法環境的棧頂向下查詢,如果在詞法環境中的某個塊中查找到了,就直接返回給 JavaScript 引擎,如果沒有查找到,那么繼續在變量環境中查找。
變量聲明提升補充:
- var的創建和初始化被提升,賦值不會被提升。
- let的創建被提升,初始化和賦值不會被提升。
- function的創建、初始化和賦值均會被提升。
調用棧
調用棧是用來管理函數調用關系的一種數據結構。在函數調用的時候,JavaScript 引擎會創建函數執行上下文,而全局代碼下又有一個全局執行上下文,這些執行上下文會使用一種叫棧的數據結果來管理。
所以 JavaScript 的調用棧,其實就是 執行上下文棧 。舉例代碼執行,入棧如圖所示:
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
調用棧既然是一種數據結構,所以是存在大小的,超出了棧大小就會出現棧溢出報錯,比如斐波那契數列,執行10000次,超過了最大棧調用大小(Maximum call stack size exceeded)。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(10000) // Maximum call stack size exceeded
該函數是遞歸的,雖然只有一種函數調用,但是還是會一直創建執行上下文壓入調用棧中,導致超過最大調用棧大小報錯,可以通過 Chrome 調式看到 Call Stack 的情況
總結:
- 每調用一個函數,JavaScript 引擎會為其創建執行上下文,并把該執行上下文壓入調用棧,然后 JavaScript 引擎開始執行函數代碼。
- 如果在一個函數 A 中調用了另外一個函數 B,那么 JavaScript 引擎會為 B 函數創建執行上下文,并將 B 函數的執行上下文壓入棧頂。
- 當前函數執行完畢后,JavaScript 引擎會將該函數的執行上下文彈出棧。
- 當分配的調用棧空間被占滿時,會引發“堆棧溢出”問題。
所以,斐波那契數列函數優化的手段就是使用循環來減少函數調用,從而減少函數執行上下文的創建壓入棧的情況,就可以解決棧溢出的報錯了。(遞歸尾部優化無法解決問題,Chrome瀏覽器還是棧溢出),使用蹦床函數來解決:
function runStack (n) {
if (n === 0) return 100;
return runStack.bind(null, n- 2); // 返回自身的一個版本
}
// 蹦床函數,避免遞歸
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
trampoline(runStack(1000000))
可以看到,調用棧中一直是保持3個執行上下文而已,多余的都及時的pop掉了。
作用域鏈
每個執行上下文的變量環境中,都包含了一個外部引用,用來指向外部的執行上下文,我們把這個外部的引用稱為 outer。
當一段代碼使用一個變量是,JavaScript 引擎首先會在“當前的執行上下文”中查找該變量,如果找不到就會繼續在 outer 所指向的執行上下文中查找。我們把這個查找的鏈條就稱為作用域鏈。
詞法作用域
詞法作用域就是指作用域是由代碼中函數聲明的位置來決定的,所以詞法作用域是靜態的作用域,通過它就能夠預測代碼在執行過程中如何查找標識符。詞法作用域是代碼階段決定好的,和函數是怎么調用的沒有關系。
塊級作用域中的變量查找
- 從當前執行上下文的詞法環境,自頂向下查找(棧中的內存塊),然后再從當前執行向下文中的變量環境中查找;
- 查找不到,則繼續在outer指向的執行上下文繼續依次先從詞法環境,再到變量環境查找。
閉包
有詞法作用域的規則可以知道,內部函數總是可以訪問他們的外部函數中的變量,當外部函數執行完畢后,pop stack了,遺留下了外部環境形成的閉包 Closure 環境,該環境內存中還保存著那些可以訪問的變量,類似一個專屬背包,除了內部函數訪問,氣氛方式無法訪問該專屬背包,我們就包這個背包稱為外部函數的閉包(那些內部函數引用外部函數的變量依然保存在內存中,我們把這些變量的集合稱為閉包)。
閉包是怎么回收的
如果引用閉包的函數是一個全局變量,那么閉包會一直存在知道頁面關閉;如果這個閉包以后不再使用的話,就會造成內存泄漏。
如果引用閉包的函數是一個局部變量,等函數銷毀后,下次 JavaScript 引擎執行垃圾回收時,判斷閉包這塊內容如果不再被使用了,那么 JavaScript 引擎的垃圾回收器就會回收這塊的內存。
使用閉包的原則:如果閉包會一直使用,那么它可以作為全局變量而存在;但如果使用頻率不高,而且占用內存有比較大的話,那就盡量讓它成為一個局部變量。
this
let a = { name: 'this解釋' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'this解釋''
參考資源:《瀏覽器的工作原理與實踐》極客時間-李兵