Skip to content

JavaScript 中的运行环境:浏览器、Node.js 与宿主能力

很多人学 JavaScript 时,最容易混的一件事,不是语法本身,而是:这段代码到底运行在哪里。

比如:

  1. 为什么浏览器里可以直接访问 document
  2. 为什么 Node.js 里可以直接用 fs
  3. 为什么有些项目里既能写 import.meta.env,又不能在浏览器里直接访问 process

这些问题表面上像“API 记不住”,本质上其实是在问:JavaScript 语言本身,和它所在的运行环境,到底是什么关系。

1. 为什么必须区分“语言”和“运行环境”

JavaScript 可以看成两层:

  1. 语言本身
  2. 宿主环境提供的能力

语言本身主要包括:

  1. 变量
  2. 函数
  3. 对象
  4. 原型
  5. Promise
  6. async/await

这些能力的核心特点是:只要有 JavaScript 引擎和对应实现,它们就有机会工作。

但像下面这些东西,就不完全属于语言本身:

  1. window
  2. document
  3. localStorage
  4. fetch
  5. process
  6. Buffer
  7. fs

更贴近它们定位的说法是:宿主环境为了让 JavaScript 真正能做事,而额外提供出来的运行时能力。

🌟 所以最核心的一条判断是:JavaScript 不是“自带 DOM 的语言”,也不是“天然会读文件的语言”。这些能力来自宿主环境。

2. 什么是宿主环境

宿主环境可以看成 承载 JavaScript 执行,并给它补上外部世界能力的那一层

如果只讲 JavaScript 语言,你只能写:

js
const user = {
  name: 'Tom',
}

function sayHello(name) {
  return `Hello, ${name}`
}

但你没法仅靠语言本身完成:

  1. 修改网页
  2. 发送 HTTP 请求
  3. 读取本地文件
  4. 启动一个服务端进程

这些事情都需要宿主环境补能力。

前端里最常见的两类宿主环境,就是:

  1. 浏览器环境
  2. Node.js 环境

3. 浏览器环境到底提供了什么

浏览器环境的目标,是让 JavaScript 能控制页面并和用户交互。

所以它最核心的能力集中在:

  1. 页面结构
  2. 页面渲染
  3. 用户事件
  4. 网络通信
  5. 浏览器存储

3.1 浏览器里最常见的全局对象

浏览器里最容易看到的全局对象是:

  1. window
  2. document
  3. location
  4. history
  5. navigator

可以直接把它理解成:

对象可以看成什么
window浏览器页面上下文里的顶层全局对象
document当前页面的 DOM 文档对象
location当前页面地址信息
history浏览器历史记录控制能力
navigator浏览器和设备相关信息

其中:

  1. DOM 是 Document Object Model,中文通常叫“文档对象模型”,本质上是页面结构在 JavaScript 里的对象化表示
  2. BOM 是 Browser Object Model,通常指浏览器窗口、地址栏、历史记录等更偏浏览器容器层的能力

3.2 浏览器环境更擅长解决什么问题

浏览器环境更擅长的是:

  1. DOM 查询与修改
  2. 事件监听与交互响应
  3. 页面渲染协作
  4. 基于 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 语法,还依赖:

  1. document
  2. DOM 事件系统
  3. fetch

这些都是浏览器运行时能力。

3.3 浏览器环境的边界是什么

浏览器并不是一个“什么都能干”的宿主环境。

它有明显边界:

  1. 不能像服务端脚本那样随意访问本机文件系统
  2. 不能默认获得操作系统级权限
  3. 不能直接使用 Node.js 的 fspathnet 等 API

这背后的核心原因是:浏览器首先要保护用户设备和页面安全,而不是给脚本无限权限。

4. Node.js 环境到底提供了什么

Node.js 的目标不是操作页面,而是:让 JavaScript 能脱离浏览器,在操作系统和服务端语境里执行。

所以它最核心的能力集中在:

  1. 文件系统访问
  2. 进程与环境变量
  3. 网络服务
  4. 命令行脚本
  5. 包管理与工程工具生态

4.1 Node.js 里常见的全局对象和运行时能力

Node.js 里比较常见的全局或运行时能力包括:

  1. global
  2. process
  3. Buffer
  4. setTimeout
  5. setImmediate

其中:

对象或能力可以看成什么
globalNode.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 更擅长的是:

  1. 写本地脚本
  2. 跑开发服务
  3. 处理文件与目录
  4. 启动 HTTP 服务
  5. 作为构建工具和工程脚本的运行底座

这也是为什么:

  1. npm
  2. Vite
  3. Webpack
  4. ESLint
  5. TypeScript

这类前端工程工具,大多都运行在 Node.js 环境里。

4.3 Node.js 环境的边界是什么

Node.js 没有浏览器页面上下文,所以通常不能直接使用:

  1. window
  2. document
  3. alert
  4. localStorage

如果你在纯 Node.js 脚本里直接写:

js
console.log(document.body)

通常就会直接报错,因为这里根本没有 DOM 文档对象。

5. 浏览器和 Node.js 到底有哪些关键差异

这两种环境都能执行 JavaScript,但重点完全不同。

