普通视图

发现新文章,点击刷新页面。
昨天以前首页

Chrome偷藏了你的JS!V8引擎到底做了什么?

作者 牛奶
2026年4月7日 09:57

Chrome偷藏了你的JS!V8引擎到底做了什么?

你有没有想过:为什么 JavaScript 能"秒执行"?你写的 console.log('Hello') 到底经历了什么?从 Chrome 偷藏你的代码,到 V8 引擎对你的 JS 做了什么——今天全部揭秘!


原文地址

墨渊书肆/Chrome偷藏了你的JS!V8引擎到底做了什么?


V8 是什么?

JavaScript 引擎

浏览器能执行 JavaScript,全靠 JavaScript 引擎

常见的引擎有:

  • V8 — Chrome、Node.js、Deno 在用
  • SpiderMonkey — Firefox 在用
  • JavaScriptCore — Safari 在用
  • Chakra — 旧版 Edge 在用

V8 是 Google 开发的高性能引擎,用 C++ 编写,让 JS 执行速度可以媲美编译型语言。

V8 的工作流程

你写的 JS 代码,V8 要做的事情很简单:

JS代码 → 解析 → 编译 → 执行

但这中间,V8 做了大量偷跑优化

V8 架构演进

时代 架构 说明
早期 Full Codegen → Crankshaft 快速生成机器码,但维护困难
现在 Ignition → TurboFan 字节码+优化编译器,更高效
最新 Ignition + TurboFan + Sparkplug 新增无解释的 baseline JIT

代码是怎么跑起来的?

从 JS 到机器码

你写了一段代码:

function add(a, b) {
  return a + b;
}

console.log(add(1, 2));

V8 拿到这段代码后,经历了这些阶段:

1. 解析(Parser)
   
   把JS代码变成 AST(抽象语法树)
   
2. 解释(Ignition)
   
   编译成字节码,立即执行
   
3. 优化编译(TurboFan)
   
   热代码被编译成高效的机器码
   
4. 执行

Ignition — 解释器

字节码是什么?

V8 首先用 Ignition 解释器处理代码。

Ignition 会把你的 JS 代码编译成字节码——一种中间代码,比机器码容易生成,但比 JS 容易执行。

// 你写的 JS
function add(a, b) {
  return a + b;
}

对应的字节码(简化版):

# 字节码类似这样
LdaSmi [1]      # 加载小整数 1
StaA [0]        # 存到 [0] 位置(寄存器)
LdaSmi [2]      # 加载小整数 2
AddA [0]        # 加上 [0] 位置的数
Return           # 返回结果

为什么要转字节码?

直接执行 JS 转字节码再执行
每次都要重新解析 字节码更紧凑
无法优化 可以记录执行信息
启动慢 启动更快

Ignition 不只解释执行,还会记录信息——哪些函数被调用多次、参数类型是什么。这些信息给后续优化用。

Ignition 的执行反馈

function add(a, b) {
  return a + b;
}

add(1, 2);      // 第1次:记录类型
add(3, 4);      // 第2次:类型一致,继续记录
add("x", "y");  // 第3次:类型变了!记录下来

Ignition 维护一个 Feedback Vector(反馈向量),记录每段代码的类型信息。


TurboFan — 优化编译器

JIT 是什么?

JIT(Just-In-Time)= 即时编译。

不是提前编译好,而是一边执行一边编译。执行次数多的代码,会被更高效的机器码替代。

TurboFan 优化流程

TurboFan 不是直接生成最优机器码,而是层层优化:

字节码 + 执行反馈
   
Sea of Nodes(中间表示)
    优化 Pass 1: 类型推导
    优化 Pass 2: 内联
    优化 Pass 3: 环路优化
    优化 Pass 4: 寄存器分配
   
机器码

热代码检测

V8 有一套"热点检测"机制:

function add(a, b) {
  return a + b;
}

// 这个函数被调用了10000次
for (let i = 0; i < 10000; i++) {
  add(1, 2);
}
调用次数 < 1000

Ignition 解释器执行(字节码)

调用次数 > 1000

TurboFan 优化编译(机器码)

优化与反优化

TurboFan 很聪明,但也有"翻车"的时候:

function add(a, b) {
  return a + b;
}

// 前1000次调用,参数都是整数
for (let i = 0; i < 1000; i++) {
  add(1, 2);  // TurboFan 优化:整数加法
}

// 第1001次,参数变成字符串
add("hello", "world");  // 反优化!退回字节码

TurboFan 发现类型变了,会反优化(Deoptimization),退回字节码。

