likes
comments
collection
share

浏览器中的JS执行机制3--面试难点:闭包

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

在前面几篇文章中我们已经大致了解了JS代码在浏览器中是怎么被执行的,接下来我们将完整的捋顺他的指向过程,通过引入闭包的概念,完全掌握一般Jscript代码的执行机制。

一,复习预热

我们通过一串代码块来预热一下

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

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

var myName = 'Jerry'
foo()

首先我们看全局,发现有foo()函数和bar()函数,还有一个myName变量的声明;我们把他们放进全局执行上下文中,还记得吗?执行上下文中存在两个环境---变量环境和词法环境,let,const声明的变量装入环境中,其他的函数体和var声明等放入变量环境中;如此,我们的全局执行上下文就被编译好啦,下一刻开始执行,发生foo()函数的调用,所以我们现在需要创建一个foo的执行上下文对象。同样分好变量环境和词法环境,将声明myName赋在变量环境中,发生bar函数的调用,创建bar执行上下文。执行bar函数,打印myName,可是V8在bar的上下文对象中找了一圈后发现没有关于myName的定义,于是outer在这时出现了,他说可以带V8链接外部,访问外部变量。outer指向谁取决于其函数的词法作用域在哪里。 因为bar函数处于全局作用域中,所以bar的outer指向全局作用域,outer指针指向在全局声明的myName = 'Jerry',最后打印出结果。

调用栈的图示如下,可以帮助大家准确的分析编码结构,使得预编译过程一目了然~ 浏览器中的JS执行机制3--面试难点:闭包

二,词法作用域

分析以上代码,找准词法作用域相当重要,为了帮助大家理解我们在引入一道题:

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);
        }
    }
}

请画出上文中main,bar,foo的作用域和词法作用域

在作用域基础一文中我们就已经讲过,并且明确了全局作用域,函数作用域,块级作用域。全局作用域自然不必多说,就是最外层的大框框window;函数作用域包含{}中的所有代码以及内部参数;块级作用域为由let,const声明的{}内的变量函数或类,var声明不受块级作用域影响。

那词法作用域呢?我们要知道,词法作用域由函数声明的位置来决定,跟函数在哪里调用没有关系如果函数声明在全局,那么该函数的词法作用域就在全局,如果声明在另外一个函数体内,那词法作用域就在该函数体内。大家请看图示:

浏览器中的JS执行机制3--面试难点:闭包 包含结构一目了然,往后定勿要犯错!

三,闭包closure

大家还记得调用栈是什么吗?让我们复习一下,当代码开始执行时,首先会创建一个全局作用域,并将其压入调用栈;当一个函数被调用时,会为函数创造一个新的执行上下文,并将其压入栈的顶部;如果该函数中调用了另一个函数,那么新函数的执行上下文也会被压入栈的顶部。哎,所以呢大家看,这栈不就是一种存储数据的空间吗,没错,调用栈的本质是存储空间,但他不是普通的存储空间,他是一个具有特点结构和功能的站数据结构空间。那么他既然是一个存储空间,那必然要考虑存储空间大小的问题,现在编码这几块代码还好,但如果是遇到几千行几万的代码时怎么办?难道要一直保存几万的执行上下文吗?为了节省空间,闭包应运而生~

大家看这么一段代码:

function foo() {

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

    return bar
}
var myName = 'Jerry'

var fn = foo()//foo调用完的结构,即bar。因为return出来的
fn()//调用fn,求fn最后输出的结果

我们轻车熟路的画出他的调用栈以及他的编译和执行结果,即v8语言转化示意图:

浏览器中的JS执行机制3--面试难点:闭包 简简单单啊,全局之下定义了一个myname,一个fn并且调用了fn,构造了一个foo函数;最后将'jerry'赋值给了myname,将function赋值给了foo,将foo赋值给了fn;foo的()调用了foo函数,所以创建一个foo的执行上下文对象;在foo这个函数作用域中,有一个函数bar,赋值一个myName='Tom',并且返回了bar。注意,return只是在返回该函数的引用,并不是执行他。在这里bar返回给了foo,就是foo()函数的输出结果为bar(),即*fn=bar()*~重新赋值后,调用bar函数,产生一个bar函数的执行上下文对象

在v8定义的机制中,一个函数只要被执行完毕他的执行上下文一定会在调用栈中被销毁,以释放内存。那么问题出现了,bar在执行中需要打印myname这个属性,因此他会通过作用域链的outer指向上一级foo的执行上下文,但是在foo执行完毕时他已经被销毁了,bar是怎么访问的呢?

原来啊,v8在销毁执行上下文(或者说AO对象)时,他保留了内部函数对外部函数的有用的变量 把他们放进了一个小背包里,这个背包就叫闭包! 所以,v8真正的执行过程应该是这样的:

浏览器中的JS执行机制3--面试难点:闭包 如果foo中没有myName也会留下一个closure,不过是空包。闭包保存了对外部词法环境的引用,形成了自己的变量环境,同时闭包自身也构成了一个新的词法环境。其中仍保留了outer属性,所以不会报错,而是向下指向全局~

看懂了上文的执行过程后我们来总结一下:

1.什么是闭包

在js中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量, 当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用。 那么这些被引用的变量会以一个集合的方式被保存下来,这个集合就是闭包。闭包可以是空的,没有下级作用域并不会影响闭包的形成。只要函数引用了其外部作用域的变量,并在其外部被访问或调用,就形成了闭包。 闭包的存在主要与函数的作用域和函数的执行上下文有关。

2.闭包的形成条件

一个函数嵌套在另一个函数内部,内部函数可以访问外部函数的变量。 外部函数返回内部函数,这样内部函数可以在其外部被访问和执行。

3.闭包的作用

  1. 数据封装:闭包可以用来创建私有变量,提高代码的封装性。这有助于防止外部代码意外地修改内部变量,从而增强了代码的健壮性。
  2. 保持状态:闭包可以在多次调用之间保留状态。当一个函数返回另一个函数时,这个被返回的函数将携带它所在作用域的信息,这样可以让我们在多次调用之间保存一些信息,如计数器或者累加器。
  3. 控制资源访问:通过闭包,我们可以控制对特定资源的访问,使得只有特定的函数能够访问和操作这些资源。这有助于提高代码的安全性和可维护性。 此外更好地组织代码,将相关的逻辑放在一起,闭包可以存储并记住其所在的词法环境,因此即使在函数执行完毕后,其内部的变量仍然可以被访问。这种特性使得闭包非常适合用于实现延迟执行和回调函数。

用闭包实现累加法

如何实现一个数字的累加呢,我们在前文讨论声明提升问题时讨论过var和let的累加问题 (为什么一个是10个10,一个是1到9),那么这次我们运用闭包的思想来构造一个累计器会是什么样子?现在你可以秒了呀兄弟!我们在全局定义一个res,为他构造累计函数add,并且调用()他。来到function add()的内部,我们先确定:我们要声明一个数为0因为从0开始累加,再构造一个函数用于循环累加,我们需要反复执行这个函数,并且每次都需要输出结果(return function)。

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

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

v8解析图也秒了:

浏览器中的JS执行机制3--面试难点:闭包

结语

JS在浏览器,即在JS解释器,V8引擎中的执行机制是十分重要的知识点,明白一套语言的执行机制是学好这门语言的基础!

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