JavaScript运行机制解析(五)JS闭包

112 阅读3分钟

一、概念

什么是闭包?

根据MDN中定义,闭包是那些能够访问自由变量的函数。

什么是自由变量?

自由变量是指在函数中使用的,但不是函数的参数,也不是函数内部定义的局部变量的变量。

总结:闭包 = 函数 + 函数中能访问的自由变量。

例如:

var a = 100;
function f1() {
  console.log(a)
}
f1();

f1 函数可以访问变量 a,但是 a 既不是 f1 函数的局部变量,也不是 f1 函数的参数,所以 a 就是自由变量。

那么,函数 f1 + f1 函数访问的自由变量 a 不就是构成了一个闭包嘛……

没错,就是这样!!!

《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

这里跟我们平时遇到的闭包就有点不同了,实际上闭包分为两种概念:

从理论上讲:

所有的函数都可以称之为闭包。因为它们满足上面说的这种概念。

从实践中讲:

函数满足下面条件才是闭包:

  1. 尽管创建它(自由变量)的上下文已经销毁,但是它仍然存在。
  2. 在代码中引用了自由变量

二、闭包形成过程

先看来一个例子:

function foo(){
    var a = 100;
    function bar(){
        console.log(a++);
    }
    return bar;
}
​
var fn = foo();
fn();

一个简单的闭包例子,fn执行结果为 100

问题:foo()执行完毕之后,函数上下文就出栈了(销毁),而变量a是存在于执行上下文中的VO变量中的,为什么fn还能访问到这个这个作用域的值?

分析

我们通过之前学的执行上下文知识进行分析上面过程:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 foo 函数,创建 foo 函数执行上下文,foo 执行上下文被压入执行上下文栈
  4. foo 执行上下文初始化,创建作用域链、变量对象、this等
  5. foo 函数执行完毕,foo 执行上下文从执行上下文栈中弹出
  6. 执行 bar 函数,创建 bar 函数执行上下文,bar 执行上下文被压入执行上下文栈
  7. bar 执行上下文初始化,创建作用域链、变量对象、this等
  8. bar 函数执行完毕,bar 函数上下文从执行上下文栈中弹出 通过这个过程,我们知道了bar执行上下文会维护一个作用域链:
barContext = {
    Scope: [AO, fooContext.AO, globalContext.VO],
}

没错,foo执行上下文是被销毁了,但是bar内部是有作用域链的。同时我们上节中说到,在函数创建的时候,会copy自己创建的作用域链到当前上下文[[scope]]属性中。

所以,当 bar 函数引用了 fooContext.AO 中的值的时候,即使 fooContext 被销毁了,但是 JavaScript 依然会让 fooContext.AO 活在内存中,bar 函数依然可以通过 bar 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

例题练习:

// 示例一
var data = [];
​
for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
​
data[0]();
data[1]();
data[2]();