(1)作用域
一個變數的作用域(scope)是程式原始碼中定義的這個變數的區域。
1. 在JS中使用的是詞法作用域(lexical scope)
不在任何函數內宣告的變數(函數內省略var的也算全域)稱為全域變數(global scope)
函數內宣告的變數具有函數作用域(function scope),屬於局部變數
局部變數優先權高於全域變數
函數內省略var的,會影響全域變量,因為它實際上已經被重寫成全域變數
函數作用域,就是說函數是作用域的基本單位,js不像c/c 那樣有區塊級作用域 例如 if for 等
當然了,js裡邊還使用到了高階函數,其實可以理解成巢狀函數
test1()之後將呼叫外層函數,傳回了一個內層函數,再繼續(),就對應呼叫執行了內層函數,所以就輸出 ”one"
巢狀函數涉及到了閉包,後面再談..這裡內層函數可以存取到外層函數中聲明的變數name,這就涉及到了作用域鏈機制
2. JS中的陳述提前
js中的函數作用域是指在函數內宣告的所有變數在函數體內始終是可見的。而且,變數在宣告之前就可以使用了,這種情況就叫做宣告提前(hoisting)
tip:宣告提前是在js引擎預編譯時就進行了,在程式碼被執行之前已經有宣告提前的現象產生了
如
上邊就達到下面的效果
再試試把var去掉?這是函數內的name已經變成了全域變量,所以不再是undefined
3. 值得注意的是,上面提到的都沒有傳參數,如果test有參數,又如何呢?
之前說過,基本型別是按值傳遞的,所以傳進test裡面的name其實只是一個副本,函數回傳之後這個副本就被清除了。
千萬不要以為函數裡邊的name="two"把全域name修改了,因為它們是兩個獨立的name
(2)作用域鏈
上面提到的高階函數就牽涉到了作用域鏈
1. 引入一大段話來解釋:
每一段js程式碼(全域程式碼或函數)都有一個與之關聯的作用域鏈(scope chain)。
這個作用域鍊是一個物件清單或是鍊錶,這組物件定義了這段程式碼中「作用域中」的變數。
當js需要查找變數x的值的時候(這個過程稱為變數解析(variable resolution)),它會從鏈的第一個物件開始查找,如果這個物件有一個名為x的屬性,則會直接使用這個屬性的值,如果第一個物件中沒有名為x的屬性,js會繼續尋找鏈上的下一個物件。如果第二個物件仍然沒有名為x的屬性,則會繼續尋找下一個,以此類推。如果作用域鏈上沒有任何一個物件含有屬性x,那麼就認為這段程式碼的作用域鏈上不存在x,最後拋出一個引用錯誤(ReferenceError)異常。
2. 作用域鏈舉例:
在js最頂層程式碼中(也就是不包括任何函數定義內的程式碼),作用域鏈由一個全域物件組成。
在不包含巢狀的函數體內,作用域鏈上有兩個對象,第一個是定義函數參數和局部變數的對象,第二個是全域對象。
在一個嵌套的函數體內,作用域上至少有三個物件。
3. 作用域鏈建立規則:
當定義一個函數時(注意,是定義的時候就開始了),它實際上保存一個作用域鏈。
當呼叫這個函數時,它會建立一個新的物件來儲存它的參數或局部變量,並將這個物件加入儲存到那個作用域鏈上,同時建立一個新的更長的表示函數呼叫作用域的「鏈」。
對於巢狀函數來說,情況又有所變化:每次呼叫外部函數的時候,內部函數又會重新定義一次。因為每次呼叫外部函數的時候,作用域鏈都是不同的。內部函數在每次定義的時候都要微妙的差異---在每次呼叫外部函數時,內部函數的程式碼都是相同的,而且關聯這段程式碼的作用域鏈也不相同。
(tip: 把上面三點理解好,記住了,最好還要能用自己的話說出來,不然就背下來,因為面試官就直接問你:請描述一下作用域鏈... )
舉個作用域鏈的實用例子:
上邊是個巢狀函數,對應的應該是作用域鏈上有三個物件
那麼在呼叫的時候,就需要找出name的值,就在作用域鏈上找
當成功呼叫test1()的時候,順序為 test1()->test()->全域物件window 因為在test1()上就找到了name的值three,所以完成搜尋回傳
當成功呼叫test1()的時候,順序為test2()->test()->全域物件window 因為在test2()上沒找到name的值,所以找test()中的,找到了name的值two,就完成搜尋回傳
還有一個例子有時候我們會犯錯的,面試的時候也常常被騙到。