[nodejs 內功心法][作用域與閉包系列二] 函數作用域和塊作用域

函數作用域

函數作用域的含義是指,屬於這個函數的全部變量都可以在整個函數的範圍內使用及複用(事實上在嵌套的作用域中也可以使用)。
不廢話我們看代碼來說明問題

// 例1
function demo(a) {
	var b = 10;

	function internal() {
		//.... 幹一些事
	}
	// .... 一些代碼
	var c = 20
}

在上面的代碼中例1中demo(…)作用域包含了 a, b, internal, c四個標識符。無論標識符 聲明出現在作用域中的何處,這個標識符所代表的變量或函數都將附屬於所處的整個作用域中,下一篇文章變量提升將解釋其原理。

這裏 demo函數擁有自己的作用域,全局作用域也有自己的作用域不過它只包含一個標識符:demo.
由於 a, b, internal, c 都附屬於demo(…)的函數作用域中,因此無法從demo函數外部對它們進行訪問。也就是說這些標識符都無法從全局作用域中進行訪問。看如下實例。

function demo(a) {
	var b = 10;

	function internal() {
		//.... 幹一些事
	}
	// .... 一些代碼
	var c = 20
}

internal() // ReferenceError: internal is not defined
console.log(a, b, c) //ReferenceError: a is not defined

但是,這些標識符(a、b、c、demo 和 internal)在 demo(…) 的內部都是可以被訪問的,同樣在 internal(…) 內部也可以被訪問(假設 internal(…) 內部沒有同名的標識符聲明)。

隱藏內部實現

// 例2
function worker(a) {
	b = a + doSomething( a * 2)
	console.log(b * 3)
}

function doSomething(a) {
	return a - 1
}

var b

worker(4) // 33

在例2這個代碼片段中,變量 b 和函數 doSomething(…) 應該是 worker(…) 內部具體 實現的“私有”內容。給予外部作用域對 b 和 doSomething(…) 的“訪問權限”不僅 沒有必要,而且可能是“危險”的,因爲它們可能被有意或無意地以非預期的方式使用, 從而導致超出了 worker(…) 的適用條件。更“合理”的設計會將這些私有的具體內 容隱藏在 worker(…) 內部,例如:

function worker(a) {
	function doSomething(a) {
		return a - 1
	}
	var b
	b = a + doSomething(a*2)
	console.log(b * 3)
}

worker(4) // 33

現在,b 和 doSomething(…) 都無法從外部被訪問,而只能被 worker(…) 所控制。 功能性和最終效果都沒有受影響,但是設計上將具體內容私有化了,設計良好的軟件都會依此進行實現。

函數表達式

在上面的例子中,必須聲明一個具名函數 worker(),意味着 woker 這個名稱本身“污染”了所在作用域(在這個 例子中是全局作用域)。其次,必須顯式地通過函數名(woker())調用這個函數才能運行其 中的代碼。如果函數不需要函數名(或者至少函數名可以不污染所在作用域),並且能夠自動運行, 這將會更加理想.

var a = 2;

(function demo(){
	var a = 3
	console.log(a); // 3
})()

console.log(a) // 2

