今天看筆記發現本身以前記了一個關於同名標識符優先級的內容,具體是下面這樣的:javascript
arguments
而後我就想,爲何會有這樣的優先級呢,規定的?可是好像沒有這個規定,因而開始查閱資料,就有了下文html
Execution Context
Execution Context
是Javascript
中一個抽象概念,它定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。爲了便於理解,咱們能夠近似將其等同於執行當前代碼的環境,JavaScript
的可執行代碼包括前端
eval()
代碼每當執行流轉到這些可執行代碼時,就會「新建」一個Execution Context
並進入該Execution Context
java
在上圖中,共有4個Execution Context
,其中有一個是Global Execution Context
(有且僅有一個),還有三個Function Execution Context
segmentfault
Execution Context Stack
瀏覽器中的JavaScript
解釋器是單線程的,每次建立並進入一個新的Execution Context
時,這個Execution Context
就會被推(push
)進一個環境棧中,這個棧稱爲Execution Context Stack
,噹噹前Execution Context
的代碼執行完以後,棧又會將其彈(pop
)出,並銷燬這個Execution Context
,保存在其中的變量及函數定義也隨之被銷燬,而後把控制權返回給以前的Execution Context
(Global Execution Context
例外,它要等到應用程序退出後 —— 如關閉網頁或瀏覽器 —— 纔會被銷燬)瀏覽器
JavaScript
的執行流就是由這個機制控制的,如下面的代碼爲例說明:閉包
var sayHello = 'Hello'; function name(){ var fisrtName = 'Cao', lastName = 'Cshine'; function getFirstName(){ return fisrtName; } function getLatName(){ return lastName; } console.log(sayHello + getFirstName() + ' ' + getLastName()); } name();
script
的時候,默認會進入Global Execution Context
,因此Global Execution Context
永遠是在棧的最下面。name()
,此時新建並進入Function Execution Context name
,Function Execution Context name
入棧;getFirstName()
,因而新建並進入Function Execution Context getFirstName
,Function Execution Context getFirstName
入棧,因爲該函數內部不會再新建其餘Execution Context
,因此直接執行完畢,而後出棧,控制權交給Function Execution Context name
;getLastName()
,因而新建並進入Function Execution Context getLastName
,Function Execution Context getLastName
入棧,因爲該函數內部不會再新建其餘Execution Context
,因此直接執行完畢,而後出棧,控制權交給Function Execution Context name
;console
後,函數name
也執行完畢,因而出棧,控制權交給Function Execution Context name
,至此棧中又只有Global Execution Context
了關於Execution Context Stack
有5個關鍵點:異步
Global Execution Context
Function Execution Context
每一個函數調用都會建立新的Execution Context
,即便是本身調用本身,以下面的代碼:函數
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
Execution Context Stack
的狀況以下圖所示:ui
Execution Context
每一個Execution Context
在概念上能夠當作由下面三者組成:
Variable object
,簡稱VO
)Scope Chain
)this
Variable object
)該對象與Execution Context
相關聯,保存着Execution Context
中定義的全部變量、函數聲明以及函數形參,這個對象咱們沒法訪問,可是解析器在後臺處理數據是用到它(注意函數表達式以及沒用var/let/const
聲明的變量不在VO
中)
Global Execution Context
中的變量對象VO
根據宿主環境的不一樣而不一樣,在瀏覽器中爲window
對象,所以全部的全局變量和函數都是做爲window
對象的屬性和方法建立的。
對於Function Execution Context
,變量對象VO
爲函數的活動對象,活動對象是在進入Function Execution Context
時建立的,它經過函數的arguments
屬性初始化,也就是最初只包含arguments
這一個屬性。
在JavaScript
解釋器內部,每次調用Execution Context
都會經歷下面兩個階段:
建立階段(發生在函數調用時,可是內部代碼執行前,這將解釋聲明提高現象)
VO
this
的值激活/代碼執行階段
其中建立階段的第二步建立變量對象VO
的過程能夠理解成下面這樣:
Global Execution Context
中沒有這一步) 建立arguments
對象,掃描函數的全部形參,並將形參名稱 和對應值組成的鍵值對做爲變量對象VO
的屬性。若是沒有傳遞對應的實參,將undefined
做爲對應值。若是形參名爲arguments
,將覆蓋arguments
對象掃描Execution Context
中全部的函數聲明(注意是函數聲明,函數表達式不算)
VO
的屬性VO
已經存在同名的屬性,則覆蓋這個屬性掃描Execution Context
中全部的變量聲明
undefined
) 組成,做爲變量對象的屬性好~~如今咱們來看代碼捋一遍:
function foo(num) { console.log(num);// 66 console.log(a);// undefined console.log(b);// undefined console.log(fc);// f function fc() {} var a = 'hello'; var b = function fb() {}; function fc() {} } foo(66);
當調用foo(66)時,建立階段時,Execution Context
能夠理解成下面這個樣子
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 66, length: 1 }, num: 66, fc: pointer to function fc() a: undefined, b: undefined }, this: { ... } }
當建立階段完成之後,執行流進入函數內部,激活執行階段,而後代碼完成執行,Execution Context
能夠理解成下面這個樣子:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 66, length: 1 }, num: 66, fc: pointer to function fc() a: 'hello', b: pointer to function fb() }, this: { ... } }
Scope Chain
)當代碼在一個Execution Context
中執行時,就會建立變量對象的一個做用域鏈,做用域鏈的用途是保證對執行環境有權訪問的全部變量和函數的有序訪問
Global Execution Context
中的做用域鏈只有Global Execution Context
的變量對象(也就是window
對象),而Function Execution Context
中的做用域鏈還會有「父」Execution Context
的變量對象,這裏就會要牽扯到[[Scopes]]
屬性,能夠將函數做用域鏈理解爲---- 當前Function Execution Context
的變量對象VO
(也就是該函數的活動對象AO
) + [[Scopes]]
,怎麼理解呢,咱們繼續往下看
[[Scopes]]
屬性[[Scopes]]
這個屬性與函數的做用域鏈有着密不可分的關係,JavaScript
中每一個函數都表示爲一個函數對象,[[Scopes]]
是函數對象的一個內部屬性,只有JavaScript
引擎能夠訪問。
結合函數的生命週期:
函數定義
[[Scopes]]
屬性在函數定義時被存儲,保持不變,直至函數被銷燬[[Scopes]]
屬性連接到定義該函數的做用域鏈上,因此他保存的是全部包含該函數的 「父/祖父/曾祖父...」 Execution Context
的變量對象(OV
),咱們將其稱爲全部父變量對象(All POV
)[[Scopes]]
是在定義一個函數的時候決定的函數調用
Function Execution Context
,根據前面討論過的調用Function Execution Context
的兩個階段可知:先建立做用域鏈,這個建立過程會將該函數對象的[[Scopes]]
屬性加入到其中AO
(做爲該Function Execution Context
的變量對象VO
),並將建立的這個活動對象AO
加到做用域鏈的最前端this
的值經過上面的過程咱們大概能夠理解:做用域鏈 = 當前Function Execution Context
的變量對象VO
(也就是該函數的活動對象AO
) + [[Scopes]]
,有了這個做用域鏈, 在發生標識符解析的時候, 就會沿着做用域鏈一級一級地搜索標識符,最開始是搜索當前Function Execution Context
的變量對象VO
,若是沒有找到,就會根據[[Scopes]]
找到父變量對象,而後繼續搜索該父變量對象中是否有該標識符;若是仍沒有找到,便會找到祖父變量對象並搜索其中是否有該標識符;如此一級級的搜索,直至找到標識符爲止(若是直到最後也找不到,通常會報未定義的錯誤);注意:對於this
與arguments
,只會搜到其自己的變量(活動)對象爲止,而不會繼續按着做用域鏈搜素。
如今再結合例子來捋一遍:
var a = 10; function foo(d) { var b = 20; function bar() { var c = 30; console.log(a + b + c + d); // 110 //這裏能夠訪問a,b,c,d } //這裏能夠訪問a,b,d 可是不能訪問c bar(); } //這裏只能訪問a foo(50);
當瀏覽器第一次加載script的時候,默認會進入Global Execution Context
的建立階段
Scope Chain
(做用域鏈)window
對象。而後會掃描全部的全局函數聲明,再掃描全局變量聲明。以後該變量對象會加到Scope Chain
中this
的值此時Global Execution Context
能夠表示爲:
globalEC = { scopeChain: { pointer to globalEC.VO }, VO: { a: undefined, foo: pointer to function foo(), (其餘window屬性) }, this: { ... } }
接着進入Global Execution Context
的執行階段
遇到賦值語句var a = 10
,因而globalEC.VO.a = 10
;
globalEC = { scopeChain: { pointer to globalEC.VO }, VO: { a: 10, foo: pointer to function foo(), (其餘window屬性) }, this: { ... } }
遇到foo
函數定義語句,進入foo
函數的定義階段,foo
的[[Scopes]]
屬性被肯定
foo.[[Scopes]] = { pointer to globalEC.VO }
遇到foo(50)
調用語句,進入foo
函數調用階段,此時進入Function Execution Context foo
的建立階段
Scope Chain
(做用域鏈)foo
的活動對象。先建立arguments
對象,而後掃描函數的全部形參,以後會掃描foo
函數內全部的函數聲明,再掃描foo
函數內的變量聲明。以後該變量對象會加到Scope Chain
中this
的值此時Function Execution Context foo
能夠表示爲
fooEC = { scopeChain: { pointer to fooEC.VO, foo.[[Scopes]] }, VO: { arguments: { 0: 66, length: 1 }, b: undefined, d: 50, bar: pointer to function bar(), }, this: { ... } }
接着進入Function Execution Context foo
的執行階段
遇到賦值語句var b = 20;
,因而fooEC .VO.b = 20
fooEC = { scopeChain: { pointer to fooEC.VO, foo.[[Scopes]] }, VO: { arguments: { 0: 66, length: 1 }, b: 20, d: 50, bar: pointer to function bar(), }, this: { ... } }
遇到bar
函數定義語句,進入bar
函數的定義階段,bar
的[[Scopes]]
`屬性被肯定
bar.[[Scopes]] = { pointer to fooEC.VO, pointer to globalEC.VO }
遇到bar()
調用語句,進入bar
函數調用階段,此時進入Function Execution Context bar
的建立階段
Scope Chain
(做用域鏈)bar
的活動對象。先建立arguments
對象,而後掃描函數的全部形參,以後會掃描foo
函數內全部的函數聲明,再掃描bar
函數內的變量聲明。以後該變量對象會加到Scope Chain
中this
的值此時Function Execution Context bar
能夠表示爲
barEC = { scopeChain: { pointer to barEC.VO, bar.[[Scopes]] }, VO: { arguments: { length: 0 }, c: undefined }, this: { ... } }
接着進入Function Execution Context bar
的執行階段
遇到賦值語句var c = 30
,因而barEC.VO.c = 30
;
barEC = { scopeChain: { pointer to barEC.VO, bar.[[Scopes]] }, VO: { arguments: { length: 0 }, c: 30 }, this: { ... } }
遇到打印語句console.log(a + b + c + d);
,須要訪問變量a,b,c,d
bar.[[Scopes]].globalEC.VO.a
訪問獲得a=10
bar.[[Scopes]].fooEC.VO.b,bar.[[Scopes]].fooEC.VO.d
訪問獲得b=20,d=50
barEC.VO.c
訪問獲得c=30
110
bar
函數執行完畢,Function Execution Context bar
銷燬,變量c
也隨之銷燬foo
函數執行完畢,Function Execution Context foo
銷燬,b,d,bar
也隨之銷燬Global Execution Context
才銷燬,a,foo
纔會銷燬經過上面的例子,相信對Execution Context
和做用域鏈的理解也更清楚了,下面簡單總結一下做用域鏈:
Execution Context
的變量對象;Execution Context
,以此類推;Global Execution Context
的變量對象;Execution Context
可經過做用域鏈訪問外部Execution Context
;反之不能夠;下面兩種語句能夠在做用域鏈的前端臨時增長一個變量對象以延長做用域鏈,該變量對象會在代碼執行後被移除
try-catch
語句的catch
塊with
語句
將指定的對象添加到做用域鏈中
function buildUrl(){ var qs = "?debug=true"; with(location){ var url = href + qs; } //console.log(href) 將會報href is not defined的錯誤,由於with語句執行完with建立的變量對象就被移除了 return url; }
with
語句接收window.location
對象,所以其變量對象就包含了window.location
對象的全部屬性,而這個變量對象被添加到做用域鏈的前端。因此在with
語句裏面使用href
至關於window.location.href
。
如今咱們來解答最開始的優先級問題
形參優先級高於當前函數名,低於內部函數名
function fn(fn){ console.log(fn);// cc } fn('cc');
函數fn
屬於Global Execution Context
,而形參fn
屬於Function Execution Context fn
,此時做用域的前端是Function Execution Context fn
的變量對象,因此console.log(fn)
爲形參的值
function fa(fb){ console.log(fb);// ƒ fb(){} function fb(){} console.log(fb);// ƒ fb(){} } fa('aaa');
調用fa
函數時,進入Function Execution Context fa
的建立階段,根據前面所說的變量對象建立過程:
先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
掃描函數聲明時,若是變量對象VO
中已經存在同名的屬性,則覆蓋這個屬性
咱們能夠獲得fa
的變量對象表示爲:
fa.VO = { arguments: { 0:'aaa', length: 1 }, fb: pointer to function fb(), }
因此console.log(fb)
獲得的是fa.VO.fb
的值ƒ fb(){}
形參優先級高於arguments
function fn(aa){ console.log(arguments);// Arguments ["hello world"] } fn('hello world'); function fn(arguments){ console.log(arguments);// hello world } fn('hello world');
調用fn
函數時,進入Function Execution Context fn
的建立階段,根據前面所說的變量對象建立過程:
先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
先建立arguments對象,後掃描函數形參,若是形參名爲arguments,將會覆蓋arguments對象
因此當形參名爲arguments
時,console.log(arguments)
爲形參的值hello world
。
形參優先級高於只聲明卻未賦值的局部變量,可是低於聲明且賦值的局部變量
function fa(aa){ console.log(aa);//aaaaa var aa; console.log(aa);//aaaaa } fa('aaaaa');
調用fa
函數時,進入Function Execution Context fa
的建立階段,根據前面所說的變量對象建立過程:
先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
掃描函數內的變量聲明時,若是變量名與已經聲明的形參或函數相同,此時什麼都不會發生,變量聲明不會干擾已經存在的這個同名屬性
因此建立階段以後Function Execution Context fa
的變量對象表示爲:
fa.VO = { arguments: { 0:'aaaaa', length: 1 }, aa:'aaaaa', }
以後進入Function Execution Context fa
的執行階段:console.log(aa);
打印出fa.VO.aa
(形參aa
)的值aaaaa
;因爲var aa;
僅聲明而未賦值,因此不會改變fa.VO.aa
的值,因此下一個console.log(aa);
打印出的仍然是fa.VO.aa
(形參aa
)的值aaaaa
。
function fb(bb){ console.log(bb);//bbbbb var bb = 'BBBBB'; console.log(bb);//BBBBB } fb('bbbbb');
調用fb
函數時,進入Function Execution Context fb
的建立階段,根據前面所說的變量對象建立過程:
先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
掃描函數內的變量聲明時,若是變量名與已經聲明的形參或函數相同,此時什麼都不會發生,變量聲明不會干擾已經存在的這個同名屬性
因此建立階段以後Function Execution Context fb
的變量對象表示爲:
fb.VO = { arguments: { 0:'bbbbb', length: 1 }, bb:'bbbbb', }
以後進入Function Execution Context fb
的執行階段:console.log(bb);
打印出fb.VO.bb
(形參bb
)的值'bbbbb';遇到var bb = 'BBBBB';
,fb.VO.bb
的值將被賦爲BBBBB
,因此下一個console.log(bb);
打印出fb.VO.bb
(局部變量bb
)的值BBBBB
。
函數和變量都會聲明提高,函數名和變量名同名時,函數名的優先級要高。
console.log(cc);//ƒ cc(){} var cc = 1; function cc(){}
根據Global Execution Context
的建立階段中建立變量對象的過程:是先掃描函數聲明,再掃描變量聲明,且變量聲明不會影響已存在的同名屬性。因此在遇到var cc = 1;
這個聲明語句以前,global.VO.cc
爲ƒ cc(){}
。
執行代碼時,同名函數會覆蓋只聲明卻未賦值的變量,可是它不能覆蓋聲明且賦值的變量
var cc = 1; var dd; function cc(){} function dd(){} console.log(cc);//1 console.log(dd);//ƒ dd(){}
Global Execution Context
的建立階段以後,Global Execution Context
的變量對象能夠表示爲:
global.VO = { cc:pointer to function cc(), dd:pointer to function dd() }
而後進入Global Execution Context
的執行階段,遇到var cc = 1;
這個聲明賦值語句後, global.VO.cc
將被賦值爲1
;而後再遇到var dd
這個聲明語句,因爲僅聲明未賦值,因此不改變global.VO.dd
的值;因此console.log(cc);
打印出1
,console.log(dd);
打印出ƒ dd(){}
每一個Execution Context
都會有變量建立這個過程,因此會有聲明提高;根據做用域鏈,若是局部變量與外部變量同名,那麼最早找到的是局部變量,影響不到外部同名變量
JavaScript基礎系列---變量及其值類型
Understanding Scope in JavaScript
What is the Execution Context & Stack in JavaScript?
深刻探討JavaScript的執行環境和棧
做用域原理
JavaScript執行環境 + 變量對象 + 做用域鏈 + 閉包