JavaScript 學習筆記 之 作用域 (三) - 函數作用域和塊作用域

3.1 函數中的作用域

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

 

3.2 隱藏內部實現

從所寫代碼中挑出一個任意的片段,然後用函數聲明進行包裝,實際上就是把這些代碼隱藏起來了。

也就是說這段代碼中的任何聲明(變量或函數)都將綁定在這個新創建的包裝函數的作用域中,然後用這個作用域來“隱藏”他們

 

爲什麼要隱藏他們?

很多原因促成了這種方法,大都是從最小特權原則引申出來的,也叫最小授權或最小暴露原則。

這個原則是指在軟件設計中,應該最小限度的暴露必要內容,而將其他內容都“隱藏”起來,比如模塊或者API設計

規避衝突

“隱藏”作用域中的變量和函數所帶來的另一個好處,是可以避免同名標識符之間的衝突,兩個標識符可能具有相同的名字但用途卻不一樣,無意間可能造成命名衝突,衝突會導致變量的值被意外覆蓋。

function foo() {
	function bar(a) {
		i = 3; //修改for循環所屬作用域中的i
		console.log(a + i);
	}
	for(var i = 0; i < 10; i++) {
		bar(i * 2); //糟糕,無限循環了!
	}
}

bar(..)內部的複製表達式 i=3 意外的覆蓋了foo(..)內部for循環中的i。導致無限循環。

解決方案:

定義一個本地變量,var i (遮蔽了foo作用域中的i)或者使用完全不同的一個標識名,但是有些情況下要求同樣的標識符名,在這種情況下,只用作用域來隱藏內部聲明是唯一的最佳選擇

1、全局命名空間

變量衝突的一個典型例子存在於全局作用域中。當程序中加載了多個第三方庫時,如果他們沒有妥善的把內部私有的函數或者變量隱藏起來,就會很容易引發衝突

這些庫通常會在全局作用域中聲明一個名字足夠獨特的變量,通常是一個對象。這個對象被用作庫的命名空間,所有需要暴露給外界的功能都會成爲這個對象(明明空間)的屬性,而不是把自己的標識符暴露在頂級詞法作用域中。

2、模塊管理

另一種避免衝突的方式與現代的模塊機制很接近,就是從衆多模塊管理器中挑選一個來使用。使用這些工具,任何庫都無需將標識符加入到全局作用域,而是以來管理器的機制顯示的導入到另一個特定的作用域中。

 

3.3 函數作用域

我們已經知道,在任意代碼片段外部添加包裝函數,可以將內部的變量和函數定義“隱藏”起來,外部作用域無法訪問包裝函數內部的任何內容。

 

var a = 2;

function foo() {//<--添加這一行
	var a = 3;
	console.log(a);
}//<--以及這一行
foo();//<--以及這一行

console.log(a);

雖然這種技術可以解決一些問題,但是它並不理想,因爲會導致一些額外的問題。首先,必須聲明一個具名函數foo(),意味着foo這個名稱本身污染了所在作用域。其次,必須通過顯示的通過函數名(foo())調用這個函數才能運行其中的代碼。

如果不需要函數名(至少函數名可以不污染所在作用域),並且能夠自動運行,這將會更加理想。

JavaScript提供了以下方法:

var a = 2;
(function foo() { //<--添加這一行
	var a = 3;
	console.log(a) //3
})(); //<--添加這一行
console.log(a); //2

首先,包裝函數的聲明以(function...開始而不僅僅以function開始。這導致函數會被當做函數表達式而不是一個標準的函數聲明來處理。

函數聲明和函數表達式之間最重要的區別是他們的名稱標識符將會綁定在何處

  • 函數聲明的名稱標識(foo)被綁定在所採作用域中,可以直接通過foo()來調用他
  • 函數表達式的foo則被綁定在函數表達式自身的函數中,而不是所在作用域中

換句話說,(function foo(){...})作爲函數表達式意味着foo只能在...代表的位置中被訪問。foo變量名被隱藏在自身中意味着不會非必要的污染外部作用域。

3.3.1 匿名和具名

函數表達式最常見的應用(回調函數)

setTimeout(function() {
	console.log("I waited 1 second!")
}, 1000);

這叫做匿名函數表達式,因爲function()..沒有任何名稱標識符。函數表達式是可以匿名的,但函數聲明不可以省略函數名(在JavaScript裏是非法的)

缺點:

  1. 匿名函數在棧追蹤中不會顯示有意義的函數名,不利於調試。
  2. 如果沒有函數名,函數需要調用自身的時候只能使用已經過期的arguments.callee引用,比如在遞歸中,比如在事件觸發後事件偵聽器需要解綁自身。
  3. 匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。

解決方式:

添加函數名,匿名和具名之間的區別不會對行內函數表達式造成影響。

3.3.2 立即執行函數表達式

var a = 2;
(function foo() {
	var a = 3;
	console.log(a) //3
})();//<--加括號
console.log(a); //2

通過在末尾加上另一個()可以立即執行這個函數,比如function foo(){..})()

第一個()將函數變成表達式,第二個()執行了這個函數。

這種模式有一種專門的術語:IIFE(Immediately Invoked Function Expression),代表立即執行函數表達式。

很多人喜歡另一個改進的形式:(function(){..}()) 。將用來調用的()移進了用來包裝的()中,功能是一致的,使用哪種憑自己喜好。

 

IIFE的另一個非常普遍的進階用法是把它們當做函數調用並傳參數進去。

var a = 2;
(function IIFE(global) {
	var a = 3;
	console.log(a);
	console.log(global.a); //2
})(window);
console.log(a); //2

將windows作爲對象引用傳遞進去,但將參數命名爲global,來改進代碼風格。

IIFE還有一個用法是用來解決undefined標識符的默認值被錯誤覆蓋導致的異常(不常見,瞭解爲主)

(function(undefined){})()

這樣就可以確保代碼塊中undefined真的是undefined;

IIFE還有一種變化的用法用來倒置代碼的運行順序,將需要運行的代碼放在第二位,在IIFE執行後當做參數傳遞進去。

(function IIFE(def){
    def(window);
})(function def(global){
    ..
})

 

3.4 塊作用域

塊作用域是一個用來對之前的最小授權原則進行拓展的工具,將代碼從在函數中隱藏信息拓展爲在塊中隱藏信息。

for 循環 和 if 語句不管 var 聲明變量寫在哪裏都會屬於外部作用域,寫在作用域裏只是爲了風格更易讀而僞裝的形式作用域。

3.4.1 with

用with從對象中創建出的作用域僅在with聲明中而非外部作用域有效。

3.4.2 try/catch

ES3規範中規定,try/catch分句中會創建一個塊作用域,其中聲明的變量僅在catch中有效。

try{
	undefined();
}catch(err){
	//TODO handle the exception
	console.log(err);//能正常執行
}
console.log(err);//ReferenceError: err not found

3.4.3 let

ES6引入了新的let關鍵字,提供了var以外的另一種變量聲明。

let可以將變量綁定到所在的任意作用域中(通常是{..}內部)。換句話說,let爲其聲明的變量隱式地劫持了所在的塊作用域。

用let將變量附加在一個已經存在的塊作用域上是隱式的,如果沒有密切關注哪些塊作用域有綁定的變量,習慣性的移動這些塊或者將其包含在其他塊中會導致代碼混亂。

解決方法:顯式的創建塊可以部分的解決這個問題

var foo = true;
if(foo) {
	{//<--顯式的塊
		let bar = foo * 2;
		bar = somthing(bar);
		console.log(bar);
	}
}
console.log(bar); //ReferenceError

在這個例子中,我們在if聲明內部顯式的創建了一個塊,如果需要對其進行重構,整個塊都可以方便的移動而不會對外部if聲明的位置和語義產生任何的影響。(if(){var a;if(a..){}}可以重構成if(){var a;}if(a..){},但let不行)。

注:let進行的聲明不會在塊作用域中提升。在被運行之前,聲明不存在。(提升是指聲明會被視爲存在於其所出現的作用域的整個範圍內。)

1.垃圾收集

function process(data) {
	///...
}

var someReallyBigData = { ...
};

process(someReallyBigData);

$("#my_btn")[0].addEventListener("click", function(evt) {
	console.log("button clicked");
}, /*capturingPhase=*/ false);

click函數形成了一個覆蓋整個作用域的閉包,JavaScript引擎有可能依然保存着這個結構(取決於具體結構)。

function process(data) {
	///...
}

{//在這個塊中定義的內容完事可以銷燬!
	let someReallyBigData = { ...
	};

	process(someReallyBigData);
}

$("#my_btn")[0].addEventListener("click", function(evt) {
	console.log("button clicked");
}, /*capturingPhase=*/ false);

塊作用域可以讓引擎清楚的知道沒有必要繼續保存someReallyBigData了。

2.let循環

for循環頭部的let不僅將i綁定到了for循環的塊中,事實上它將其重新綁定到了循環的每一個迭代中,確保上一個循環結束時重新進行賦值。類似於下面的方式。

{
	let j;
	for(j=0;j<10;j++){
		let i=j;//每個迭代重新綁定
		console.log(i);
	}
}

3.4.4 const

除了let,ES6還引入了const,同樣可以用來創建塊作用域變量,但其值是固定的(常量)。之後任何試圖修改值的操作都會引起錯誤。

const a=2;
a=3;//錯誤!

 

3.5 小結

函數是JavaScript中最常見的作用域單元。本質上,聲明在一個函數內部的變量或者函數會在所處的作用域中“隱藏”起來,這是有意爲之的良好軟件的設計原則。

但函數不是唯一的作用域單元。塊作用域指的是變量和函數不僅可以屬於所處的作用域,也可以屬於某個代碼塊(通常指{..}內部)。

從ES3開始,try/catch結構在catch分句中具有塊作用域。

在ES6中引入了let關鍵字(var關鍵字的表親),用來在任意代碼塊中聲明變量。if(..){let a = 2;}會聲明一個劫持了if的{..}塊的變量,並將變量添加到這個塊中。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章