1、作用域是什么
作用域它是一套設(shè)計(jì)良好的規(guī)則,是能夠儲(chǔ)存變量中的值,并且能在之后對(duì)這個(gè)值進(jìn)行訪問或修改的規(guī)則。用來管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找。共有兩種主要模型。第一種最為普遍:詞法作用域。另外一種是動(dòng)態(tài)作用域。
先從編譯原理開始講。在傳統(tǒng)編譯語言的流程中,程序中的一段源代碼在執(zhí)行之前會(huì)經(jīng)歷三個(gè)步驟,統(tǒng)稱為:編譯。
- 分詞/詞法分析(Tokenizing/Lexing):將由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。例如var a = 2;這通常會(huì)被分為var、a、=、2;空格取決于語言是否具有意義。
- 解析/語法分析(Parsing):將詞法單元流轉(zhuǎn)換為一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語法結(jié)構(gòu)的樹--抽象語法樹(Abstract Syntax Tree)
- 代碼生成:將AST轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成。
但JavaScript引擎要復(fù)雜得多。對(duì)于JavaScript來說,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微米(甚至更短)。就是說任何JavaScript代碼片段在執(zhí)行前都要進(jìn)行編譯。下面用var a = 2;舉例。
- 首先遇到var a,編譯器會(huì)詢問作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域的集合中。如果是,編譯器會(huì)忽略該聲明,繼續(xù)編譯;否則會(huì)按要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為a。
- 接著編譯器會(huì)為引擎生成所需的代碼,處理a=2這個(gè)賦值操作。引擎運(yùn)行時(shí)會(huì)首先詢問作用域,在當(dāng)前作用域集合中是否存在一個(gè)叫做a的變量。如果是,引擎就會(huì)使用這個(gè)變量;否則,引擎會(huì)繼續(xù)查找該變量。
- 最后如果引擎找到了a變量,就會(huì)將2賦值給它。否則引擎將拒收示意并拋出一個(gè)異常。
在第二步中,引擎是通過LHS和RHS查詢查找變量的。RHS查詢與簡單地查找某個(gè)變量的值是一樣的,而LHS則是找到變量容器本身,從而對(duì)其賦值。
2、作用域有幾種?(比較基礎(chǔ))
詞法作用域:編譯器的第一個(gè)工作階段叫做詞法化,詞法化的過程會(huì)對(duì)源代碼中的字符進(jìn)行檢查,如果有狀態(tài)的解析過程,還會(huì)賦予單詞語義。簡單來說,詞法作用域就是定義在詞法階段的作用域,是由你在寫代碼將變量和塊作用域?qū)懺谀睦飦頉Q定的。
function foo(a){
var b = a * 2;
function bar(c)
{
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); //2,4,12
這個(gè)例子有三個(gè)逐級(jí)嵌套的作用域是由其對(duì)應(yīng)的作用域塊代碼寫在哪里決定的。引擎執(zhí)行console.log()聲明,并查找a、b、c三個(gè)變量的引用。先從bar()函數(shù)的作用域開始查找,若沒有再去上一級(jí)foo()繼續(xù)查找。作用域查找會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。詞法作用域查找只會(huì)查找一級(jí)標(biāo)識(shí)符,比如a、b、c。如果代碼中引用了foo.bar.baz,詞法作用域查找只會(huì)試圖查找foo標(biāo)識(shí)符,找到變量后,對(duì)象屬性訪問規(guī)則會(huì)分別接管對(duì)bar和baz屬性的訪問。
函數(shù)是Javascript中最常見的作用域單元。本質(zhì)上,聲明在一個(gè)函數(shù)內(nèi)部的變量或函數(shù)會(huì)在所處的作用域中“隱藏”起來,外部作用域無法訪問包裝函數(shù)內(nèi)部的任何內(nèi)容。
塊作用域,JavaScript的ES3規(guī)范中規(guī)定try/catch分局會(huì)創(chuàng)建一個(gè)塊作用域,其中聲明的變量僅在catch內(nèi)部有效。ES6中引入了let關(guān)鍵字,可以將變臉綁定到所在的任意作用域中(通常是{...})。一個(gè)典型例子就是for循環(huán).
for (let i = 0; i < 10 i++)
{
console.log(i);
}
console.log(i); //ReferenceError
for循環(huán)頭部的let不僅將i綁定到了for循環(huán)的塊中,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。
動(dòng)態(tài)作用域:讓作用域作為一個(gè)運(yùn)行時(shí)就被動(dòng)態(tài)確定的形式。
function foo(){
console.log(a); // 2
}
function bar(){
var a = 3;
foo();
}
var a = 2;
bar();
詞法作用域讓foo()中的a通過RHS引用到了全局作用域中的a,因此會(huì)輸出2。而動(dòng)態(tài)作用域并不關(guān)心函數(shù)和作用域是如何聲明以及在何處聲明的,只關(guān)心它們從何處調(diào)用。換句話說,作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中的作用域嵌套。所以如果JavaScript具有動(dòng)態(tài)作用域,理論上,foo()在執(zhí)行是將會(huì)輸出3.因?yàn)閒oo()無法找到a的變量引用時(shí),會(huì)順著調(diào)用棧在調(diào)用foo()的地方查找a,而不是在嵌套的詞法作用域鏈中向上查找。由于foo()是在bar()中調(diào)用的,引擎會(huì)檢查bar()的作用域,并在其中找到值為3的變量a。
3、提升
已經(jīng)學(xué)習(xí)了作用域的概念,以及根據(jù)聲明的位置和方式將變量分配給作用域的相關(guān)原理??梢钥偨Y(jié)為:任何聲明在某個(gè)作用域內(nèi)的變量,都將附屬于這個(gè)作用域。但作用域同其中的變量聲明出現(xiàn)的位置有某種微妙的聯(lián)系。javaScript代碼在執(zhí)行時(shí)并不完全是由上到下一行一行執(zhí)行的。例如a=2;var a;console.log(a);并不是輸出underfined而是輸出2。原因是包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前搜先被處理。當(dāng)看到var a=2;時(shí)JavaScript會(huì)將其分為var a和a=2;。第一個(gè)定義聲明是在編譯階段進(jìn)行的。第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段。這就意味著無論作用域中的聲明出現(xiàn)在什么地方,都將在代碼本身被執(zhí)行前首先進(jìn)行處理。這個(gè)過程被成為“提升”