1. 首页
  2. 后端

90%的面试官都会问的问题!JavaScript知识点-闭包

  90%的面试官都会问的问题!JavaScript知识点-闭包

==============================

前言

==

2024年的秋招,张三去了一家互联网公司面试,张三面对面试官的问题对答如流,已经胜券在握了,正当兴奋要拿到offer时,面试官问出了这样一个问题:

var arr=[]


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

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

“请看以上代码,请问这段代码的运行结果是什么?”

张三大致看了一眼,非常疑惑,这是一个很简单的问题,甚至刚学js的程序员都能够答上来,面试官怎么会问出这样一个问题呢?有诈!

张三又仔细看了一眼,果不其然!

因为JavaScript预编译的特性,所以这段代码执行的结果并不是”0,1,2″而是”3,3,3″

见张三答出来了,面试官笑着看着他问出第二个问题:

在不修改代码逻辑的情况下,让这个代码的执行结果为”0,1,2″

张三心里一紧,开始了头脑风暴。这个代码有什么特性?有哪些地方是可以修改的?
这样?不对……那样?也不对……

有了!

循环中定义ivar改成let!因为JavaScript中,使用 var 声明的变量具有函数作用域,而let 具有块级作用域,这时,每次循环都会创建一个新的块级作用域,并在该作用域内绑定一个新的 i 变量。

面试官给出了赞赏的眼神,并问出了最后一个问题:

还有别的方法吗?

张三顿时冷汗直下,他想不到方法了。

但是他要是看了我这篇文章就能够答出来了

正文

要了解闭包,我们先要知道JavaScript中的几个特性:

词法作用域

词法作用域,又称为静态作用域,决定了变量和函数的可见性基于其在源代码中的位置。换句话说,函数的作用域在它被定义的时候就已经确定,而不是在运行时。这意味着,无论函数在哪里被调用,它都能访问到定义时所在的作用域中的变量。

作用域链

作用域链是JavaScript引擎在查找变量时遵循的一套规则体系。当一段代码执行时,其所在的执行上下文会被创建,这个上下文中包含了当前作用域的所有变量和函数定义。有趣的是,每个函数在预编译阶段都会生成自己的作用域,并且拥有一个指向其外层作用域的引用——outer属性。这个引用链形成了所谓的“作用域链”。

什么意思呢?我们来看一下这一段代码

function bar(){
    console.log(a);
}
function foo() {
    var a=100
    bar()
}

var a=200
foo()

思考一下,这一段代码的运行结果

很多人会认为是100,bar()函数是在foo()内部调用的,变量应该是取foo()函数的

其实不是的

因为作用域链这一套规则体系,bar()函数和foo()函数都在预编译阶段生成了作用域,并且都生成了一个指向其外层作用域的引用outer属性。

正是因为bar()函数和foo()函数都是声明在外部的,所以他们outer指向都是整个外层作用域的。

当bar()函数要输出a但是其内部没有a时,就会去到它的outer的词法作用域中寻找a=200

所以运行结果为

image.png

这种机制确保了代码的可预测性,使得开发者能够根据代码的结构而非执行流程来推断变量的可访问性。因此,即便函数作为参数传递或赋值给其他变量,它依然保持着对定义时周围作用域的访问能力。

形象地说,如果一个函数试图访问一个变量,JavaScript引擎会首先在当前作用域查找该变量。如果未找到,它不会立即报错,而是继续沿着作用域链向外层作用域搜索,直到找到该变量或者到达全局作用域为止。这种从内到外、逐级遍历的查找机制,就是作用域链的工作原理。

我们再来练习一下

function bar() {
    var myname = 'Tom'
    let test1 =100
    if (1){
        let myname = 'Jerry'
        console.log(test,myname)
    }
}
function foo(){
    var myname = '彭于晏'
    let test = 2
    {
        let test =3
        bar()
    }
}
var myname = '晟哥'
let test =1
foo()

请来判断这个代码的结果

明白了上述原理,相信答案也已经很明了的,bar()函数的词法作用域是整个外层作用域,所以其结果为

image.png

明白了词法作用域和作用域链,我们来看看终极大boss-闭包

闭包是JavaScript中一个令人着迷的概念,它直接源于词法作用域的规则。简而言之,当一个内部函数可以访问其外部函数的变量时,便形成了闭包。特别地,如果这个内部函数以某种方式(如返回)被外部访问,它会“记住”那些外部变量,即使外部函数已经执行完毕。

我们来看这个例子:

function foo() {
    var name= '大仙'
    function bar() {
        console.log(count,age)
    }

    var count =1
    var age =18
    return bar
}
var age=20
const baz = foo()
baz()

在这个代码中,我们先看函数声明和变量声明,外部函数声明了foo()函数,和age变量
,在foo()函数内部,bar()函数被声明了。

我们继续来看运行,主运行程序先入调用栈,然后再是foo()函数,最后再是bar()函数进入调用栈

但是

我们调用foo()函数时,将bar()函数传给了baz,最后才是baz调用

你可曾发现,在函数foo()被调用后,foo()函数就应该被删除了,它已经被执行完了,所以我们的bar()的outer的词法作用域也找不到countage的值,所以我们的输出应该是两个null一个undefined

然而

image.png这才是我们的答案

为什么?

明明foo()函数已经运行完毕了,词法作用域已经消失了,为什么bar()函数依旧能够访问到它的值?

这就是闭包

当通过调用一个外部函数返回的一个内部函数后,即使外部函数执行已经结束了,但是内部函数引用了外部函数中的变量也依旧需要被保存在内存中,我们把这些变量的集合叫做闭包

所以bar()依旧能够访问到foo()中定义的变量的值并输出

练习一下吧!

var count = 0;
function add() {
   var count =0
   function foo(){
    count++
    return count
   }
   return foo
}
var bar = add()
console.log(bar())
console.log(bar())
console.log(bar())

答案是:

image.png

闭包的作用:

  • 实现共有变量:在模块化开发中,闭包可以用来创建私有变量并暴露有限的公共接口,实现数据的封装和隔离。
  • 做缓存:利用闭包可以存储计算结果,避免重复计算,提高程序效率。
  • 封装模块,防止全局变量污染:通过闭包封装变量和函数,可以有效减少全局作用域的污染,保持代码的整洁和可维护性。

然而,闭包的魔力并非没有代价。由于闭包维持对外部变量的引用,如果处理不当,可能会导致这些变量及相关的整个作用域链长时间驻留在内存中,从而引发内存泄漏。尤其是在大量使用闭包或者循环中创建闭包的情况下,必须谨慎处理,确保不再使用的变量能够适时释放,避免不必要的内存占用。

来回顾一下面试题吧

写一个闭包,让其只访问其函数外留下的闭包,这就是让张三汗流浃背的答案:

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

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

求点赞评论收藏! 有问题随时私信博主!

原文链接: https://juejin.cn/post/7373488886460366900

文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17106.html

QR code