EC 裡面裝些什麼?Scope Chain 怎麼來的 ?
當我們遇到 function 被呼叫時,JS 生出 EC,每一個 EC 都會有 variable Object (之後簡稱 VO), 這個 VO 裡面初始化function 裡面的變數以及 function 的宣告。
初始化:
EC: {
VO{
a:undefined
}
}
function test() {
var a = 666
}
test()
EC 初始化後,進入執行階段後會變這樣:
EC: {
VO{
a:666
}
}
function test() {
var a = 666
}
test()
如果 function 帶有參數的話呢? 有值的會帶入值,如果沒有值就會初始化成 undefined。
EC: {
VO{
a:666,
b:undefined
}
}
function test(a, b) {
//...
}
test(666)
而如果遇到 function呢? 一樣會先初始化。
EC: {
VO{
a:666,
b:undefined,
c:function
}
}
function test(a, b) {
//...
function c () { //... }
c()
}
test(666)
那如果剛好這個 function 跟 參數名字一樣呢,那就會被取代:
EC: {
VO{
a: <function>,
}
}
function test(a) {
function a () { //... }
}
test(666)
而對於如果遇到宣告的變數是同名呢? 那就會忽略這個變數宣告,值也不會被改變,因為剛剛已經宣告過了。如果原本沒存在的,就是 undefined。
EC: {
VO{
a: 666,
b: <function>,
c: 888
}
}
function test(a,b) {
function b () { //... }
var a = 777 // 被跳過了
var c = 888
}
test(666)
好,這只是 EC 裡面 VO 的部分, EC 裡面還有一個很重要的東西叫 Scope Chain,當我們進入 EC 時,會建立 VO 以及 建立 Scope Chain 還有個 this,而且如果 VO 裡面也有 function 宣告的話會為這個 function 建立 [[Scope]],而 Scope Chain 跟 VO 一樣,也是在做初始化的事情。
如果此 EC 是 global EC,則 global EC 中的 Scope Chain 就稱為 global variable object (global VO)。
如果是此 EC 是 function 的話,就會有 activation object
(簡稱 AO),,什麼是 AO 呢? 可以把他跟 VO 當成是一樣的就好,只是在 global EC 裡面我們稱為 VO,在 function EC 裡我們稱為 AO。
function EC 跟 global EC 一樣 裡面放著 variable object 、function EC 的 Scope Chain (自己的AO + function EC的 [[Scope]] )、this、以及如果有遇到 function 的宣告就產生的
[[ Scope ]]
我們帶個例子來看會比較清楚:
var a = 1
function test() {
var b = 2
function inner() {
var c = 3
}
inner()
}
test()
首先是進入 global EC :
此時的 global.scopeChain = [globalEC.VO]
此時的 test.[[Scope]] = globalEC.scopeChain = [globalEC.VO]
之後執行完畢,會遇到 test() ,一樣先初始化:
testEC.scopeChaine = [testEC.AO, test.[[Scope]] ] = [testEC.AO, [globalEC.VO] ]
inner.[[Scope]] = testEC.scopeChain
初始化完成,繼續執行 => 遇到 inner() => 初始化 inner :
scopeChain: [innerEC.AO,testEC.scopeChain] = [innerEC.AO, testEC.AO, gloEC.VO]
hoisting
認識了 Scope Chain 後,默的就幫我們演示了一層一層往上找的 Scope Chain 的及 hoisting 過程了,不是嗎?
進入 某 EC 的時候,會先宣告變數或 function,因為還沒有賦值,在執行賦值之前如果我們去使用變數,這個時候的變數就是 undefined,如此而已。
例如當我們在宣告變數之前就使用變數的話,是不會丟出錯誤的,只是會變成 undefined。
console.log(b) // undefined
var b = 666
其實我們可以解讀成這樣子:
var b
console.log(b)
b = 666
還有我們常使用的 function 也是有 hoisting 的效果,看下面的例子,程式不會報錯
,也會順利執行 test function 。
test()
function test(){
//....
}
再提供一些比較 tricky 的例子:
var a = 666
function test(){
console.log(a)
var a = 777
}
test()
曾經的我,一直覺得他會 console 666 ,但其實不是喔!!! 因為在建立 testEC AO 的時候,我們將 var a 放進去了,所以進入賦值之前是 undefined 。
還有 function 宣告 hoisting 會比變數還高 :
function test(){
console.log(a) // <function>
var a = 777
function a() {
console.log('OMG')
}
}
test()
那麼如果是帶參數的 function 並且又宣告一樣的變數呢? 這樣參數會蓋過去。
function test(a){
console.log(a) // 666
var a = 777
console.log(a) // 777
}
test(666)
但是如果 function 的話,優先度會提高:
function test(a){
console.log(a) // <function>
function a(){
}
}
test(666)
總之優先順序是 1. function > 2. arguments > 3. var
還有要注意在寬鬆模式下的話,如果有個變數在 global 中沒有宣告,但是在 function 裡面出現一個沒宣告過的變數,但是有給賦值,JS 就會在 global 新增這個變數,並且給予值。(如果是嚴格模式下就會報錯)
function fn(){
fn2()
function fn2(){
b='OMG'
}
}
fn()
console.log(b) // OMG
let, const 與 var
let, const 與 var 最大的不同是, var 變數的生存範圍是以 function 區塊來界定,然而 const, let 是以 block 來界定的。
比如說 if { .... }
或迴圈都是 block,如果在 block 裡面使用 let, const ,在 block 外面使用變數的話是會報錯的。
Temporal Dead Zone,TDZ
之前的我一直以為 let, const 是沒有 hoisting 的,但是如果沒有 hoisting ,下面的程式會跑出 666 才對,但卻是跑出 ReferenceError ...
。
let a = 666
function test(){
console.log(a)
let a = 777
}
test()
let 與 const 其實是有 hoisting 的,但不是像 var 一樣,初始化時會是 undefined,並且在賦值以前如果去存取,會拋出錯誤。
在 hoisting 至 賦值以前 這段時間,我們稱它為 TDZ,在這段時間內存取該變數,會拋出錯誤!
let a = 666
function test(){
// ======= a TDZ 開始
console.log(a)
.
.
.
let a = 777 // ======= a TDZ 結束
}
test()
let a = 666
function test(){
test2() // ======= a TDZ 開始
let a = 777 // ======= a TDZ 結束
funciton test2(){
console.log(a)
}
}
test()
總之,就是記得賦值以前去存取 let 與 const 宣告的變數,會發生錯誤就對了。