闭包

2022/12/1

我想,在大多数面试中,大家都会被面试官询问到闭包这一道经典的面试题吧,那么今天我们就常考的几道面试题来谈谈闭包。

  • 闭包是什么?
  • 闭包的作用与应用是什么?
  • 闭包产生的变量如何被回收?
  • 过度使用闭包会产生什么问题?

# 闭包是什么?

# 概念

官方:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

自我理解:就是一个函数嵌套另一个函数。并且被嵌套函数可以使用外部函数的变量,就产生了闭包。换言之就是外部变量+函数

# 基础代码

function fn1() {
  var num = 1;
  function fn2() {
    console.log(num);
  }
  return fn2;
}
var fn3 = fn1();
fn3();

上述代码有以下几个作用域:

  • fn1 全局作用域
  • fn1 函数作用域内包含 num 变量和 fn2 函数
  • fn2 函数作用域,能够访问父级函数的变量 num
  • fn3 全局作用域,赋值 fn1,由于引用关系实际上就是 fn2 函数本身,同样能够访问 fn1 作用域内的 num 变量,也就是上述概念所说的 fn2 在当前词法作用域之外执行

说到这,我们来进一步解释下作用域链和词法作用域,方便更好理解闭包。

# 作用域链

作用域:变量、函数等起作用的范围

  • 全局作用域:在代码中的任何地方都能访问,生命周期伴随着页面的生命周期
  • 函数作用域:函数内部定义的变量或函数,并且定义的变量或者函数只能在函数内部被使用,函数执行结束之后,函数内部定义的变量会被销毁
  • 块级作用域:作用块内声明的变量不影响块外面的变量

先来看一段代码

function fn1() {
  var str1 = "我是fn1内部的";
}
function fn2() {
  var str2 = "我是fn2内部的";
  console.log("我想输出fn1内部的变量", str);
}
fn1();
fn2();

执行之后会发现,控制台报错:“Uncaught ReferenceError: str is not defined”

那是因为 str2 是局部变量,是属于 fn1 的私有财产,不能想拿别人的 💰 就拿,会被抓走的。

那么我们再来看下一段代码

function fn1() {
  console.log(str);
}
function fn2() {
  var str = "我是fn2内部的";
  fn1();
}
var str = "我是全局上下文的";
fn2();

猜猜此刻输出什么?

答案是:“我是全局上下文的”

在每个执行上下文的变量环境之中,都会包含一个外部引用 ➡️ 用来指向外部的执行上下文。 当一段代码使用了一个变量后,js 引擎会在“当前的执行上下文”中查找该变量。

也就是说,在执行 fn1 函数的时候,会查找 str 这个变量,如果在当前的变量环境中没有查找到,就会到外部引用所指向的执行上下文查找。

fn1 和 fn2 函数的外部指向都是指向全局上下文的,换言之,如果在 fn1 和 fn2 函数使用了外部变量,那么 js 引擎就会到全局执行上下文查找。

这时候大多数同学就有一个疑问了,fn1 函数不是在 fn2 中被调用的吗?为什么不是指向 fn2 中的变量?

那是因为作用域链是由词法作用域决定的。

# 词法作用域

作用域是由代码中函数声明的位置来决定的。

词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符

来看下面一段代码

var str = "我是全局上下文的";
function fn3() {
  console.log("fn3想输出str的变量", str);
}
function fn1() {
  var str1 = "我是fn1内部的";
  var str = "我是fn1内部的str";
  function fn2() {
    var str2 = "我是fn2内部的";
    console.log("fn2想输出str1的变量", str1);
    console.log("fn2想输出str的变量", str);
  }
  fn2();
  fn3();
}
fn1();
  • 全局作用域:str、fn1、fn3
  • fn1 函数作用域:str1、str、fn2
  • fn2 函数作用域:str2

输出结果为

fn2想输出str1的变量 我是fn1内部的
fn2想输出str的变量 我是fn1内部的str
fn3想输出str的变量 我是全局上下文的

因为 js 作用域是由词法作用域决定的,所以在 fn2 中整个词法作用域链的顺序是:fn2 函数作用域 ➡️fn1 函数作用域 ➡️ 全局作用域

而因为 fn3 的代码位置是在全局作用域下的,即便是被 fn1 引用,它的词法作用域链是:fn3 函数作用域 ➡️ 全局作用域

结论:词法作用域和代码位置相关,和函数怎么调用没有关系

# 闭包的作用与应用

  • 闭包是 js 自身语法的一部分,保证了 js 自身的运行
  • 保护私有变量
  • 维持内部私有变量的状态
  • 定义模块,将操作函数暴露给外部,细节隐藏在模块中
  • 实现私有成员
  • 解决 i 的问题
  • 防抖和节流

# 闭包是如何被回收的?

因为闭包函数的存在,延长了外部局部变量的生存周期,如果闭包会一直存在直到页面关闭,但如果这个闭包不在使用的话,就会造成内存泄露。

那么,js 如何知道什么时候变量该被回收呢?

那就不得不谈谈垃圾回收机制

# 垃圾回收机制

javascript 使用的是自动垃圾回收策略,通过向下移动执行状态的指针来销毁该函数保存在栈中的执行上下文。

也就是说,当函数执行后,局部变量通常就会被标记为“可回收”,然后在下一次自动垃圾收集时(GC)回收

而在闭包中,fn1 中的变量 num 本该在 fn1 函数结束后被回收,但是因为被 fn2 调用,导致了延长了 num 的生命周期

闭包会保留全部父级变量,不管有没有用

# 过度使用闭包会产生什么问题?

导致内存泄漏。

如果使用频率不高,而且占用内存比较大的话,尽量让闭包成为一个局部变量。

上次更新: 2024/2/25 23:51:56