# 闭包 、作用域面试题答案解析

关于答案解析

  • JS 闭包 、作用域 相关面试题答案解析过程是根据自身项目实践以及查阅官方文档等最终的得出结论。
  • 仅供学习参考,评论区感谢补充和纠错 !

# 1、闭包里面的变量为什么不会被垃圾回收(快手、滴滴、58 篇、字节、小米、腾讯、网易)

① 首先我们来了解下什么是垃圾回收 ?

详细解读

在 js 中所谓的垃圾就是指不会再被使用的值,就会被当成垃圾回收掉。

  • javaScript 会自动回收不再使用的变量,释放其所占的内存,开发人员不需要手动的做垃圾回收的处理。

  • 垃圾回收机制只会回收局部变量,全局变量不会被回收,因为全局变量随时有可能被使用。(全局变量在浏览器关闭之后会回收)所以当我们定义了一个全局对象时,在使用完毕之后,最好给它重新复值为 null,以便释放其所占用的内存。

  • 目前浏览器基本使用标记清除引用计数两种垃圾回收策略

    • 标记清理
      • 当函数被调用,变量进入上下文时,会被加上存在上下文标记,是不会被清理的。
      • 当函数执行完成后,就会去掉存在上下文中的标记,随后垃圾回收程序会做一次内存清理,销毁这些变量。
    function fn() {
      var a = 1; // 函数调用时 被标记 进入上下文
    }
    test(); // 函数执行完毕,a的标记去掉,被回收
    
    • 引用计数
      • 引用计数就是追踪被引用的次数。声明变量并给它赋一个引用类型值时,这个值的引用数 为 1。
      • 如果同一个值又被赋给另一个变量,那引用数+1 。如果保存该值引用的变量被其它值覆 盖了,则引用数减 1。
      • 当引用计数为 0 时,表示这个值不再用到,垃圾收集器就会回收他所占 用的内存。
<script>
  var a = [1, 2, 3]; // [1,2,3]的引用计数为1
  var b = a; // 变量b也引用了这个数组,所以[1,2,3]的引用数为2
  var a = null; // [1,2,3]的引用被切断,引用数-1,所以[1,2,3]的引用数为1
  // 如果只是到这里,那[1,2,3]不所占的内存不会被回收
  var b = null; // [1,2,3] 的引用被切断,引用数-1,所 [1,2,3]的引用数为0
  // 到这里,垃圾收集器在下一次清理内存时,就会把[1,2,3]所占的内存清理掉
</script>

引用计数有一个很大的坑,就是循环引用时,会造成内存永远无法释放。

② 为什么闭包中的变量不会被垃圾回收 ?

答案解析

这里我们要明确一个点,如果闭包函数的引用计数为 0 时,函数就会释放,它引用的变量也会被释放。

  • 只有当闭包函数的引用计数不为 0 时,说明闭包函数随时有可能被调用,他被调用后,就会引用他在定义时所处的环境的变量。
  • 闭包中的变量就得一直需要在内存中,则就不会被垃圾回收掉。

# 2、说说 JS 作用域及作用域链(字节、小米、腾讯、商汤)

答案解析

要回答这个问题 ,我们可以从以下几个方面来展开讲解:

  • 什么是作用域 ?
  • js 中作用域的分类 ?
  • 每种作用域的特点 ?(作用或创建 ,销毁,变量和函数访问权限)
  • 什么是作用域链 ?
  • 什么是作用域链查找 ?

# ① 什么是作用域 ?

简单点理解:

  • 作用域是一套规则,规定了代码的作用范围。
  • 这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
  • 简单来说,作用域规定了如何查找变量。比如函数外部是不能访问函数里面的变量(闭包除外),函数里面是可以访问函数外面的变量。
<script>
  var b = 2;
  function fn() {
    var a = 1;
    console.log(b);
  }
  fn(); // 2
  console.log(a); // a is not defined  不能访问函数作用域中的变量a
</script>

全面理解

  • 作用域就是代码的执行环境。执行环境定义了变量或函数有没有权访问其他数据。
  • 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
  • 虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

比如特殊的全局执行环境中的变量对象 window 对象,因此所有全局变量和函数都作为 window 对象的属性和方法创建的。
在 Node 环境中,全局执行环境是 global 对象

<script>
  var a = 1;
  function sum(a, b) {
    return a + b;
  }
  var n = window.sum(2, 3); // sum 相当于window对象上的方法
  console.log(window.n); // 5  n相当于window对象的属性
  console.log(window.a); // 1  a相当于window对象的属性
</script>
  • 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。
  • 而在函数执行之后,栈将被环境弹出,把控制权返回给之前的执行环境。
  • ECMAScript 程序中的执行流正是由这个方便的机制控制着。

# ② JS 中作用域的分类

JS 中有 3 种类型的作用域:

  • 全局作用域
  • 局部作用域(函数作用域)
  • 块级作用域

全局作用域

详细解答

编写在 script 标签中的 js 代码(或单独 js 文件),都是在全局作用域中。

  • 全局作用域 在页面打开时创建,在页面关闭时销毁。我们可以把全局作用中不再使用的引用类型变量重新赋值为 null
  • 这样垃圾回收器就会把切断的引用类型数据当成垃圾回收掉,释放其在内存中占用的空间。