常见的优化场景

// ✅ 好优化:类型稳定
function length(arr) {
  return arr.length;  // 数组 length 是稳定的
}
length([1, 2, 3]);
length([4, 5]);

// ❌ 难优化:类型不稳定
function getX(obj) {
  return obj.x;  // obj 可能是任意类型
}
getX({ x: 1 });
getX("string");  // 字符串没有 x 属性!

隐藏类 — 快速属性访问

对象属性查找

JS 里访问对象属性很快,这要归功于隐藏类(Hidden Class),也叫 ShapesMaps

const person = { name: 'Tom', age: 18 };

V8 内部会为这个对象创建一个隐藏类:

隐藏类 HC0
├── name: offset 0
└── age: offset 1

属性访问加速原理

当你访问 person.name 时:

// 幕后发生的事情
person.name
  → 通过隐藏类 HC0
  → 直接定位到 offset 0
  → 拿到值 "Tom"

就像图书馆的书有固定编号(隐藏类),管理员知道每本书在哪个书架第几格。

隐藏类转换

对象属性改变时,会产生新的隐藏类:

const obj = { x: 1 };
//   ↓ 添加 y
obj.y = 2;
//   ↓ 修改 x
obj.x = 10;
HC0: { x: 1 }
   添加 y 属性
HC1: { x: 1, y: 2 }
   修改 x 属性(值变化不改变结构)
HC1(不变)

属性顺序很重要!

// 好:属性顺序一致 → 共享同一个隐藏类
const p1 = { x: 1, y: 2 };
const p2 = { x: 3, y: 4 };

// 差:属性顺序不一致 → 产生多个隐藏类
const p3 = { y: 1, x: 2 };  // 新建 HC1!

多态与全态

// 单态(Monomorphic):一种隐藏类,最快
function getX(obj) { return obj.x; }
getX({ x: 1 });      // HC0
getX({ x: 2 });      // 还是 HC0,命中缓存

// 多态(Polymorphic):2-4种隐藏类,较慢
function getX(obj) { return obj.x; }
getX({ x: 1, a: 0 });    // HC0
getX({ x: 2, b: 0 });    // HC1

// 全态(Megamorphic):5+种隐藏类,最慢
function getX(obj) { return obj.x; }
getX({ ... });  // 每次都是新结构

内联缓存 — 加速函数调用

函数调用有多慢?

函数调用看起来简单:

function getName(user) {
  return user.name;
}

const user = { name: 'Tom' };
getName(user);

但每次调用,V8 都要查找 user.name 在哪里。

内联缓存的原理

V8 第一次执行 getName(user) 时:

第1次调用:
1. 查找 user 的隐藏类  HC0
2. 查找 name 属性在 HC0 的位置  offset 0
3. 返回结果
4. 记录:HC0 的对象调用这个函数,返回 offset 0

之后调用同样的函数,直接跳过查找

第2次调用:
1. 检查隐藏类是 HC0 
2. 直接用记录的 offset 0
3. 返回结果

这就是内联缓存(Inline Cache)——把查找结果"缓存"起来。

IC 的类型状态

Uncached  Monomorphic  Polymorphic(2-4)  Megamorphic(5+)
                                           
 每次查     命中缓存       部分命中         全局查表

垃圾回收 — 内存管理

什么是垃圾?

程序里不再使用的对象就是"垃圾":

function createUser() {
  const user = { name: 'Tom' };
  return user.name;  // user 对象还在用
}  // 但 user 变量没了

createUser();
// 之后再也访问不到这个 { name: 'Tom' } 对象了
// 它就成了"垃圾"

V8 的内存布局

┌─────────────────────────────┐
          新生代                新对象
    (New Space / Semi-Space) 
├─────────────────────────────┤
          老生代                存活久的对象
    (Old Space)              
├─────────────────────────────┤
        大对象区                 无法放入其他区的对象
    (Large Object Space)    
├─────────────────────────────┤
        代码区                   JIT 编译后的机器码
    (Code Space)            
├─────────────────────────────┤
        Cell / Map              特殊对象
    (Cell / Map Space)       
└─────────────────────────────┘

V8内存布局图

V8 的垃圾回收策略

V8 采用分代回收

代际 对象来源 回收频率 算法
新生代 新创建的对象 频繁 Scavenge(复制)
老生代 经历一次 GC 仍存活 较少 Mark-Sweep-Compact

新生代:Scavenge 算法

新生代内存分两半:FromTo

