函数与闭包的前世今生(一)

函数与闭包的前世今生(一)
0

1、浅谈闭包

要掌握 JavaScript,闭包是一个必须理解的概念。

1.1、闭包的定义

我查阅了维基百科和一些技术博客, 闭包(closure)的定义有两种说法:

  1. (可以访问函数体以外定义的自由变量)的函数
  2. (可以访问函数体以外定义的自由变量)的函数及其可以访问的自由变量组成的集合。

1.2、自由变量

上面闭包的定义中说到的“自由变量”是与“全局变量”“函数参数”和“局部变量”相对比而言的。

全局变量可以看作是所有函数都能访问的变量。函数参数则是指函数的输入参数。局部变量按字面意思理解就是作用域局限在被定义的函数中的变量。函数参数和局部变量都是在函数定义中被定义的。一般我们提到函数参数和局部变量的时候,都只在其被定义的函数中说的,或者说一般只有在定义它们的函数使用它们的时候,我们才会称它们为参数和局部变量。

例如,有一个全局变量 a,有一个函数 x,x 有一个输入参数 b 和一个局部变量 c,还有一个函数 y。在函数 x 和函数 y 中都能访问全局变量 a,函数 x 中能使用参数 b 和局部变量 c,但函数 y 通常与 b 和 c 就没有关系了。(没有闭包而且不能通过引用间接访问的话,函数 y 一定不能访问 b 和 c 的。)

自由变量则是在一个函数内被定义,但可以被其他函数访问的一种变量。也就是说,一般我们说到自由变量的时候,就隐含了“一个函数访问它以外的函数中定义的变量”这个情形。

在上述的例子中,如果函数 y 能访问 b 或者 c,那么 b / c 对于函数 y 而言就是自由变量。

1.3、JavaScript 中的闭包

1.3.1、JavaScript 中变量的作用域

一个变量的作用域是指能够使用该变量的范围。

在 JavaScript 中,不在任何函数中定义的变量是全局变量。全局变量的作用域在此不再赘述。除了全局变量以外的变量的作用域,包括函数参数,首先都被局限在定义它的函数内。

在实现 ECMAScript 版本 6 (不含)以前的 JavaScript,定义局部变量必须使用 var 关键字或者 function 关键字(函数作为局部变量),例如:

function x(a, b) {
    var c = 1; // c是函数x的一个局部变量
    function y(d, e) { // y是函数x的一个局部变量
        console.log(d + e);
    }
    y(a + b, c);
}

根据变量提升规则,JavaScript 解释器在遇到使用 var 关键字或者 function 关键字定义的局部变量,会将其视为在函数的最开头定义的。因此下面的两段代码是等价的:

function x(a, b) {
    y(a + b, c);
    if (a > b) {
        var c = 1;
        function y(d, e) {
            console.log(d + e);
        }
    }
}
function x(a, b) {
    var c; // 此时c为undefined
    function y(d, e) { // c和y的定义会被提升到函数体的最开头
        console.log(d + e);
    }
    y(a + b, c);
    if (a > b) {
        c = 1;
    }
}

由于变量提升规则允许在声明某个变量之前就使用它,可能会导致一些不符合直观的执行结果,所以在 ECMAScript 6 规范中,加入了 letconst 关键字用来定义块级作用域的变量。块级作用域的变量的作用域是定义变量的方括号内,而且没有变量提升的规则,因此块级作用域的变量都必须先定义后使用。

块级作用域的变量的例子:

function x(a, b) {
    return a + b + c; // 会报ReferenceException:c未被定义
    if (a > b) {
        let c = 1; // c的作用域被局限在这个if语句的大括号内
    }
}
function x(a, b) {
    if (a > b) {
        return a + b + c; // 会报ReferenceException:不能在c初始化前访问c
        let c = 1; // 在c的定义之前不能使用c
    }
    return a + b - c;
}

1.3.2、JavaScript 中的闭包

在一个变量的作用域内定义的函数的函数体内也可以访问该变量,或者说在一个变量的作用域内定义的函数的函数体也属于该变量的作用域,又或者说一个变量是在其作用域内定义的函数的自由变量。

还是举一个例子:

function x(a) {
    let b = 1 - a;
    if (a > b) {
        return function y(c) {
            return a - b + c; // 函数y内也可以访问x函数定义里的参数a和局部变量b
        };
    }
    return a + b + c;
}
function z() {
    // 不能访问x函数的参数a和局部变量b,因为这里已经超出了它们的作用域
}

下面再以一个常见面试题来举例说明闭包:

function x() {
    const result = [];
    let i;
    for (i = 0; i < 5; i++) {
        result[i] = function y() { // 变量i与函数y组成一个闭包
            console.log(i); // 变量i是函数y的自由变量
        };
    }
    return result;
}
const functions = x();
for (let j = 0; j < functions.length; j++) (functions[j])();

这里会输出 5 个 5,而不是 0、1、2、3、4,因为 functions 中的 5 个函数中使用的 i 其实是同一个变量,而且与函数 x 执行时 for 循环中的变量 i 是同一个变量。在函数 x 中跳出 for 循环后到执行 (functions[i])() 时,变量 i 就已经变成了 5。

最后留一个思考题,以下代码会输出什么呢?为什么会这样输出呢?

function x() {
    const result = [];
    for (let i = 0; i < 5; i++) {
        result[i] = function y() { // 变量i与函数y组成一个闭包
            console.log(i); // 变量i是函数y的自由变量
        };
    }
    return result;
}
const functions = x();
for (let j = 0; j < functions.length; j++) (functions[j])();