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