Appearance
JavaScript 中的运行环境:浏览器、Node.js 与宿主能力
很多人学 JavaScript 时,最容易混的一件事,不是语法本身,而是:这段代码到底运行在哪里。
比如:
- 为什么浏览器里可以直接访问
document - 为什么 Node.js 里可以直接用
fs - 为什么有些项目里既能写
import.meta.env,又不能在浏览器里直接访问process
这些问题表面上像“API 记不住”,本质上其实是在问:JavaScript 语言本身,和它所在的运行环境,到底是什么关系。
1. 为什么必须区分“语言”和“运行环境”
JavaScript 可以看成两层:
语言本身宿主环境提供的能力
语言本身主要包括:
- 变量
- 函数
- 对象
- 原型
- 类
- Promise
async/await
这些能力的核心特点是:只要有 JavaScript 引擎和对应实现,它们就有机会工作。
但像下面这些东西,就不完全属于语言本身:
windowdocumentlocalStoragefetchprocessBufferfs
更贴近它们定位的说法是:宿主环境为了让 JavaScript 真正能做事,而额外提供出来的运行时能力。
🌟 所以最核心的一条判断是:JavaScript 不是“自带 DOM 的语言”,也不是“天然会读文件的语言”。这些能力来自宿主环境。
2. 什么是宿主环境
宿主环境可以看成 承载 JavaScript 执行,并给它补上外部世界能力的那一层。
如果只讲 JavaScript 语言,你只能写:
js
const user = {
name: 'Tom',
}
function sayHello(name) {
return `Hello, ${name}`
}但你没法仅靠语言本身完成:
- 修改网页
- 发送 HTTP 请求
- 读取本地文件
- 启动一个服务端进程
这些事情都需要宿主环境补能力。
前端里最常见的两类宿主环境,就是:
浏览器环境Node.js 环境
3. 浏览器环境到底提供了什么
浏览器环境的目标,是让 JavaScript 能控制页面并和用户交互。
所以它最核心的能力集中在:
- 页面结构
- 页面渲染
- 用户事件
- 网络通信
- 浏览器存储
3.1 浏览器里最常见的全局对象
浏览器里最容易看到的全局对象是:
windowdocumentlocationhistorynavigator
可以直接把它理解成:
| 对象 | 可以看成什么 |
|---|---|
window | 浏览器页面上下文里的顶层全局对象 |
document | 当前页面的 DOM 文档对象 |
location | 当前页面地址信息 |
history | 浏览器历史记录控制能力 |
navigator | 浏览器和设备相关信息 |
其中:
DOM是 Document Object Model,中文通常叫“文档对象模型”,本质上是页面结构在 JavaScript 里的对象化表示BOM是 Browser Object Model,通常指浏览器窗口、地址栏、历史记录等更偏浏览器容器层的能力
3.2 浏览器环境更擅长解决什么问题
浏览器环境更擅长的是:
- DOM 查询与修改
- 事件监听与交互响应
- 页面渲染协作
- 基于 Web API 的网络请求和存储
例如:
js
const button = document.querySelector('#save')
button.addEventListener('click', async () => {
const res = await fetch('/api/user')
const data = await res.json()
console.log(data)
})这段代码依赖的并不只是 JavaScript 语法,还依赖:
document- DOM 事件系统
fetch
这些都是浏览器运行时能力。
3.3 浏览器环境的边界是什么
浏览器并不是一个“什么都能干”的宿主环境。
它有明显边界:
- 不能像服务端脚本那样随意访问本机文件系统
- 不能默认获得操作系统级权限
- 不能直接使用 Node.js 的
fs、path、net等 API
这背后的核心原因是:浏览器首先要保护用户设备和页面安全,而不是给脚本无限权限。
4. Node.js 环境到底提供了什么
Node.js 的目标不是操作页面,而是:让 JavaScript 能脱离浏览器,在操作系统和服务端语境里执行。
所以它最核心的能力集中在:
- 文件系统访问
- 进程与环境变量
- 网络服务
- 命令行脚本
- 包管理与工程工具生态
4.1 Node.js 里常见的全局对象和运行时能力
Node.js 里比较常见的全局或运行时能力包括:
globalprocessBuffersetTimeoutsetImmediate
其中:
| 对象或能力 | 可以看成什么 |
|---|---|
global | Node.js 里的传统全局对象 |
process | 当前进程信息和控制能力 |
Buffer | 二进制数据处理对象 |
fs | 文件系统模块 |
path | 路径处理模块 |
例如:
js
import fs from 'node:fs/promises'
const text = await fs.readFile('./data.txt', 'utf-8')
console.log(text)这段代码能成立,不是因为 JavaScript 天生会读文件,而是因为 Node.js 提供了文件系统能力。
4.2 Node.js 环境更擅长解决什么问题
Node.js 更擅长的是:
- 写本地脚本
- 跑开发服务
- 处理文件与目录
- 启动 HTTP 服务
- 作为构建工具和工程脚本的运行底座
这也是为什么:
npmViteWebpackESLintTypeScript
这类前端工程工具,大多都运行在 Node.js 环境里。
4.3 Node.js 环境的边界是什么
Node.js 没有浏览器页面上下文,所以通常不能直接使用:
windowdocumentalertlocalStorage
如果你在纯 Node.js 脚本里直接写:
js
console.log(document.body)通常就会直接报错,因为这里根本没有 DOM 文档对象。
5. 浏览器和 Node.js 到底有哪些关键差异
这两种环境都能执行 JavaScript,但重点完全不同。
| 维度 | 浏览器环境 | Node.js 环境 |
|---|---|---|
| 主要目标 | 页面交互与渲染 | 服务端、脚本、工程工具 |
| 核心全局对象 | window、document | global、process |
| 擅长能力 | DOM、事件、页面 API | 文件、进程、网络服务 |
| 常见存储 | localStorage、sessionStorage、IndexedDB | 文件系统、数据库连接、缓存服务 |
| 权限边界 | 更强调沙箱与安全限制 | 更接近操作系统能力 |
| 典型使用场景 | Web 页面、H5、浏览器应用 | CLI、服务端、构建工具、本地脚本 |
实际写项目时,更多是这样:浏览器关心的是页面和用户,Node.js 关心的是系统、服务和工程。
6. 为什么有些 API 看起来两边都能用
前端学习中还有一个很容易让人误解的现象:明明说浏览器和 Node.js 不一样,为什么 setTimeout、console,甚至现在的 fetch,看起来两边都能用?
这里要注意两件事。
6.1 同名,不一定等于同一层来源
比如:
consolesetTimeoutfetch
它们可能在多个宿主环境里都存在,但并不意味着它们就“属于 JavaScript 语言本体”。
把这层关系拆开看:
- JavaScript 语言定义语法和核心对象体系
- 宿主环境决定还要再额外暴露哪些 API
6.2 现代运行时之间在互相靠近
现代 Node.js 也在逐步提供更多 Web 风格 API,例如:
fetchURLAbortController
这会让开发体验更统一,但仍然不能得出结论说:浏览器环境和 Node.js 环境已经没有差别。
要注意的是:
- 语义是否完全一致
- 支持版本是否一致
- 使用场景是否一致
7. globalThis 为什么重要
过去大家常会分别记:
- 浏览器里是
window - Node.js 里是
global
但这会带来一个问题:跨环境代码不够统一。
globalThis 的意义就在这里。
它可以看成 一个更通用的“当前全局对象入口”。
例如:
js
console.log(globalThis)这样写的好处是:
- 不必强依赖当前一定是浏览器还是 Node.js
- 更适合写跨运行时的通用逻辑
但要注意:能拿到全局对象,不等于所有环境都拥有同样的全局 API。
8. 版本差异和环境差异不是一回事
这是另一个特别容易混的边界。
很多人口中的“JavaScript 版本差异”,常常混在了两件不同的事情里:
ECMAScript 语言规范演进不同运行时对这些能力的支持情况
例如:
let、const、箭头函数,属于语言层的新特性- 某个浏览器版本是否支持它,属于运行时实现和兼容性问题
- 某个 Node.js 版本是否原生支持
fetch,属于 Node.js 运行时能力问题
所以更贴近工程现实的说法是:“这个特性能不能用”至少要同时看语言规范、运行时版本、构建转译策略三件事。
这也是为什么工程里会出现:
BabelPolyfill- 浏览器兼容性配置
它们解决的不是同一层问题。
9. 构建阶段环境和运行阶段环境也不是一回事
现代前端里还有一条很容易踩坑的边界:写在同一个项目里的代码,不一定运行在同一个环境。
例如在一个 Vite 项目中,常见会同时存在:
- 浏览器里运行的业务代码
- Node.js 里运行的构建配置
- 开发服务器中的中间处理逻辑
这也是为什么:
vite.config.ts里通常可以用 Node.js API- 浏览器业务代码里不能直接访问
fs import.meta.env更像构建工具注入给前端代码的环境入口
🌟 真正要先问的问题,不是“这个 API 好不好用”,而是:这段代码会在构建阶段执行,还是在浏览器运行时执行,还是在 Node.js 运行时执行。
10. 一张总关系图
mermaid
flowchart TD
A[JavaScript 语言] --> B[浏览器宿主环境]
A --> C[Node.js 宿主环境]
B --> D[DOM BOM Web API]
C --> E[文件系统 进程 网络服务]
A --> F[ECMAScript 版本演进]
F --> G[运行时实现支持]
G --> H[兼容性 转译 Polyfill]这张图最重要的意思是:
- JavaScript 语言是一条主线
- 浏览器和 Node.js 是两类重要宿主环境
- 版本演进和环境差异会交叉影响工程写法
11. 工程里最常见的误区
11.1 误把浏览器 API 当成 JavaScript 语法
比如以为:
document是 JavaScript 自带的localStorage到处都能用
这会导致一切一离开浏览器就报错。
11.2 误把 Node.js API 当成前端页面能力
比如在浏览器代码里直接写:
fs.readFilepath.joinprocess.cwd()
这类问题在刚接触构建工具时特别常见。
11.3 误把“版本不支持”和“环境不存在”混为一谈
例如:
- 某个语法浏览器太老,不支持
- 某个 API 根本不是这个环境里的能力
这两类问题的解决方式完全不同。
11.4 不区分业务运行时代码和构建脚本代码
结果就是:
- 在前端业务代码里写 Node.js 逻辑
- 在构建脚本里误以为能直接操作 DOM
12. 一份检查清单
写一段 JavaScript 代码前,可以自查:
- 这段代码运行在浏览器、Node.js,还是构建阶段
- 我用到的是语言能力,还是宿主 API
- 这个 API 是当前环境原生提供的吗
- 如果涉及新语法或新 API,当前目标版本是否支持
- 这类问题该靠改代码解决,还是靠转译、Polyfill、环境切分解决
13. 小结
这一篇最想建立的核心认识是:
- JavaScript 语言本身不等于浏览器 API
- 浏览器环境和 Node.js 环境都能执行 JavaScript,但提供的能力边界不同
- 版本差异、环境差异、构建差异,是三条相关但不能混在一起的主线
如果只压缩成一句话,可以记住:
先分清代码运行在哪里,再讨论这段 JavaScript 能做什么。