全部 / 前端 / 技术 · 2022年4月30日 0

14 – JavaScript 中的闭包

广告位招租 (vx: ghostcode, 备注:网站广告)

原文地址:https://dev.to/bhagatparwinder/closures-in-javascript-1f6k

什么是闭包?

我认为 JavaScript 中的闭包是一个高级话题,是一个面试中经常被提到的问题。

若你读了我之前的文章或了解 JavaScript 中的作用域,那理解闭包会轻松些。

函数作用域是指函数中声明的变量只能在函数中使用,同样也可以被它内部的函数引用到。但闭包更进一步,它使父级函数的作用域在执行结束后依旧可以被获得。

function outer() {
    const outerVariable = "outer";
    function inner() {
        const innerVariable = "inner";
        console.log(`${outerVariable} ${innerVariable}`); // outer inner
    }
    inner();
}

outer();

上面的例子中,我创建并执行了函数 outer,同时内部也创建和调用了 inner 函数。inner 函数成功打印出了父级函数中声明的变量和期望的一样,因为子函数可以获取父函数的作用域。

现在我们来返回 inner 函数而不是调用它。

function outer() {
    const outerVariable = "outer";
    function inner() {
        const innerVariable = "inner";
        return (`${outerVariable} ${innerVariable}`);
    }
    return inner;
}

const returnFunction = outer();
console.log(returnFunction); // Function returnFunction

变量 returnFunction 是一个函数因为它是 outer 返回的。不要惊讶。

此时,outer 函数已经执行完毕,返回的值赋值给了一个新变量。

当一个函数从调用栈中被抛出时,JavaScript 垃圾收集器此时应该删除了所有对 outerVarible 的引用。我们来执行 returnFunction。

function outer() {
    const outerVariable = "outer";
    function inner() {
        const innerVariable = "inner";
        return (`${outerVariable} ${innerVariable}`); // outer inner
    }
    return inner;
}

const returnFunction = outer();
console.log(returnFunction); // Function returnFunction
console.log(returnFunction()); // outer inner

惊不惊奇,意不意外!它依旧可以打印出父函数中声明的变量即使父函数已经执行完。

JavaScript 垃圾收集器并没有清除父函数中被子函数返回的变量,这些稍后执行的子函数根据词法作用域原则依旧可以引用父函数的作用域。

这种垃圾收集器的行为并不仅仅局限于子函数,若一个变量只要任何一个东西对它存在引用就不会被垃圾收集器回收。

真实的例子

我们假设我在编写一个关于汽车的程序,这辆车可以像真实世界一样加速,只要加速汽车的速度就会提高。

function carMonitor() {
    var speed = 0;

    return {
        accelerate: function () {
            return speed++;
        }
    }
}

var car = new carMonitor();
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(car.accelerate()); // 3
console.log(car.accelerate()); // 4

如你所见,carMonitor 中声明了汽车的速度,同时 accelerate 函数可以引用它。每次我调用 accelerate时,不仅仅是可以获取变量而且是在上次的值基础上再增加然后返回。

使用闭包创建私有变量

我们继续使用 carMonitore 的例子。

function carMonitor() {
    var speed = 0;

    return {
        accelerate: function () {
            return speed++;
        }
    }
}

var car = new carMonitor();
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(car.accelerate()); // 3
console.log(car.accelerate()); // 4
console.log(speed); // speed is not defined

你可以看见 speed 是 carMonitor 的私有变量,只能被子函数 accelerate 引用,函数外部都不能引用它。有人可能会争辩正因函数作用域才会如此。它是 carMonitor 的私有变量同时每个 carMonitor 实例的私有变量。

每个实例都维护着对它的拷贝。

这可以帮助你认识到闭包的强大。

function carMonitor() {
    var speed = 0;

    return {
        accelerate: function () {
            return speed++;
        }
    }
}

var car = new carMonitor();
var redCar = new carMonitor()
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(redCar.accelerate()); // 0
console.log(redCar.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(redCar.accelerate()); // 2
console.log(speed); // speed is not defined

car 和 redCar 维护着它们自己的私有 speed 变量,同时 speed 在外部不能被访问。

我们强制用户使用定义在函数或类中的方法来改变属性而不是直接引用它,这就是你应该如此封装代码。

我希望这篇文章清除了 JavaScript 中闭包的任何疑问。

常见面试问题

下面是一些面试中经常问道的闭包问题:

你认为下面的代码输出什么:

for (var i = 0; i <= 5; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

若你猜间隔一秒打印出 0 到 5,你会感到惊讶。随着 setTimeout 的一秒后,i 已经变成了 6 !我们将借助闭包来帮助你实现预期的答案:

for (var i = 0; i <= 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    })(i);
}

还有另一个解决问题的方法,使用 let 关键字。

循环中的 var 声明了函数作用域的变量 i,这就导致循环中绑定了同一个变量 i 。当 6 次倒计时结束后,它们都使用了最后相同的值 6 。

let 创建的是块级作用域当用在循环中时,为每次循环创建了一个绑定。循环中的每次倒计时获得了从 0 到 5 不同的值。

for (let i = 0; i <= 5; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

现在将会打印出 0 到 5,若你的目标环境是 ES5 使用 IIFE 加闭包的方法,若目标是 ES6 请使用 let 方法。

这也是 Babel 在内部把 ES6 的代码转为 ES5 使用的方法,把以 let 为基础的代码转换为闭包和 IIFE 的结合体。