《你不知道的JavaScript》学习笔记(第一、二章)对JavaScript的进一步的理解

181 阅读4分钟

一、JavaScript 不是“纯解释型语言”

学习收获

在学习之前,我以为 JS 是解释型脚本语言。
但书中指出:JavaScript 实际上也会在执行前经历“编译阶段”!

核心知识点

  1. 引擎执行的三大步骤:

    • 词法分析(Tokenizing) :将源码拆分为一个个语法单元(token)。
    • 语法解析(Parsing) :将 token 组合成一棵语法树(AST)。
    • 代码生成(Code Generation) :把 AST 转化为机器可以执行的指令。
  2. JavaScript 的编译是 即时完成的(Just-In-Time,JIT) ,在代码执行前的几微秒内完成。

  3. 因此,像 变量提升作用域解析 等现象,其实都发生在编译阶段,而非运行时。


二、作用域的三位主角

三大角色

角色职责
引擎(Engine)负责代码的执行
编译器(Compiler)在代码运行前进行编译
作用域(Scope)负责收集变量、控制访问权限

变量创建过程

var a = 2;

编译阶段:

编译器告诉作用域:“我要声明一个变量 a。”

执行阶段:

引擎在作用域中找到变量 a,并赋值 2


三、LHS 与 RHS 查询

在作用域中查找变量时,存在两种模式:

类型查找目的示例
LHS(Left-hand Side)查找变量容器,用于赋值a = 2
RHS(Right-hand Side)查找变量的值,用于取值console.log(a)

示例

var a = 2;        // LHS 查询:找到 a,用来放入值 2
console.log(a);   // RHS 查询:读取 a 的值进行打印
  • RHS 查询失败 → 抛出 ReferenceError
  • LHS 查询失败(非严格模式) → 自动在全局创建变量( 危险)

四、词法作用域(Lexical Scope)

学习收获

JavaScript 的作用域 不是运行时决定的,而是在定义阶段确定的

核心理解

  • JS 使用 词法作用域(Lexical Scope)
  • 函数在定义时就决定了能访问哪些变量。
  • 调用位置不会改变作用域链。

示例

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  bar();
}
foo(); // 输出 2

函数 bar 在定义时就“记住”了外部的作用域,因此能访问变量 a
这正是 闭包 的理论基础。


五、作用域链与嵌套

每当定义一个函数,都会创建一个新的作用域“气泡”。
内层作用域可以访问外层作用域的变量,查找遵循由内向外、就近优先原则。

function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(a + b);
  }
  bar();
}
foo(); // 输出 3

六、同名标识符与遮蔽效应(Shadowing)

定义

多层嵌套的作用域中可以定义同名变量,内层变量会遮蔽外层同名变量。

示例

var a = 1;

function foo() {
  var a = 2; // 内层同名变量
  console.log(a);
}

foo();        // 输出 2
console.log(a); // 输出 1

查找在找到第一个匹配标识符时立即停止。
因此,内层变量会“遮蔽”外层变量(Shadowing)。


延伸例子

var a = "global";

function outer() {
  var a = "outer";
  function inner() {
    var a = "inner";
    console.log(a);
  }
  inner();        // inner
  console.log(a); // outer
}
outer();
console.log(a);   // global

七、欺骗词法作用域(Cheating Lexical Scope)

JS 的词法作用域在定义时固定,但 eval()with() 可以在运行时篡改作用域。


1. eval() 的作用

eval() 能动态执行字符串形式的代码。

function foo(str) {
  eval(str);
  console.log(a);
}

foo("var a = 42;"); // 输出 42

在执行时,eval() 会动态向当前作用域注入变量。

  • 在严格模式下('use strict'),eval() 创建独立作用域,不影响外层变量。

  • 不论是否严格模式,都会:

    • 破坏引擎的静态作用域分析;
    • 影响性能;
    • 带来安全隐患。

2. with() 的作用

with() 可以临时将对象添加到作用域链前端。

var obj = { a: 1, b: 2 };

with (obj) {
  a = 3; // 修改 obj.a
  c = 4; // 若 c 不在 obj 中,则创建全局变量 c
}

console.log(obj.a); // 3
console.log(c);     // 4

问题:

  • 引擎无法确定变量属于对象属性还是外部作用域;
  • 编译器无法进行静态分析;
  • 严格模式下,with完全禁止

八、欺骗词法作用域导致性能下降的原因

原因说明
无法在编译阶段确定变量绑定关系引擎无法预先建立作用域映射,只能运行时动态查找
编译优化失效JS 引擎优化器无法进行作用域内联或常量折叠
查找成本上升每次访问变量都要重新判断归属
破坏词法一致性代码可读性与可预测性下降,调试困难

eval()with() 是引擎优化的“天敌”, 在现代开发中应完全避免使用。


通过学习《你不知道的 JavaScript》第一、二章,我获得了对 JavaScript 运行机制更深层的理解:

  1. JavaScript 并非单纯的解释型语言,而是会先经历编译过程(词法分析 → 语法解析 → 代码生成),这解释了“变量提升”等机制的根本原因。
  2. 词法作用域是 JS 的灵魂:变量的可访问性在函数定义阶段就已决定,而非运行时动态变化。
  3. 作用域链与**查找方式(LHS / RHS)**揭示了变量是如何被定位、赋值、取值的。
  4. 遮蔽效应提醒我们命名规范的重要性,避免变量名冲突带来的混乱。
  5. eval() 与 with() 是“欺骗词法”的陷阱:它们让引擎无法在编译阶段优化代码,既影响性能,又破坏可维护性。