引子
在進(jìn)入本文的主題前君旦,首先請大家判斷如下代碼輸出結(jié)果為什么陕贮?并說明理由。
function foo(){
console.log(a);
}
function bar(){
var a=3;
foo();
}
var a=2;
bar();
不管大家的答案是什么,這里正確得答案是2住练,至于為什么,就請先看下文蛹尝。相信大家看完本文之后痢缎,應(yīng)該能找到真正得原因。
為什么需要作用域涌穆?
在介紹作用域前怔昨,首先請看如下兩個(gè)函數(shù),哪個(gè)函數(shù)功能更強(qiáng)大宿稀?
function func1(){
return 1+2
}
function func2(a,b){
return a+b;
}
顯而易見趁舀,使用了變量的函數(shù)功能更為強(qiáng)大。變量給予了程序更加強(qiáng)大的功能祝沸,如果沒有變量矮烹,程序只能做一些很簡單得運(yùn)算,有了變量罩锐,就能做更多更有意思的事情奉狈,但是程序在運(yùn)行時(shí),在需要時(shí)又是如何找到變量并使用它呢涩惑?
此時(shí)就引出了本文今天的主角——作用域仁期,作用域永遠(yuǎn)都是任何一門編程語言中的重中之重,它控制著變量的可見性與生命周期。在javascript中跛蛋,作用域在代碼編譯與運(yùn)行過程中幫助javascript引擎根據(jù)標(biāo)識符名稱(即變量名)進(jìn)行變量的查找與訪問碰纬。
javascript作用域
從源代碼到運(yùn)行結(jié)果
大家都知道我們現(xiàn)有的馮諾依曼體系的計(jì)算機(jī),歸根結(jié)底運(yùn)行的始終只能是機(jī)器語言问芬,機(jī)器語言是用二進(jìn)制代碼表示的計(jì)算機(jī)能直接識別和執(zhí)行的一種機(jī)器指令的集合悦析。而我們平常編程采用的編程語言,比如java此衅,c强戴,c++,Object C挡鞍,javascript等骑歹,都是屬于高級語言,高級語言是面向用戶的語言墨微,因?yàn)樗鼘θ祟惛押玫烂模桌斫馀c編寫,但是我們采用高級語言編寫的代碼始終是要在計(jì)算機(jī)上運(yùn)行的翘县,所以最域,我們用任何一種高級語言編寫的代碼程序,都必須要經(jīng)過處理锈麸,生成為機(jī)器可以理解執(zhí)行的機(jī)器語言镀脂,而把高級語言代碼轉(zhuǎn)換為機(jī)器語言指令的過程就叫編譯。
傳統(tǒng)的編程語言忘伞,在執(zhí)行前會經(jīng)過如下三個(gè)步驟:
- 詞法分析:這個(gè)過程將由字符組成的代碼字符串分解成有意義得代碼塊薄翅,這些代碼塊即為詞法單元,如var a=1,這個(gè)語句會被分解為如下詞法單元:var氓奈、a翘魄、=、1舀奶;
- 語法分析:這個(gè)過程是將詞法單元(數(shù)組)轉(zhuǎn)換為一個(gè)由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹暑竟,稱為“抽象語法樹”(Abstract Syntax Tree,AST)伪节;
- 代碼生成:將AST轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成光羞。即將AST轉(zhuǎn)換為機(jī)器指令。
如c語言這類靜態(tài)編譯語言怀大,首先編譯生成對象文件纱兑,然后再使用對象文件去運(yùn)行,所以可以在運(yùn)行前進(jìn)行大量的細(xì)致的優(yōu)化處理化借,而javascript是動態(tài)編譯的潜慎,它是編譯了就馬上執(zhí)行,并不生成特定對象文件。這就決定了javascript無法在編譯階段進(jìn)行過多得優(yōu)化處理铐炫。這就是javascript執(zhí)行性能始終不如C這類靜態(tài)編譯語言的原因垒手。這兩者的區(qū)別如圖:
也許大家會好奇,本文的主旨不是作用域嗎倒信?怎么說到編譯來了科贬,這是因?yàn)閖avascript在編譯階段就已經(jīng)使用了作用域!
javascript編譯與運(yùn)行階段的作用域
在說javascript編譯與運(yùn)行階段的作用域前鳖悠,首先榜掌,這里說一下javascript中得三個(gè)關(guān)鍵概念:
- 引擎:從頭至尾負(fù)責(zé)整個(gè)javascript的編譯及運(yùn)行;
- 編譯器:引擎對代碼進(jìn)行的編譯即由編譯器支持的乘综,主要負(fù)責(zé)語法分析憎账,代碼生成等;
- 作用域:負(fù)責(zé)在javascript編譯階段與運(yùn)行階段收集并維護(hù)所有聲明的標(biāo)識符(變量)的查找與訪問權(quán)限的控制卡辰;
三者關(guān)系如下圖:
下面以 var a=1
為例胞皱,闡述一下作用域如何參與到j(luò)avascript的編譯階段與運(yùn)行階段中。
大部分程序員看到var a=1
這條語句時(shí)九妈,都會以為這是一句聲明反砌,但是對于javascript引擎來說,卻并非如此允蚣,javascript引擎會把這條代碼一分為二于颖,拆開來看,分別為var a;
與a=1
嚷兔,前者由編譯器在編譯時(shí)進(jìn)行處理,后者做入,由引擎在運(yùn)行時(shí)處理冒晰;
- 當(dāng)javascript引擎遇到
var a
時(shí),編譯器會在當(dāng)前作用域中查找該變量a竟块,如果能找到壶运,編譯器會忽略該聲明,否則浪秘,它會要求在當(dāng)前作用域中聲明一個(gè)新的變量蒋情,并命名為a;然后編譯器會為引擎生成運(yùn)行時(shí)所需得代碼耸携; - 當(dāng)javascript引擎在運(yùn)行代碼遇到
a=1
這個(gè)賦值操作時(shí)棵癣,也會首先詢問作用域中是否存在一個(gè)叫做a的變量,如果存在夺衍,則使用該表明了狈谊,如果不存在,在根據(jù)作用域鏈規(guī)則,去當(dāng)前作用域的上一層級的作用域中查找變量河劝,最終找到變量a壁榕,則將1賦值給它,否則引擎拋出異常赎瞎。
由此可見牌里,javascript引擎,在編譯時(shí)务甥,如果遇到變量聲明二庵,就會去作用域中查詢是否已有該變量,沒有則在作用域中聲明記錄缓呛,有則跳過催享;在運(yùn)行階段,當(dāng)程序需要使用變量時(shí)哟绊,則會去作用域中查找該變量因妙,有則,使用變量票髓,無則拋出異常攀涵。
javascript的作用域模型
目前,作用域共有兩種主要的工作模型洽沟,第一種是最為普遍以故,被大多數(shù)編程語言如java、c裆操、c++等所采用的詞法作用域怒详,另一種是動態(tài)作用域,只有少數(shù)編程語言使用踪区,如bash腳本昆烁,perl等;而javascript采用的就是詞法作用域缎岗。
詞法作用域:詞法作用域就是定義在詞法階段(編譯階段的詞法分析階段)的作用域静尼。換句話說,詞法作用域是由程序員在編寫代碼時(shí)將變量與函數(shù)聲明寫在哪里來決定的传泊。詞法作用域的作用域鏈?zhǔn)腔诖a中得作用域嵌套鼠渺。
-
動態(tài)作用域:動態(tài)作用域并不關(guān)心函數(shù)和作用域是如何聲明以及在何處聲明的,只關(guān)心它們從何處調(diào)用眷细,也就是說拦盹,動態(tài)作用域的作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中得作用域嵌套薪鹦。
這里我們回到文件開頭得那段代碼:function foo(){ console.log(a); } function bar(){ var a=3; foo(); } var a=2; bar();
在詞法作用域與動態(tài)作用域下掌敬,作用域關(guān)系圖分別如下:
在詞法作用域下惯豆,函數(shù)foo中并沒有變量a,所以會沿著作用域鏈向它得上一級作用域奔害,也就是全局作用域查找變量a楷兽,此時(shí)查找到a,且a的值為2华临,所以芯杀,代碼輸出結(jié)果為2;
而在動態(tài)作用域下雅潭,函數(shù)foo中也沒有變量a揭厚。所以會沿著作用域鏈向它得上一級作用域,也就是函數(shù)bar的作用域中查找變量a扶供,此時(shí)筛圆,函數(shù)bar中定義了一個(gè)局部變量a,且a的值為3椿浓,所以輸出結(jié)果會為3太援。
那么接下來,請思考扳碍,如果把代碼改為如下形式提岔,輸出結(jié)果又是什么?
function foo(){
console.log(a);
}
function bar(){
var a=3;
foo();
}
bar();
var a=2;
如果改成如下代碼笋敞,結(jié)果又如何碱蒙?
function foo(){
console.log(a);
}
function bar(){
var a=3;
foo();
}
bar();
let a=2;
ES6明確規(guī)定,如果區(qū)塊中存在let和const命令夯巷,這個(gè)區(qū)塊對這些命令聲明的變量赛惩,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量鞭莽,就會報(bào)錯(cuò)坊秸。
總之,在代碼塊內(nèi)澎怒,使用let命令聲明變量之前,該變量都是不可用的阶牍。這在語法上喷面,稱為“暫時(shí)性死區(qū)”(temporal dead zone,簡稱 TDZ)走孽。
結(jié)語
本文簡單介紹了一下javascript的作用域的基本知識惧辈,作用域是所有編程語言的重中之重,更是javascript中得重中之重磕瓷,是理解javascript中閉包概念的基礎(chǔ)盒齿。如果你的目標(biāo)是精通JavaScript語言念逞,深入的理解它的各個(gè)組成,那么理解作用域便是你的起點(diǎn)边翁。