你真的懂作用域吗?从编译原理角度深度 JS 的作用域

前端进阶系列 · JavaScript 核心机制深度解析

20 篇文章,从编译原理到引擎内部,覆盖作用域与闭包、this 与原型链、类型与强制转换、异步编程模型、ES6+ 进阶特性五大模块。

写在前面

做前端开发快八年了。说实话,有很长一段时间,我觉得自己什么都会一点------React 能写,Vue 能写,Node 也能写。但真要让我说清楚"闭包到底是什么"或者"Event Loop 完整执行顺序",话到嘴边又觉得讲不透。

我相信很多人跟我有同样的感觉:瓶颈期。什么都能干,但深入一步就有点虚。

这个系列就是我对这些年积累的系统复盘。从 JavaScript 最底层的概念出发------作用域、原型、类型、异步------把那些"似懂非懂"的东西彻底掰开讲清楚。先做 JavaScript 系列,后面还会有 CSS 和浏览器相关的内容。

希望这个系列的总结也能帮你突破那个瓶颈。

JavaScript 是一门编译型语言

你有没有遇到过这种情况------变量明明写在代码里,运行时报了 ReferenceError?或者 console.log 出来的值跟你预期的是两回事?这些令人抓狂的 bug,根源往往只有一个:你对作用域的理解还停留在表面。要真正理解作用域,得从 JS 代码执行之前说起。

你可能听过这句话:"JavaScript 是解释型语言"。这其实不准确。所有现代 JavaScript 引擎(V8、SpiderMonkey、JavaScriptCore)都采用了 JIT(Just-In-Time)编译,整个流程如下:

graph LR A[源代码] --> B[词法分析 Parsing] B --> C[AST 抽象语法树] C --> D[字节码 Bytecode] D --> E[JIT 编译] E --> F[优化后的机器码] F --> G[执行]
  1. 词法分析(Parsing) :引擎将字符串源码分解为有意义的 token(vara=2;)。
  2. 语法分析:将 token 流转换为 AST(Abstract Syntax Tree),一棵描述程序结构的树。
  3. 生成字节码:编译器将 AST 转为中间表示------字节码。
  4. JIT 编译:在运行时,引擎会识别"热点代码"(hot code),将其编译为高度优化的机器码。

JavaScript 实际上是先编译,再执行的。搞懂了编译阶段,作用域也就清楚了。

编译阶段发生了什么

当引擎执行 var a = 2; 这条语句时,实际上会发生两次处理:

javascript 复制代码
// 你看到的是一行代码,但引擎处理了两个步骤:
var a = 2;

// 步骤1(编译阶段):编译器在当前作用域中声明变量 a
// 步骤2(执行阶段):引擎在当前作用域中查找变量 a,并赋值为 2

这个过程里,三个角色分工明确:

角色 职责
引擎(Engine) 负责整个程序的编译和执行
编译器(Compiler) 负责语法分析和代码生成
作用域(Scope) 负责维护变量查找规则,管理标识符的可访问性

编译器负责声明,引擎负责执行,作用域负责管理这些声明的关系。

词法作用域:在编译时就已确定

JavaScript 采用的是 词法作用域(Lexical Scope) ,这意味着作用域在编译阶段就已经被确定,与运行时无关。作用域是由你写代码时变量和函数声明的位置决定的。

javascript 复制代码
var globalVar = 'I am global';

function outer() {
    var outerVar = 'I am from outer';

    function inner() {
        var innerVar = 'I am from inner';
        console.log(innerVar);   // 'I am from inner'
        console.log(outerVar);   // 'I am from outer'
        console.log(globalVar);  // 'I am global'
    }

    inner();
}

outer();

与之相对的是 动态作用域(Dynamic Scope) ,它取决于函数在哪里被调用,而不是在哪里被定义。某些语言(如早期的 Perl)使用动态作用域,但 JavaScript 始终使用词法作用域。记住这一点就够了:只看函数定义的位置,就能确定它的作用域

作用域链:从内向外逐级查找

当引擎需要查找一个变量时,它会沿着 作用域链(Scope Chain) 从内向外逐级搜索:

