本文實例分析了javascript作用域鏈(Scope Chain)用法。分享給大家參考,具體如下:
關於js的作用域鏈,早有耳聞,也曾看過幾篇介紹性的博文,但一直都理解的模棱兩可。近日又精心翻看了一下《悟透Javascript》這本書,覺得寫得太深刻,在「程式碼的時空」一節裡有一段介紹作用域鏈的地方寥寥數語,回味無窮(其實還是理解的模稜兩可^_^)。現在整理下自己的讀書筆記,順便藉鏡網路資源,寫下來。
一、從一個簡單的問題說起
下面的js程式碼在頁面中運行顯示什麼結果:
var arg = 1; function fucTest(arg) { alert(arg); var arg = 2; //alert(arg); } fucTest(10);
您的答案是什麼?沒錯,就是彈出10。我的理解是這樣的,funTest函式有一個形參arg,funTest函式傳入實參10,alert方法把10彈出就是了,囧。
好,問題又來了:
var 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」,瀑布汗.
二、理解作用域鏈,從javascript運作機制說起
1、js的運行順序
如果一個文檔流中包含多個script代碼段(用script標籤分隔的js代碼或引入的js文件),它們的運行順序是:
步驟1. 讀入第一個程式碼段(js執行引擎並非一行一行地執行程序,而是一段一段地分析執行的)
步驟2. 做語法分析,有錯則報語法錯誤(例如括號不符等),並跳到步驟5
步驟3. 對var變數和function定義做「預解析」(永遠不會報錯的,因為只解析正確的宣告)
步驟4. 執行程式碼段,有錯則報錯(如變數未定義)
步驟5. 如果還有下一個程式碼段,則讀入下一個程式碼段,重複步驟2
步驟6. 結束
上面的分析已經足夠清楚,步驟二、三和步驟四里的紅色字體可能是我們新手理解上的一個盲點,尤其是步驟三的“預解析”,如果不清楚什麼叫預解析,總覺得不踏實。而步驟四的「有錯則報錯」也是常碰到的。舉例來說:
function funcTest() { alert(arg); var arg = 2; } funcTest();
上面这段代码执行时,弹出“undefined”,也就是说arg没有定义,js的变量不是不用定义也可以吗?
2、语法分析和“预解析”
(1)、从解释型语言的编译过程说起
众所周知,javascript是解释型语言,它不同于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)。
现在回过头来分析第二个问题:
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)。当然,如果没有找到就到上层作用域查找,依此类推(作用域范围可以持续到javascript运行环境的根:window对象)。
最后,让你看的更清楚,上面的代码其实可以等价于:
var arg = 1; function funcTest() { var arg; //默认值undefined alert(arg); arg = 2; } arg = 10; funcTest();
c、閉包(closure)
當一個函數實例執行時,會建立或關聯到一個閉包。 (關於閉包,打算另寫一篇學習筆記)
scriptObject用來靜態保存與函數相關的變數表,而閉包則在執行期間動態保存這些變數表及其運行值;
閉包的生命週期有可能比函數實例長。函數實例在活動引用為空後會自動銷毀;
閉包則要等要資料引用為空後,由javascript引擎回收(有些情況下不會自動回收,就導致了記憶體洩漏)。
ps:關於「執行過程」這一段比較拗口,名詞很多,不過別被它們嚇住,一旦理解了執行環境(execution context)、調用對象(call object)、詞法作用域(lexical scope)、作用域鏈(scope chain)、閉包(closure)等這些概念,javascript的許多現像都能迎刃而解。
三、結語
透過第二段的分析,對照第一段筆者曾經做出的判斷(你是不是也覺得筆者曾經的分析和結論很幼稚(哪怕有時結果碰巧也對!)?!不是一般的膚淺啊,^_^),你會發現原來javascript還有這麼多“玄機”,而要真正理解精通又談何容易?先「悟透」再說吧。
希望本文所述對大家JavaScript程式設計有所幫助。