let arr = [1, 2, 3]; // 堆内存开辟空间,用来保存[1,2,3]
arr = null; // 垃圾回收器会把[1,2,3]回收掉,释放其在堆内存中占用的空间
  • 全局作用域中有一个全局对象 window,代表一个浏览器窗口,由浏览器创建,可以直接使用
  • 在代码的任何地方,都可以访问全局作用域中的变量

局部作用域(函数作用域):

详细解答

写在函数内部的代码,就是在局部作用域中

  • 每调用一次函数就会创建一个新的私有函数作用域,形参和当前私有函数作用域中声明的变量都是私有变量,保存在内部的一个变量对象中。
  • 函数被调用时创建函数作用域,函数执行完毕后,函数作用域被销毁,保存在其中的变量和函数定义了随之被销毁(闭包除外,只有当闭包函数的引用次数为 0 时,闭包函数和闭包中的变量被销毁
  • 函数里能访问函数外变量,但函数外部是不能访问函数里面的变量,闭包除外,闭包函数会记住它在定义时所处的环境
<script>
  function fn(a, b) {
    var c = 10;
    console.log((a + b) * c);
  }
  fn(1, 2); //函数调用创建函数作用域,代码执行用,作用域和变量a,b,c销毁
  fn(2, 3); //函数调用创建函数作用域,代码执行用,作用域和变量a,b,c销毁
</script>

特殊的闭包

详细解答

<script>
  function checkWeight(weight) {
    return function (_weight) {
      weight > _weight ? alert("过胖") : alert("ok达标");
    };
  }
  var P1 = checkWeight(100); // 调用完毕,作用域和变量weight不会被销毁
  P1(110); // 调用完毕,作用域和变量_weight会被销毁
</script>
  • 如果我们在最后加上P1 = null,则垃圾回收器回在下一次清理内存时
  • 销毁掉 checkWeight 调用形成的作用域和作用域中的变量 weight。

块级作用域

使用 let 或 const 关键字声明的变量,会形成块级作用域。

  • 在 {}、if 、for 里用 let 来声明变量,会形成块级作用域。{} 之外是不能访问 {} 里面的内容。
  • 块级作用域中定义的变量,在 if 或 for 等语句执行完后,变量就会被销,不占用内存
{
  let a = 1;
}
console.log(a); // 会报错,{}里是块级作用域,外面是访问不到里面的变量的
<script>
  for (let i = 0; i < 3; i++) {
    console.log(i); // 0 1 2
  }
  console.log(i); // i is not defined
</script>

注意点:

对象的 { } 不会形成块级作用域

# ③ 作用域链:

详细解读

当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)

  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象
  • 作用域链中的下一个对象来自于外部环境,再下一个变量对象则来自下下一个外部环境,一直到全局执行环境
  • 全局执行环境的变量对象始终都是作用域链上的最后一个对象

作用域链查找:

内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。

  • 在内部函数中,需要访问一个变量的时候,首先会访问函数本身的变量对象,是否有这个变量,如果没有,那么会继续沿作用域链往上查找
  • 如果在某个变量对象中找到则使用该变量对象中的变量值,如果没有找到,则会一直找到全局作用域。如果最后还找不到,就会报错。
点击查看源代码
<script>
  var a = 1;
  var c = 4;
  function fn1() {
    var a = 2;
    var b = 3;
    function fn2() {
      var b = 2;
      console.log(a); // 2  自身没有,沿着作用域链向上找
      console.log(b); //2  自身有,就用自身的
      console.log(c); //4  自身没有,沿着作用域链向上找,直到全局作用域中找到c=4
    }
    fn2();
  }
  fn1();
</script>

# 3、怎么理解 JS 静态作用域和动态作用域(小米)

答案解析


  • 静态作用域:又称词法作用域,是指作用域在词法阶段就被确定了(函数定义的位置就决定了函数的作用域)不会改变,javascript 采用的是词法作用域。
  • 动态作用域:函数的作用域在函数调用时才决定的。
<script>
  var a = 1;
  function fn() {
    console.log(a);
  }
  function test() {
    var a = 2;
    fn();
  }
  test(); // 1
</script>

最终输出的结果为 1

说明 fn 中打印的是全局下的 a ,这也印证了 JavaScript 使用了静态作用域。

静态作用域执行过程

当执行 fn 函数时,先从内部的AO对象查找是否有a变量,如果没有,沿着作用域链往上查找(由于JavaScript是词法作用域),上层为全局GO,所以结果打印1

# 4、以下代码输出的结果是 ?(小米)

答案解析


Object.prototype.a = 10;
var s = Symbol();
var obj = {
    [s]: 20,
    b: 30
}
Object.defineProperty(obj, 'c', {
    enumerable: truevalue: 40
})
for(let val in obj) {
    console.log(val)
}

以上输出的结果为:b c a

for...in语句以任意顺序遍历一个对象的可枚举属性(除 Symbol 类型的属性)

上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X