害怕面试遇到闭包?看完这篇文章,闭包再也不在话下!

77 阅读5分钟

在学习闭包之前,我们先学习两个重要的概念:作用域链和词法作用域。

作用域链

作用域链是JavaScript中一种用于解析变量的方法。当一个函数被调用时,函数在执行前预编译,会创建一个执行上下文对象,这个对象包含了变量环境和函数的外部引用。变量环境中有一个特殊的属性outer,它指向外层作用域,outer的指向是根据词法作用域来定的。如下图所示:

lQLPKHQg3tJPo2nNBDjNB4Cw-VFYKqVHLNkGPTuk4f-fAA_1920_1080.png bar()函数内输出a,a在bar()内未找到,outer将会指向外层作用域去查找,bar()函数外层是全局作用域,全局作用域中定义了var a = 200,故bar()函数输出a的值为200;如果在第六行再添加一个console.log(a),则这里输出的a的值为100,因为js引擎在查找变量时,会先在函数中查找,在函数中找到了var a = 100。 因此我们得到结论:

  • 当JavaScript引擎查找变量时,首先会在当前函数的作用域内查找。如果找不到,就会根据outer属性的指向逐层向外查找,直到找到所需的变量或达到全局作用域。这种逐层查找变量的过程称为作用域链。
    作用域链的作用:
  • 确保了在不同的作用域中查找变量时,能够正确地找到所需的变量。

词法作用域

词法作用域是指一个函数在定义时所处的作用域。换句话说,它是由代码书写的位置决定的,而不是运行时的位置决定的。在函数定义时,它已经“记住”了外部作用域的环境。例如:

function outer() {
    let outerVar = '!!!';
    
    function inner() {
        console.log(outerVar); // '!!!'
    }
    
    inner();
}

outer();

因为inner函数是在outer函数内定义的,它的词法作用域包含了outer函数的变量环境。 掌握了作用域链和词法作用域,我们就可以来学习今天的主题——闭包

闭包

闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。 换句话说,即使外部函数已经执行完毕,内部函数引用的外部函数中的变量依旧保存在内存中。这些变量的集合被称为闭包。 接下来我将举个例子,带大家更好的理解闭包:

lQLPJwAEZUvrkFnNBDjNB4Cwda3EgNekJUgGPURvHF0bAA_1920_1080.png 这个例子的执行过程:

① 定义阶段
foo()函数、agebaz被定义;

age赋值和调用foo()
age赋值20后,当执行 const baz = foo(); 时,调用了 foo 函数,创建了 foo() 的执行上下文。namebar()countage被定义,之后namecountage被初始化,最后,foo 返回 bar 函数,并将其赋值给 baz
③ 调用baz函数
调用 baz() 实际上是在调用 bar 函数,bar 函数是在 foo 的作用域内定义的,因此它形成了一个闭包,能够访问 foo 作用域中的变量 countagebaz() 被调用时,会输出 countage 的值,输出 1 18
在这里,还有另一种形式能形成闭包,如果面试官问你,你就可以拿下面的例子说明:

function foo() {
    var a = 1
    function bar() {
        console.log(a);
    }
    // return bar
    window.fn = bar
}
// const baz = foo()
// baz()
foo()
window.fn()

这里不需要新定义baz。

闭包的作用

  1. 实现共有变量 (企业的模块开发):闭包可以用于在不同函数间共享变量。
  2. 做缓存:闭包可以用于缓存计算结果,避免重复计算。
  3. 封装模块,防止全局变量污染:闭包可以用于创建私有作用域,从而避免变量污染全局作用域。

闭包的缺点

尽管闭包有许多优点,但它也有一些缺点,最主要的是可能导致内存泄漏。由于闭包会保留对其外部作用域变量的引用,这些变量在闭包存在期间无法被垃圾回收,从而占用内存。为避免内存泄漏,开发者需要在不再需要闭包时,显式地解除对外部变量的引用。

闭包的应用(面试)

如果面试时,面试官问你:运用闭包,将下列代码改为输出0,1,2,3,4,5,6,7,8,9

var arr=[];
 for (var i = 0; i <10; i++) {
     arr[i] =function(){
         console.log(i);
     }
    
 }  
 arr.forEach(function(item){
     item()
 });

分析代码

首先,这个代码输出为10,10,10,10,10,10,10,10,10,10,之所以输出十个10,是因为for循环后,i的值为10,i为var声明,会发生声明提升,i是作用在整个函数作用域内,for循环后i的值就为10,执行到arr.forEach(function(item){ item() });时,调用每一个item函数都是同一个i变量10。

解法 ①

  • var i = 0改为leit i = 0。 当你使用 let 声明变量时,i 是在块级作用域内的。每次迭代时,i 都是一个新的变量,每个闭包都能捕获到当前迭代的 i 值。

解法 ②

var arr = [];
for (var i = 0; i <10; i++) {
     function foo(){
         var j=i
         arr[i] =function(){
             console.log(j);
         }
     }
     foo();

 }     

 arr.forEach(function(item){
     item()
 });

在这段代码中,for循环时每一遍都会调用foo()函数,都会创建一个新的局部变量 j,并将当前的 i 值赋给 j,遍历 arr 并调用每个函数,这些函数分别会打印它们各自闭包内捕获的 j 值。

这里最好两个解法都要掌握,毕竟傻瓜才做选择,聪明人两个都要。