1. 兩種作用域
“作用域”我們知道是一套規(guī)則凡资,用來(lái)管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找松逊。
作用域有兩種主要工作模型:詞法作用域和動(dòng)態(tài)作用域旧乞。
大多數(shù)語(yǔ)言采用的都是詞法作用域椭赋,少數(shù)語(yǔ)言采用動(dòng)態(tài)作用域(例如 Bash 腳本)辩恼,這里我們主要討論詞法作用域。
2. 詞法
大部分標(biāo)準(zhǔn)語(yǔ)言編譯器的第一個(gè)工作階段叫作詞法化瞧甩。
簡(jiǎn)單地說(shuō),詞法作用域是由你在寫代碼時(shí)將變量和函數(shù)(塊)作用域?qū)懺谀睦飦?lái)決定的弥鹦。當(dāng)然肚逸,也會(huì)有一些方法來(lái)動(dòng)態(tài)修改作用域,后邊我會(huì)介紹彬坏。
舉個(gè)例子:
var a = 2;
function foo1 () {
console.log(a);
}
function foo2 () {
var a = 10;
foo1();
}
foo2();
這里輸出結(jié)果是多少呢朦促?
注意,這里結(jié)果打印的是 2栓始。
可能會(huì)有一些同學(xué)認(rèn)為是 10务冕,那就是沒有搞清楚詞法作用域的概念。
前邊介紹了幻赚,詞法作用域只取決于代碼書寫時(shí)的位置禀忆,那么在這個(gè)例子中臊旭,函數(shù) foo1 定義時(shí)的位置決定了它的作用域,通過(guò)下圖理解:
foo1 和 foo2 都是分別定義在全局作用域中的函數(shù)箩退,它們是并列的离熏,所以在 foo1 的作用域鏈中并不包含 foo2 的作用域,雖然在 foo2 中調(diào)用了 foo1戴涝,但是 foo1 對(duì)變量 a 進(jìn)行 RHS 查詢時(shí)滋戳,在自己的作用域沒有找到,引擎會(huì)去 foo1 的上級(jí)作用域(也就是全局作用域)中查找啥刻,而并不會(huì)去 foo2 的作用域中查找奸鸯,最終在全局作用域中找到 a 的值為 2。
總結(jié)來(lái)說(shuō)可帽,無(wú)論函數(shù)在哪里被調(diào)用娄涩,也無(wú)論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定蘑拯。
3. 欺騙詞法
JavaScript 中有 3 種方式可以用來(lái)“欺騙詞法”钝满,動(dòng)態(tài)改變作用域。
第一種: eval
JavaScript 中 eval(...) 函數(shù)可以接受一個(gè)字符串作為參數(shù)申窘,并將其中的內(nèi)容視為好像在書寫時(shí)就存在于程序中這個(gè)位置的代碼弯蚜。
在執(zhí)行 eval(...) 之后的代碼時(shí),引擎并不知道或在意前面的代碼是以動(dòng)態(tài)形式插入進(jìn)來(lái)并對(duì)詞法作用域環(huán)境進(jìn)行修改的剃法,引擎只會(huì)像往常一樣正常進(jìn)行詞法作用域的查找碎捺。
舉個(gè)例子:
function foo (str) {
eval(str); // "欺騙"詞法
console.log(a);
}
var a = 2;
foo("var a = 10;");
如大家所想,輸出結(jié)果為 10贷洲。
因?yàn)?eval("var a = 10;") 在 foo 的作用域中新創(chuàng)建了一個(gè)同名變量 a收厨,引擎在 foo 作用域中對(duì) a 進(jìn)行 RHS 查詢,找到了新定義的 a优构,值為 10诵叁,所以不再向上查找全局作用域中的 a,所以導(dǎo)致輸出結(jié)果為 10钦椭,這就是 eval(...) 的作用拧额。
在嚴(yán)格模式下,eval(...) 在運(yùn)行時(shí)有自己的詞法作用域彪腔,意味著其中的聲明無(wú)法修改所在的作用域侥锦。
'use strict;'
function foo (str) {
eval(str); // eval() 有自己的作用域,所以并不會(huì)修改 foo 的詞法作用域
console.log(a);
}
var a = 2;
foo("var a = 10;");
這里輸出結(jié)果為 2德挣。
JavaScript 中還有一些功能和 eval(...) 類似的函數(shù)恭垦,例如 setTimeout(...) 和 setInterval(...) 的第一個(gè)參數(shù)可以是一個(gè)字符串,字符串的內(nèi)容可以解釋為一段動(dòng)態(tài)生成的代碼。這些功能已經(jīng)過(guò)時(shí)并且不被提倡番挺,最好不要使用它們唠帝。new Function(...) 函數(shù)的最后一個(gè)參數(shù)也可以接受代碼字符串,并將其轉(zhuǎn)化為動(dòng)態(tài)生成的函數(shù)建芙,也盡量避免使用没隘。
在程序中動(dòng)態(tài)生成代碼的使用場(chǎng)景非常罕見,因?yàn)樗鶐?lái)的好處無(wú)法抵消性能上的損失禁荸。
第二種: with
with 通常被當(dāng)做重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式右蒲,可以不需要重復(fù)引用對(duì)象本身。
舉個(gè)例子:
var obj = {
a: 2,
b: 3
};
with (obj) {
console.log(a); // 2
console.log(b); // 3
c = 4;
};
console.log(c); // 4, c 被泄露到全局作用域上
如上所示赶熟,我們對(duì) c 進(jìn)行 LHS 查詢瑰妄,因?yàn)樵?with 引入的新作用域中沒有找到 c,所以向上一級(jí)作用域(這里是全局作用域)查找映砖,也沒有找到间坐,在非嚴(yán)格模式下,在全局對(duì)象中新建了一個(gè)屬性 c 并賦值為 4邑退。
with 可以將一個(gè)沒有或有多個(gè)屬性的對(duì)象處理為一個(gè)完全隔離的詞法作用域竹宋,因此這個(gè)對(duì)象的屬性也會(huì)被處理為定義在這個(gè)作用域中的詞法標(biāo)識(shí)符。
盡管 with 塊可以將一個(gè)對(duì)象處理為詞法作用域地技,但是這個(gè)塊內(nèi)部正常的 var 聲明并不會(huì)限制在這個(gè)塊作用域中蜈七,而是被添加到 with 所處的函數(shù)作用域中。
嚴(yán)格模式下莫矗,with 被完全禁止使用飒硅。
'use strict';
var obj = {
a: 2,
b: 3
};
with (obj) {
console.log(a);
console.log(b);
c = 4;
};
console.log(c);
第三種: try...catch
try...catch 可以測(cè)試代碼中的錯(cuò)誤。try 部分包含需要運(yùn)行的代碼作谚,而 catch 部分包含錯(cuò)誤發(fā)生時(shí)運(yùn)行的代碼三娩。
舉個(gè)例子:
try {
foo();
} catch (err) {
console.log(err);
var a = 2;
// 打印出 "ReferenceError: foo is not defined at <anonymous>:2:4"
}
console.log(a); // 2
當(dāng) try 中的代碼出現(xiàn)錯(cuò)誤時(shí),就會(huì)進(jìn)入 catch 塊妹懒,此時(shí)會(huì)把異常對(duì)象添加到作用域鏈的最前端雀监,類似于 with 一樣,catch 中定義的局部變量也都會(huì)添加到包含 try...catch 的函數(shù)作用域(或全局作用域)中眨唬。
4. 性能
JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)性能優(yōu)化滔悉。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)定義的位置单绑,才能在執(zhí)行過(guò)程中快速找到標(biāo)識(shí)符。
但如果引擎在代碼中發(fā)現(xiàn)了 eval(...)曹宴、with 和 try...catch 搂橙,它只能簡(jiǎn)單的假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷都是無(wú)效的,因?yàn)闊o(wú)法在詞法分析階段明確知道 eval(...) 會(huì)接受到什么代碼,這些代碼會(huì)如何對(duì)作用域進(jìn)行修改区转,也無(wú)法知道傳遞給 with 用來(lái)創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底是什么苔巨。
最悲觀的情況是如果出現(xiàn)了這些動(dòng)態(tài)添加作用域的代碼,所有的優(yōu)化可能都是無(wú)意義的废离,因此最簡(jiǎn)單的做法就是完全不進(jìn)行任何優(yōu)化侄泽。
如果代碼中大量使用 eval(...) 和 with,那么運(yùn)行起來(lái)一定會(huì)變得非常緩慢蜻韭。
5. 結(jié)論
很多時(shí)候我們對(duì)代碼的分析出錯(cuò)悼尾,就是源于對(duì)詞法作用域的忽略,所以讓我們重新審視代碼肖方,繼續(xù)努力闺魏!