你不知道的Javascript(上卷) | 第一章:作用域是什么

前言:为什么我们需要了解作用域?

作为一名前端开发者,你可能每天都在与变量、函数和作用域打交道。但你是否真正理解当你在控制台写下var a = 2;时,JavaScript引擎背后都做了些什么?今天,就让我们一起揭开JavaScript作用域的神秘面纱!

一、JavaScript的"编译"过程

1.1 传统编译语言 vs JavaScript

传统编译语言(如C++)的编译过程通常发生在代码执行前的几小时甚至几天。但JavaScript的编译过程非常特殊------它发生在代码执行前的几微秒(有时甚至更短)!

有趣的事实:JavaScript引擎其实没有时间进行传统意义上的优化编译,所以它使用了JIT(Just-In-Time)即时编译技术,可以在执行过程中进行编译甚至重新编译。

1.2 编译的三个阶段

让我们以var a = 2;为例,看看JavaScript引擎如何处理这行简单的代码:

  1. 分词/词法分析:把代码字符串分解成有意义的代码块(词法单元)

    • vara=2;被识别为独立的词法单元
  2. 解析/语法分析:将词法单元流转换为抽象语法树(AST)

    • 这棵树会表示出代码的语法结构
  3. 代码生成:将AST转换为可执行代码

    • 这里会创建变量a(分配内存等),并将值2存储在a中

思考题:为什么JavaScript要这么着急地编译和执行代码?这与它的设计初衷(作为浏览器脚本语言)有什么关系?

二、理解作用域的三位主角

想象一下,当JavaScript执行代码时,有三个重要角色在协同工作:

2.1 引擎

  • 负责整个JavaScript程序的编译和执行过程
  • 就像汽车的发动机,驱动整个程序运行

2.2 编译器

  • 负责语法分析和代码生成
  • 就像汽车的设计师和装配工人

2.3 作用域

  • 负责收集并维护所有声明的变量
  • 实施严格的访问规则
  • 就像交通警察,确保每个变量都在自己的"车道"上行驶

现实比喻:你可以把这三个角色想象成一家餐厅:

  • 编译器是厨师(准备食材)
  • 引擎是服务员(上菜执行)
  • 作用域是餐厅经理(确保每道菜送到正确的餐桌)

三、变量声明的幕后故事

让我们深入分析var a = 2;这行代码的执行过程:

3.1 编译阶段(声明变量)

  1. 编译器询问作用域:"当前作用域中是否已经有名为a的变量?"
    • 如果有:忽略声明,继续编译
    • 如果没有:要求作用域声明新变量a

重要概念:这就是变量提升的本质!变量声明在代码执行前就已经处理好了。

3.2 执行阶段(赋值操作)

  1. 引擎询问作用域:"当前作用域中是否有名为a的变量?"
    • 如果有:使用这个变量
    • 如果没有:向外层作用域查找
    • 如果最终都没找到:抛出异常

小测验

javascript 复制代码
function foo() {
    console.log(a);  // 输出什么?
    var a = 2;
}
foo();

答案:undefined,因为变量a被提升但还未赋值

四、LHS和RHS查询:引擎如何查找变量

当引擎需要变量时,它会进行两种查询:

4.1 LHS(Left-Hand Side)查询

  • 目的是找到变量的容器本身
  • 发生在赋值操作左侧时
  • 例如:a = 2;(查找a以赋值)

4.2 RHS(Right-Hand Side)查询

  • 目的是获取变量的值
  • 发生在赋值操作右侧或函数调用时
  • 例如:console.log(a);(查找a的值)

有趣示例

javascript 复制代码
function foo(a) {    // 对a进行LHS查询(隐式赋值)
    console.log(a);  // 对a进行RHS查询,对console进行RHS查询(找到console的定义)
}
foo(2);             // 对foo进行RHS查询
// LHS 查询:1 次(函数参数 `a` 的隐式赋值)
// RHS 查询:3 次(查找 `foo`、`console` 和 `a` 的值)

4.3 查询失败会发生什么?

RHS查询失败

  • 抛出ReferenceError异常
  • "你找的变量根本不存在"

LHS查询失败

  • 非严格模式:自动创建全局变量(危险!)
  • 严格模式:抛出ReferenceError

