JavaScript作为一门语言,在不同运行环境中暴露的接口差别巨大。浏览器和Node.js虽然都使用同一套ECMAScript核心语法,但它们各自所处的宿主环境赋予了完全不同的能力集合。理解这些差异,是避免写出“在Node里报document未定义”这类错误的基础。

全局对象与运行环境差异
浏览器中的JavaScript运行在窗口环境里,全局对象为window,所有DOM相关的document、location、history都挂载在它之下。你在控制台输入window.document能拿到页面结构,输入window.fetch能发起网络请求。这种环境天然面向用户交互,因此提供了大量与渲染、事件、存储相关的API。
Node.js的全局对象则是global,它没有window,也没有document。如果你在Node脚本里写console.log(window),会直接抛出ReferenceError。Node的设计目标是服务端和脚本执行,所以它提供了process、Buffer、setImmediate等面向操作系统和底层资源的全局变量。例如process.env可以读取环境变量,这在浏览器中只能通过打包工具注入。
这种差异导致很多前端工具函数不能直接在Node复用。比如下面这段代码在浏览器正常,在Node会崩溃:
// 浏览器可用,Node报错
function getPageTitle() {
return window.document.title;
}
console.log(getPageTitle());
正确的跨环境写法应当做存在性判断,或者将环境相关逻辑抽离。很多库使用typeof window !== 'undefined'来区分环境,从而导出不同实现。
模块系统与代码组织方式
浏览器传统上依靠<script>标签加载脚本,全局作用域容易污染。后来ES Module(import/export)成为标准,通过<script type="module">启用,依赖浏览器原生解析。它的路径必须是URL形式,且受CORS限制,不能直接读本地文件。
Node.js早期使用CommonJS,以require和module.exports同步加载模块,文件系统在本地,因此可以用相对路径如./utils直接引入。Node从v12起逐步支持ES Module,但需要文件后缀为.mjs或在package.json设type: module。两者混用容易出错,比如用require导入ESM包会报ERR_REQUIRE_ESM。
下面展示两种写法对比:
// CommonJS (Node传统)
const fs = require('fs');
fs.writeFileSync('./a.txt', 'hello');
// ES Module (浏览器/Node新标准)
import fs from 'fs' assert { type: 'commonjs' };
import { readFile } from 'fs/promises';
在浏览器里你不能import fs from 'fs',因为没有文件系统概念;在Node里若用ESM写前端DOM代码,同样会因缺少document失败。因此模块系统的选择必须匹配目标环境。
异步API与内置功能模块
浏览器异步常围绕用户行为:fetch请求接口、setTimeout做动画、IntersectionObserver监听元素。这些API由浏览器引擎调度,和页面生命周期绑定。比如fetch返回Promise,但底层是浏览器网络栈。
Node.js的异步更多面向I/O:fs.readFile、net.createServer、child_process.spawn。它提供libuv事件循环,和浏览器微任务宏任务模型相似但有差异,例如setImmediate是Node特有。下面用Node读取文件,浏览器无此能力:
const fs = require('fs');
fs.readFile('/etc/hostname', 'utf8', (err, data) => {
if (err) throw err;
console.log('文件内容:', data);
});
浏览器对应的“文件读取”是<input type="file">加FileReader,且只能操作用户主动选择的文件。Storage方面,浏览器有localStorage和sessionStorage,Node则用process.env或第三方库如dotenv管理配置。理解这些边界,才能正确设计同构应用或命令行工具。
常见误区与迁移建议
一个典型误区是认为“JavaScript代码到处都能跑”。实际上语言核心(如数组方法、Promise)可移植,但宿主API不可移植。有人把前端项目里的localStorage.getItem直接塞进Node脚本,结果得到localStorage is not defined。应当用process.env或文件读写替代。
另一个误区是混用模块系统不报错就代表兼容。有些打包工具(如Webpack)在构建时把Node模块转成了浏览器可用代码,掩盖了差异。但一旦脱离打包直接运行,问题就暴露。建议写库时用条件导出,在package.json的exports字段分别指定import和require入口。
迁移时推荐先列出代码依赖的宿主API,再按环境做适配层。例如网络请求统一封装,浏览器用fetch,Node用axios或undici,上层调用不变。这样既能利用两端优势,又避免环境耦合过深。
JavaScriptNode_jsAPI_difference修改时间:2026-08-17 07:06:12