维度浏览器环境Node.js 环境
主要目标页面交互与渲染服务端、脚本、工程工具
核心全局对象windowdocumentglobalprocess
擅长能力DOM、事件、页面 API文件、进程、网络服务
常见存储localStoragesessionStorageIndexedDB文件系统、数据库连接、缓存服务
权限边界更强调沙箱与安全限制更接近操作系统能力
典型使用场景Web 页面、H5、浏览器应用CLI、服务端、构建工具、本地脚本

实际写项目时,更多是这样:浏览器关心的是页面和用户,Node.js 关心的是系统、服务和工程。

6. 为什么有些 API 看起来两边都能用

前端学习中还有一个很容易让人误解的现象:明明说浏览器和 Node.js 不一样,为什么 setTimeout、console,甚至现在的 fetch,看起来两边都能用?

这里要注意两件事。

6.1 同名,不一定等于同一层来源

比如:

  1. console
  2. setTimeout
  3. fetch

它们可能在多个宿主环境里都存在,但并不意味着它们就“属于 JavaScript 语言本体”。

把这层关系拆开看:

  1. JavaScript 语言定义语法和核心对象体系
  2. 宿主环境决定还要再额外暴露哪些 API

6.2 现代运行时之间在互相靠近

现代 Node.js 也在逐步提供更多 Web 风格 API,例如:

  1. fetch
  2. URL
  3. AbortController

这会让开发体验更统一,但仍然不能得出结论说:浏览器环境和 Node.js 环境已经没有差别。

要注意的是:

  1. 语义是否完全一致
  2. 支持版本是否一致
  3. 使用场景是否一致

7. globalThis 为什么重要

过去大家常会分别记:

  1. 浏览器里是 window
  2. Node.js 里是 global

但这会带来一个问题:跨环境代码不够统一。

globalThis 的意义就在这里。

它可以看成 一个更通用的“当前全局对象入口”

例如:

js
console.log(globalThis)

这样写的好处是:

  1. 不必强依赖当前一定是浏览器还是 Node.js
  2. 更适合写跨运行时的通用逻辑

但要注意:能拿到全局对象,不等于所有环境都拥有同样的全局 API。

8. 版本差异和环境差异不是一回事

这是另一个特别容易混的边界。

很多人口中的“JavaScript 版本差异”,常常混在了两件不同的事情里:

  1. ECMAScript 语言规范演进
  2. 不同运行时对这些能力的支持情况

例如:

  1. letconst、箭头函数,属于语言层的新特性
  2. 某个浏览器版本是否支持它,属于运行时实现和兼容性问题
  3. 某个 Node.js 版本是否原生支持 fetch,属于 Node.js 运行时能力问题

所以更贴近工程现实的说法是:“这个特性能不能用”至少要同时看语言规范、运行时版本、构建转译策略三件事。

这也是为什么工程里会出现:

  1. Babel
  2. Polyfill
  3. 浏览器兼容性配置

它们解决的不是同一层问题。

9. 构建阶段环境和运行阶段环境也不是一回事

现代前端里还有一条很容易踩坑的边界:写在同一个项目里的代码,不一定运行在同一个环境。

例如在一个 Vite 项目中,常见会同时存在:

  1. 浏览器里运行的业务代码
  2. Node.js 里运行的构建配置
  3. 开发服务器中的中间处理逻辑

这也是为什么:

  1. vite.config.ts 里通常可以用 Node.js API
  2. 浏览器业务代码里不能直接访问 fs
  3. 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]

这张图最重要的意思是:

  1. JavaScript 语言是一条主线
  2. 浏览器和 Node.js 是两类重要宿主环境
  3. 版本演进和环境差异会交叉影响工程写法

11. 工程里最常见的误区

11.1 误把浏览器 API 当成 JavaScript 语法

比如以为:

  1. document 是 JavaScript 自带的
  2. localStorage 到处都能用

这会导致一切一离开浏览器就报错。

11.2 误把 Node.js API 当成前端页面能力

比如在浏览器代码里直接写:

  1. fs.readFile
  2. path.join
  3. process.cwd()

这类问题在刚接触构建工具时特别常见。

11.3 误把“版本不支持”和“环境不存在”混为一谈

例如:

  1. 某个语法浏览器太老,不支持
  2. 某个 API 根本不是这个环境里的能力

这两类问题的解决方式完全不同。

11.4 不区分业务运行时代码和构建脚本代码

结果就是:

  1. 在前端业务代码里写 Node.js 逻辑
  2. 在构建脚本里误以为能直接操作 DOM

12. 一份检查清单

写一段 JavaScript 代码前,可以自查:

  1. 这段代码运行在浏览器、Node.js,还是构建阶段
  2. 我用到的是语言能力,还是宿主 API
  3. 这个 API 是当前环境原生提供的吗
  4. 如果涉及新语法或新 API,当前目标版本是否支持
  5. 这类问题该靠改代码解决,还是靠转译、Polyfill、环境切分解决

13. 小结

这一篇最想建立的核心认识是:

  1. JavaScript 语言本身不等于浏览器 API
  2. 浏览器环境和 Node.js 环境都能执行 JavaScript,但提供的能力边界不同
  3. 版本差异、环境差异、构建差异,是三条相关但不能混在一起的主线

如果只压缩成一句话,可以记住:

先分清代码运行在哪里,再讨论这段 JavaScript 能做什么。

基于 VitePress 构建的个人技术笔记。