你不知道的Javascript(上卷)-作用域筆記

旦願發表於2020-11-01

作用域是什麼

1.理解作用域

首先看一段宣告程式碼

var a = 2;

在此宣告變數並賦值的操作中會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數(如果之前沒有宣告過),然後在執行時引擎會在作用域中查詢該變數,如果能夠找到就會對它賦值。

對於第一個操作中,引擎為變數a進行LHS查詢。另外一個查詢的型別叫作RHS查詢。當變數出現在賦值操作的左側時進行LHS查詢,出現在右側時進行RHS查詢。

如果查詢的目的是對變數進行賦值,那麼就會使用LHS查詢;如果目的是獲取變數的值,就會使用RHS查詢。RHS查詢與簡單地查詢某個變數的值別無二致,而LHS查詢則是試圖找到變數的容器本身,從而可以對其賦值。
 

2.作用域巢狀

當一個塊或函式巢狀在另一個塊或函式中時,就發生了作用域的巢狀

因此,在當前作用域中無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數,或抵達最外層的作用域(也就是全域性作用域)為止。

function foo(a){
	console.log(a + b);
}
var b = 2;
foo(2); //4

比如在上面這個例子中,對b進行的RHS引用無法在函式foo內部完成,但可以在上一級作用域(在這個例子中就是全域性作用域)中完成。

遍歷巢狀作用域鏈的規則很簡單:引擎從當前的執行作用域開始查詢變數,如果找不到,就向上一級繼續查詢。當抵達最外層的全域性作用域時,無論找到還是沒找到,查詢過程都會停止。
 

3.異常

在變數未宣告的情況下,LHS查詢RHS查詢的行為是有所不同的。

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

        foo(2);

在上面這段程式碼中,第一次對b進行RHS查詢是無法找到該變數的,此變數未宣告,因為在所有作用域中都無法找到它。

RHS查詢在所有巢狀的作用域中遍尋不到所需的變數,引擎就會丟擲ReferenceError異常。值得注意的是,ReferenceError是非常重要的異常型別。

LHS查詢時,如果在頂層(全域性作用域)中也無法找到目標變數,全域性作用域中就會建立一個具有該名稱的變數,並將其返還給引擎,前提是程式執行在非“嚴格模式”下。
 

4.詞法作用域

詞法作用域就是定義在詞法階段的作用域。

換句話說,詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理程式碼時會保持作用域不變(大部分情況下是這樣的)。

查詢

在查詢的時候,作用域查詢會在找到第一個匹配的識別符號時停止。在多層的巢狀作用域中可以定義同名的識別符號,這叫作“遮蔽效應”(內部的識別符號“遮蔽”了外部的識別符號)。

欺騙詞法

使用eval()with() 可以修改詞法作用域,儘量少使用這兩個函式,因為此時效能會變差

eval可以對一段包含一個或多個宣告的“程式碼”字串進行演算,並藉此來修改已經存在的詞法作用域(在執行時),函式如果接受了含有一個或多個宣告的程式碼,就會修改其所處的詞法作用域,而with宣告實際上是根據你傳遞給它的物件 ,將物件的屬性當做作用域中的識別符號來處理,從而憑空建立了一個全新的詞法作用域

eval(…)函式可以接受一個字串為引數,並將其中的內容視為好像在書寫時就存在於程式中這個位置的程式碼。

function foo(str,a){
	eval(str); // 欺騙
	console.log(a,b);
}

var b = 2;
foo("var = b;",1);

在上面這段程式碼中,eval(…)呼叫中的"var b = 3; "這段程式碼會被當作本來就在那裡一樣來處理。由於那段程式碼宣告瞭一個新的變數b,因此它對已經存在的foo(…)的詞法作用域進行了修改。事實上,和前面提到的原理一樣,這段程式碼實際上在foo(…)內部建立了一個變數b,並遮蔽了外部(全域性)作用域中的同名變數。
 
with通常被當作重複引用同一個物件中的多個屬性的快捷方式,可以不需要重複引用物件本身。

function foo(obj){
	with(obj){
		a = 2;
	}
}

var o1 = {
	a:3;
}

var o2 = {
	b:2;
}

foo(o1);
foo(o2);
console.log(o1);//2
console.log(o2);//undefined
console.log(a); //2 a被洩露到全域性作用域中

對於上面的程式碼,foo()函式接受了一個obj引數,此引數是一個物件引用,並對這個物件引用執行了with(obj){…}.首先我們將o1傳遞進去,a = 2的賦值操作找到了o1.a並賦值給它。

對於o2來說,它並沒有a屬性,因此不會建立這個屬性而保持undefined。

在最後一句程式碼中,o2的作用域、foo(…)的作用域和全域性作用域中都沒有找到識別符號a,因此當a=2執行時,自動建立了一個全域性變數(因為是非嚴格模式)。

 

5.函式作用域

函式作用域的含義是指,屬於這個函式的全部變數都可以在整個函式的範圍內使用及複用(事實上在巢狀的作用域中也可以使用)。

這種設計方案是非常有用的,能充分利用JavaScript變數可以根據需要改變值型別的“動態”特性。

函式宣告和表示式
如果function是宣告中的第一個詞,那麼就是一個函式宣告,否則就是一個函式表示式。
var a = 2;
(function foo(){
	var a = 3;
	console.log(3);
})();
console.log(2);

比如上述函式的宣告是以(function… 而不是function來開始,因此它是函式表示式。

(function foo(){ … })作為函式表示式意味著foo只能在..所代表的位置中被訪問,外部作用域則不行。foo變數名被隱藏在自身中意味著不會非必要地汙染外部作用域。同時foo也是一個立即執行函式表示式(IIFE).。

 

6.塊作用域

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

在進行for迴圈中,不應該把變數i汙染到整個函式作用域中,如果在錯誤的地方使用變數將導致未知變數的異常。變數i的塊作用域(如果存在的話)將使得其只能在for迴圈內部使用,如果在函式中其他地方使用會導致錯誤。這對保證變數不會被混亂地複用及提升程式碼的可維護性都有很大幫助。

let關鍵字
let關鍵字可以將變數繫結到所在的任意作用域中(通常是{ .. }內部)。
換句話說,let為其宣告的變數隱式地劫持了所在的塊作用域。
{
	console.log(bar); //ReferenceEroor
	let bar = 2;
}

使用let進行的宣告不會在塊作用域中進行提升。宣告的程式碼被執行之前,宣告並不“存在”。

 

6.提升

先有宣告,後有賦值

只有宣告本身會被提升,而賦值或其他執行邏輯會留在原地。如果提升改變了程式碼執行的順序,會造成非常嚴重的破壞。

foo();

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

由於函式提升,變成了下面的樣子

function foo(){
	var a;
	console.log(a);//undefined
	a = 2;
}
函式宣告會被提升,而函式表示式不會提升
foo();// TypeError

var foo = function bar(){...};

foo時宣告提升後並沒有賦值(如果它是一個函式宣告而不是函式表示式,那麼就會賦值)。foo()由於對undefined值進行函式呼叫而導致非法操作,因此丟擲TypeError異常。

函式會首先被提升,然後才是變數。
foo();//1

var foo;

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

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

宣告提升後

function foo(){
	console.log(1);
}
foo();//1

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

var foo儘管出現在function foo()….的宣告之前,但它是重複的宣告(因此被忽略了),因為函式宣告會被提升到普通變數之前。但是儘管重複的var宣告會被忽略掉,但出現在後面的函式宣告還是可以覆蓋前面的。

相關文章