你该知道的JS知识---闭包

191 阅读6分钟

你该知道的JS知识---闭包

什么是闭包?

闭包的定义:指有权访问另一个函数作用域中的变量的函数,一般情况下就是在一个函数中包含另一个函数。
闭包有何作用?
闭包能够访问函数内部的变量,保持函数在环境中一直存在,不会被垃圾回收机制所处理
函数内部声明的变量是局部的,只能够在函数内部访问到,在函数外部无法访问函数内部的变量。但是,函数外部的变量对函数内部是可见的。
子级可以向父级查找变量,逐级查找,直到找到为止或全局作用域查找完毕。
因此,我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后就可以访问其变量。

function foo(){
    let value=1;
    function bar(){
        console.log(value);
    }
    return bar();
}
const baz=foo();
baz();
//以上代码就形成了闭包,调用foo函数,就会执行里面的bar函数,bar函数可以访问外部的`value`

bar包含foo内部作用域的闭包,使得该作用域能够一直存活,不会被垃圾回收机制处理掉,这就是闭包的作用,以供bar在任何时间都能够进行引用。

闭包是怎么运作的?

function foo(){
    var a=2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz=foo();
baz();
  1. 代码执行流进入全局执行环境,并对全局执行环境中的代码进行声明提升。
  2. 执行流执行var baz =foo() ,调用foo 函数,此时执行流进入foo执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在两个执行环境,foo函数为当前执行流所在执行环境。
  3. 执行流执行代码var a=2;a进行LHS查询,给a赋值2.
  4. 执行流执行return bar(),将bar函数作为返回值返回。按道理此时foo函数已经执行完毕,应该销毁其执行环境,等待垃圾回收。但因为其返回值是bar函数。bar函数中存在自由变量a,需要通过作用域链到foo函数的执行环境里找到a变量的值,所有虽然foo函数的执行环境被销毁,但其变量对象不能被销毁,只是从活动状态变成非活动状态;而全局环境的对象则变成活动状态;执行流继续执行var baz=foo(),把foo函数的返回值bar函数赋值给baz
  5. 执行流执行baz,通过在全局执行环境中查找baz的值,baz保存着foo函数的返回值bar。所以这时执行baz,会调用bar函数,此时执行流进入bar函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在三个执行环境,bar函数为当前执行流所在的执行环境。
  6. 在声明提升的过程中,由于a是给自由变量,需要通过bar函数的作用域链bar->foo->全局作用域进行查找,最终在foo函数中找到var a=2;,然后在foo函数的执行环境中找到a的值是2,所以给a赋值为2。
  7. 执行流执行console.log(a),调用内部对象console,并从console对象中找到log方法,将a作为参数传递过去,从bar函数的执行环境中找到a的值是2,所以最终在控制台中显示2.
  8. 执行流执行完bar函数后,bar的执行环境被弹出执行环境栈,并被销毁,等待垃圾回收,控制权还给全局执行环境。
  9. 当页面关闭时,所有执行环境都被销毁。
//执行上下文
ESCack=[
    globalContext
]
//全局执行上下文
global={
    VO:[global],
    Scope:[globalContext.VO],
    this:globalContext.VO
}
//函数foo被创建,保存作用域链到函数内部属性[[Scopes]]
foo.[[Scopes]]=[
    globalContext.VO
]
//foo函数执行上下文
fooContext={
    AO:{
        a:undefined,
        bar:function(){
            console.log(a);
        },
        arguments:[]
    },
    Scope:[AO,globalContext.VO],
    this:undefined,
}
//bar函数执行上下文
barContext={
    AO:{
        a:undefined,
        arguments:[]
    },
    Scope:[AO,globalContext.VO],
    this:undefined,
}

bar函数执行的时候,foo函数上下文已经被销毁了(即从执行上下文栈中被弹出),怎么还会读取到foo作用域下的a值呢?
当我们了解了具体执行过程后,我们知道bar函数执行上下文维护了一个作用域链:

barContext={
    Scope:[AO,fooContext.AO,globalContext.VO]
}

就是因为这个作用域链,bar函数依然可以读取到fooContext.AO的值,说明当bar函数引用了fooContext.AO中的值的时候,即使fooContext被销毁了,但是JavaScrpit依然会让fooContext.AO存活在内存中,bar函数依然可以通过bar函数的作用域链找到它,正是因为JavaScript做到了这一点,从而实现了闭包这一概念

闭包有什么用处呢?

闭包的常见应用场景:

  • 函数嵌套:函数里面的函数能够保证外面的函数的作用域不会被销毁,所以无论是在函数里面还是在外面调用函数里面的函数都可以访问到外层函数的作用域,具体做法可以将函数当返回值返回后通过两次的括号调用
  • 回调函数:回调函数会保留当前外层的作用域,然后回调到另一个地方执行,执行的时候就是闭包
  • 匿名函数自执行:严格算也不是闭包,就是(function(){})()这种形式
function fn(a,b){
    return function(){
        console.log(a,b);
    }
}
const foo=fn(1,2);
setTimeout(foo,1000)

一般setTimeout的第一个参数是个函数,但是不能传值。如果想传值进去,可以调用一个函数返回一个内部函数的调用,将内部函数的调用传给setTimeout。内部函数执行所需的参数,外部函数传给他,在setTimeout函数中也可以访问到外部函数。

优缺点

  • 优点:能够让希望一个变量长期驻扎在内存中成为可能,避免全局变量的污染,以及允许私有成员的存在
  • 缺点:就是常驻内存会增大内存使用量,并且使用不当容易造成内存泄漏 如果不是因为某些特殊要求而需要使用闭包,在没有必要的情况下,在其他函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。