graph TD G["全局作用域 globalVar"] --> O["outer 函数作用域 outerVar"] O --> I["inner 函数作用域 innerVar"] style G fill:#e1f5fe,stroke:#01579b style O fill:#fff3e0,stroke:#e65100 style I fill:#e8f5e9,stroke:#1b5e20

查找规则很简单:

  • 先在当前作用域找
  • 找不到就去外层作用域找
  • 一直找到全局作用域
  • 如果全局都没找到,非严格模式下会创建一个全局变量(严格模式报 ReferenceError
javascript 复制代码
var name = 'Global';

function showName() {
    var name = 'Local';
    console.log(name); // 'Local',先在当前作用域找到就停止
}

showName();
console.log(name);     // 'Global',不受内部影响

这就是"遮蔽效应(Shadowing)":内部作用域的变量"遮蔽"了外部同名的变量。

两个不该碰的黑魔法:eval() 和 with()

JavaScript 里有两个"后门"可以在运行时篡改词法作用域,但你不该用:

eval() :将字符串当作代码执行,可以动态声明变量:

javascript 复制代码
function badEval(str) {
    eval(str);
    console.log(b); // 42,eval 在运行时在当前作用域创建了变量 b
}

badEval('var b = 42;');
// console.log(b); // 报错,b 在 badEval 的作用域内

with() :将一个对象处理为一个独立的作用域:

javascript 复制代码
var obj = { a: 1, b: 2 };

with (obj) {
    a = 3;  // 修改 obj.a
    b = 4;  // 修改 obj.b
    c = 5;  // 小心!obj 没有 c,所以 c 泄漏到了全局!
}

console.log(obj.c);  // undefined
console.log(c);      // 5,泄漏到全局作用域!

为什么不该碰?

第一,引擎在编译阶段做的优化全废掉了;

第二,性能断崖式下降;

第三,严格模式下 with 直接禁掉,eval 也没法动外部作用域。

函数作用域 vs 全局作用域

在 ES6 之前,JavaScript 只有两种作用域:

javascript 复制代码
// 全局作用域:在任何函数外声明的变量
var global = 'I live everywhere';

function myFunc() {
    // 函数作用域:函数内部声明的变量只在此函数内可访问
    var local = 'I live only here';

    if (true) {
        var stillLocal = 'I am also local to the function'; // 注意:不是块级作用域!
    }

    console.log(stillLocal); // 可以访问,var 会忽略 {} 块
    console.log(global);     // 可以访问,作用域链向上查找
}

myFunc();
// console.log(local);       // ReferenceError

这就是 var 的"函数级作用域"特性------只有函数能创建新的作用域,ifforwhile 等代码块对 var 无效。这一特性引发了很多令人困惑的 bug,也直接催生了 ES6 的 letconst

总结

JavaScript 是先编译后执行的,作用域在编译阶段就已确定------词法作用域 。引擎、编译器、作用域三者协作完成变量的声明和赋值,变量查找沿着作用域链 从内向外进行。eval()with() 是两个不该碰的后门,代价远高于便利。而 var 只有函数作用域、没有块级作用域,这正是下一篇要解决的问题。

本文原创首发于公众号【我做开发那些年】,现同步转载至本平台。

声明:如需转载本文至其他平台,请注明文章来源及公众号信息,感谢您对原创内容的尊重与支持!

相关推荐
Darling噜啦啦2 小时前
二叉树与递归算法实战:从树结构到 LeetCode 爬楼梯,一文吃透前端数据结构与递归思维
前端·javascript·数据结构
星栈2 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
前端·rust
Aolith2 小时前
React 路由守卫:我用一个组件替代了 Vue 的 beforeEach
前端·react.js
Daybreak2 小时前
从 PDD、DDD、SDD 到 TDD:我是如何用一套 Agent 工程方法论推进 My-Notion 的
前端
HjhIron2 小时前
从零实现一个待办事项应用:前端必学的Ajax与Node.js实战
前端·后端
yingyima2 小时前
JavaScript 正则表达式:从零开始的实战对比
前端
Sammyyyyy3 小时前
月之暗面 Kimi Code 0.4.0 发布,终端 AI 编码助手全面采用 TypeScript,实现毫秒级启动
前端·javascript·人工智能·ai·typescript·servbay
范什么特西3 小时前
配置文件xml和properties
xml·前端
jnene3 小时前
html 时间、价格筛选样式处理
前端·css·html