本文首發於貝殼社區FE專欄,歡迎關注!javascript
- 分詞/詞法分析(Tokenizing/Lexing) 這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代 碼塊被稱爲詞法單元(token)。例如,考慮程序var a = 2;。這段程序一般會被分解成 爲下面這些詞法單元:var、a、=、2 、;。空格是否會被看成詞法單元,取決於空格在 這門語言中是否具備意義。
- 解析/語法分析(Parsing) 這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法 結構的樹。這個樹被稱爲「抽象語法樹」(Abstract Syntax Tree,AST)。 var a = 2; 的抽象語法樹中可能會有一個叫做 VariableDeclaration 的頂級節點,接下 來是一個叫做 Identifier(它的值是 a)的子節點,以及一個叫做 AssignmentExpression 的子節點。AssignmentExpression 節點有一個叫做 NumericLiteral(它的值是 2)的子 節點。
- 代碼生成 將 AST 轉換爲可執行代碼的過程稱被稱爲代碼生成。這個過程與語言、目標平臺等息 息相關。 拋開具體細節,簡單來講就是有某種方法能夠將 var a = 2; 的 AST 轉化爲一組機器指 令,用來建立一個叫做 a 的變量(包括分配內存等),並將一個值儲存在 a 中。
簡而言之:java
整個編譯過程有三個角色須要登場:node
那麼整個 var a = 2;
的編譯過程以下:es6
var a = 2;
這段代碼,進行語法分析。var a
,向做用域進行變量定義操做。
a = 2
這段代碼編譯爲及其語言傳給引擎。a = 2
向做用域中去查找 a 變量,準備賦值操做。
Refence Error
錯誤。我對於 LHS 和 RHS 的理解是:全部賦值操做都是 LHS,如 a = 2;
;而全部的取值操做都是 RHS,如 console.log(a);
。面試
當變量出如今賦值操做的左側時進行 LHS 查詢,出如今右側時進行 RHS 查詢。 —— 《你不知道的 JavaScript》編程
在非嚴格模式下,當變量 a 未被定義,像 console.log(a)
這樣的RHS 查找會報 ReferenceError
的錯誤,而像 b = 2
這樣的 LHS 查找會在全局做用域下建立變量並進行賦值。數組
console.log(a); // type: RHS, output: ReferenceError
b = 2; // type: LHS, output: 2
複製代碼
而在嚴格模式下,LHS 和 RHS 的效果是相同的,都會報 ReferenceError
。安全
function foo(a){
console.log(a);
}
foo(2);
複製代碼
在以上例子中有 3 次 RHS 和 1 次 LHSbash
foo(2)
查找 foo 函數。foo(2)
隱藏着 a = 2
賦值行爲。console.log(a)
查找 console 對象console.log(a)
查找 a 變量function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
複製代碼
找出 3 次 LHS 4 次 RHS。閉包
foo(2)
查找 foo 函數。foo(2)
隱藏有 a = 2
賦值行爲。var c = foo(2)
是賦值行爲。var b = a
查找 a 變量。var b = a
是賦值行爲。return a + b
查找 a 變量。return a + b
查找 b 變量。詞法做用域就是指咱們代碼詞法所表示的做用域。看下以下代碼:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
複製代碼
這段代碼的詞法做用域如圖:
其實就是咱們在代碼編寫時所定義的做用域即詞法做用域。
固然也有不按詞法規則來的寫法,稱爲欺騙詞法。
相似於 eval()
方法會將字符串解析成 JS 語言的執行。它將破壞詞法做用域的規則。如
function foo() {
eval('var a = 3')
console.log(a) // 3
}
var a = 2;
foo();
複製代碼
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;
}
複製代碼
兩種賦值方式看似等價。但若是賦值目標是 obj 對象中沒有的變量,兩種賦值效果是不一樣的。
var obj = {
a: 1,
b: 2,
c: 3
};
obj.d = 11;
console.log(obj) // { a: 1, b:2, c:3, d: 11 }
console.log(d) // ReferenceError
複製代碼
var obj = {
a: 1,
b: 2,
c: 3
};
with (obj) {
d = 11;
}
console.log(obj) // { a: 1, b:2, c:3 }
console.log(d) // 11
複製代碼
能夠看到在 with 函數中的對於變量 d 的賦值行爲(LHS)是定義在了 window 對象上的。
一般狀況下,函數內的變量沒法在函數外調用。即變量存在於函數做用域下,因此函數做用域起到了局部變量或者變量隱藏的做用。以下例子
var a = 2;
function foo() {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
複製代碼
以上寫法將 foo 方法中的 a 變量隱藏了起來。不過也產生了一個問題 —— 全局做用域下多了一個 foo 函數變量。解決這種污染的方式是當即執行函數(IIFE),咱們將上面的代碼進行改造:
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
console.log(foo) // ReferenceError
複製代碼
這種寫法就能夠將 foo 函數變量也隱藏起來,避免對全局做用域的濡染。
塊級做用域存在於 if
, for
, while
, {}
等語法中,這些做用域中使用 var 定義的變量是不在這個做用域內的。
塊做用域和函數定義域的區別在於:函數定義域隱藏函數內的變量,而塊做用域隱藏塊中的變量。舉個栗子:
// 函數做用域,隱藏變量a
function test() {
var a = 2
}
console.log(a) // ReferenceError
複製代碼
// 塊做用域,隱藏變量 i
// 不隱藏變量 a (不是函數做用域)
for (let i = 0; i < 10; i++) {
var a = 2;
}
console.log(i) // ReferenceError
複製代碼
with 和 catch 關鍵字都會建立塊級做用域,由於他們建立的做用域在外部做用域中無效。
var obj = {
a: 1
}
with(obj) {
a = 2
}
console.log(obj) // { a: 2 }
console.log(a) // ReferenceError
複製代碼
try {
undefined(); // 執行一個非法操做來強制製造一個異常
} catch (err) {
console.log(err); // 可以正常執行!
}
console.log(err); // ReferenceError
複製代碼
let 和 const 關鍵字能夠將變量綁定到所在的任意做用域中。換句話說,let 和 const 爲其聲明的變量隱式地了所在的塊做用域。
{
let a = 2;
}
console.log(a) // ReferenceError
複製代碼
可見 const 和 let 可以保證變量隱藏在所在做用域中。
因爲 ES5 只有全局做用域和函數做用域,沒有塊級做用域,這帶來不少不合理的場景。
而 ES6 所提出的 let 和 const 爲 JavaScript 帶來了塊做用域解決了這個問題。
下面列出4點 var 與 let 的差別之處:
console.log(foo); // undefined
var foo = 2;
console.log(bar); // ReferenceError
let bar = 2;
複製代碼
var a = 3
{
console.log(a) // ReferenceError
let a
}
console.log(a) // 3
複製代碼
在使用 var 定義變量和使用 function 定義函數時,會出現變量提高的狀況。
看幾個例子來理解下變量提高:
var a = 2;
console.log( a );
// JavaScript 的處理邏輯
var a;
a = 2;
console.log(a); // 2
複製代碼
console.log( a );
var a = 2;
// JavaScript 的處理邏輯
var a;
console.log(a); // undefined
a = 2;
複製代碼
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
// JavaScript 的處理邏輯
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
複製代碼
**爲何呢?**回憶一下上文說到的編譯過程就能理解了。看圖!
能夠看到編譯器會將變量都定義到做用域中,而後再編譯代碼給引擎去執行代碼命令。即 var a = 2;
是被拆開執行的且 var a
變量會提早被定義。
再來看一個不靠譜的函數定義方法:
foo(); // "b"
var a = true;
if (a) {
function foo() {
console.log("a");
}
} else {
function foo() {
console.log("b");
}
}
複製代碼
輸出結果與《你不知道的 JavaScript》中的有所不一樣,在 node v10.5.0 中輸出的是 TypeError
而非 b
。這個差別有待考證。
雖然函數和變量都會提高,可是編譯器會先提高函數,再是變量。看以下例子:
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
複製代碼
同時是函數定義,可是第二種是定義變量的形式,因此聽從函數優先原則,以上代碼會變爲:
function foo() {
console.log(1);
}
var foo; // 無心義
foo(); // 1
foo = function () {
console.log(2);
};
複製代碼
下面是人見人怕的閉包。
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包。 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包。 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包。 重要的定義說三遍!
function foo() {
var a = 2;
function bar() {
return a;
}
return bar;
}
var baz = foo();
console.log(baz()); // 2 <-- 這就是閉包
複製代碼
按照咱們對於函數做用域的理解,函數做用域外是沒法獲取函數做用域內的變量的。
可是經過閉包,函數做用域被持久保存,而且閉包函數能夠訪問到做用域下的變量。
下面再展現幾個閉包便於理解:
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 將 baz 分配給全局變量
}
function bar() {
fn(); // <-- 閉包!
}
foo();
bar(); // 2
複製代碼
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function bar(fn) {
fn(); // <-- 閉包!
}
複製代碼
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
// timer 持有 wait 函數做用域,因此是閉包。
複製代碼
上面幾個例子能夠概括下閉包的特性:
就這麼簡單!按照這個定義其實全部的回調函數都屬因而閉包。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
複製代碼
看看以上寫法最終輸出的是什麼呢?因爲 var i = 0 是在全局做用域下,且沒有任何地方存 i 的變化值,因此最終輸出是 5 個 6
。
解決方案有兩種:
// 閉包寫法
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
複製代碼
// 塊做用域寫法
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
複製代碼
本文旨在更方便和全面的理解做用域的相關知識,但願能對你有所幫助 JavaScript 的做用域知識不論是在面試中仍是在實際工做中都是很是重要的。