ES6 学习笔记之二 块做用域与闭包

“闭包是函数和声明该函数的词法环境的组合。”chrome

这是MDN上对闭包的定义。闭包

《JavaScript高级程序设计》中则是这样定义的:闭包是指有权访问另外一个函数做用域中的变量的函数。函数

我的更倾向于MDN的闭包定义,缘由有三:测试

其一,若是仅将闭包定义为可访问其父做用域(链)的局部变量的函数,那么就忽视了它持有外部环境(使外部做用域不被销毁)的意义。spa

其二,闭包有权访问的必然是其父做用域(链)中的局部变量,“另外一个函数做用域”的说法不够明确清晰。firefox

其三,就是本篇博文的主题了,闭包在ES6中,是不限于访问另外一个函数的做用域的,还能够是块做用域。固然,《JavaScript高级程序设计》这本书出版时,尚未ES6,书里也明确说明JavaScript是没有块做用域的,所以这一点不能成为批评《JavaScript高级程序设计》的理由。设计

定义一般讲究严谨、言简意赅,也就意味着不太好理解。code

换个通俗点的说法,闭包就是指在一个非全局做用域中声明的函数及其所在的这个做用域,这个函数在该做用域外被调用时,仍然可以访问到该做用域内的(局部)变量。若是不在声明函数的做用域外调用,或者该函数没有访问外部做用域的局部变量,闭包也就没有什么存在的意义了。blog

因为ES6出现之前,没有块做用域,这个非全局做用域就只能是一个函数了,那么闭包就是声明在另外一个函数内部的函数(及其所在的函数)了。为了实如今声明它的做用域外也能调用该函数,就须要将该函数做为一个返回值,返回到父做用域(父级函数)以外了。ip

举例说明(例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.

究其缘由,在于函数是在块做用域内声明的,所以它在被调用时,会优先访问块做用域内的局部变量。又由于它虽然是在块内声明,却被提高至其父做用域,因此能够在块做用域外被访问。

不过这种写法,意图不够清晰,且在多层做用域的状况下,容易产生混乱,严格模式下,还会致使错误。

 

如今再来看上一篇博文中的循环变量的例子(例1七、例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]();

比较而言,用函数做为外部做用域的闭包,能够用返回闭包函数的方式将闭包函数传递到闭包做用域外。而块做用域闭包没办法使用return,就只能是直接为外部做为域的变量赋值的方式,将闭包函数传递出去。

不过,对于例19,能够改形成不使用返回值,直接在闭包函数内使用外部做用域变量的形式:

var i;
var fn  = [];
for (i = 0; i < 3; i++) {
    (function (k) {
        fn.push(function () {
            console.log(k);
        });
    })(i));
}
fn[0]();
fn[1]();
fn[2]();

因为这种匿名函数当即调用的方式构造的闭包只执行一次,要将闭包函数传递给哪一个变量,也是coding时可以肯定的,返回值传递,仍是直接使用外部变量,都是同样的。而这种形式,在ES6中均可以用块做用域闭包代替。

就代码自己的理解难度而言,ES6的块级做用域更容易一些。

回到本文开头的闭包定义,广义的解读,因为任何一个函数必然有声明它的记法环境,因此全部的函数和声明它的记法环境都构成闭包。好比全局做用域内的函数,它和全局做用域就构成了闭包。这也是《ES6标准入门》(阮一峰)在「let 和 const」一章中解释例17时,会说fn[*]()的调用是经过闭包获取的全局变量 i 的缘由吧。

PS:

顺便说一下块做用域函数,《你不知道的JavaScript》中,关于块做用域函数有两个示例,其一见上文例3。

另外一个例子以下(例4):

if (something) {
    function foo() {
        console.log("1");
    }
} else {
    function foo() {
        console.log("2");
    }
}
foo();

原文说,在前ES6环境中(应该至关于非严格模式),无论something的值是什么,foo()都会打印出“2”,由于两个函数声明都被提高到了块外,第二个总会胜出。

经在chrome(64.0) 和 firefox(58.0)版中测试,实际运行结果是:something为真,foo()打印“1”,something为假,foo()打印“2”。

严格模式下,则与其预期相符,抛出一个ReferenceError。

其实这种全局定义函数,在ES6中,与函数变量方式相比,不能算是最佳实践了。

相关文章
相关标签/搜索