1. 程式人生 > >javascript在詞法環境中註冊識別符號

javascript在詞法環境中註冊識別符號

JavaScript作為一門程式語言,其設計的基本原則是易用性。這也是不需要指定函式返回值型別、函式引數型別、變數型別等主要原因。你已經瞭解到JavaScript是逐行執行的。

檢視程式碼如下:

firstRonin = 'Kiyokawa';

secondRoin = 'Knodo';





將Kiyokawa賦值給firstRonin,將Knodo賦值給標識給識別符號secondRoin。對比下面程式碼:

console.log("------------------------在詞法環境中註冊識別符號--------------------");

const firstRoin = 'Kiyokawa';

check(firstRoin);

function check(ronin) {

if (ronin === 'Kiyokawa') {

console.log("The ronin was checked!");

}

}

 

在上面的程式碼中,我們將Kiyokawa賦值給firstRoin,然後呼叫check函式,傳入引數firstRoin。先等一下,如果JavaScript是逐行執行的,我們可以呼叫check函式嗎?函式還沒有執行到函式的check的宣告,所以javascript引擎不應該認識check函式。

註冊識別符號的過程

除了易用性,程式碼逐行執行,JavaScript引擎是如何知道check函式存在的?JavaScript程式碼的執行事實上是分兩個階段進行的。

一旦建立了新的詞法環境,就會執行第一階段。在第一階段,沒有執行程式碼,但是JavaScript引擎會訪問並註冊在當前詞法環境中所宣告的變數和函式。javascript在第一階段完成之後,開始第二階段,具體如何執行取決於變數的型別(let、var、const和函式宣告)以及環境型別(全域性環境、函式環境或塊級作用域)。

具體的處理過程如下:

1.如果是建立一個函式環境,那麼建立形參及函式引數的預設值。如果是非函式環境,將跳過此步驟。

2.如果是建立全域性或函式環境,就掃描當前程式碼進行函式宣告(不會掃描其他函式的函式體),但是不會執行函式表示式或箭頭函式。對於所找到的函式宣告,將建立函式,並繫結到當前環境與函式名相同的識別符號上。若該識別符號已經存在,那麼該識別符號將被重寫。如果是塊級作用域,將跳過此步驟。

3.掃描當前程式碼進行變數宣告。在函式或全域性環境中,查詢所有當前函式以及其他函式之外通過var宣告的變數,並查詢所有通過let或const定義的變數。在塊級環境中,僅查詢當前塊中通過let或const定義的變數。對於所查詢到的變數,若該識別符號不存在,進行註冊並將其初始化為undefined。若識別符號已經存在,將保留其值。

 

整個處理過程如下圖所示:

 

 

在函式宣告之前呼叫函式

JavaScript易用性的一個典型特徵,函式的宣告順序無關緊要。在JavaScript中,我們可以在函式宣告前對其進行呼叫。

console.log('-----------------------在函式宣告之前訪問函式----------------');
//若函式是作為函式宣告進行定義的,則可以在函式宣告之前訪問函式
if (typeof fun === 'function') {
  console.log("fun is a function even through its definition is not reached yet!");
}
//若函式是通過函式表示式或箭頭函式進行定義的,則不可以在函式定義之前訪問函式
if (typeof myFunExp === 'undefined') {
  console.log("But we cannot access function expressions.");
}

if (typeof myArrow === 'undefined') {
  console.log("Nor arrow functions");
}

//作為函式宣告進行定義
function fun() {};

//myFunExpr指向函式表示式
var myFunExpr = function () {};

//myArrow指向箭頭函式
var myArrow = (x) => x;

 

我們甚至可以在函式定義之前訪問函式。我們可以這麼做的原因是fun是通過函式宣告進行定義的,第二階段表明函式已經通過宣告進行定義,在當前詞法環境建立時已在其他程式碼執行之前註冊了函式識別符號。所以,在執行函式呼叫之前,fun函式已經存在。

JavaScript引擎通過這種方式為開發者提供便利,允許我們直接使用函式的引用,而不需要強制指定函式的定義順序。在程式碼執行之前,函式已經存在了。

需要注意的是,這種情況僅針對函式宣告有效。函式表示式與箭頭函式都不在此過程中,而是在程式執行過程中執行定義的。這就是不能訪問myFunExp與myArrow函式的原因。

函式過載

第二個難題是處理過載函式識別符號的問題。
console.log('---------------------過載函式識別符號--------------------');
//fun指向一個函式
if (typeof fun === 'function') {
  console.log('We access the function.');
}
//定義變數fun並賦值為數字3
var fun = 3;
//fun指向一個數字
if (typeof fun === 'number') {
  console.log('Now we access the number.');
}
//函式宣告
function fun() {}

//fun仍然指向數字
if (typeof fun === 'number') {
  console.log('Still a number.');
}

 

 

上述程式碼中,宣告的變數與函式均使用相同的名字fun。通過Log發現:三個if語句都通過了,第一個if語句中,識別符號fun指向一個函式。第二個if語句中,識別符號fun指向一個數字。

JavaScript的這種行為是由識別符號註冊的結果直接導致的。通過函式宣告進行定義的函式在程式碼執行之前對函式進行建立,並賦值給對應的識別符號;在第3步中,處理變數的宣告,那些在當前環境中未宣告的變數,將被賦值為undefined。

在上述程式碼中,註冊函式宣告時,由於識別符號fun已經存在,並且未被賦值給undefined。這就是第1個測試fun是否是函式的if語句執行通過的原因。

之後,執行賦值語句var fun = 3,將數字3賦值給識別符號fun。執行完這個賦值語句後,fun函式就不再指向函數了,而是指向數字3。

在程式的實際執行過程中,跳過了函式宣告部分,所以函式的宣告不會影響識別符號fun的值。

變數提升(variable hoisting)

如果你已閱讀關於解釋處理識別符號的一些JavaScript部落格或圖書,你可能已經遇到這個詞:變數提升。例如,變數的宣告提升至函式頂部,函式的宣告提升至全域性程式碼頂部。

但是,正如上述例項中看見的,並沒有那麼簡單。變數和函式的宣告並沒有實際發生移動。只是在程式碼執行之前,先在詞法環境中進行註冊。雖然描述為提升了,並且進行了定義,這樣更容易理解JavaScript的作用域的工作原理,但是,我們可以通過詞法環境對整個處理過程進行更深入地理解,瞭解真正的原理。

 

參考《JavaScript忍者祕籍》