likes
comments
collection
share

JS中你必须要了解的闭包

作者站长头像
站长
· 阅读数 49

前言:

它是指函数与其周围的词法环境的组合。这意味着函数可以访问其定义时所在的词法作用域中的变量,即使函数在定义之后被移到了其他地方执行也是如此。闭包在 JavaScript 中经常用于创建私有变量、模块化设计以及异步编程等方面。

正文:

作用域链:

JavaScript 中的作用域链是指在函数嵌套结构中,每个函数都会创建自己的作用域,而在函数内部可以访问外部函数中声明的变量。作用域链的形成是由函数在定义时捕获其周围的词法环境所决定的。

在 JavaScript 中,当函数被调用时,会创建一个执行环境(execution context),该执行环境包括了该函数的作用域。当函数在内部访问一个变量时,JavaScript 引擎会首先在当前函数的作用域中查找该变量,如果找不到,则会向上一级作用域链中查找,直到找到该变量或者到达全局作用域。

function bar() {
    console.log(myName);
}

function foo() {
    var myName = 'Tom'
    bar()
}

var myName = 'Jerry'
foo()

// 这种关系叫做作用域链

解析:

V8引擎先编译再执行:

  1. 创建一个全局作用域,这里用全局上下文来讲,形成词法环境和变量环境两块。词法环境由let和const定义构成,所以没有。变量环境中bar,foo赋值后为function,myName赋值是Jerry。
  2. 再是foo的调用。
  3. 创建一个foo的执行上下文,形成词法环境和变量环境。变量环境中myName重新的赋值是Tom,再到bar的调用。
  4. 同理,创建bar的执行上下文,词法环境和变量环境均为空,再是myName 的执行。
  5. bar中没有myName的赋值,由outer指向bar的词法作用域,即全局作用域,跳到myName 的赋值为Jerry,最后打印Jerry。

JS中你必须要了解的闭包

更加形象的可以根据我绘制的图片来理解:

JS中你必须要了解的闭包

这里解释一下outer声明的作用域,可以通俗的理解是函数声明所在位置的作用域,因为foo和bar均在全局声明,所以foo和bar的词法作用域均是全局作用域,上述我们把这种关系称作作用域链。

词法作用域的位置

let count = 1
let a = 1

function main() {
    let count = 2
    let a = 2
    function bar() {
        let count = 3

        function foo() {
            let count = 4
            console.log(a);
        }
        foo()
    }
    bar()
}
main()

根据作用域链的关系,依次形成全局,main,bar,foo的作用域,逐层递增递增的寻找a值,a最后的值为2。

JS中你必须要了解的闭包

这里能更清晰的确认词法作用域和全局作用域的关系,就是这样一个简单的逻辑。

闭包(重点)

在JavaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时, 即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

这里可能比较拗口,用代码来解释:

var arr = []

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

for(var j = 0; j < arr.length; j++) {
    arr[j]()
}

可以先自己想想这段代码的执行结果。

JS中你必须要了解的闭包

JS中你必须要了解的闭包

结果是十个10,在for的循环下,依次打印十个 function() { console.log(i); }

再是到第二个for循环,把数组函数体取出来再调用,十个函数都执行打印,因为i值最后是10,最后打印十个10。所以为什么不是0-9?又该如何形成?

解法一:

函数function中的outer指向全局,function声明在全局中,因为for不构成作用域。若是让function不声明在全局,就让for形成作用域。这里就可以用let和const取代var,let和const和{}形成块级作用域,那么function就声明在了let形成的作用域中。

此时形成的第一个块级作用域包含了第一个i的值,第二个块级作用域就包含了第二个i值...依次形成十个不同的块级作用域且i值均不同,而function声明在了每个块级作用域之中,此时log打印的值就是0-9了。

var arr = []

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

for(var j = 0; j < arr.length; j++) {
    arr[j]()
}

根据图片更形象的理解:

JS中你必须要了解的闭包

JS中你必须要了解的闭包

解法二:

先看这段代码:

function foo() {

    function bar() {
        var age = 18
        console.log(myName);
    }   
    var myName = 'Tom'

    return bar
}

var myName = 'Jerry'
var fn = foo()
fn()

bar的词法作用域是foo的作用域,var fn = foo(),将fn这个变量赋值为foo的执行结果,因为foo最终的执行结果是返回一个bar,所以此时的fn的值即为bar。

14行代码fn的调用,即为bar的调用,最终执行打印myName。

根据作用域链的关系,先后生成全局,foo,bar的执行上下文,词法环境没有,变量环境中依次存入对应值。当执行完foo的执行上下文时,为了不占用V8引擎中的内存,就要在执行完毕的时候消除这段执行上下文,最后执行到bar的调用打印myName。本应该 bar中outer的指向是foo的作用域,由于foo的作用域被清除,outer没有对象指定,此时按道理打印会报错。但是,最终的结果并没有报错,这是为什么?

此时长安把上面的思路整理成图片,更具大家形象化的理解:

JS中你必须要了解的闭包

插入对解法二的原因解释:

域中的上下文执行完毕时,这里你可以想象一个清洁工就会过来访问上下文:“兄弟,你执行完毕没?OK的话那我就将你执行完剩下的空壳清扫了。”一般正常的上下文对象都会来声OK。但是当问到foo的执行上下文时,foo确是这样说的:“哥们,不是我不让你清扫,而是我自己都不知道我有没有执行完,虽然我体内诞生了bar的函数体,但是被放到外面调用了,它并没有受我的掌控,更主要的是,它依旧访问了我【foo】体内的变量。”清洁工哥们就很疑惑,我到底该不该会回收你呢,不回收又会造成空间的浪费?

V8此时这样设定的:这时判断到底是哪些如bar这样的崽子,它到底调用了哪些变量,直接就让清洁工留下这些变量的空间,其他的清理掉,此时就是最优解。

所以此时foo的执行上下文依旧销毁,但是在原来的位置放上了新的空间,防止报错。这个空间小背包储存了myName = 'Tom', 所以bar的outer最终找到了foo中的这个小背包,访问了myName 的值打印Tom。

这个小背包空间就叫闭包,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

JS中你必须要了解的闭包

闭包的作用:实现变量私有化

function add() {
    let count = 0
    function fn() {
        count++
        return count
    }
    return fn
}
 var res = add()

console.log(res());
console.log(res());
console.log(res());

根据闭包关系,最终打印出1 2 3

总结:

了解好作用域链的关系,知道oute指向的作用域,这样才能更好的理解闭包的关系,内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

JS中你必须要了解的闭包

转载自:https://juejin.cn/post/7365793739891884032
评论
请登录