標籤:引擎 請求 timer 訪問規則 原理 抽象 完全 回調 函數
一、編譯過程常見編譯性語言,在程式碼執行之前會經曆三個步驟,稱為編譯。步驟一:分詞或者詞法分析將由字元組成的字串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元。例子:
var a = 2;
這一句通常被分解成為下面這些詞法單元:var 、a 、 = 、2、; 。 步驟二:解析或者文法分析將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程式文法結構的樹。這個樹被稱為“抽象文法樹”(Abstract Syntax Tree, AST)例子:var 、a 、 = 、2、; 會產生類似與下面的文法樹:
步驟三:代碼產生
將 抽象文法樹 (AST)轉換為可執行代碼。
然而對於解釋型語言(例如JavaScript)來說,通過詞法分析和文法分析得到文法樹,沒有產生可執行檔的這一過程,就可以開始解釋執行了。
對於 var a = 2; 進行處理的時候,會有 引擎、編譯器、還有範圍的參與。
引擎:從頭到尾負責整個 Javascript 程式的編譯及執行過程。
編譯器:負責文法分析及代碼產生等。
範圍:負責收集並維護由所有聲明的標識符(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符(變數)的存取權限。
他們是這樣合作的:
首先編譯器會進行如下處理:
1、var a,編譯器會從範圍中尋找是否已經有一個該名稱的變數存在於同一個範圍的集合中。如果是,編譯器會自動忽略該聲明,繼續進行編譯;否則它會要求範圍在當前範圍的集合中聲明一個新的變數,並命名為 a 。
2、接下來編譯器會為引擎產生運行時所需的代碼,這些代碼用來處理 a = 2 這個賦值操作。引擎運行時會首先從範圍中尋找 當前範圍集合中是否存在 變數 a。如果有,引擎就會使用這個變數。如果沒有,引擎就會繼續向上一級範圍集合中尋找改變數。
然後 如果引擎最終找到了 變數 a,就賦值 2 給它。如果沒有找到,就會拋出一個異常。
總結:
變數的賦值操作分兩步完成:第一步 由編譯器在範圍中聲明一個變數(前提是之前沒有聲明過),第二步 是在運行時引擎會在範圍中尋找該變數,如果可以找到,就對其賦值。
二、範圍
1、RL 查詢
在上一部分我們說到了,引擎會對變數 a 進行尋找。而尋找分為兩種,一是 LHS(Left-Hand-Side) 查詢,二是 RHS(Right-Hand-Side) 查詢。
LHS 查詢:試圖找到變數的容器本身,從而可以對其賦值。也就是尋找 變數 a 。
RHS 查詢:尋找某個變數的值。尋找變數 a 的值,即 2。
例子:
console.log(a); // 這裡對 a 是一個 RHS 查詢,找到 a 的值,並 console.log 出來。
a = 2; // 這裡對 a 是一個 LHS 查詢,找到 變數 a,並對其賦值為 2 。
function foo(a){ console.log(a); // 2}foo(2); // 這裡首先對 foo() 函數調用,執行 RHS 查詢,即找到 foo 函數,然後 執行了 a = 2 的傳參賦值,這裡首先執行 LHS 查詢 找到 a 並賦值為 2,然後 console.log(a) 執行了 RHS 查詢。
這裡還要說一下 範圍的嵌套:當一個塊或函數嵌套在另一個塊或函數中時,就發生了所用的嵌套。遍曆尋找嵌套範圍,是首先從當前範圍中尋找變數,如果找不到,就像上一級繼續尋找,當抵達全域範圍時,無論找到還是沒有找到,尋找都將結束。 如果 RHS 尋找在所有嵌套範圍中都沒有找到所需變數,引擎就會拋出 ReferenceError。如果找到了所需變數,但你想要進行不合理的操作,比如對非函數類型的值進行調用等,引擎就會拋出 TypeError 。如果 LHS 尋找在頂層全域範圍中都沒有找到所需變數,如果是在非strict 模式下,全域範圍會建立一個具有該名稱的變數,並將其返回給引擎,如果是在strict 模式下,引擎就會拋出 ReferenceError。 ReferenceError 和 TypeError 是比較常見的異常,你需要知道它們的不同,對你排除程式問題有很大協助。 2、詞法範圍由你在寫代碼時將變數和塊範圍寫在哪裡來決定的。例子:
function foo(a){ var b = a*2; function bar(c){ console.log(a,b,c); } bar(b*3);}foo(2); // 2,4,12
在這段代碼中有三層範圍,嵌套:
範圍1:包含著全域範圍,其中有標識符:foo.範圍2:包含著 foo 所建立的範圍,其中有三個標識符:a、 b、 bar。範圍3:包含著 bar 所建立的範圍,其中有標識符:c。 在尋找變數時,範圍尋找會在找到第一個匹配的標識符時停止。而且它只尋找一級標識符,比如a 、b、c,而對於 foo.bar.baz ,詞法範圍只會尋找 foo 標識符,找到這個變數之後,對象屬性訪問規則會分別接管對 bar 和 baz 屬性的訪問。 這裡還要說一點,全域變數會自動成為全域對象的屬性,所以可以間接的通過全域對象屬性的引用來對其進行訪問。
window.a
3、提升變數和函數在內的所有聲明都會在任何代碼被執行前首先被處理。舉個例子:當你看到 var a = 2; 時,可能會認為這是一個聲明,但實際上 Javascript 會將其看成兩個聲明:var a ; 和 a = 2;並且在不同階段執行。var a 是在編譯階段進行的,而 a = 2 會被留在原地等待執行階段。這個過程就好像變數和函式宣告從它們在代碼中出現的位置被“移動”到了最上面,這個過程就叫做變數提升。 例子:
foo();function foo(){ console.log(a); var a = 2;}
學了上面的知識,你應該可以猜到 foo() 可以正常執行,而 console.log(a) 會打出 undefined; 原因是當把提升應用到上面代碼,代碼就相當於 下面的形式:
function foo() { var a; console.log(a); a = 2; }foo();
對於變數提升要注意另外兩個知識點:1、函式宣告會被提升,而函數運算式卻不會被提升。區分函式宣告和函數運算式最簡單的方式是看 function 關鍵字出現在聲明中的位置。如果 function 時聲明中的第一個詞,那麼就是函式宣告,否則就是一個函數運算式。例子:函式宣告:
function foo() { var a; console.log(a); a = 2; }
函數運算式:
var foo = function() { var a; console.log(a); a = 2; }(function foo(){ var a; console.log(a); a = 2; })();
那麼對於提升,來看個例子:
foo(); // 報TypeError錯誤var foo = function() { var a; console.log(a); a = 2; }
這段代碼相當於
var foo;foo(); // 此時 foo 為 undefined,而我們嘗試對它進行函數式調用,屬於不合理操作,報 TypeError 錯誤。foo = function() { var a; console.log(a); a = 2; }
2、函數會被優先提升,然後是變數。例子:
foo(); // 1 var foo;function foo(){ console.log(1);}foo = function(){ console.log(2);}
會輸出 1 為不是 2,這段代碼提升之後相當於:
function foo(){ console.log(1);}foo(); foo = function(){ console.log(2);}
注意,var foo 儘管出現在 function foo() 之前,但它是重複的聲明,因為函式宣告會被提升到普通變數之前。重複的 var 聲明會被忽略,但出現在後面的函式宣告卻會覆蓋前面的。例子:
foo(); // 3function foo(){ console.log(1);}var foo = function(){ console.log(2)}function foo(){ console.log(3)}
三、閉包所謂 閉包:當函數可以記住並訪問所在的詞法範圍時,就產生了閉包,即使函數是在當前詞法範圍之外執行。例子:
function foo(){ var a = 2; function bar(){ console.log(a); } return bar;}var baz = foo();baz(); //2
例子中,通過調用 baz 來調用 foo 內部的 bar , bar 在自己定義的詞法範圍以外的地方執行,在 foo 執行之後,通常會期待 foo 的整個內部範圍被銷毀,因為引擎的記憶體回收行程會釋放不再使用的記憶體空間。看上去 foo 不再被使用,所以很自然的考慮到對其進行回收,然而閉包就是阻止這樣的事情發生,事實上內部範圍依然存在,沒有被回收,因為 bar 依然在使用該範圍。bar 擁有 涵蓋 foo 內部範圍的閉包,使得該範圍能夠一直存活,以供 bar 在之後任何時間進行引用。bar 依然持有對該範圍的引用,而這個引用就叫作 閉包。bar 在定義時的詞法範圍以外的地方被調用,閉包使得函數可以繼續訪問定義時的範圍。 在舉個例子:
function wait(message){ setTimeout(function timer(){ console.log(message) },1000)}wait("hi");
timer 具有 涵蓋 wait 範圍的閉包,保有對 message 的引用。wait 執行 1s 後,它的內部範圍不會消失,timer 依然保有 wait 範圍的閉包,所以 可以獲得 message 並 console.log,這就是閉包。 也就是說,如果將函數作為第一級的實值型別併到處傳遞,你就會看到閉包在這些函數中的應用。像定時器、事件監聽器、Ajax 請求、跨視窗通訊或者任何非同步任務中,只要使用了回呼函數,實際上就是在使用閉包。例子:
function foo(){ var a = 2; function baz(){ console.log(a); // 2 } bar(baz);}function bar(fn){ fn(); //閉包}
內建函式 baz 傳遞給 bar,當在 bar 中調用 baz時,就可以訪問到他定義時所在的範圍中的變數,console.log 出 a。 例子:
for (var i=0;i<=5;i++){ setTimeout(function timer(){ console.log(i) },i*1000)}
我們預期輸出數次1-5,每秒一次,每次一個。然而 真正的 輸出結果卻是 五個 6。原因是 timer 在 迴圈結束之後即 i 等於 6 時, 才執行,就算你將 setTimeout 的時間 設為 0 ,回呼函數也會在迴圈結束之後才執行。那麼我們應該怎麼解決呢?我們希望每次迴圈時,timer 都會給自己捕獲一個 i 的副本,然而根據範圍的原理,實際情況卻是 儘管 迴圈的 5 個函數是在各個迭代中被分別定義的,但是它們都封閉在一個共用的全域範圍中,因此實際上只有一個 i,所有函數共用一個 i 的引用。如果將延遲函數的回調重複定義5次,不使用迴圈,那它同這段代碼完全等價的。所以解決辦法就是 迴圈的過程中每個迭代都需要一個閉包範圍。而 立即執行函數 正好可以做到這一點。
for (var i=0;i<=5;i++){ (function(i){ setTimeout(function timer(){ console.log(i) },i*1000) })(i)}
在迴圈中使用 立即執行函數會為每個迭代產生一個新的範圍,使得延遲函數的回調可以將新的範圍封閉在每個迭代的內部,每個迭代都會含有一個正確的 i 等待 console。 問題解決。 現在 你應該真正的明白 範圍和閉包了,找點題做吧,加深一下印象,不然你還會回來的。 學習並感謝《你不知道的JavaScript》上卷 (炒雞推薦大家看)
【 js 基礎 】範圍和閉包