JavaScript作用域和作用域鏈捺僻,說起來很簡單涨椒,但是細細分析,大有玄機黄选。只能真正理解了作用域鏈原理伏伯,才能寫出更高效的JavaScript代碼橘洞。下面,讓我們慢慢走近這個并不神秘的區(qū)域......
1. 作用域和執(zhí)行上下文
參考:深入理解JavaScript作用域和作用域鏈 - 感謝@qwelz訂正
JavaScript 的執(zhí)行分為:解釋和執(zhí)行兩個階段,這兩個階段所做的事并不一樣:
- 解釋階段:
- 詞法分析
- 語法分析
- 作用域規(guī)則確定
- 執(zhí)行階段:
- 創(chuàng)建執(zhí)行上下文
- 執(zhí)行函數(shù)代碼
- 垃圾回收
JavaScript 解釋階段便會確定作用域規(guī)則舵鳞,因此作用域在函數(shù)定義時就已經(jīng)確定了震檩,而不是在函數(shù)調(diào)用時確定,但是執(zhí)行上下文是函數(shù)執(zhí)行之前創(chuàng)建的。執(zhí)行上下文最明顯的就是 this 的指向是執(zhí)行時確定的抛虏。而作用域訪問的變量是編寫代碼的結構確定的博其。
作用域和執(zhí)行上下文之間最大的區(qū)別是:
執(zhí)行上下文在運行時確定,隨時可能改變迂猴;作用域在定義時就確定慕淡,并且不會改變。
2. 執(zhí)行上下文
執(zhí)行JavaScript代碼時沸毁,JavaScript引擎會創(chuàng)建一個執(zhí)行上下文峰髓,它設定了代碼執(zhí)行時所處的環(huán)境。
下面一步步剖析~
當頁面加載完畢后(含有需要執(zhí)行的JavaScript代碼)息尺,JavaScript引擎會做哪些事情携兵?
- 創(chuàng)建一個全局的執(zhí)行上下文(
this
指向我們熟知的window); - 每執(zhí)行一個JavaScript函數(shù)搂誉,都會創(chuàng)建一個對應的執(zhí)行上下文徐紧;
- 函數(shù)里面可能執(zhí)行嵌套函數(shù)......繼續(xù)創(chuàng)建子函數(shù)的執(zhí)行上下文;
- 最終炭懊,會創(chuàng)建出一個棧并级,當前作用域在棧頂,全局作用域在棧底侮腹;
棧頂?shù)暮瘮?shù)會最先運行嘲碧,運行完畢后出棧,繼續(xù)運行一下個函數(shù)......直到棧清空父阻。
3. 作用域鏈
每個執(zhí)行上下文都有一個與之關聯(lián)的作用域鏈愈涩。
當函數(shù)被創(chuàng)建時(注意,不是執(zhí)行)至非,JavaScript引擎會把創(chuàng)建時執(zhí)行上下文的作用域鏈賦給函數(shù)內(nèi)部屬性[Scope]
钠署。
然后糠聪,函數(shù)被執(zhí)行荒椭,JavaScript引擎創(chuàng)建一個活動對象(Active object),添加到作用域鏈頂部舰蟆。
用一個例子做進一步說明:
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
var total = add(5, 10);
(圖例來自網(wǎng)絡)
執(zhí)行上面的JavaScript代碼趣惠,但還沒有執(zhí)行add函數(shù)時,add函數(shù)scope chain只有一個值身害,指向global object味悄。
然后,執(zhí)行add函數(shù)塌鸯,一個活動對象被創(chuàng)建侍瑟,并且被加到scope chain頂部。
由此,執(zhí)行add函數(shù)時涨颜,一個兩層的作用域鏈被建立费韭。
小貼士
無論是全局對象還是活動對象,都會在初始化時給this, arguments賦值庭瑰;
也會給局部變量星持,局部參數(shù)賦值。
顯而易見弹灭,add函數(shù)被執(zhí)行時督暂,需要尋找num1和num2的值做計算。
如果在頂層作用域找不到這兩個值穷吮,那么逻翁,JavaScript引擎會沿著作用域鏈,在下一層活動對象/全局對象中查找......找到即返回捡鱼,找不到繼續(xù)往下......直到全局對象window卢未。
4. 性能優(yōu)化:盡可能使用局部變量
通過上面的分析,可以得出結論堰汉,如果在越靠近棧頂?shù)膶ο笾辛缮纾梢哉业疆斍昂瘮?shù)執(zhí)行時所需的變量,那么翘鸭,函數(shù)執(zhí)行速度是最快的滴铅。
也就是說,讀取變量值的總耗時隨著查找作用域鏈的逐層深入而不斷增加就乓!
因此汉匙,為了寫出更高效的JavaScript代碼,盡可能在函數(shù)內(nèi)部使用局部變量生蚁。比如下面的寫法就不好:
function createChild(elemID) {
var element = document.getElementById(elemID); // 在global對象中查找document
var newElem = document.createElement('div'); // 在global對象中查找document
element.appendChild(newElem); //總計查找兩次
}
應該改為:
function createChild(elemID) {
var doc = document; // 在global對象中查找document
var element = doc.getElementById(elemID);
var newElem = doc.createElement('div');
element.appendChild(newElem); //總計查找一次
}
小結
可見噩翠,要想寫出高性能的JavaScript代碼并不難,一點小修改也有大作為~