🧠JavaScript 是一门基于词法作用域(Lexical Scope)的语言,其变量查找、函数执行、内存管理等核心机制都依赖于 作用域链(Scope Chain) 和 闭包(Closure) 。本文将从底层原理出发,结合 V8 引擎的执行模型、执行上下文、调用栈、词法环境、变量提升(Hoisting)、块级作用域等多个维度,系统性地剖析 JavaScript 中作用域链的形成、查找规则、运行时行为及其在实际开发中的应用。
🔍 1. 作用域基础:什么是作用域?
作用域(Scope) 是指程序源代码中定义变量的区域,它决定了当前执行环境中哪些变量是可访问的。JavaScript 的作用域主要有以下三种:
- 全局作用域(Global Scope) :最外层的作用域,所有未在函数或块中声明的变量都属于全局作用域。
- 函数作用域(Function Scope) :由
function声明创建的作用域,使用var声明的变量具有函数作用域。 - 块级作用域(Block Scope) :ES6 引入,由
{}包裹的代码块构成,使用let和const声明的变量具有块级作用域。
⚠️ 注意:
var不支持块级作用域,即使写在if或for块中,也会被提升到函数或全局作用域顶部。
📜 2. 词法作用域 vs 动态作用域
JavaScript 采用的是 词法作用域(Lexical Scope) ,也称为 静态作用域(Static Scope) 。这意味着:
函数的作用域在其 定义时 就已确定,而不是在 调用时 决定。
✅ 示例对比(来自 1.js)
function bar() {
console.log(myName); // 输出 '极客时间'
}
function foo() {
var myName = '极客邦';
bar(); // 在 foo 中调用 bar
}
var myName = '极客时间';
foo();
尽管 bar() 是在 foo() 内部调用的,但由于 bar 定义在全局作用域中,它的作用域链为:
bar 的作用域 → 全局作用域
因此,myName 会从全局作用域中查找到 '极客时间',而非 foo 中的 '极客邦'。
这与 动态作用域(如 Bash 脚本)完全不同——动态作用域会在 调用栈 中向上查找变量,而 JavaScript 永远只看 函数定义的位置。
🧱 3. 执行上下文与调用栈
JavaScript 引擎(如 V8)在执行代码时,会通过 执行上下文(Execution Context) 来管理变量、作用域和 this。
执行上下文的生命周期
每个执行上下文分为两个阶段:
-
创建阶段(Creation Phase)
- 创建变量对象(Variable Object, VO)
- 建立作用域链(Scope Chain)
- 确定
this指向
-
执行阶段(Execution Phase)
- 变量赋值
- 函数调用
- 表达式求值
调用栈(Call Stack)
- 全局代码执行 → 创建 全局执行上下文,压入栈底。
- 调用函数 → 创建 函数执行上下文,压入栈顶。
- 函数返回 → 执行上下文弹出栈。
📌 最底部永远是全局执行上下文,函数执行上下文按调用顺序压栈。
🔗 4. 作用域链的本质
作用域链(Scope Chain) 是一个由多个 词法环境(Lexical Environment) 组成的链表,用于变量查找。
作用域链的构成
- 每个函数在 定义时 会捕获其所在环境的词法环境,并存储在内部属性
[[Scope]]中。 - 当函数被调用时,新的执行上下文会将其 变量对象(VO) 添加到
[[Scope]]链的前端,形成完整的作用域链。
例如:
var a = 1;
function outer() {
var b = 2;
function inner() {
var c = 3;
console.log(a, b, c); // 1, 2, 3
}
inner();
}
outer();
inner 的作用域链为:
inner 的 VO → outer 的 VO → 全局 VO
查找规则(自内向外)
- 在当前作用域查找变量;
- 若未找到,沿作用域链向上查找父级作用域;
- 直到全局作用域;
- 若仍未找到,抛出
ReferenceError。
🔄 作用域链是 静态的,在 编译阶段(词法分析) 就已确定,与运行时调用位置无关。
🎒 5. 闭包:作用域链的“背包”
闭包(Closure) 是指一个函数能够访问并操作其 词法作用域外部 的变量,即使该外部函数已经执行完毕。
闭包形成的条件(来自 readme.md)
- 函数嵌套;
- 内部函数被 返回 或 传递到外部;
- 内部函数 引用了外部函数的变量。
✅ 示例(来自 3.js)
function foo() {
var myName = '极客世界';
let test1 = 1;
const test2 = 2;
var innerBar = {
getName: function() {
console.log(test1); // 1
return myName;
},
setName: function(newName) {
myName = newName;
}
};
return innerBar; // 返回对象,内部方法形成闭包
}
var bar = foo(); // foo 执行完毕,执行上下文出栈
bar.setName('极客邦');
console.log(bar.getName()); // '极客邦'
虽然 foo() 已经执行结束,其执行上下文从调用栈弹出,但 getName 和 setName 仍然能访问 myName 和 test1,因为:
这些变量被“打包”进了一个 闭包背包(Closure Bag) ,随函数一起保留在内存中。
这个“背包”就是 外部函数的词法环境,通过作用域链维持对自由变量(Free Variables)的引用。
📦 自由变量:不是在当前函数中声明,但被当前函数使用的变量。
🧪 6. 实际案例深度分析
📌 案例一:变量查找路径(来自 2.js)
function bar() {
var myName = '极客世界';
var test1 = 100;
if (1) {
let myName = 'Chrome 浏览器';
console.log(test); // 输出 1
}
}
function foo() {
var myName = '极客邦';
let test = 2;
{
let test = 3;
bar(); // 调用 bar
}
}
var myName = '极客时间';
let myAge = 18;
let test = 1;
foo();
输出:1
分析过程:
bar()定义在全局作用域 → 其作用域链为:bar → 全局。bar内部没有声明test,于是沿作用域链查找。bar的父级是 全局作用域(不是foo!),所以查到let test = 1。- 输出
1。
❗ 关键点:
bar的作用域链 不包含foo,因为词法作用域只看定义位置。
📌 案例二:闭包与私有状态(来自 note.md)
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count
};
}
const counter = createCounter();
counter.increment(); // 1
counter.getCount(); // 1
count是私有变量,外部无法直接访问。increment和getCount通过闭包捕获count。- 每次调用
createCounter()都会创建 新的闭包环境,彼此独立。
⚙️ 7. V8 引擎底层机制补充
编译阶段 vs 执行阶段
-
编译阶段(词法分析) :
- 解析代码结构;
- 确定函数声明、变量声明;
- 构建作用域链(静态);
- 处理 变量提升(Hoisting) 。
-
执行阶段:
- 创建执行上下文;
- 压入调用栈;
- 执行代码,赋值、调用函数;
- 弹出上下文,可能触发垃圾回收(除非被闭包引用)。
变量提升(Hoisting)
var声明会被提升到函数或全局顶部(仅声明,不赋值);let/const也有提升,但处于 暂时性死区(TDZ) ,不可访问;- 函数声明整体提升(包括函数体)。
💡 变量提升的存在,正是因为作用域和变量对象在 编译阶段 就已构建完成。
🧩 8. 作用域链与块级作用域
ES6 引入 let/const 后,JavaScript 支持 块级作用域。
{
let x = 1;
const y = 2;
}
console.log(x); // ReferenceError
- 块级作用域由
{}创建; - 每个块都有自己的 词法环境;
- 作用域链会包含这些嵌套的块环境。
例如:
function demo() {
let a = 1;
if (true) {
let b = 2;
console.log(a); // 1(向上查找)
}
}
作用域链:if 块 → demo 函数 → 全局
🛠️ 9. 性能优化与最佳实践
✅ 优化策略
- 避免深层嵌套:作用域链越长,变量查找越慢;
- 缓存外层变量:
// 低效
function slow() {
for (let i = 0; i < 1e6; i++) {
console.log(document.title); // 每次遍历作用域链
}
}
// 高效
function fast() {
const title = document.title; // 缓存到局部
for (let i = 0; i < 1e6; i++) {
console.log(title);
}
}
✅ 现代 JS 最佳实践
- 优先使用
const/let; - 避免全局变量污染;
- 使用 模块模式 或 ES6 模块 封装逻辑;
- 利用闭包实现 私有状态;
- 箭头函数继承外层
this,简化上下文绑定。
🧠 10. 总结:作用域链的核心思想
- 作用域 是变量的可访问范围;
- 词法作用域 是静态的,由代码结构决定;
- 作用域链 是变量查找的路径,从内到外;
- 闭包 是作用域链在函数返回后的延续;
- 执行上下文 是运行时的作用域载体;
- 调用栈 管理上下文的生命周期;
- V8 引擎 在编译阶段就构建好作用域结构。
🌟 正如《你不知道的JS》所言:“JavaScript 的复杂性,很大程度上来自于它的作用域规则和闭包机制。理解了这些,你就掌握了 JavaScript 的精髓。”
掌握作用域链,不仅能写出更健壮、高效的代码,还能深入理解 React Hooks、Vue 响应式系统、模块打包器等现代前端技术的底层逻辑。
📚 参考资料
- 《JavaScript语言精粹》— Douglas Crockford
- 《你不知道的JS》— Kyle Simpson
- 《JavaScript高级程序设计》— Matt Frisbie
- ECMAScript 官方规范
- MDN Web Docs - JavaScript 作用域与闭包