從JS的執行機制的角度談談作用域

不思發表於2019-03-10

JS中的作用域、閉包、this機制和原型往往是最難理解的概念之一。筆者將通過幾篇文章和大家談談自己的理解,希望對大家的學習有一些幫助。如果有什麼理解偏差的地方,希望大家可以評論指出,相互學習。

有過一定程式設計經驗的同學,一定不會對作用域感到陌生,在C/C++/Java中等語言中,作用域從來沒有JavaScript中的作用域那樣令人困惑以致於成為一個大多數JS開發者都難以跨過的門檻。

作用域形成機制

JS中存在的三種作用域型別:全域性作用域,函式作用域和ES6中新加入的塊級作用域。

var a = 1;

function foo() {
    var b = 2;
    console.log(a);		// 1
    console.log(b);		// 2
    console.log(c);		// ReferenceError
}

function foo1() {
    var c = 3;
    console.log(a);		// 1
    console.log(b);		// ReferenceError
    console.log(c);		// 3
}

console.log(a);			// 1
console.log(b); 		// ReferenceError
console.log(c);			// ReferenceError
foo();
foo1();
複製程式碼

從上面的例子可以看到,每個函式內部形成了屬於自己的作用域,函式內部宣告的變數僅僅在定義的函式內部才可以訪問。全域性作用域中可以訪問到的有a,foo,foo作用域中可以訪問到的有b,foo,a,foo1的作用域中可以訪問到的有c,foo,a。因為foo的作用域巢狀在全域性作用域之中,當console.log(a);執行的時候,JS在foo的作用域查詢不到a,就會到它的上層(這裡是foo的上層直接就是全域性作用域)查詢,發現這裡宣告瞭一個a,將它的值列印了出來。這種從裡到外的查詢就是根據作用域鏈查詢。foo1和foo的作用域沒有巢狀關係,所以相互隔離。

如果函式中使用了未宣告的變數怎麼辦?

function foo() {
	a = 2;
}

foo();
console.log(a);		// 2
複製程式碼

JS引擎在foo中查詢不到a的宣告,便會到它的上層(這裡是全域性作用域中)查詢,這個時候還是沒有查詢到a的宣告,在非嚴格模式下,JS引擎會在全域性中自動宣告一個a,這個時候,未經宣告的變數a實際上洩漏到了全域性作用域中

只有使用未宣告的變數才會出現變數洩漏的問題麼,其實,不僅僅這種寫法會出現,更常見的也會出現在for迴圈和if程式碼塊中也會出現。

for(var i=1;i<10;i++) {
    console.log(i);
}

console.log(i);		// 10,	這裡的i洩漏到了全域性作用域中

if(true) {
    var a = 2;
}

console.log(a);		// 2, 這裡的a也洩漏到了全域性變數之中
複製程式碼

如果你學習過C語言系列的語法,往往很容易感到困惑,if和for居然沒有作用域,這真是太奇怪了。這一切的問題的根源,都是由於ES6之前沒有塊級作用域導致的。所以可想而知,if包裹的程式碼塊,同樣裡面的宣告也是暴露出來的~

一切問題的解決直到ES6中引入了let和const得以完美的解決。使用let和const,將可以使用塊級作用域,使得宣告變數洩漏的問題得以解決。

for(let i=1;i<10;i++) {
    console.log(i);
}

console.log(i);		// ReferenceError

if(true) {
    let a = 2;
}

console.log(a);		// ReferenceError
複製程式碼

宣告提升機制

對於在JS中宣告的不論是變數還是函式,基本上都會存在著變數宣告提升的行為,將變數的宣告提升到所在作用域的頂端。ES6中的let和const不會,在未宣告之前都不可以使用。

看看下面的程式碼

console.log(a);				// undefined
console.log(b);				// undefined
console.log(foo);			// Function
console.log(foo2);		// ReferenceError

function foo () {
    console.log('宣告提升了哈');
}

var a = 1;

var b = function foo2() {
    console.log('不同的函式宣告方式提升的結果也不一樣哦');
};
複製程式碼

JS 引擎解釋這段程式碼之前首先對程式碼中所有的變數進行了宣告的提升,函式宣告的提升的優先順序是高於普通變數的,函式宣告會整個提升到所在作用域的頂端(但是以函式表示式方式宣告的函式不會),程式碼實際上是下面這個樣子:

function foo () {
    console.log('宣告提升了哈');
}
var a;
var b;

console.log(a);
console.log(b);
console.log(foo);
console.log(foo2);

b = function foo2 () {
    console.log('不同的函式宣告方式提升的結果也不一樣哦,這裡foo2不會作為函式提升哦');
}
複製程式碼

靜態作用域機制(詞法作用域)

關於JS中的作用域,需要明確的一點就是,JS中只存在靜態作用域(詞法作用域)。靜態作用域是什麼意思呢?意思就是它的作用域在你寫下程式碼的時候就已經確定了,和函式的呼叫順序無關,瞭解這一點。就可以對一些常見的現象進行解釋。

var a = 2;
function foo() {
  console.log(a);
}

var obj = {
    a: 3,
    foo: foo
}

obj.foo();		// 2
複製程式碼

foo中的a在程式碼寫完時就確認了,指向了全域性作用域中的a,一旦確定就無法更改了。
同理,下面的程式碼

function foo() {
    console.log(b);			// ReferenceError
}

function foo1 () {
    var b = 1;
    foo();
}

foo1();
複製程式碼

這裡,JS引擎在全域性作用域中查詢不到b,所以會丟擲一個異常。所以可以明確的道理是,foo的作用域和foo1的作用域仍然是相互獨立的,不會因為呼叫時候的順序而更改作用域的巢狀順序,靜態作用域在程式碼書寫時就已經確定無法更改了,明白這一點在分析JS程式碼的時候尤為重要。

坑外話

變數的遮蔽效應

在函式中定義的變數會遮蔽上層作用域中同名的變數,兩個變數互不影響。

var a = 1;
function foo() {
    var a = 2;
    console.log(a);	// 2
}
console.log(a);		// 1
複製程式碼

Try-Catch 中的塊級作用域

try-catch的catch中會建立一個塊級作用域,該作用域內變數的表現同樣遵守變數的宣告提升規則。

try {
    throw undefined;
}catch(e) {
    a = 1;
    console.log(e);		// undefined
}

console.log(a);			// 1,	變數提升規則
console.log(e);			// ReferenceError,catch的塊作用域中定義的變數
複製程式碼

隱式宣告

以引數形式傳入的變數在函式內部實際上存在的隱式的宣告,使用時不算作未宣告的變數。

function foo(a) {
    a = 1;
    console.log(a);
}

foo();						// 1
console.log(a);		// ReferenceError
複製程式碼

本來想一篇文章寫完作用域和閉包的,想例子實在是累,就拆作兩篇吧,逃~

相關文章