ES6 学习笔记之二 块级作用域与闭包
ES6 学习笔记之二 块级作用域与闭包
这是MDN上对闭包的定义。
《JavaScript高级程序设计》中则是这样定义的:闭包是指有权访问另一个函数作用域中的变量的函数。
个人更倾向于MDN的闭包定义,原因有三:
其一,如果仅将闭包定义为可访问其父作用域(链)的局部变量的函数,那么就忽视了它持有外部环境(使外部作用域不被销毁)的意义。
其二,闭包有权访问的必然是其父作用域(链)中的局部变量,“另一个函数作用域”的说法不够明确清晰。
其三,就是本篇博文的主题了,闭包在ES6中,是不限于访问另一个函数的作用域的,还可以是块作用域。当然,《JavaScript高级程序设计》这本书出版时,还没有ES6,书里也明确说明JavaScript是没有块作用域的,因此这一点不能成为批评《JavaScript高级程序设计》的理由。
定义通常讲究严谨、言简意赅,也就意味着不太好理解。
换个通俗点的说法,闭包就是指在一个非全局作用域中声明的函数及其所在的这个作用域,这个函数在该作用域外被调用时,仍然能够访问到该作用域内的(局部)变量。如果不在声明函数的作用域外调用,或者该函数没有访问外部作用域的局部变量,闭包也就没有什么存在的意义了。
由于ES6出现以前,没有块作用域,这个非全局作用域就只能是一个函数了,那么闭包就是声明在另一个函数内部的函数(及其所在的函数)了。为了实现在声明它的作用域外也能调用该函数,就需要将该函数作为一个返回值,返回到父作用域(父级函数)之外了。
举例说明(例1):
var age = 30; var fn; fn = (function () { var age = 20; var name = "Tom"; return function () { console.log("name is " + name + "."); console.log("age is " + age + "."); }; })(); fn();
运行结果:
name is Tom.
age is 20.
可以看到,age 获取的是匿名函数中声明的局部变量 age 的值 20,不是全局变量 age 的值 30。name 更是干脆没有同名全局变量,只有匿名函数中声明的局部变量。
对于ES6,因为块作用域的存在,闭包就有了另一种实现,举例如下(例2)
let age = 30; let fn; { let age = 20; let name = "Tom"; fn = function () { console.log("name is " + name + "."); console.log("age is " + age + "."); }; } fn();
运行结果与例1相同:
name is Tom.
age is 20.
可见,在ES6中,声明在块作用域内的函数,在离开块作用域后,优先访问的依然是声明它的块作用域的局部变量。
在《你不知道的JavaScript》中文版下卷中,曾经提到过块作用域函数,即声明在块作用域内的函数,在块外无法调用。
原文的例子如下(例3):
{ foo(); function foo() { //... } } foo();
书中认为,第一个foo()调用会正常返回结果,第二个foo()调用会报 ReferenceError 错误。实际在 chrome(64.0) 和 firefox(58.0)版中测试,均与其预期不符,两个调用均正常返回结果,不会出现 ReferenceError 错误。
也就是说,声明在块作用域内的函数,是在块作用域外的父作用域中有效的。
这也导致了例2还有一个变体(例4):
let age = 30; { let age = 20; let name = "Tom"; function fn() { console.log("name is " + name + "."); console.log("age is "+ age + "."); } } fn();
其结果也是:
name is Tom.
age is 20.
究其原因,在于函数是在块作用域内声明的,因此它在被调用时,会优先访问块作用域内的局部变量。又因为它虽然是在块内声明,却被提升至其父作用域,所以可以在块作用域外被访问。
不过这种写法,意图不够清晰,且在多层作用域的情况下,容易产生混乱。
现在再来看上一篇博文中的循环变量的例子(例17、例18和例19):
(例17)
var i; var fn = []; for (i = 0; i < 3; i++) { fn.push(function () { console.log(i); }); } fn[0](); fn[1](); fn[2]();
之所以会输出三个3,是因为函数在调用时才会尝试获取i值,而不是在定义时就获取了i的值,而调用是在循环之后发生的。调用时因为i是全局变量,其值已经在循环中自增到了3。因此3次调用均返回3。
(例19)
var i; var fn = []; for (i = 0; i < 3; i++) { fn.push((function (i) { return function () { console.log(i); } })(i)); } fn[0](); fn[1](); fn[2]();
实际是个障眼法,循环内部的函数定义中,形参使用了和全局变量 i 同名的变量,由于子作用域同名变量的遮蔽作用,函数内部的 i 实际已经不是全局变量 i 了,而是一个匿名函数内部的局部变量。调用匿名函数时,将全局变量 i 的值传递给了局部变量 i 。而返回的那个闭包函数,按照闭包的定义,无论在何处调用,都只会先访问其父作用域中的局部变量。
如果把匿名函数中的 i 换个名字,就更能清晰地看出闭包在这里的作用了:
var i; var fn = []; for (i = 0; i < 3; i++) { fn.push((function (k) { return function () { console.log(k); } })(i)); } fn[0](); fn[1](); fn[2]();
(例18):
var fn = []; for (let i = 0; i < 3; i++) { fn.push(function () { console.log(i); }); } fn[0](); fn[1](); fn[2]();
就刚好是本篇博文所说的块作用域闭包。每个循环都会产生一个块作用域;而 for 语句中的 let,会在每个循环产生的块作用域内生成一个局部变量 i;声明在每个循环内的匿名函数,都会优先访问声明自己的那个循环产生的块作用域中的 i 的值。
其实际意义与如下例子是一样的:
var fn = []; for (let i = 0; i < 3; i++) { let k = i; fn.push(function () { console.log(k); }); } fn[0](); fn[1](); fn[2]();