JavaScript 作用域链与闭包:底层机制解析

86 阅读5分钟

JavaScript 作用域链与闭包:底层机制解析

JavaScript 作为一门动态编程语言,其变量查找、函数执行等底层机制依赖于作用域链、词法作用域和闭包等核心概念。理解这些机制不仅能帮助我们写出更健壮的代码,更能深入掌握 JavaScript 的运行原理。本文将结合具体代码示例,详细解析这些核心概念。

一、JavaScript 底层运行基础

在 V8 引擎中,JavaScript 的执行过程主要依赖两大核心机制:执行上下文调用栈

  • 执行上下文:可以理解为函数执行时的 "环境",包含了函数运行所需的变量、函数声明、this 指向等信息。全局环境会形成全局执行上下文,每个函数调用时会创建函数执行上下文。
  • 调用栈:用于管理执行上下文的 "栈结构"。函数调用时,其执行上下文入栈;函数执行结束后,执行上下文出栈,释放资源。

作用域则定义了变量的查找范围和生命周期,是 JavaScript 中变量访问规则的核心。

二、词法作用域:作用域的静态规则

词法作用域(也叫静态作用域)是 JavaScript 作用域的核心特性,其核心规则是:作用域由函数声明时的位置决定,而非调用时的位置。这意味着函数的变量查找范围在代码编译阶段就已确定,与函数何时、何地被调用无关。

代码示例 1:词法作用域的体现

// scope_chain/1.js
function bar() {
    console.log(myName); // 查找 myName
}
function foo() {
    var myName = '极客邦'; // foo 函数内的 myName
    bar(); // 调用 bar 函数
}
var myName = '极客时间'; // 全局 myName
foo(); // 输出:"极客时间"

代码解释

  • bar 函数在全局作用域中声明,因此其作用域链为:bar 函数作用域 -> 全局作用域
  • 当 bar 中打印 myName 时,会先在自身作用域查找,未找到则沿作用域链向上到全局作用域,找到全局的 myName = '极客时间'
  • 虽然 bar 在 foo 中被调用,但 foo 中的 myName 不在 bar 的作用域链上,因此不会被访问到。这正是词法作用域 "声明时确定范围" 的体现。

image.png

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

作用域链是由多个作用域组成的链式结构,它定义了变量查找的顺序:当前作用域找不到变量时,会沿作用域链向上查找,直到全局作用域

作用域链的形成规则:

  1. 函数自身的作用域为链的起点;
  2. 向上依次是外层函数的作用域(若有);
  3. 最终指向全局作用域。

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

// scope_chain/2.js
function bar () {
  var myName = "极客世界"; // bar 函数作用域的 myName
  let test1 = 100; // bar 函数作用域的 test1
  if (1) {
    let myName = "Chrome 浏览器" // if 块级作用域的 myName
    console.log(test) // 查找 test
  }
}
function foo() {
  var myName = "极客邦"; // foo 函数作用域的 myName
  let test = 2; // foo 函数作用域的 test
  {
    let test = 3; // 块级作用域的 test
    bar() // 调用 bar
  }
}
var myName = "极客时间"; // 全局 myName
let myAge = 10; // 全局 myAge
let test = 1; // 全局 test
foo(); // 输出:1

代码解释

  • bar 函数中 if 块内的 console.log(test) 需要查找 test 变量:

    1. 先在 if 块级作用域查找,未找到;
    2. 向上到 bar 函数作用域,仍未找到;
    3. 继续向上到全局作用域,找到 let test = 1,因此输出 1
  • 块级作用域(由 let/const 声明变量产生)会影响作用域链的结构,使变量查找范围更精确。

image.png

四、闭包:作用域链的特殊应用

闭包是 JavaScript 中基于词法作用域的高级特性,其核心是:内层函数引用外层函数作用域中的变量,且内层函数在外部可被访问时,外层函数的变量不会被垃圾回收

闭包的形成条件:

  1. 函数嵌套(内层函数定义在外侧函数内部);
  2. 内层函数引用外层函数的变量;
  3. 内层函数在外部可被访问(如通过 return 暴露)。

代码示例 3:闭包的实际应用

// scope_chain/3.js
function foo() {
    var myName = '极客时间' // foo 作用域的 myName
    let test1 = 1 // foo 作用域的 test1
    var innerBar = {
        getName:function(){
            console.log(test1); // 引用 foo 中的 test1
            return myName; // 引用 foo 中的 myName
        },
        setName:function(newName){
            myName = newName // 修改 foo 中的 myName
        }
    }
    return innerBar; // 暴露 innerBar 到外部
}

var bar = foo()  // foo 执行完出栈,但 innerBar 被外部引用
bar.setName('极客邦') // 调用 setName,修改 foo 中的 myName
bar.getName() // 输出:1(test1 的值)
console.log(bar.getName()); // 输出:1 "极客邦"(修改后的 myName)

代码解释

  • foo 函数执行时创建执行上下文,内部定义了 myNametest1 和 innerBar 对象(包含 getName 和 setName 方法)。
  • getName 和 setName 方法引用了 foo 作用域中的 myName 和 test1,且通过 return innerBar 被外部的 bar 变量引用。
  • 当 foo 执行完出栈后,其执行上下文本应被销毁,但由于 getName 和 setName 仍引用 foo 作用域的变量,这些变量会被保留(形成 "闭包背包"),供内层函数后续使用。
  • 因此,bar.setName 能修改 myNamebar.getName 能访问 test1 和更新后的 myName,这就是闭包的核心作用:保留外层函数的变量供内层函数使用

image.png

总结

JavaScript 的作用域链和闭包是基于词法作用域的核心机制:

  • 词法作用域决定了作用域链的静态结构(声明时确定);
  • 作用域链定义了变量查找的路径(从当前作用域向上到全局);
  • 闭包则是作用域链的特殊应用,通过保留外层函数变量,实现了变量的 "持久化" 访问。