一文彻底搞懂 JS 闭包、作用域链与词法环境

65 阅读5分钟

深入理解 JavaScript:从词法作用域到闭包

在 JavaScript 开发中,我们经常听到“作用域”、“闭包”这样的术语。它们看似抽象,却是掌握 JS 执行机制的关键。本文将从 词法作用域作用域链 出发,逐步揭示 闭包 的本质,并通过几个典型例子帮助你真正理解这个“无处不在的高级概念”。


一、什么是词法作用域?

JavaScript 采用的是 词法作用域(Lexical Scoping) ,也叫 静态作用域

词法

新手常常难以理解词法的含义,我在这里给出一个容易理解的概念,词法” = “与源代码书写位置有关”,也叫 静态的,它强调的是某些行为在编写代码时就已经确定了,你也可以理解为是书面的,你是怎么样编写代码的,那么代码就是怎么运行的,他是符合直觉的,这也是为什么我们说let/const是存在于词法环境中,因为他不能在声明前访问,这是符合直觉的。

词法作用域的意思是:函数的作用域在编写代码时就已经确定了,而不是在运行时动态决定的。

换句话说,一个函数能访问哪些变量,取决于它 在源码中被定义的位置,而不是它 在哪里被调用

示例 1:词法作用域

function bar() {
  console.log(myName);
}

function foo() {
  var myName = "极客邦";
  bar(); // 调用 bar
}

var myName = "极客时间";
foo();

输出结果是:"极客时间"

为什么不是 "极客邦"
因为 bar 函数是在 全局作用域 中定义的,所以它的词法作用域就是全局。而变量的查找并不是看函数在什么地方执行而是函数在什么地方声明,bar首先会查找自身的执行上下文中是否存在,没有就会继续向外查找(在这里就是全局执行上下文)
下面这张图可以帮你更好的理解:

lQLPKdec6QcoeWPNAqPNBHawbkdsmOcRCAIJAuLT_ldOAA_1142_675.png

这说明:作用域链在编译阶段就已确定,与函数调用位置无关。


二、作用域链:变量查找的路径

当 JavaScript 引擎执行一段代码时,会创建 执行上下文(Execution Context) 。每个上下文都包含一个 词法环境(Lexical Environment) ,其中记录了当前作用域的变量,并维护一个指向外部作用域的引用 —— 这就构成了 作用域链(Scope Chain)

  • 全局上下文在栈底。
  • 函数调用时,会压入新的函数上下文。
  • 变量查找沿着作用域链 由内向外 进行,直到找到或到达全局。

示例 2:块级作用域与作用域链


function bar() {
  var myName = "极客世界";
  let test1 = 100;
  if (1) {
    let myName = "Chrome 浏览器";
    console.log(test); // 报错?test 未定义?
  }
}

function foo() {
  var myName = "极客邦";
  let test = 2;
  {
    let test = 3;
    bar();
  }
}

var myName = "极客时间";
let myAge = 10;
let test = 1;
foo();

这段代码的执行结果是什么呢?会是test未定义吗?实际上不会,他会输出1,原因是什么已经相信你在了解了上面所讲的词法作用域后一定明白了--因为bar被声明在全局作用域中,所以当test被使用时就会沿着执行上下文一层一层向外找,这样的查询路径就是一条作用域链

lQLPKH_1fPDDKSPNAx3NBHawwG1CecfwPpQJAuhBZSjnAA_1142_797.png 在js设计时,就在执行上下文中定义了一个outer指针,他指向当前函数执行上下文的外层是什么 当我们嵌套的函数越多,他就形成了一条词法作用域链 -->:

6015a4dc111a7e775ab7e5cac2c8e273.png 相信这样的解释一定让你能够清晰的明白词法作用域链到底是什么

三、闭包:词法作用域的延伸

什么是闭包?

闭包(Closure)是指一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。

形成闭包的两个关键条件:

  1. 函数嵌套(内部函数引用了外部函数的变量)
  2. 内部函数被返回或传递到外部,从而在外部被调用

示例 3:经典的闭包结构

function foo() {
  var myName = "极客时间";
  let test1 = 1;
  const test2 = 2;

  var innerBar = {
    getName: function () {
      console.log(test1);
      return myName;
    },
    setName: function (newName) {
      myName = newName;
    }
  };

  return innerBar; // 返回内部对象,使得内部函数可被外部访问
}

var bar = foo(); // foo 执行完毕,执行上下文出栈
bar.setName("极客邦");
bar.getName();
console.log(bar.getName()); // 输出 "极客邦"
关键点解析:
  • 垃圾回收失效?
    我们通常的认为,当一个函数执行完毕后,它就应该从函数的调用栈中弹出,从而不影响其他函数的继续执行,这也就是JS的垃圾回收机制。但在这段代码里,我们依旧能够访问到foo()中的变量,是的,这就是闭包霸道的力量!
  • 闭包:等等!我拒绝回收!
    这背后的原因是因为bar依然保留有对foo()函数的引用,而这个引用就是闭包,它会阻止foo()执行上下文的完全销毁,保留下需要被引用的变量

91647c77a8494223e939c23c77bb79ff.png

  • 闭包:我只拿走我需要的,你可以走了
    闭包的形成并不会阻塞程序的执行,它会让foo()函数执行上下文出栈(兄弟你可以放心走了,后面我抗着),但把自己需要的部分装进一个背包,而这个背包就是闭包,当下一个函数执行上下文入栈时不会影响整个调用栈。
    最后,一张图带你彻底认清闭包:

536a315a83aa48b870d03dd921b6c02a.png

闭包的本质:函数 + 它被定义时的词法环境。

总结

  • 词法作用域 :作用域由函数定义位置决定,静态不变
  • 词法作用域链 : 变量查找路径,从当前作用域逐层向外,通过outer指针"导航"
  • 闭包 :内部函数引用外部变量,并在外部被调用,形成对词法环境的“捕获”

理解这些机制,不仅能写出更健壮的代码,还能在面试中从容应对“闭包是什么?”这类经典问题。

记住:闭包不是魔法,它是词法作用域的自然结果。