┌─────────────────┬─────────────────┐
│      FromTo        │
│   (使用中)     │   (空闲)       │
└─────────────────┴─────────────────┘

1. From 满了,存活对象复制到 To
2. From 清空
3. FromTo 交换

晋升:经历两次 Scavenge 仍存活的对象,会进入老生代。

老生代:Mark-Sweep-Compact

步骤1:标记(Mark)

遍历所有根对象(全局变量、栈上变量)
    
标记能访问到的对象为"存活"
    
没被标记的就是垃圾

步骤2:清除(Sweep)

回收没有标记的对象的内存

步骤3:压缩(Compact)

存活对象移动到一起

解决内存碎片问题

增量 GC

为了避免长时间停顿(Stop-The-World),V8 使用增量标记:

传统 GC:
████████████████████████████  100% 停顿
     执行时间 ←────────────────→

增量 GC:
███    ████    ███    ██
                    
执行  执行  执行  执行

Orinoco — 并行与并发 GC

现代 V8 使用更先进的 GC 算法:

技术 说明 效果
并行 GC GC 多线程并行执行 充分利用多核 CPU
增量 GC GC 分多次小步执行 减少停顿时间
并发 GC GC 与 JS 执行同时进行 几乎无停顿

深入了解 V8 🔬

V8 执行流程全图

JS代码
    Parser
AST(抽象语法树)
    Ignition
字节码 + Feedback Vector(反馈向量)
    (热代码触发)
TurboFan
   
优化机器码
    (类型不稳定)
反优化  退回字节码

V8执行流程详图

为什么 V8 这么快?

优化手段 作用
JIT 即时编译 热代码用机器码执行
隐藏类 对象属性快速访问
内联缓存 函数调用加速
分代回收 高效内存管理
懒解析 延迟解析,只解析用到的
并行 GC 多核加速垃圾回收

Sparkplug — 无解释的 Baseline JIT

V8 最近引入了 Sparkplug,一个超快的 baseline JIT:

之前:JS  Ignition 字节码  TurboFan 机器码
现在:JS  Ignition 字节码  Sparkplug 机器码  TurboFan 优化机器码

Sparkplug 不做任何优化,直接把字节码转成机器码,比 Ignition 快 2-5 倍。

TurboFan 优化的代码例子

// 优化前:字节码执行
function sum(arr) {
  let total = 0;
  for (let i = 0; i < arr.length; i++) {
    total += arr[i];
  }
  return total;
}

// 优化后:TurboFan 可能生成的机器码
// 1. 使用寄存器代替变量
// 2. 循环展开(Loop Unrolling)
// 3. 预取数据到 CPU 缓存

编写高性能 JS

// ✅ 好:保持属性类型一致
const p1 = { x: 1, y: 2 };
const p2 = { x: 3, y: 4 };

// ✅ 好:避免类型变化
function add(a, b) {
  return a + b;
}
add(1, 2);       // 都是整数
add(3.14, 2.86); // 都是浮点数

// ❌ 差:属性顺序不一致
const a = { x: 1, y: 2 };
const b = { y: 1, x: 2 };  // 新建隐藏类!

// ❌ 差:类型乱变
function example(x) {
  return x.value;  // x 可能是对象,可能是 undefined
}

// ✅ 好:使用固定形状的对象
const cache = {};
for (let i = 0; i < 1000; i++) {
  cache.key = i;  // 每次都用相同的 key
}

V8 性能陷阱

陷阱 说明 解决方案
隐藏类爆炸 对象结构不一致 保持属性顺序一致
类型不稳定 参数类型经常变化 使用多态函数时要小心
内存泄漏 闭包引用大量对象 及时解除引用
大对象 大数组、大对象放新生代 手动管理或拆分

总结

概念 作用 比喻
Ignition 解释器,生成字节码 + 记录反馈 同声传译先听懂意思
TurboFan 优化编译器,生成高效机器码 翻译稿润色升级
JIT 即时编译,热代码加速 多次练习后越说越溜
隐藏类 快速属性访问 图书馆编号系统
内联缓存 函数调用加速 记住常走的路
分代回收 高效内存管理 新书放前台,旧书放仓库
Sparkplug 超快 baseline JIT 不用练习,直接上岗

写在最后

现在你知道了:

  • V8 不是直接执行 JS,而是经过 Parser → Ignition → TurboFan
  • JIT 让热代码越来越快,但类型变化会导致反优化
  • 隐藏类和内联缓存,是 JS 快的秘密
  • 写代码时保持类型一致,能帮助 V8 优化
  • 新生代用复制算法,老生代用标记清除

下次有人说"JS 慢",你可以理直气壮地说:你了解 V8 吗?

为什么禁止我请求别的网站的接口?——跨域与CORS

作者 牛奶
2026年4月7日 09:54

你有没有遇到过这种情况:在自己的网页上想请求别人的API,结果浏览器直接报错:Access-Control-Allow-Origin' header is missing。为什么浏览器要阻止你?服务器不响应不就完了吗?

今天,用小区门禁的故事,来讲讲 跨域CORS


原文地址

墨渊书肆/为什么禁止我请求别的网站的接口?——跨域与CORS


什么是"跨域"?

同源策略 — 浏览器的安全基石

浏览器有个同源策略Same-Origin Policy):只有来自同一个"家"的资源才能随便用。

什么叫"同一个家"?看三个条件:协议(http/https)、域名(example.com)、端口(:8080)。三个都一样,才是同源;有一个不一样,就是跨域。

跨域的例子

http://example.com 和 http://example.com/profile     // 协议+域名+端口都相同 → 同源
✅ https://example.com 和 https://example.com           // 协议+域名+端口都相同 → 同源
❌ http://example.com 和 https://example.com           // 协议不同 → 跨域
❌ http://example.com 和 http://api.example.com        // 域名不同(子域名)→ 跨域
❌ http://example.com:8080 和 http://example.com:3000  // 端口不同 → 跨域

跨域限制了什么?

浏览器的同源策略主要限制了三件事:

  • DOM 访问:无法读取不同源的 iframe 内容、无法修改不同源的 iframe DOM
  • AJAX 请求:无法请求不同源的 API
  • Cookie/LocalStorage:无法访问不同源的数据

为什么要限制跨域?

模拟一个攻击场景

想象一下:你登录了银行网站 bank.com,浏览器保存了你的登录 Cookie。

然后你手滑点进了一个恶意网站 evil.com,这个网站里有一段代码:

<form action="http://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="hacker">
  <input type="hidden" name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>

如果没有同源策略,这个表单请求会自动带上 bank.com 的 Cookie,银行服务器以为是你本人操作的——钱就没了。

同源策略就是浏览器的"门禁":只有同一家人才能进,陌生人要查证件。

💡 注意:<img> 标签的 GET 请求虽然也会带 Cookie,但现代浏览器有 SameSite Cookie 保护。上面表单 POST 场景更典型。


CORS — 跨域的"通行证"

CORS 是什么?

CORS(Cross-Origin Resource Sharing)= 跨域资源共享。

它的工作原理很简单:让服务器告诉浏览器,"我允许来自这些源的请求"

简单请求 vs 预检请求

简单请求

满足以下条件的请求是"简单请求":

条件 要求
请求方法 GETPOSTHEAD
请求头部 只有几种常见类型
Content-Type 只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求的流程:

1. 浏览器发送请求(自动带上 Origin 头)
   
2. 服务器检查 Origin,决定是否允许
   
3. 服务器返回响应头 Access-Control-Allow-Origin
   
4. 浏览器检查响应头,允许就完事

服务器端示例(Node.js):

app.get('/api/data', (req, res) => {
  const origin = req.headers.origin;

  if (origin === 'https://example.com') {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }

  res.json({ data: '这是返回的数据' });
});

响应头

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Content-Type: application/json

{"data": "这是返回的数据"}

预检请求(Preflight)

不满足"简单请求"条件的,浏览器会先发一个 OPTIONS 请求"探路":

1. 浏览器发送 OPTIONS 预检请求
   
2. 服务器检查方法/头部/Origin
   
3. 服务器返回允许的头 Access-Control-*
   
4. 浏览器发送实际请求

预检请求检查什么?

预检请求(OPTIONS)就像登机前的安检——先检查你带没带危险品。

浏览器会问服务器三件事:

  • 我从哪来?(Origin)
  • 我想用什么方法?(Access-Control-Request-Method)
  • 我想带什么头?(Access-Control-Request-Headers)

服务器回答"可以",浏览器才放行实际请求。

# 请求(浏览器发给服务器)
OPTIONS /api/data HTTP/1.1
Origin: https://example.com              # 我从哪来
Access-Control-Request-Method: PUT        # 我想用 PUT 方法
Access-Control-Request-Headers: Content-Type, Authorization  # 我想带这些头

---

# 响应(服务器告诉浏览器)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com  # 允许这个源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE  # 允许这些方法
Access-Control-Allow-Headers: Content-Type, Authorization  # 允许这些头
Access-Control-Max-Age: 86400          # 预检结果缓存24小时

服务器端处理

app.options('/api/data', (req, res) => {
  const origin = req.headers.origin;

  if (origin === 'https://example.com') {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
  }

  res.status(204).send();
});

CORS 响应头详解

常用响应头

响应头 作用 例子
Access-Control-Allow-Origin 允许的源 *https://example.com
Access-Control-Allow-Methods 允许的方法 GET, POST, PUT
Access-Control-Allow-Headers 允许的头部 Content-Type, Authorization
Access-Control-Max-Age 预检缓存时间 86400(秒)
Access-Control-Allow-Credentials 是否允许带 Cookie true

credentials 模式

默认情况下,CORS 不带 Cookie。如果需要携带 Cookie:

前端

fetch('/api/data', {
  credentials: 'include'
});

服务端

res.setHeader('Access-Control-Allow-Origin', 'https://example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');

注意:Access-Control-Allow-Origin 不能用 *,必须是具体域名。


跨域的解决方案

1. JSONP(已不推荐)

利用 <script> 标签不受同源策略限制的特性:

<script>
  function handleData(data) {
    console.log(data);
  }
</script>
<script src="http://api.example.com/data?callback=handleData"></script>
缺点 说明
只支持 GET 无法处理 POST 等请求
有安全风险 可能被注入恶意代码
无法捕获错误 错误处理困难

2. 代理服务器

在自己的服务器上转发请求,"伪装"成同源:

浏览器 ──> 我的服务器(同一源) ──> 目标服务器

Nginx 代理

location /api/ {
  proxy_pass http://target-server.com/;
}

Node.js 代理

app.get('/api/data', async (req, res) => {
  const response = await fetch('http://target-server.com/data');
  const data = await response.json();
  res.json(data);
});

3. Webpack/Vite 开发代理

开发环境配置代理:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://target-server.com',
        changeOrigin: true
      }
    }
  }
};

4. postMessage

不同窗口/iframe 之间的通信:

window.addEventListener('message', (event) => {
  if (event.origin === 'https://example.com') {
    console.log('收到消息:', event.data);
  }
});

iframe.contentWindow.postMessage('hello', 'https://example.com');

深入了解 CORS 🔬

第三方 Cookie 的限制

现代浏览器正在逐步限制第三方 Cookie:

浏览器 政策
Chrome 计划逐步淘汰第三方 Cookie
Safari 默认阻止第三方 Cookie
Firefox 提供第三方 Cookie 阻止选项

CORS 和 CSRF 的区别

CORS CSRF
是什么 跨域资源共享机制 跨站请求伪造攻击
作用 服务端允许/禁止跨域请求 利用用户已登录状态发起攻击
防御 服务端配置 Access-Control-* Token、SameSite Cookie、验证码

为什么 OPTIONS 叫"预检"?

"预检"就像登机前的安检——先检查你带没带危险品(方法、头部),没问题了才让你登机(发送实际请求)。


常见错误排查

错误 1:No 'Access-Control-Allow-Origin' header

原因 解决
服务端没配置 CORS 添加 Access-Control-Allow-Origin
Origin 不匹配 检查配置的域名是否正确
credentials 时用了 * 必须指定具体域名

错误 2:Method not allowed

原因 解决
请求方法(如 PUT)不在允许列表 检查 Access-Control-Allow-Methods

错误 3:Header not allowed

原因 解决
请求头部(如 Authorization)不在允许列表 检查 Access-Control-Allow-Headers

错误 4:预检请求 404

原因 解决
服务端没有处理 OPTIONS 请求 中间件或网关要放行 OPTIONS

总结

概念 像什么 作用
同源策略 小区门禁 限制不同源的访问,保护安全
CORS 通行证 告诉浏览器哪些跨域请求是允许的
简单请求 普通访客 不需要预检,直接请求
预检请求 安检验票 先检查再放行,更安全的请求
JSONP 走后门 已不推荐,有安全风险
代理 同一个家门 绕过跨域,最推荐的开发方案

写在最后

现在你应该明白了:

  • 跨域是浏览器的安全机制,不是为了刁难你
  • CORS 是服务器授权机制,服务器说可以,浏览器才放行
  • 预检请求 = 安检,OPTIONS 通过了才能发送实际请求
  • 生产环境推荐用代理,开发环境用 webpack/vite 代理

下次遇到跨域错误,先看浏览器控制台的报错信息——是"缺通行证"(header 缺失)还是"通行证不对"(origin 不匹配),处理方式不一样的。

❌
❌