上面的代碼,包裝函數的聲明以 (function… 而不僅是以 function… 開始。儘管看上去這並不 是一個很顯眼的細節,但實際上卻是非常重要的區別。函數會被當作函數表達式而不是一 個標準的函數聲明來處理
區分函數聲明和表達式最簡單的方法是看 function 關鍵字出現在聲明中的位 置(不僅僅是一行代碼,而是整個聲明中的位置)。如果 function 是聲明中 的第一個詞,那麼就是一個函數聲明,否則就是一個函數表達式。

函數聲明和函數表達式的區別

區別是它們的名稱標識符將會綁定在何處。
比較一下前面兩個代碼片段。第一個片段中 worker 被綁定在所在作用域中,可以直接通過 worker() 來調用它。第二個片段中 demo 被綁定在函數表達式自身的函數中而不是所在作用域中。換句話說,(function demo(){ … })作爲函數表達式意味着demo只能在…所代表的位置中被訪問,外部作用域則不行。demo 變量名被隱藏在自身中意味着不會非必要地污染外部作用域。

IIFE(Immediately Invoked Function Expression)

上面的例子也叫IIFE,中文翻譯叫立即執行表達式。 它除了上面那種寫法還有另外一種寫法

var a = 2;

(function iife(){
	var a = 3
	console.log(a)
}())

console.log(a)

(function(){…})() 和 (function(){…}()) 這兩種寫法功能是一致的,選擇哪一種全憑個人喜好。

// 進階用法
var a = 2;
(function iife(global) {
	var a = 3
	console.log("local a", a) // local a 3
	console.log(global.a) // 2
})(window)

塊作用域

使用js 我們最常用的都是函數作用域,很少用到塊作用域。在es6之前,es3的規範中規定try/catch的catch 分句會創建一個塊作用域,其中聲明的變量僅在catch中有效。

try {
	undefined()
} catch (err) {
	console.log(err.message) // undefined is not a function
}

console.log(err) // ReferenceError: err is not defined
// err 僅存在 catch 分句內部,當試圖從別處引用它時會拋出錯誤

let 和 const

ES6 帶來了塊作用域的福音,引入了新的 let 和 const關鍵字,提供了除 var 以外的另一種變量聲明方式。let 和 const 關鍵字可以將變量綁定到所在的任意作用域中(通常是 { … } 內部)。

let
var foo = 1;

if (foo) {
	let bar = foo * 4; 
	console.log( bar );
}

console.log(bar); // ReferenceError: bar is not defined

用 let 將變量附加在一個已經存在的塊作用域上的行爲是隱式的。 我們也可以顯示創建塊作用域,使變量的附屬關係變得更加清晰。

var foo = 1;

if (foo) {
	{ // 添加大括號顯示的創建塊作用域
		let bar = foo * 4; 
		console.log( bar );
	}
}

console.log(bar); // ReferenceError: bar is not defined
//只要聲明有效,塊作用域可以顯示的寫在任何地方

if(true{
	console.log("start...")
	{
		let a = 2
		console.log("internal block", a)
	}
	console.log(a) // ReferenceError: a is not defined
}

// 我們可以使用這個技巧防止因爲函數生成覆蓋整個作用域的閉包而影響垃圾回收。導致不必要的變量佔用內存而不能釋放
// let 在for 循環中的用法
for (let i=0; i<10; i++) { 
	console.log( i );
}

console.log( i ); // ReferenceError: i is not defined

下一篇文章會討論變量提升,提升是指聲明會被視爲存在於其所出現的作用域的整個範圍內。 但是使用 let 進行的聲明不會在塊作用域中進行提升。聲明的代碼被運行之前,聲明並不“存在”。

{
console.log( bar ); // ReferenceError! 
let bar = 2;
}
const

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

if (true) {
	var a = 2;
	const b = 3; // 包含在 if 中的塊作用域常量
	a = 3; // 正常!
	b = 4; // 錯誤! 
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

總結

  1. 函數是 JavaScript 中最常見的作用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的作用域中“隱藏”起來,這是有意爲之的良好軟件的設計原則。
  2. 但函數不是唯一的作用域單元。塊作用域指的是變量和函數不僅可以屬於所處的作用域,也可以屬於某個代碼塊(通常指 { … } 內部)。
  3. 從 ES3 開始,try/catch 結構在 catch 分句中具有塊作用域。
  4. 在 ES6 中引入了 let 關鍵字(var 關鍵字的表親),用來在任意代碼塊中聲明變量。if (…) { let a = 2; } 會聲明一個劫持了 if 的 { … } 塊的變量,並且將變量添加到這個塊 中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章