摘要:所以上面那段代碼鏈中最初應(yīng)該是之后之后所以最后的輸出結(jié)果是作用域鏈概念看了前面一個(gè)完整的函數(shù)執(zhí)行過程,讓我們來說下作用域鏈的概念吧。而這一條形成的鏈就是中的作用域鏈。
1. 什么是作用域
作用域是你的代碼在運(yùn)行時(shí),某些特定部分中的變量,函數(shù)和對(duì)象的可訪問性。換句話說,作用域決定了變量與函數(shù)的可訪問范圍,即作用域控制著變量與函數(shù)的可見性和生命周期。
2. JavaScript中的作用域在 JavaScript 中有兩種作用域
全局作用域
局部作用域
如果一個(gè)變量在函數(shù)外面或者大括號(hào){}外聲明,那么就定義了一個(gè)全局作用域,在ES6之前局部作用域只包含了函數(shù)作用域,ES6為我們提供的塊級(jí)作用域,也屬于局部作用域
2.1 全局作用域擁有全局作用域的對(duì)象可以在代碼的任何地方訪問到, 在js中一般有以下幾種情形擁有全局作用域:
最外層的函數(shù)以及最外層變量:
var globleVariable= "global"; // 最外層變量 function globalFunc(){ // 最外層函數(shù) var childVariable = "global_child"; //函數(shù)內(nèi)變量 function childFunc(){ // 內(nèi)層函數(shù) console.log(childVariable); } console.log(globleVariable) } console.log(globleVariable); // global globalFunc(); // global console.log(childVariable) // childVariable is not defined console.log(childFunc) // childFunc is not defined
從上面代碼中可以看到globleVariable和globalFunc在任何地方都可以訪問到, 反之不具有全局作用域特性的變量只能在其作用域內(nèi)使用。
未定義直接賦值的變量(由于變量提升使之成為全局變量)
function func1(){ special = "special_variable"; var normal = "normal_variable"; } func1(); console.log(special); //special_variable console.log(normal) // normal is not defined
雖然我們可以在全局作用域中聲明函數(shù)以及變量, 使之成為全局變量, 但是不建議這么做,因?yàn)檫@可能會(huì)和其他的變量名沖突,一方面如果我們?cè)偈褂?b>const或者let聲明變量, 當(dāng)命名發(fā)生沖突時(shí)會(huì)報(bào)錯(cuò)。
// 變量沖突 var globleVariable = "person"; let globleVariable = "animal"; // Error, thing has already been declared
另一方面如果你使用var申明變量,第二個(gè)申明的同樣的變量將覆蓋前面的,這樣會(huì)使你的代碼很難調(diào)試。
var name = "koala" var name = "xiaoxiao" console.log(name); // xiaoxiao2.2 局部作用域
和全局作用于相反,局部作用域一般只能在固定代碼片段內(nèi)可以訪問到。最常見的就是函數(shù)作用域。
2.2.1 函數(shù)作用域定義在函數(shù)中的變量就在函數(shù)作用域中。并且函數(shù)在每次調(diào)用時(shí)都有一個(gè)不同的作用域。這意味著同名變量可以用在不同的函數(shù)中。因?yàn)檫@些變量綁定在不同的函數(shù)中,擁有不同作用域,彼此之間不能訪問。
//全局作用域 function test(){ var num = 9; // 內(nèi)部可以訪問 console.log("test中:"+num); } //test外部不能訪問 console.log("test外部:"+num);
注意點(diǎn):
如果在函數(shù)中定義變量時(shí),如果不添加var關(guān)鍵字,造成變量提升,這個(gè)變量成為一個(gè)全局變量。
function doSomeThing(){ // 在工作中一定避免這樣寫 thing = "writting"; console.log("內(nèi)部:"+thing); } console.log("外部:"+thing)
任何一對(duì)花括號(hào){...}中的語句集都屬于一個(gè)塊, 在es6之前,在塊語句中定義的變量將保留在它已經(jīng)存在的作用域中:
var name = "程序員成長(zhǎng)指北"; for(var i=0; i<5; i++){ console.log(i) } console.log("{}外部:"+i); // 0 1 2 3 4 {}外部:5
我們可以看到變量name和變量i是同級(jí)作用域。
2.2.2 在ES6塊級(jí)作用域未講解之前注意點(diǎn)變量提升英文名字hoisting,MDN中對(duì)它的解釋是變量申明是在任意代碼執(zhí)行前處理的,在代碼區(qū)中任意地方申明變量和在最開始(最上面)的地方申明是一樣的。也就是說,看起來一個(gè)變量可以在申明之前被使用!這種行為就是所謂的“hoisting”,也就是變量提升,看起來就像變量的申明被自動(dòng)移動(dòng)到了函數(shù)或全局代碼的最頂上。
看一段代碼:
var tmp = new Date(); function f() { console.log(tmp); if(false) { var tmp="hello"; } }
這道題應(yīng)該很多小伙伴在面試中遇到過,有人會(huì)認(rèn)為輸出的是當(dāng)前日期。但是正確的結(jié)果是undefined。這就是由于變量提升造成的,在這里申明提升了,定義的內(nèi)容并不會(huì)提升,提升后對(duì)應(yīng)的代碼如下:
var tmp = new Date(); function f() { var tmp; console.log(tmp); if(false) { tmp="hello"; } } f();
console在輸出的時(shí)候,tmp變量?jī)H僅申明了但未定義。所以輸出undefined。雖然能夠輸出,但是并不推薦這種寫法推薦的做法是在申明變量的時(shí)候,將所用的變量都寫在作用域(全局作用域或函數(shù)作用域)的最頂上,這樣代碼看起來就會(huì)更清晰,更容易看出來哪個(gè)變量是來自函數(shù)作用域的,哪個(gè)又是來自作用域鏈
看一個(gè)例子:
// var var name = "koloa"; console.log(name); // koala if(true){ var name = "程序員成長(zhǎng)指北"; console.log(name); // 程序員成長(zhǎng)指北 } console.log(name); // 程序員成長(zhǎng)指北
雖然看起來里面name申明了兩次,但上面說了,js的var變量只有全局作用域和函數(shù)作用域兩種,且申明會(huì)被提升,因此實(shí)際上name只會(huì)在最頂上開始的地方申明一次,var name="程序員成長(zhǎng)指北"的申明會(huì)被忽略,僅用于賦值。也就是說上面的代碼實(shí)際上跟下面是一致的。
// var var name = "koloa"; console.log(name); // koala if(true){ name = "程序員成長(zhǎng)指北"; console.log(name); // 程序員成長(zhǎng)指北 } console.log(name); // 程序員成長(zhǎng)指北
如果有函數(shù)和變量同時(shí)聲明了,會(huì)出現(xiàn)什么情況呢?看下面但代碼
console.log(foo); var foo ="i am koala"; function foo(){}
輸出結(jié)果是function foo(){},也就是函數(shù)內(nèi)容
如果是另外一種形式呢?
console.log(foo); var foo ="i am koala"; var foo=function (){}
輸出結(jié)果是undefined
對(duì)兩種結(jié)果進(jìn)行分析說明:
第一種:函數(shù)申明。就是上面第一種,function foo(){}這種形式
另一種:函數(shù)表達(dá)式。就是上面第二種,var foo=function(){}這種形式
第二種形式其實(shí)就是var變量的聲明定義,因此上面的第二種輸出結(jié)果為undefined應(yīng)該就能理解了。
而第一種函數(shù)申明的形式,在提升的時(shí)候,會(huì)被整個(gè)提升上去,包括函數(shù)定義的部分!因此第一種形式跟下面的這種方式是等價(jià)的!
var foo=function (){} console.log(foo); var foo ="i am koala";
原因是:
函數(shù)聲明被提升到最頂上;
申明只進(jìn)行一次,因此后面var foo="i am koala"的申明會(huì)被忽略。
函數(shù)申明的優(yōu)先級(jí)優(yōu)于變量申明,且函數(shù)聲明會(huì)連帶定義一起被提升(這里與變量不同)
接下來講,在ES6中引入的塊級(jí)作用域之后的事!
2.2.2 塊級(jí)作用域ES6新增了let和const命令,可以用來創(chuàng)建塊級(jí)作用域變量,使用let命令聲明的變量只在let命令所在代碼塊內(nèi)有效。
let 聲明的語法與 var 的語法一致。你基本上可以用 let 來代替 var 進(jìn)行變量聲明,但會(huì)將變量的作用域限制在當(dāng)前代碼塊中。塊級(jí)作用域有以下幾個(gè)特點(diǎn):
變量不會(huì)提升到代碼塊頂部且不允許從外部訪問塊級(jí)作用域內(nèi)部變量
console.log(bar);//拋出`ReferenceErro`異常: 某變量 `is not defined` let bar=2; for (let i =0; i<10;i++){ console.log(i) } console.log(i);//拋出`ReferenceErro`異常: 某變量 `is not defined`
其實(shí)這個(gè)特點(diǎn)帶來了許多好處,開發(fā)者需要檢查代碼時(shí)候,可以避免在作用域外意外但使用某些變量,而且保證了變量不會(huì)被混亂但復(fù)用,提升代碼的可維護(hù)性。就像代碼中的例子,一個(gè)只在for循環(huán)內(nèi)部使用的變量i不會(huì)再去污染整個(gè)作用域。
不允許反復(fù)聲明
ES6的let和const不允許反復(fù)聲明,與var不同
// var function test(){ var name = "koloa"; var name = "程序員成長(zhǎng)指北"; console.log(name); // 程序員成長(zhǎng)指北 } // let || const function test2(){ var name ="koloa"; let name= "程序員成長(zhǎng)指北"; // Uncaught SyntaxError: Identifier "count" has already been declared }
看到這里是不是感覺到了塊級(jí)作用域的出現(xiàn)還是很有必要的。
3. 作用域鏈在講解作用域鏈之前先說一下,先了解一下 JavaScript是如何執(zhí)行的?
3.1 JavaScript是如何執(zhí)行的?
JavaScript代碼執(zhí)行分為兩個(gè)階段:
javascript編譯器編譯完成,生成代碼后進(jìn)行分析
分析函數(shù)參數(shù)
分析變量聲明
分析函數(shù)聲明
分析階段的核心,在分析完成后(也就是接下來函數(shù)執(zhí)行階段的瞬間)會(huì)創(chuàng)建一個(gè)AO(Active Object 活動(dòng)對(duì)象)
3.1.2 執(zhí)行階段分析階段分析成功后,會(huì)把給AO(Active Object 活動(dòng)對(duì)象)給執(zhí)行階段
引擎詢問作用域,作用域中是否有這個(gè)叫X的變量
如果作用域有X變量,引擎會(huì)使用這個(gè)變量
如果作用域中沒有,引擎會(huì)繼續(xù)尋找(向上層作用域),如果到了最后都沒有找到這個(gè)變量,引擎會(huì)拋出錯(cuò)誤。
執(zhí)行階段的核心就是找,具體怎么找,后面會(huì)講解LHS查詢與RHS查詢。
3.1.3 JavaScript執(zhí)行舉例說明看一段代碼:
function a(age) { console.log(age); var age = 20 console.log(age); function age() { } console.log(age); } a(18);
前面已經(jīng)提到了,函數(shù)運(yùn)行的瞬間,創(chuàng)建一個(gè)AO (Active Object 活動(dòng)對(duì)象)
AO = {}
第一步:分析函數(shù)參數(shù):
形式參數(shù):AO.age = undefined 實(shí)參:AO.age = 18
第二步,分析變量聲明:
// 第3行代碼有var age // 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事 即AO.age = 18
第三步,分析函數(shù)聲明:
// 第5行代碼有函數(shù)age // 則將function age(){}付給AO.age AO.age = function age() {}
函數(shù)聲明注意點(diǎn):AO上如果有與函數(shù)名同名的屬性,則會(huì)被此函數(shù)覆蓋。但是一下面這種情況
var age = function () { console.log("25"); }
聲明的函數(shù)并不會(huì)覆蓋AO鏈中同名的屬性
分析階段分析成功后,會(huì)把給AO(Active Object 活動(dòng)對(duì)象)給執(zhí)行階段,引擎會(huì)詢問作用域,找的過程。所以上面那段代碼AO鏈中最初應(yīng)該是
AO.age = function age() {} //之后 AO.age=20 //之后 AO.age=20
所以最后的輸出結(jié)果是:
function age(){ } 20 203.2 作用域鏈概念
看了前面一個(gè)完整的javascript函數(shù)執(zhí)行過程,讓我們來說下作用域鏈的概念吧。JavaScript上每一個(gè)函數(shù)執(zhí)行時(shí),會(huì)先在自己創(chuàng)建的AO上找對(duì)應(yīng)屬性值。若找不到則往父函數(shù)的AO上找,再找不到則再上一層的AO,直到找到大boss:window(全局作用域)。 而這一條形成的“AO鏈” 就是JavaScript中的作用域鏈。
3.3 找過程LHS和RHS查詢特殊說明LHS,RHS 這兩個(gè)術(shù)語就是出現(xiàn)在引擎對(duì)變量進(jìn)行查詢的時(shí)候。在《你不知道的Javascript(上)》也有很清楚的描述。在這里,我想引用freecodecamp 上面的回答來解釋:
LHS = 變量賦值或?qū)懭雰?nèi)存。想象為將文本文件保存到硬盤中。 RHS = 變量查找或從內(nèi)存中讀取。想象為從硬盤打開文本文件。 Learning Javascript, LHS RHS3.3.1 LHS和RHS特性
都會(huì)在所有作用域中查詢
嚴(yán)格模式下,找不到所需的變量時(shí),引擎都會(huì)拋出ReferenceError異常。
非嚴(yán)格模式下,LHR稍微比較特殊: 會(huì)自動(dòng)創(chuàng)建一個(gè)全局變量
查詢成功時(shí),如果對(duì)變量的值進(jìn)行不合理的操作,比如:對(duì)一個(gè)非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用,引擎會(huì)拋出TypeError異常
3.3.2 LHS和RHS舉例說明例子來自于《你不知道的Javascript(上)》
function foo(a) { var b = a; return a + b; } var c = foo( 2 );
直接看引擎在作用域找這個(gè)過程:
LSH(寫入內(nèi)存):
c=, a=2(隱式變量分配), b=
RHS(讀取內(nèi)存):
讀foo(2), = a, a ,b (return a + b 時(shí)需要查找a和b)3.4 作用域鏈總結(jié)
最后對(duì)作用域鏈做一個(gè)總結(jié),引用《你不知道的Javascript(上)》中的一張圖解釋
今天就分享這么多,如果對(duì)分享的內(nèi)容感興趣,可以關(guān)注公眾號(hào)「程序員成長(zhǎng)指北」,或者加入技術(shù)交流群,大家一起討論。
文章同步到
程序員成長(zhǎng)指北(ID:coder_growth)
作者:koala 一個(gè)有趣的人
github博客地址:
https://github.com/koala-codi...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/105684.html
摘要:函數(shù)作用域要理解閉包,必須從理解函數(shù)被調(diào)用時(shí)都會(huì)發(fā)生什么入手。可以說,閉包是函數(shù)作用域的副產(chǎn)品。無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會(huì)持有對(duì)原始定義作用域的引用,無論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。 函數(shù)作用域 要理解閉包,必須從理解函數(shù)被調(diào)用時(shí)都會(huì)發(fā)生什么入手。 我們知道,每個(gè)javascript函數(shù)都是一個(gè)對(duì)象,其中有一些屬性我們可以訪問到,有一些不可以訪問,...
摘要:理解作用域高級(jí)程序設(shè)計(jì)中有說到對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的在全局函數(shù)中,等于,而當(dāng)函數(shù)被作為某個(gè)對(duì)象調(diào)用時(shí),等于那個(gè)對(duì)象。指向與匿名函數(shù)沒有關(guān)系如果函數(shù)獨(dú)立調(diào)用,那么該函數(shù)內(nèi)部的,則指向。 理解this作用域 《javascript高級(jí)程序設(shè)計(jì)》中有說到: this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中,this等于window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象調(diào)用時(shí),t...
摘要:作用域與作用域鏈每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境。這是初步了解作用域,如想更深入了解作用域,請(qǐng)看下面鏈接作用域原理作用域鏈由一道題圖解的作用域或者看權(quán)威指南和高級(jí)程序設(shè)計(jì) 本文是我學(xué)習(xí)JavaScript作用域整理的筆記,如有不對(duì),請(qǐng)多指出。 作用域 一個(gè)變量的作用域是程序源代碼中定義這個(gè)變量的區(qū)域。 而在ES5中只分為全局作用域和函數(shù)作用域,也就是說for,if,while等語句是不會(huì)創(chuàng)建...
摘要:函數(shù)高級(jí)作用域與作用域鏈一作用域作用域個(gè)數(shù)定義的函數(shù)個(gè)數(shù)全局作用域理解就是一塊地盤一個(gè)代碼段所在的區(qū)域。函數(shù)執(zhí)行上下文環(huán)境是在調(diào)用函數(shù)時(shí)函數(shù)體代碼執(zhí)行之前創(chuàng)建。 JavaScript函數(shù)高級(jí)——作用域與作用域鏈 一、作用域 作用域個(gè)數(shù) = n(定義的函數(shù)個(gè)數(shù)) + 1(全局作用域)(1)理解 就是一塊地盤, 一個(gè)代碼段所在的區(qū)域。 它是靜態(tài)的(相對(duì)于上下文對(duì)象), 在編寫代碼時(shí)就確定...
摘要:之前一篇文章我們?cè)敿?xì)說明了變量對(duì)象,而這里,我們將詳細(xì)說明作用域鏈。而的作用域鏈,則同時(shí)包含了這三個(gè)變量對(duì)象,所以的執(zhí)行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當(dāng)前的函數(shù)調(diào)用棧,為當(dāng)前正在被執(zhí)行的函數(shù)的作用域鏈,為當(dāng)前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學(xué)JavaScrip...
閱讀 2518·2023-04-25 22:09
閱讀 1036·2021-11-17 17:01
閱讀 1577·2021-09-04 16:45
閱讀 2629·2021-08-03 14:02
閱讀 823·2019-08-29 17:11
閱讀 3261·2019-08-29 12:23
閱讀 1096·2019-08-29 11:10
閱讀 3287·2019-08-26 13:48