墨言妹帶你細讀《你不知道的
JavaScript
》系列的世界,深入JavaScript
語言內部,弄清楚JavaScript
每一個零部件的用途,知其然更要知其所以然。
導讀
在第
1
章中,學習了 作用域,它是一套規則,用來管理引擎如何在當前作用域以及巢狀的子作用域中根據識別符號名稱進行變數查詢。
作用域共有兩種主要的工作模型,一是 詞法作用域( JavaScript
等);二是 動態作用域( bash
指令碼等)。
詞法作用域
- 什麼是詞法作用域
- 為什麼廢棄欺騙詞法作用域的兩種機制
2.1 詞法階段
在第1章學習了,大部分標準語言編譯器的 第一個工作階段 是詞法化( token
化 )。
詞法作用域 就是在詞法分析時定義的作用域,即在寫程式碼時,由變數和塊作用域的位置決定的。因此,在詞法分析時也是固定不變的了(不考慮欺騙詞法作用域情況)。
下面這段示例程式碼有三個巢狀作用域:
- 圈1 包含了全域性作用域,只有一個識別符號號
foo
- 圈2 包含
foo
作用域,有三個識別符號a
、bar
、b
- 圈3 包含
bar
作用域 ,有一個識別符號c
作用域的範圍 是根據作用域程式碼塊定義的位置決定的,在這裡每個函式建立了一個作用域。
這裡作用域巢狀是嚴格的,一個函式不能同時存在於兩個外部函式中。
2.1.1 查詢
-
作用域查詢會在找到第一個匹配的識別符號時停止。
-
遮蔽效應:在多層巢狀作用域中可以定義同名的識別符號,內部的識別符號會 遮蔽 外部的識別符號。
-
全域性變數是全域性物件的屬性,被覆蓋的非全域性物件則無法被訪問到。
window.a 複製程式碼
-
詞法作用域查詢只會查詢一級識別符號, 比如
a
、b
、c
。如果程式碼中引用了foo.bar.baz
,詞法作用域只會查詢foo
識別符號,找到後,物件屬性訪問規則 會分別接管對bar
和baz
屬性的訪問。
2.2 欺騙詞法
欺騙詞法作用域 會導致效能下降,以下兩種方法都 不推薦使用。
2.2.1 eval
eval(...)
函式可以接受一個字串作為引數,並把字串的內容當作程式碼執行,從而實現對詞法作用域環境的修改。- 在執行
eval()
之後的那些程式碼,引擎不知道、也不去關心前面的程式碼是 動態編譯 的,且 修改 了詞法作用域環境。引擎只會一如既往地進行詞法作用域查詢。
非嚴格模式下:
function foo(a, str){
console.log(str); //2 // var b = 3;
eval(str); // 欺騙!
console.log(eval(str)); //2 //undefined
console.log(a, b); //0 2 // 1 3
console.log(a, window.b); //0 2 //1 2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;");
複製程式碼
eval()
被呼叫時,字串引數“ var b = 3; ”
被當作真正的程式碼宣告瞭變數 b ,並修改了foo()
的詞法作用域。在foo()
內部建立了一個變數b ,
遮蔽了外部全域性作用域中的同名變數b
。console.log()
被執行時,會在foo()
的內部同時找到a
和b
, 但是永遠也無法找到外部的b
。因此會輸出1 , 3
,而不是正常情況下會輸出的1 ,2
。
擴充
eval()
函式,理解值為undefiend
的知識,請點選如下:
嚴格模式下:
function foo(a, str){
"use strict";
console.log(str); //2 // var b = 3;
eval(str);
console.log(eval(str)); //2 //undefined
console.log(a, b); //0 2 // 1 2
console.log(a, window.b); //0 2 //1 2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;");
複製程式碼
eval()
在嚴格模式下,有自己的詞法作用域,其中的宣告無法修改作用域。setTimeout(...)
和setInterval(...)
的第一個引數可以是字串,字串的內容會被解釋為一段動態生成的函式程式碼。已廢棄使用。- 建構函式
new Function()
的最後一個引數可以接受程式碼字串(前面的引數是新生成的函式的形參), 避免使用。
2.2.2 with
with
通常被當作重複引用同一個物件中的多個屬性快捷方式,可不用重複引用物件本身。
var obj = {
a:1,
b:2,
c:3
};
//單調乏味的重複“ obj ”
obj.a = 2;
obj.b = 3;
obj.c = 4;
//簡單的快捷方式
with(obj){
a = 3;
b = 4;
c = 5;
}
複製程式碼
不僅僅是一個屬性訪問的 快捷方式 。如下:
function foo(obj){
with(obj){
a = 2;
}
}
var o1 = {
a : 3
};
var o2 = {
b : 4
};
console.log(o1.a);//3
foo(o1);
console.log(o1.a);//2
foo(o2);
console.log(02.a);//undefined
console.log(a);//2 -> 不好,a 被洩露到全域性作用域上了!
複製程式碼
o1
傳進後,with
宣告的作用域是o1
,a = 2
賦值操作找到o1.a
並將2
賦值給它。o2
傳進後,作用域o2
中沒有a
屬性,則進行LHS
識別符號查詢,o2
的作用域、foo()
的作用域 和全域性作用域都沒找到識別符號a
,因此當a = 2
執行時,產生副作用,自動建立了一個全域性變數(非嚴格模式)a
,並將2
賦值給a
,所以o2.a
保持undefined
。
在嚴格模式下,with 語句被完全禁用,eval() 則只保留核心功能,都不推薦使用。
2.2.3 效能
JavaScript
引擎在 編譯階段 進行各種效能優化,一些優化在詞法分析階段,靜態分析了程式碼,預先確定了變數和函式宣告的位置,所以在執行期間就可以快速解析識別符號。
2.3小結
詞法作用域只由函式被宣告時所處的位置決定。
以下兩個機制可以 欺騙 詞法作用域:
eval(...)
: 對一段包含一個或多個宣告的 程式碼 字串進行演算,藉此來修改已經存在的詞法作用域(執行時)。with
: 將一個物件的引用 當作 作用域,將物件的屬性當作作用域的識別符號,建立一個新的詞法作用域(執行時)。
副作用 是引擎無法在編譯時對作用域查詢進行優化。因為引擎只能謹慎地認為這樣的優化是無效的,使用任何一個機制都將導致程式碼執行變慢。廢棄它們。
最後, 讀書是由厚到薄,又由薄到厚的雙向過程,注重領悟、實踐,不斷踩坑、提升,若有幫助,請點個贊,謝謝您的支援與指教。
參考文獻:
歷史文章: