一、V8 引擎工作原理

 JS 源码
   │
   ▼
 解析器 Parser
   │
   ▼
 AST 抽象语法树
   │
   ▼
 解释器 Ignition
   │
   ▼
 字节码 Bytecode
   │
   ├─ 不是热点代码 → 直接执行字节码
   │
   └─ 是热点代码 → 优化编译器 TurboFan
                     │
                     ▼
                  高效机器码
                     │
           (某些情况下) └→ 反优化回到字节码

简单来说:源码 → AST → 字节码 → 热点代码会被优化成机器码,非热点直接执行字节码,优化后的机器码如果条件不满足还会退回到字节码重新执行。

预解析(Preparse)

  • 立即执行的代码 → 完整解析和编译

  • 函数内部嵌套的函数(暂时不会执行) → 只进行预解析,仅检查语法错误,不生成完整 AST

  • 只有当内层函数真正被调用时,才会完整解析

 function outer() {
     // outer 被完整解析
     function inner() {
         // inner 初始只做预解析
         console.log('Hello');
     }
     inner(); // 这里才触发 inner 的完整解析
 }

 因为大部分页面中的函数并不会在首屏就被调用,这种设计能显著减少初始加载时间。

二、全局代码执行过程

JavaScript 代码执行分两个阶段:编译阶段执行阶段,整体过程如下图所示:

 编译阶段:
   创建全局对象 GO
     │
     ▼
   扫描 var 和 function 声明
     │
     ├─ var 变量 → 提升为 undefined
     │
     └─ function 声明 → 直接提升为函数对象
 ​
         ↓(编译阶段结束后进入执行阶段)
 ​
 执行阶段:
   逐行执行代码
     │
     ├─ 对变量进行实际赋值
     │
     └─ 调用函数时 → 创建函数执行上下文

关于 VO、AO、GO 的简单说明

  • 变量对象 (VO, Variable Object):每个执行上下文都有一个变量对象,用来存储该上下文中的变量、函数声明等。编译阶段创建的就是 VO。

  • 全局对象 (GO, Global Object):全局执行上下文的 VO 就是全局对象。在浏览器中是 window,在 Node.js 中是 global

  • 活动对象 (AO, Activation Object):当函数执行时,它的变量对象被激活,称为活动对象。AO 可以理解为函数执行上下文中“活”的 VO,包含参数、局部变量等。

 console.log(window.window.window === window); // true
 // GO 的 window 属性指向 GO 自身,形成循环引用

三、作用域链与变量查找

3.1

 function foo() {
     var a = b = 10;
     // 等价于:
     // var a = 10;   ← a 被 var 声明,是局部变量
     // b = 10;       ← b 无任何声明关键字,成为全局变量
 }
 ​
 foo();
 console.log(a); // ReferenceError: a is not defined
 console.log(b); // 10  ← b 泄漏到了全局!

 b = 10 前面没有 var / let / const,JS 引擎沿着作用域链向上一直找到全局都没找到,最终在全局对象上创建了 b 属性。

3.2 作用域链查找规则

当查找一个变量时,JavaScript 引擎会:

  1. 从当前作用域开始查找;

  2. 如果找不到,就向上一层作用域查找;

  3. 一直向上,直到全局作用域;

  4. 如果全局也没有:

    • 非严格模式下,会在全局自动创建一个同名变量(容易产生意外全局变量);

    • 严格模式下,抛出 ReferenceError

四、内存管理

GC 垃圾回收算法

主要有两种垃圾回收算法:

  • 引用计数:每个对象记录被引用的次数,引用次数为 0 时立即回收。但缺点是无法处理循环引用(两个对象互相引用,即使不再被使用也无法回收)。

  • 标记清除(Mark-Sweep):从根对象(如 window / global)出发,遍历并标记所有能到达的对象,然后清除未被标记的对象。即使对象间存在循环引用,只要从根出发不可达,也会被回收。

V8 使用的是标记清除算法,因此可以很好地解决循环引用问题。

 两个互相引用的对象,只要从根对象出发无法被访问到,都会被标记清除算法回收——这正是它优于引用计数的地方。

五、闭包

5.1 什么是闭包?

闭包 = 函数 + 该函数能够访问的外部自由变量。 JavaScript 中两种常见产生方式:

 // 方式一:函数作为返回值
 function createCounter() {
     let count = 0;          // 自由变量
     return function() {
         return ++count;     // 内部函数引用外部变量
     };
 }
 const counter = createCounter();
 counter(); // 1
 counter(); // 2
 ​
 // 方式二:函数作为参数
 function log(fn) { console.log(fn()); }
 function outer() {
     const msg = 'Hello Closure';
     log(function() { return msg; });  // 保留对 msg 的访问
 }
 outer(); // 'Hello Closure'

5.2 闭包的内存泄漏问题

 function foo() {
     const largeData = new Array(10000000).fill('1'); // 大内存
     const name = 'hello';
     return function bar() {
         console.log(name); // 只用到了 name
     };
 }
 const fn = foo();
 fn();
 // foo 的整个活动对象(包含 largeData)可能无法被回收

闭包会导致外层函数的活动对象无法被正常回收,因为内层函数通过 [[Scope]] 属性始终保持着对它的引用。在上面的例子中,bar 只使用了 name,但整个 foo 的作用域都被保留了下来,包括那个巨大的 largeData 数组,从而造成内存浪费。

现代 V8 会分析闭包实际使用了哪些外部变量,未引用的变量会被回收。但为了代码健壮性,建议手动释放

5.3 解决内存泄漏

 fn = null;   // 释放对 bar 的引用
 foo = null;  // 释放对 foo 的引用,整个作用域即可被 GC 回收

V8 中的小整数(Smi)只占 4 字节,利用指针标记技术直接编码整数值,非常高效。

六、this 指向

核心原则:this 的指向与函数在何处定义无关,只与函数如何被调用有关。

6.1 全局作用域下的 this

 console.log(this); // 浏览器: window   Node.js: {}(module.exports)

6.2 四大绑定规则及优先级

JavaScript 中 this 的指向由以下规则决定,优先级从高到低为:

  1. new 绑定(最高) 当函数通过 new 调用时,this 指向新创建的那个对象。

     function Person(name) { this.name = name; }
     const p = new Person('V8'); // this → 新对象 p
  2. 显式绑定 使用 callapplybind 手动指定 this 指向的对象。

     function greet() { console.log(this.name); }
     greet.call({ name: 'JS' }); // this → { name: 'JS' }
  3. 隐式绑定 当函数以 obj.method() 的形式调用时,this 指向调用该方法的对象。

     const obj = {
         bar() { console.log(this); }
     };
     obj.bar(); // this → obj
  4. 默认绑定(最低) 普通函数调用,非严格模式下 this 指向全局对象(浏览器中是 window),严格模式下 thisundefined

     function foo() { console.log(this); }
     foo(); // 非严格:window,严格:undefined

6.3 常见内置函数的 this

 // setTimeout 普通函数指向 window
 setTimeout(function() { console.log(this); }, 100); // window
 ​
 // 事件监听指向触发的 DOM 元素
 button.addEventListener('click', function() {
     console.log(this); // button 元素
 });
 ​
 // forEach / map 等可传入第二个参数绑定 this
 [1, 2, 3].forEach(function(item) {
     console.log(item * this.multiplier);
 }, { multiplier: 2 }); // 2, 4, 6

6.4 this规则之外

  1. 忽略显示绑定:显示绑定传入null/undefined,自动将this绑定成全局对象。

  2. 间接函数引用 : eg.(obj2.bar=obj1.foo)()会将其看作一个独立函数调用。

更多推荐