面试问闭包,这篇全干货精华版就够了

173 阅读6分钟

在讲解闭包之前,我们先来了解两个实现闭包的概念:

1. 作用域链

l 作用域链是在函数创建阶段确定的,即预编译,会创建执行上下文对象

l 环境变量中有一个内定存储的outer属性用于指明该函数的外层作用域是谁

l outer的指向是根据词法作用域来定的

 

2. 词法作用域(称为静态作用域):

l 词法作用域是根据代码的静态结构确定作用域规则

l 是在代码编写阶段就能确定的作用域。

l 这个域所处的环境,是由函数声明的位置来定的

 

基于作用域链词法作用域两个的特性,闭包便孕育而生。

问大家一个问题:

有没有什么办法可以将函数内部定义声明的函数在外部使用。

 

答案肯定是有的,而且有很多种方法:


1. 第一种:

在函数体内部直接调用内部函数

function foo(){

  function bar(){

    var a=100

    console.log(a);

  }

  bar()

}

foo() *//* *打印* *100*

 

2. 第二种:

通过return方式将内部函数返回赋值给外部变量使用

function foo(){

  function bar(){

    var a=100

    console.log(a);
  }
  return bar()

}

const print_a=foo()

print_a //打印100

3. 第三种

通过函数绑定将内部函数在外部使用

function foo(){

  function bar(){

    var a=100

    console.log(a); *//100*

  }
  window.fn=bar();

}

foo();

window.fn//打印100

这里注意是将bar()函数绑定到window对象中,而window对象只能在html网页中浏览,node环境下的全局变量为node

 

4. 将函数在复杂数据类型内存贮

    //通过对象将函数带出
    let Obj={
    }
    function foo(){
      function bar(){
        var a=100
        console.log(a); *//100*
      }
      Obj.func=bar()
    }
    foo()
    Obj.func


    //用数组将内部函数带出
    let arr=[]
    function foo(){
      function bar(){
        var a=100
        console.log(a); *//100*
      }
      arr.push(bar())
    }
    foo()
    arr[0]

这两种方式都可以将内部函数带到外部使用,用对象的方法将函数带出就是将内部函数作为对象键值对内的值带出,同时给赋予键名。而使用数组将函数带出,内部函数将直接存储在数组内部,通过下标值引用函数。

这里介绍完怎么将内部函数带出到外部使用后,我们再来聊一聊这样做有什么好处。

闭包的定义概括就是就是:当一个内部函数能够访问其外部函数的变量作用域,即使外部函数已经执行完毕,这个内部函数仍然保留对外部作用域变量的引用,这就形成了一个闭包。

前面已经讲了几种将内部函数带出外部使用的方法,不过是没有将外部变量在内部函数引用。这里面的外部变量是什么呢?

这里的外部变量并不是对于全局的变量而言,而是外部执行的函数内的变量。

let arr=[]

function foo(){

  var a=100

  function bar(){

    console.log(a); *//100*

  }

  arr.push(bar())

}

foo()

arr[0]//调用bar函数

 

这里我们将变量a定义在了bar()函数外foo()函数内部,当我们调用foo()后foo()函数的执行上下文应该是被清除出了调用栈的,但是我们仍然可以调用到a的值,这就是闭包的形成了。

我们画出处理图

image.png

可以看到当foo()函数结束调用之后,调用栈内部没有了foo的作用域,但是bar()函数仍然可以访问到变量a。

这是因为在foo()函数被结束调用之后,内部储存仍然被保留着,而foo()函数已经被清出调用栈了,那么只有foo()的内部函数bar()可以再调用到变量a了。

这么做的优点就是:


1. 提高了代码的安全性和模块的独立性,确保函数内部的变量不被外部直接访问和修改

2. 内部的变量在函数执行结束后仍能保持活跃状态,只要闭包存在,这些变量就不会被销毁,延长了变量的生命周期。

3. 实现私有成员,模拟面向对象编程中的私有成员特性,通过闭包封装变量,外部只能通过闭包暴露的方法来间接操作这些变量,增强了封装性。


 

但是,前面说到了闭包的形成是因为外部函数被清出调用栈时的变量内存和地址没有别清除覆盖,所以才能让内部函数依然可以访问到该变量。

 

因此也形成了一些闭包的缺点:****


1. 内存泄漏风险,由于闭包会维持对外部变量的引用,如果管理不当,可能会阻止垃圾回收机制回收不再使用的变量,从而导致内存泄漏。

2. 性能开销,闭包会占用额外的内存空间来存储函数及其引用环境,特别是在大量使用闭包或闭包层次较深的情况下,可能会引起性能下降。

3. 调试难度增加,闭包增加了代码的复杂度,尤其是在多层嵌套的情况下,理解和跟踪闭包的执行流程及变量作用域变得更加困难。

**** 

这里我们实现一个简单的闭包作用

function foo(){

  let counter=0;

  function bar(){

    counter++;

    console.log(counter)

    *//这里放需要的函数结构*

  }

return bar

}

const fooo = foo()

fooo()  //1

fooo()  //2

fooo()  //3

在每一次调用fooo时,counter的值会+1,而外部再也无法修改counter的值,这样对于一些隐私数据的调用次数的保护起着非常优秀的作用。

闭包不仅是语言特性的一种展现,更是编程思想的体现,它教会我们如何在复杂的执行环境中,构建稳定、灵活且安全的代码结构。

最后的Q and A

Q: 什么是闭包?

A: 闭包是指有权访问另一个函数作用域中变量的函数,即使外部函数已经执行结束。

Q: 闭包如何形成?

A: 形成闭包的关键在于内部函数引用了外部函数的变量,同时外部函数返回了这个内部函数,保持了对外部变量的访问路径。

Q: 闭包的作用是什么?

A: 1.实现数据隐藏 2.封装、维持变量状态 3.允许函数访问其词法作用域外的变量。

Q: 闭包会导致什么问题?

A: 可能引发内存泄漏,因为闭包会维持对外部变量的引用,阻止垃圾回收;还可能增加代码复杂度,影响性能。

Q: 如何避免闭包引起的内存泄漏?

A: 尽量减少闭包中对外部变量的引用,或在不再需要时显式设置为null,帮助垃圾回收机制回收资源。

Q: 闭包与作用域链的关系?

A: 闭包依赖于作用域链,它通过作用域链访问到外部函数的变量,即使外部函数已经执行完毕。

Q: 闭包在JavaScript中的一个典型应用场景?

A: 实现计数器功能,每次调用内部函数时,都能访问并更新外部函数中定义的计数变量。