TypeError

  • 当RHS查询成功但操作不合法时
  • 例如:对非函数值进行函数调用 null()

记忆技巧

  • ReferenceError:作用域中找不到(侦查失败)
  • TypeError:找到了但操作不合法(执行失败)

五、作用域嵌套与作用域链

JavaScript的作用域是嵌套的,就像俄罗斯套娃:

5.1 作用域嵌套规则

  • 当一个块或函数嵌套在另一个块或函数中时,就形成了作用域嵌套
  • 引擎从当前作用域开始查找变量
  • 如果找不到,会逐级向外层作用域查找
  • 直到找到变量或到达全局作用域(仍未找到则报错)

5.2 作用域链示例

javascript 复制代码
function outer() {
    var a = "outer";
    
    function inner() {
        var b = "inner";
        console.log(a); // 先在inner作用域找a,找不到再到outer作用域找
        console.log(b); // 在inner作用域找到b
    }
    
    inner();
}

outer();

性能提示:变量查找层级越深,性能开销越大。因此应尽量减少全局变量的使用!

六、ES6带来的变化

虽然本章主要讨论var,但ES6引入了let/const,它们的行为有所不同:

  1. 块级作用域:let/const具有块级作用域,而var只有函数作用域
  2. 暂时性死区(TDZ):在声明前访问let/const变量会报错
  3. 禁止重复声明:let/const不允许在同一作用域重复声明

示例对比

javascript 复制代码
console.log(a); // undefined (变量提升)
var a = 2;

console.log(b); // ReferenceError (TDZ)
let b = 3;

七、常见面试题解析

7.1 题目一

javascript 复制代码
var a = 1;
function test() {
    console.log(a);
    var a = 2;
}
test(); // 输出什么?

答案undefined(函数内a被提升)

7.2 题目二

javascript 复制代码
function foo() {
    a = 1;    // 这个a是什么?
}
foo();
console.log(a); // 输出什么?

答案1(非严格模式下,未声明的赋值会创建全局变量) 或 抛出 ReferenceError(严格模式下,未声明的赋值会直接抛出异常)

7.3 题目三

javascript 复制代码
function outer() {
    var a = 1;
    function inner() {
        console.log(a);
        var a = 2;
    }
    inner();
    console.log(a);
}
outer();

答案 :先输出undefined,然后输出1

八、最佳实践

  1. 始终声明变量:避免隐式全局变量
  2. 使用严格模式"use strict";可以避免很多陷阱
  3. 合理组织作用域:避免过深的嵌套
  4. 优先使用const:默认使用const,需要改变时用let
  5. 变量集中声明:提升代码可读性

结语

理解作用域是掌握JavaScript的关键一步。通过今天的分享,希望你不仅知道了var a = 2;背后的故事,更能理解JavaScript引擎、编译器和作用域如何协同工作。记住,好的JavaScript开发者不仅要会写代码,更要理解代码是如何被执行的!

最后的小挑战:你能解释下面代码的执行过程吗?

javascript 复制代码
function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2);

欢迎在评论区分享你的答案和见解!如果你觉得这篇文章有帮助,别忘了点赞收藏哦~

相关推荐
anyup1 分钟前
10000+ 个点位轻松展示,使用 Leaflet 实现地图海量标记点聚类
前端·数据可视化·cursor
林太白3 分钟前
Rust认识安装
前端·后端·rust
掘金酱4 分钟前
🔥 稀土掘金 x Trae 夏日寻宝之旅火热进行ing:做任务赢大疆pocket3、Apple watch等丰富大礼
前端·后端·trae
1024小神4 分钟前
tauri项目添加多文件下载功能,并支持下载进度回调显示在前端页面上
前端·javascript
Ace_31750887765 分钟前
义乌购拍立淘API接入指南
前端
不想说话的麋鹿11 分钟前
《NestJS 实战:RBAC 系统管理模块开发 (四)》:用户绑定
前端·后端·全栈
我是谁谁24 分钟前
JavaScript 中的 Map、WeakMap、Set 详解
前端
laperter35 分钟前
vue3项目第三篇
前端
呆呆的心37 分钟前
深入剖析 JavaScript 数据类型与 Symbol 类型的独特魅力😃
前端·javascript·面试
嘉小华39 分钟前
Kotlin委托机制详解
前端