1. 作用域
Javascript作用域問題簡單的可以用一句話來概括:兩種類型的作用域( global, function)、三條作用域鏈。
理解兩種三條前需要明白兩點:
1) JavaScript是詞法作用域,當一個函數被定義後,它的作用域鏈就作為函數的內部狀態被保存了。
2) 在函數被定義時雖然作用域鏈確定了,但作用域鏈中的屬性並沒有確定。
兩種類型的作用域:
1) 全局作用域 -- 變量為window對象的屬性(默認浏覽器環境中)。 <作用域鏈:window>
2) 函數作用域-- 變量查找的的順序(即作用域鏈)< 當前call object -- .... -- window object>
特殊的一條作用域鏈:
通過HTML屬性定義的事件處理代碼:<匿名函數的call object -- 當前節點 -- 父節點--...-- window object>
--------------------------------------------------------------------------------------------------------
2. this
可 以簡單概括為: "this" is a keyword not a property of any object, that refer to one object depending on the scope. In the global scope, it refers to the window object. In the function scope it refers to the object that has the function as its property.
事例說明:為什麼用原始的事件注冊時,事件處理函數內的this引用注冊該事件的元素?
原因-- 原始事件注冊實質是把事件處理程序作為該元素的一個屬性(方法)。
到現在都還糊塗的一個問題:
function outer(){
function inner(){
alert( this );
}();
}
-----------------------------結果為 window -------------------------------------------------
Why "this" refer to the window object, the function inner is not the property of window.-_-
猜想: 是不是當一個函數不作為其它對象的屬性時,默認this引用window?
----------------------------------------------------------------------------------------------------
10/9
通過詢問Arrix,猜想正確
Js代碼 ar arg = 1;
function funcTest() {
alert(arg);
var arg = 2;
}
arg = 10;
funcTest();
ar arg = 1;
function funcTest() {
alert(arg);
var arg = 2;
}
arg = 10;
funcTest();
答案是什麼?如果是5年前的我,肯定不會再往下想了,還是10!這麼簡單的問題還用想什麼呀?我的理解是這樣的:funTest函數是一個無參數的函數,函數內部通過alert方法,調用外部(全局)的變量arg,在函數執行前,arg賦值為10,彈出arg值後改變arg值為2,所以彈出值為10。
真的是10嗎?是還是不是?
測試的結果:彈出“undefined”,瀑布汗.
二、理解作用域鏈,從javas
cript運行機制說起
1、js的運行順序
如果一個文檔流中包含多個script代碼段(用script標簽分隔的js代碼或引入的js文件),它們的運行順序是:
步驟1. 讀入第一個代碼段(js執行引擎並非一行一行地執行程序,而是一段一段地分析執行的)
步驟2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到步驟5
步驟3. 對var變量和function定義做“預解析”(永遠不會報錯的,因為只解析正確的聲明)
步驟4. 執行代碼段,有錯則報錯(比如變量未定義)
步驟5. 如果還有下一個代碼段,則讀入下一個代碼段,重復步驟2
步驟6. 結束
上面的分析已經足夠清楚,步驟二、三和步驟四裡的紅色字體可能是我們新手理解上的一個盲點,尤其是步驟三的“預解析”,如果不清楚什麼叫預解析,總覺得不踏實。而步驟四的“有錯則報錯”也是經常碰到的。舉例來說:
Js代碼 function funcTest() {
alert(arg);
var arg = 2;
}
funcTest();
function funcTest() {
alert(arg);
var arg = 2;
}
funcTest();
上面這段代碼執行時,彈出“undefined”,也就是說arg沒有定義,js的變量不是不用定義也可以嗎?
2、語法分析和“預解析”
(1)、從解釋型語言的編譯過程說起
眾所周知,javas
cript是解釋型語言,它不同於c#和java等編譯型語言。對於傳統編譯型語言來說,編譯步驟分為:詞法分析、語法分析、語義檢查、代碼優化和字節生成;但對於解釋型語言來說,通過詞法分析和語法分析得到語法樹後,就可以開始解釋執行了。
a、詞法分析
簡單地說,詞法分析是將字符流(char stream)轉換為記號流(token stream)。
但是這個轉換過程並不是可以用一句話就可以概括的那麼簡單,我們可以試著用偽代碼理解一段簡單的程序:
代碼var result=x-y;的轉換大致可以表示如下:
NAME "result"
EQUALS
NAME "x"
MINUS
NAME "y"
SEMICOLON
b、語法分析
簡單地說,語法分析就是為了構造合法的語法分析樹,而語法分析樹可以直觀地表示出推導的過程。
那麼什麼是語法分析樹?簡單地說,就是程序推導過程的描述。但是到底什麼是語法樹,請參考專業文章,本篇略過。
c、其他
通過語法分析,構造出語法分析樹後,接下來還可能需要進一步的語義檢查。對於傳統強類型語言來說,語義檢查的主要部分是類型檢查,比如函數的實參和形參類型是否匹配等等。
結論:通過上面的分析可以看出,對於javascript引擎來說,肯定有詞法分析和語法分析,之後可能還有語義檢查、代碼優化等步驟,等這些編譯步驟完成之後(任何語言都有編譯過程,只是解釋型語言沒有編譯成二進制代碼),才會開始執行代碼。
(2)、執行過程
a、javascript的作用域機制
通過編譯,javascript代碼已經翻譯成了語法樹,然後會立刻按照語法樹執行。
進一步的執行過程,需要理解javascript的作用域機制:詞法作用域(lexcical scope)。通俗地講,就是javascript變量的作用域是在定義時決定而不是執行時決定,也就是說詞法作用域取決於源碼,編譯器通過靜態分析就能確定,因此詞法作用域也叫做靜態作用域(static scope)。但需要注意,with和eval的語義無法僅通過靜態技術實現,所以只能說javascript的作用域機制非常接近詞法作用域(lexical scope).
javascript引擎在執行每個函數實例時,都會創建一個執行環境(execution context)。執行環境中包含一個調用對象(call object), 調用對象是一個scriptObject結構(scriptObject是與函數相關的一套靜態系統,與函數實例的生命周期保持一致),用來保存內部變量表varDecls、內嵌函數表funDecls、父級引用列表upvalue等語法分析結構(注意varDecls和funDecls等信息是在語法分析階段就已經得到,並保存在語法樹中。函數實例執行時,會將這些信息從語法樹復制到scriptObject上)。
b、javascript作用域機制的實現方法
詞法作用域(lexical scope)是javascript的作用域機制,還需要理解它的實現方法,就是作用域鏈(scope chain)。作用域鏈是一個name lookup機制,首先在當前執行環境的scriptObject中尋找,沒找到,則順著upvalue到父scriptObject中尋找,一直lookup到全局調用對象(global object)。
現在回過頭來分析第二個問題:
Js代碼 var arg = 1;
function funcTest() {
alert(arg);
var arg = 2;
}
arg = 10;
funcTest();
var arg = 1;
function funcTest() {
alert(arg);
var arg = 2;
}
arg = 10;
funcTest();
在執行funcTest函數時,也即進入了funcTest對應的作用域,js引擎在執行時,當遇到對變量名或者函數名的使用時,會首先在當前作用域(也即funcTest對應的作用域)查找變量或者函數(顯然,arg變量在funcTest對應的作用域裡被定義為var arg=2所以alert方法的參數采用的是當前作用域的arg,但是因為arg被定義在alert方法後,所以arg變量默認值為undefined)。當然,如果沒有找到就到上層作用域查找,依此類推(作用域范圍可以持續到javas
cript運行環境的根:window對象)。
最後,讓你看的更清楚,上面的代碼其實可以等價於:
Js代碼 var arg = 1;
function funcTest() {
var arg; //默認值undefined
alert(arg);
arg = 2;
}
arg = 10;
funcTest();
var arg = 1;
function funcTest() {
var arg; //默認值undefined
alert(arg);
arg = 2;
}
arg = 10;
funcTest();
c、閉包(closure)
當一個函數實例執行時,會創建或關聯到一個閉包。
scriptObject用來靜態保存與函數相關的變量表,閉包則在執行期動態保存這些變量表及其運行值;
閉包的生命周期有可能比函數實例長。函數實例在活動引用為空後會自動銷毀;
閉包則要等要數據引用為空後,由javas
cript引擎回收(有些情況下不會自動回收,就導致了內存洩漏)。
ps:關於“執行過程”這一段比較拗口,名詞很多,不過別被它們嚇住,一旦理解了執行環境(execution context)、調用對象(callobject)、詞法作用域(lexical scope)、作用域鏈(scopechain)、閉包(closure)等這些概念,javascript的很多現象都能迎刃而解。
三、結語
通過第二段的分析,對照第一段筆者曾經做出的判斷(你是不是也覺得筆者曾經的分析和結論很幼稚(哪怕有時結果碰巧也對!)?!不是一般的膚淺啊,^_^),你會發現原來javascript還有這麼多“玄機”,而要真正理解精通又談何容易?先“悟透”再說吧。