[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 的 { … } 块的变量,并且将变量添加到这个块 中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章