在What is Scope與Lexical Scope對辭法範圍做具體的定義與基本概念介紹,也了解作用域的嵌套與找尋變量的像上搜尋,在本章節中將介紹什麼是作用域鏈
,可以把它想像為是連接著嵌套作用域之間的通道
,而這個作用域鏈是固定方向的,它意味著變量的查詢只能往上/外層作用域移動。
我們在前兩章中使用了這個程式來區分作用域,若我們在for...loop中呼叫了students
這個變量,我們在Lexical Scope提到了lookup的概念,JS引擎會先在當前的作用域中查詢使否有宣告這個變量,若沒有會持續向上層作用域中查詢直到全域作用域中,這個查詢的過程有助於我們了解作用域個概念,但在正常的情況下JS通常是不這麼做的
。
通常在編譯的過程中就會決定好變量的scope,基於這個原理,所以變量的作用域是不會根據程式運行時發生變化,由於scope是在編譯中得知並且不可變的,所以這個scope的訊息有可能與AST儲存在一起,然後當程式運行的時候使用這個訊息。
換句話說,其實JS並不需要像Lexical Scope說的要一一遍歷作用域來找到這個變量在哪個作用域中宣告,因為這個訊息其實在編譯期間就知道了,透過不需要花費時間進行搜尋,它可以使我們的程序更高效能的運行,
不過凡事都有個例外,若我們使用了一個從未在任何地方宣告個變數,因為其實每一個專案對於JS來說都是獨立的,所以就算在這個專案中沒有辦法找這個變量的宣告,但是他也有可能被宣告在共享的全域範圍中,所以不一定會是個錯誤。
因此可能將這個結果(變量準確宣告的位置)需要推遲到運行的時候才知道,所以當JS還沒編譯到其他專案的這個期間,這個變量都使處於無法確定作用域的情況,而延遲查找最終才會將這個變量宣告(有可能是全域中)但是這個動作只需要進行一次,因為作用域是無法被更改的。
當兩個或多個變量(每個同名的變量都存在於不同作用域中)時,具有不同lexical scope的問題就變個更重要,值得注意的是,當你在同一個作用域中,你不能同時宣告同一個名稱的變量,他只能接受一個名字一個宣告,因此若需要使用兩個或多個同名的變量則需要使用嵌套作用域。
var studentName = "Suzy";
function printStudent(studentName) {
studentName = studentName.toUpperCase();
console.log(studentName);
}
printStudent("Frank"); // FRANK
printStudent(studentName); // SUZY
console.log(studentName); // Suzy
在上面這段程式中我們在第一行中(全域作用域)宣個了一個變量studentName
,而相同名字的變量我們也宣告了一個,但是他是存在於printStudent(...)作用域中。
藉由lookup的概念,當我們使用了一個變量時,他會從當前的作用中往上查詢,一但找到匹配的變量就會停止,所以當function中的console.log(...)使用到studentName這個變量,他先在當前作用域中(printfStudent(...))尋找,發現在當前作用域中就找到了這個變量的宣告,那麼他就會停止收尋並直接使用這個變量(不會考慮全域作用域中的studentName)。
這個概念便是lexcal scope中的shadowing
,printfStudent(...)作用域中的studentName shadowing了全域作用域中的studentName,若是你需要使用到外部作用域的變量,那麼你就不能在自身作用域中宣告一個相同名字的變量,因為這樣做他將永遠只會使用自身作用域的變量。
注意!這不是一個好的方法,他可能造成你的程式發生非預期的錯誤
。
其實有方法可以在被變量shadowing的情況下使用到全域中的變量。
var studentName = "Suzy";
function printStudent(studentName) {
console.log(studentName);
console.log(window.studentName); //uee global
}
// "Frank"
// "Suzy"
可以使用window.studentName
這樣的方法來訪問到全域中的studentName,這是唯一能夠在shadowing情況下訪問到外部作用域變數的方法。
這個技巧只適用於訪問全域作用域中的變量(不能訪問到非全域的上一層作用域)。
var special = 42;
function lookingFor(special) {
function keepLooking() {
var special = 3.141592;
console.log(special);
console.log(window.special);
}
keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 42
在全域中宣告了一個special,也在lookingFor(...)和keepLooking(...)中宣告了
一樣名字的變數,在keepLooking(...)中使用了special變數,可是因為他自身的作用域中就有宣告這個變數,所以就直接使而不會使用lookingFor(...)和全域的,而下一行使用window
則會直接訪問到全域作用域中的變數,所以使用window只能訪問全域中的變數而不能訪問非全域的上一層作用域
。
即使是全域也只能訪問到var
與function
。
var one = 1;
let notOne = 2; //use let
const notTwo = 3; //use const
class notThree {} //object
console.log(window.one); // 1
console.log(window.notOne); // undefined
console.log(window.notTwo); // undefined
console.log(window.notThree); // undefined
var special = 42;
function lookingFor(special) {
var another = {
// use object to copy value of arguments to another.special
special: special
};
function keepLooking() {
var special = 3.141592;
console.log(special);
console.log(another.special); // Ooo, tricky!
console.log(window.special);
}
keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 112358132134
// 42
也可以透過這種方法訪問到上層作用域,這個概念是將原本被shadowing的變數複製給另一個變量
,那麼這個被複製的變量就可以被訪問到(除非他也被shadowing),但是這個行為並不代表是特殊的訪問方法,他只是藉由另一個容器(擁有被shadowing的變量的值)(有點饒舌)
讓我們可以訪問到他,它意味著我們訪問的並不是lookingFor(...)中的special。
不是所有宣告都能夠shadowing上一層作用域的變數,let可以shadowing var
,但var不能shadowing let
function something() {
var special = "JavaScript";
{
let special = 42; // totally fine shadowing
}
}
function another() {
{
let special = "JavaScript";
{
var special = "JavaScript"; // ^^^ Syntax Error
}
}
}
因為var宣告會試圖跨越同名的let宣告範圍,換句話說使用let宣告的這個special會阻止var的hoasting,所以就導致了錯誤。
var askQuestion = function ofTheTeacher() {
console.log(ofTheTeacher);
};
askQuestion(); // function ofTheTeacher()...
console.log(ofTheTeacher); // ReferenceError: ofTheTeacher is not defined
當我們呼叫askQuestion時,有成功console出ofTheTeacher(...),但是直接console ofTheTeacher卻無法成功,是因為這是一個funciton expression
,對於ofTheTeacher(...)來說,他只存活在賦予askQuestion這段時間,一但他將自身賦予給了askQuestion後他便會消失,所以在其他地方呼叫他才會是not defined。
在ES6中加入了箭頭函數
var askQuestion = () => {
//...
}
使用=>
讓我們可以不需要對這個function命名,由於這特性,意味沒辦法透過function名稱來呼叫他,雖然箭頭函數是匿名的形式,但是他的辭法規則與正常的function一樣,無論帶不帶有{...}的箭頭函數都會創建一個單獨的內部作用域,這個作用域的規則與function一樣。
參考文獻:
You Don't Know JavaScript