你了解闭包的创建过程吗?

251 阅读4分钟

前言

作为一个前端,不够了解闭包,是不合适的,这基本上是面试必问的一个问题。

说到闭包,一言以概之,是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。闭包会随着函数的创建而被同时创建。

Tip

注意理解上面这句话,有两个关键元素:

  • 一个函数(内部函数)
  • 周边环境的引用(可以理解为:内部函数使用了外部变量的引用)

闭包创建过程简析

通过一个示例,来看看闭包的创建过程。

function fn() {
    var a = 1;
    var b = 2;
    function fn2() {
        // 内部函数访问了外部函数中的变量,这里候就形成了闭包,
        console.log(a);
    }
    fn2();
}
fn();

我们通过chrome控制台看下闭包形成的过程。

  1. 执行到 fn() 时,调用栈为空。因为执行的是全局代码,作用域为全局作用域。

    闭包1.PNG

  2. 调用 fn() 时, fn 压入调用栈,此时闭包已经形成。此时作用域为 fn 的作用域

    闭包2.PNG

    • 只有内部函数引用了外部函数中的部分变量,部分变量才会被保存在函数的[[scopes]]属性中。这里的部分变量就是前言中提到的一个关键元素:周边环境的引用。其实是[[scopes]]属性中 Closure 指向了这个引用。
    • 内部函数在变量查找时,在自己作用域中找,找不到再到[[scopes]]属性中一层一层向下找
    • [[scopes]]属性中,本质记录的是全局作用域的变量对象 window 和每一次内部形成的闭包对象
    • 其实,在 JS 中,每个函数都存在一个隐式属性[[scopes]], 这个属性用来保存当前函数的外部执行上下文中的变量对象身上的一些属性, 由于在数据结构上是链式的, 也被称为作用域链
  3. 执行 var a = 1,赋值操作。

    闭包3.PNG

  4. 调用 fn2()时, 作用域为 fn2 的作用域, 可以看到函数 fn2 作用域中的闭包, 执行 console.log(a) 时,会先在本地作用域中查找,没有再去闭包中找,找到了变量 a。否则还会去全局作用域中找,找不到会报错。

    闭包4.PNG

上述 fn 的闭包会随着 fn 执行完毕而销毁。

不会被销毁的闭包

function fn() {
  var a = 1;
  var b = 2;
  function fn2() {
    console.log(a);
  }
  return fn2;
}
var fn3 = fn(); // 被赋值
fn3();
  1. 我们通过 return 把内部函数返回。此时调用栈中正在执行 fn。

    闭包5.PNG

  2. fn 执行完毕出栈,此时调用栈为空。执行到全局代码 fn3(),可以看到此时作用域为全局作用域。

    闭包6.PNG

  3. 调用 fn3(),执行的是 fn2 中的语句,可以看到调用栈中的 fn2,此时作用域也为 fn2的作用域。可以看到fn执行完毕后,闭包并没有被销毁。这里与垃圾回收算法有关,因为 fn2 存在外部引用 fn3。

    闭包7.PNG

  • 内部函数使用了外部函数的变量,同时被返回到了外部函数的外面,这时就会形成闭包
  • 主要表现在于,在外部执行被返回的函数时,可以访问他在定义时所处环境中的变量
  • 这种情况才是真正意义上的形成了闭包,因为闭包被保持下来,供后期使用

Tips:

闭包是随着 fn 的调用而被创建的。这个闭包对象中保存了内部函数引用外部函数中的那些变量。 内部函数身上的[[Scopes]]属性中,保持了对这个闭包对象的引用。

此时再理解一下前言中的定义:闭包(Closure(fn) {a: 1})是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。闭包会随着函数的创建而被同时创建。

闭包的特点

  1. 变量私有化
  2. 数据持久化,但也会一直占用内存无法被回收。

通过这两点,我们可以延伸出很多使用场景。

利用闭包的数据持久化

示例1:

function createCheckTemp(standardTemp) {
  function checkTemp(n) {
    if (n <= standardTemp) {
      console.log("你的体温正常");
    } else {
      console.log("你的体温偏高");
    }
  }
  return checkTemp;
}

// 创建一个checkTemp函数,它以37.3度为标准线
var checkTemp_A = createCheckTemp(37.3);
// 再创建一个checkTemp函数,它以37.0度为标准线
var checkTemp_B = createCheckTemp(37.0);

checkTemp_A(37.1); // 你的体温正常
checkTemp_A(37.8); // 你的体温偏高

checkTemp_B(37.1); // 你的体温偏高
checkTemp_B(36.5); // 你的体温正常

通过上述示例,我们可以看到,利用闭包的数据持久化,把参数 standardTemp 持久化了并返回给了 checkTemp_AcheckTemp_B

闭包的经典使用场景:防抖和节流。也利用了闭包数据持久化的特性。

ES6之前,js也是利用IIFE和闭包实现的模块化。

总结

  • 闭包分为可销毁的闭包和不会被销毁的闭包。我们经常说的闭包指的是不会被销毁的闭包。
  • 闭包中的持久化变量存放在堆内存中,内部函数的作用域会保存对它的引用,不会随着函数执行上下文的销毁而销毁。

相关文档

JavaScript 执行原理、闭包、垃圾回收、立即执行函数