Js 做用域和做用域鏈javascript
做用域指的就是 變量和函數 能在哪些區域能調用和使用前端
劃分區域通常指得都是函數或with、let和const(暫時性死區)、script標籤等來劃分做用域java
變量和函數做用域須要區分
數組
全局變量:聲明函數的script標籤內部和接下來要執行的script標籤,聲明變量以後調用都能訪問到,聲明以前調用獲得undefined閉包
局部變量:聲明變量以後函數內部或子函數都能訪問到,聲明以前調用獲得undefined函數
全局函數:聲明函數的script標籤內部和接下來要執行的script標籤都能調用spa
局部函數:在聲明函數的環境內或自身內部或 其子函數都能調用設計
(function(){ abc=111; })() console.log(abc)//111
(function(){ console.log(abc)//undefined var abc=111; })() console.log(abc)//ReferenceError
上述代碼中:code
第一個自執行函數內部,因沒有聲明關鍵字,自動把abc變量掛載全局對象下,因此函數外部才能訪問變量abc對象
第二個自執行函數內部,輸出adc undefined因下面有var 聲明關鍵字,形成變量提高(後面我會講到變量提高原理),子執行函數外面拋出引用錯誤,因全局對象下沒有abc變量
var g = 0; (function(){
console.log(g)//0
fnA()//ok function fnA(){ var a = 1; console.log(g);//0 console.log(a);//1 console.log(b);//ReferenceError function fnB(){ var b = 2; console.log(g);//0 console.log(a);//1 console.log(b);//2
fnB()//ok } fnB()//ok
fnA()//ok } })() console.log(g);//0 console.log(a);//ReferenceError console.log(b);//ReferenceError
上述代碼:
變量b 能在自身聲明內部調用,不能再外部調用
函數fnB 能在自身環境 自身內部 和自身子函數調用 不能再建立自身環境 父級調用(閉包除外)
前面講解了什麼是做用域,下面說說 做用域鏈
做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。《出自javascript高級程序設計第四章》
解釋上述代碼執行過程以前先說一下預編過程
1、全局
2、函數
《javascript高級程序設計第四章》中解釋到:做用域鏈的前端,始終就是當前執行的代碼所在的環境裏的AO(活動對象),我把[[scope]]對象比做成數組,數組的第一項就是自身AO對象,第二項就是父函數的AO對象,以此類推直到全局對象VO,若是自身函數活動對象AO沒有要查找的變量,就向父級函數對象查找,直到VO對象,若是都沒有就會拋出引用錯誤異常。(這種行爲叫回溯)
用做用域鏈解釋一下上述代碼的執行過程和爲何報錯:
首先預編譯過程
1、首先建立VO對象
2、檢測變量聲明,查看VO對象有沒有屬性g,若是沒有向VO對象上建立屬性g,若是有不作任何操做,賦值壓入執行棧
3、自執行函數造成執行上下文,壓入執行棧
4、剩下的三個console依次壓入執行棧
5、預編譯結束開始執行
執行過程:
1、var g = 0 當前代碼的執行是window,找到window下的屬性對象[[scope]](也就是做用域鏈),,裏面保存着VO對象,在VO對象上找到屬性g 並賦值0
2、執行 自執行函數上下文,進行預編譯
預編譯:
1、建立AO對象
2、arguments對象內的參數和方法掛載到AO上
3、檢測到函數fnA聲明,在AO上建立fnA屬性並賦值函數fnA的函數體
4、預編譯結束開始執行
執行過程:
1、執行console.log(g) console就不進去再說了。。直接說輸出g 找到當前代碼執行環境裏的屬性對象[[scope]]的自身Ao,找g屬性發現沒有,回溯查找到VO對象,找到屬性g並輸出
2、執行 fnA() 找到當前代碼執行環境裏的屬性對象[[scope]]的自身Ao,找到fnA屬性
3、執行找到的fnA函數體
依次類推。。。。
function fn(){ console.log(a)//undefined var a=1111; } fn();
預編譯進入fn函數 建立Ao對象 聲明變量a ,查到自身Ao對象上沒有屬性a ,在Ao對象上建立屬性a
執行console.log(a) 找到當前代碼執行環境裏的屬性對象[[scope]]的自身Ao,找到a屬性 這時候屬性尚未賦值 輸出默認值undefined,(這地方解釋了變量提高)
再來個例子:
function wrap(a){ console.log(a)//f a(){console.log('函數聲明')} var a = 1; function a(){ console.log('函數聲明') } a();//a is not a function a = function(){ console.log('函數表達式') } a();//函數表達式 } wrap(11111);
編譯過程:
一、建立函數對應AO對象,把arguments內的屬性和方法添加到AO對象上,
二、Var a查看AO有a屬性嗎,檢測到有a屬性(形參上有a屬性),不作任何操做
三、編譯到function a(){console.log('函數聲明')},查看AO上有a由於是聲明函數因此把function a(){console.log('函數聲明')}賦值給AO.a
四、編譯到a=function(){console.log('函數表達式')}變量賦值查看到AO有a不作任何操做
執行過程:
一、console.log(a) ; 找到代碼執行環境的Ao對象因對象有a屬性,輸出函數聲明a的函數體
二、var a = 1; 找到Ao下的屬性a賦值1
三、function a(){console.log('函數聲明')}; 函數聲明執行階段不作操做,由於a是number類型1 因此進行函數執行報錯、
四、a=function(){console.log('函數表達式')} ; 找到自身環境Ao下的屬性a 賦值 函數體
五、a(); 找到自身環境Ao下的屬性a 執行輸出 //函數表達式
瞭解做用域後,咱們知道爲何會變量提高 ,咱們也知道 js查找一次變量的消耗,因此咱們儘量地把全局的變量 賦值到的函數體內部使用,還有避免遞歸內使用全局變量
下面出個例子你們猜一下輸出內容:
var _var = 22; function wrap(_var){ console.log(_var) var _var = 11; return function _var(){ console.log(_var) } } wrap(1111)();