做用域是你的代碼在運行時,某些特定部分中的變量,函數和對象的可訪問性。換句話說,做用域決定了變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期
。javascript
在 JavaScript 中有兩種做用域java
若是一個變量在函數外面或者大括號{}
外聲明,那麼就定義了一個全局做用域
,在ES6以前局部做用域只包含了函數做用域,ES6爲咱們提供的塊級做用域
,也屬於局部做用域git
擁有全局做用域的對象能夠在代碼的任何地方訪問到, 在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
在任何地方均可以訪問到, 反之不具備全局做用域特性的變量只能在其做用域內使用。es6
function func1(){ special = 'special_variable'; var normal = 'normal_variable'; } func1(); console.log(special); //special_variable console.log(normal) // normal is not defined
雖然咱們能夠在全局做用域中聲明函數以及變量, 使之成爲全局變量, 可是不建議這麼作,由於這可能會和其餘的變量名衝突,一方面若是咱們再使用const
或者let
聲明變量, 當命名發生衝突時會報錯。github
// 變量衝突 var globleVariable = "person"; let globleVariable = "animal"; // Error, thing has already been declared
另外一方面若是你使用var
申明變量,第二個申明的一樣的變量將覆蓋前面的,這樣會使你的代碼很難調試。面試
var name = 'koala' var name = 'xiaoxiao' console.log(name); // xiaoxiao
和全局做用於相反,局部做用域通常只能在固定代碼片斷內能夠訪問到。最多見的就是函數做用域。koa
定義在函數中的變量就在函數做用域中。而且函數在每次調用時都有一個不一樣的做用域。這意味着同名變量能夠用在不一樣的函數中。由於這些變量綁定在不一樣的函數中,擁有不一樣做用域,彼此之間不能訪問。
//全局做用域 function test(){ var num = 9; // 內部能夠訪問 console.log("test中:"+num); } //test外部不能訪問 console.log("test外部:"+num);
注意點:函數
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
是同級做用域。spa
變量提高英文名字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中引入的塊級做用域以後的事!
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 }
看到這裏是否是感受到了塊級做用域的出現仍是頗有必要的。
在講解做用域鏈以前先說一下,先了解一下 JavaScript是如何執行的?
JavaScript代碼執行分爲兩個階段:
javascript編譯器編譯完成,生成代碼後進行分析
分析階段的核心,在分析完成後(也就是接下來函數執行階段的瞬間)會建立一個AO(Active Object 活動對象)
分析階段分析成功後,會把給AO(Active Object 活動對象)
給執行階段
執行階段的核心就是找
,具體怎麼找
,後面會講解LHS查詢
與RHS查詢
。
看一段代碼:
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 20
看了前面一個完整的javascript函數執行過程,讓咱們來講下做用域鏈的概念吧。JavaScript上每個函數執行時,會先在本身建立的AO上找對應屬性值。若找不到則往父函數的AO上找,再找不到則再上一層的AO,直到找到大boss:window(全局做用域)。 而這一條造成的「AO鏈」 就是JavaScript中的做用域鏈。
找
過程LHS和RHS查詢特殊說明LHS,RHS 這兩個術語就是出如今引擎對變量進行查詢的時候。在《你不知道的Javascript(上)》也有很清楚的描述。在這裏,我想引用freecodecamp
上面的回答來解釋:
LHS = 變量賦值或寫入內存。想象爲將文本文件保存到硬盤中。 RHS = 變量查找或從內存中讀取。想象爲從硬盤打開文本文件。 Learning Javascript, LHS RHS
ReferenceError
異常。LHR
稍微比較特殊: 會自動建立一個全局變量TypeError
異常例子來自於《你不知道的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)
最後對做用域鏈作一個總結,引用《你不知道的Javascript(上)》中的一張圖解釋
今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。
文章同步到
程序員成長指北(ID:coder_growth)
做者:koala 一個有趣的人
github博客地址:
https://github.com/koala-codi...