面试之执行上下文、作用域、闭包

299 阅读8分钟

面试题:用自己的话说

  • 谈谈对执行上下文的理解
  • 谈谈对作用域的理解
  • 谈谈对闭包的理解

谈谈对执行上下文的理解

image.png

参考冴羽大佬的博客:链接

首先JS它并不是一行一行的执行代码,它是一段一段的执行,这个一段一段指的就是执行上下文

当JS引擎执行全局代码、函数、eval代码,它就会创建对应的全局执行上下文、函数执行上下文、eval执行上下文

那么JS代码中那么多函数,肯定对创建很多函数执行上下文,JS怎么管理它呢?这就引出了执行上下文栈

举个列子:在全局环境下 有这样一串代码 函数f1里面又调用了一个函数f2,执行上文下栈它会怎么做呢?

首先JS引擎会执行全局代码,创建全局执行上下文,将其压入执行上下文栈中,之后JS引擎遇到调用f1()函数的代码,创建f1的函数执行上下文,将其压入执行上下文栈中,在执行 f1函数代码的时候,又调用了f2,JS创建f2的函数执行上下文,f2函数执行完毕,将f2对应的执行上下文退栈,f1函数执行完毕,将f1的函数执行上下文退栈,这个时候栈顶只有全局执行上下文了,等程序运行结束后,全局执行上下文也会被退栈,执行上下文栈被清空

每个执行上下文有三个重要的属性:变量对象、作用域链、this

每个执行上下文的生命周期包括两个阶段:创建阶段和执行阶段

  • 创建阶段:创建变量对象、建立作用域链、确定this的指向
  • 执行阶段:执行代码完成对变量的分配

主要讲创建执行上下文阶段:

变量对象VO:它存储的是执行上下文里定义的变量和函数声明,在全局执行上下文中他叫GO,在函数执行上下文中又叫AO

变量对象的创建它会经历以下几个过程:

  • 如果是函数执行上下文(针对形参),它会先构建arguments对象来初始化形参,如果没有实参则值为undefined
  • 检查函数声明,函数声明的函数名作为变量对象的属性,它的值是个引用,指向该函数在内存中的地址
  • 检查变量声明,作为变量对象的属性,值为undefined

JS是静态作用域(也叫词法作用域),函数的的作用域在定义时就已经确定,不是像动态作用域那样,在调用时才确定。这是因为js中每个函数都有一个[[scope]]属性,在创建时就会保存所有父级作用域的活动对象,形成一个层级链

作用域链:当我们查找变量时,会在当前执行上下文的变量对象中查找,找不到就去上一层执行上下文的变量对象中找,一直找到全局执行上下文的变量对象,也就是全局对象(wondow)。这样由多个执行上下文的变量对象构成的链表就叫做作用域链,作用域链的本质上是一个指向变量对象的指针列表。

作用域链是怎么创建的,结合前面的变量对象。 举个列子:在全局环境下 有这样一串代码 定义了一个函数f1并调用它

  • 因为js是静态作用域,在创建f1函数时,它的内部属性[[scope]]会保存所有父级作用域的活动对象,因为f1是在全局环境下定义的,所以它的[[scope]]中保存了全局执行上下文的VO。
  • 当函数f1被执行时,会创建f1的执行上下文,压入执行上下文栈,此时f1的执行上下文处理创建阶段,不会立即执行,会复制函数的[[scope]]属性创建作用域链,用arguments创建活动对象,随后初始化活动对象(加入形参、函数声明、变量声明),再将活动对象AO添加到作用域链的顶端,至此作用域链创建完毕
  • 之后执行上下文处理执行阶段,通过执行代码来修改变量对象的值。

谈谈对作用域的理解

image.png

作用域分为全局作用域和函数作用域,之后ES6引出了块级作用域

值得注意的是:未声明直接赋值的变量是全局作用域,在函数作用域中,内层作用域可以访问外层作用域,反之不行、块级作用域由 let const 以及{}代码块(由{}包裹的一片代码)

JS是静态作用域(也叫词法作用域),函数的的作用域在定义时就已经确定,不是像动态作用域那样,在调用时才确定。这是因为js中每个函数都有一个[[scope]]属性,在创建时就会保存所有父级作用域的活动对象,形成一个层级链

作用域链:当我们查找变量时,会在当前执行上下文的变量对象中查找,找不到就去上一层执行上下文的变量对象中找,一直找到全局执行上下文的变量对象,也就是全局对象(wondow)。这样由多个执行上下文的变量对象构成的链表就叫做作用域链,作用域链的本质上是一个指向变量对象的指针列表。

作用域链是怎么创建的,结合前面的变量对象。 举个列子:在全局环境下 有这样一串代码 定义了一个函数f1并调用它

  • 因为js是静态作用域,在创建f1函数时,它的内部属性[[scope]]会保存所有父级作用域的活动对象,因为f1是在全局环境下定义的,所以它的[[scope]]中保存了全局执行上下文的VO。
  • 当函数f1被执行时,会创建f1的执行上下文,压入执行上下文栈,此时f1的执行上下文处理创建阶段,不会立即执行,会复制函数的[[scope]]属性创建作用域链,用arguments创建活动对象,随后初始化活动对象(加入形参、函数声明、变量声明),再将活动对象AO添加到作用域链的顶端,至此作用域链创建完毕
  • 之后执行上下文处理执行阶段,通过执行代码来修改变量对象的值。

谈谈对闭包的理解

image.png 闭包的定义:闭包是指有权访问另一个函数作用域中变量的函数

使用闭包要满足两个条件

  • 闭包要形成: 在内部函数使用外部函数的变量
  • 闭包要保持: 内部函数返回到外部函数的外面

比如:

    //example1: 函数外部 不能访问函数内部的变量 
    function f1(){
       var n=999;
    }
  alert(n); // error
  // example2:使用闭包 能够访问函数内部的变量
function f1(){
    var n=999;
    
    function f2(){ // 闭包函数
      alert(n);   // 函数内部是可以访问外部的变量的
    }
    return f2;
  }
  var result=f1();
  result(); // 999

闭包里面是什么东西?
在内部函数 b 中使用了外部函数 a 中的变量, 这个变量就会作为闭包对象的属性!!,上面的例子中n=999就是闭包里面的东西

闭包的优点:

  • 打破作用域的限制,可以在函数外部访问函数内部的变量,可以使用这种方法来创建私有变量

    • 通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 让一些值继续保留在内存中

    • 这是因为现代浏览器GC算法是标记清楚,标记清楚的本质就是可达性,会定期从根对象window往下遍历,能访问到的对象就保留,不能访问到的对象就被释放。而从window遍历到闭包函数这个对象的时候,它的函数执行上下文的作用域链中保留了外部函数的变量对象,所以外部函数的变量对象并不会被回收(做了一些优化,不会将整个外部函数的AO保留下来,只保存了闭包用到的变量)

    • 在例2中,f1()执行完后,f1的函数执行上下文从执行上下文栈中弹出,之后按理来说会被销毁,但是为什么f2还能访问f1中的变量呢?这是因为f2的函数执行上下文的作用域链还有对f1变量对象的引用,f2执行上下文的作用域链如下:【f2执行上下文的变量对象 -> f1执行上下文的变量对象 -> 全局执行上下文的变量对象】

闭包的缺点:

  • 常驻内存会增大内存使用量
  • 使用不当容易造成内存泄漏

闭包是内存泄露嘛? 闭包并不是内存泄露,只是使用不当容易造成内存泄露

闭包的应用:节流、防抖、柯里化

参考链接: