很多人第一次接触 Node.js,就是在终端里敲下一行 node index.js。从结果上看,这件事似乎非常直接:写一个文件,跑一个文件,终端给你打印结果。但如果只停留在“能跑起来”这个层次,后面你很快就会在 REPL、模块执行、全局变量、路径信息和运行上下文这些地方反复困惑。
这一篇我们会先把 Node.js 程序最基础的执行模型梳理清楚。它不会直接让你写出更复杂的业务代码,但它会帮助你理解:Node 程序到底是怎么开始跑起来的、哪些能力是运行时提供的、一个文件在 Node 里为什么不是简单地从上到下裸奔执行。
从 REPL 到脚本执行:Node 程序到底怎么开始?
学习 Node.js 时,最常见的三种使用方式分别是:
1. 进入交互式环境,也就是 REPL;2. 直接执行一个脚本文件;3. 把某个文件或模块作为项目入口来运行。
这三种方式看起来都在“运行 JavaScript”,但它们适合的场景并不一样。
什么是 REPL
REPL 是 Read-Eval-Print Loop 的缩写。你可以把它简单理解成:Node.js 提供的一个交互式试验场。只要在终端输入:
node你就会进入一个可以即时输入 JavaScript 代码并立刻看到结果的环境。
它特别适合做这些事情:
- 快速试一个表达式是否成立;
- 看某个内置对象大致长什么样;
- 验证字符串、数组、Promise 之类的小片段行为;
- 临时观察某个 API 的返回结构。
REPL 的优点在于反馈非常快。你不需要新建文件,也不需要准备项目结构,输入一行就能看到结果。对刚开始认识 Node 运行时的人来说,这种即时反馈很有帮助。
但 REPL 的边界也很明显。它更适合“试验”而不是“沉淀”。一旦你开始编写多步骤逻辑、函数组织、模块导入或需要反复运行的代码,REPL 就不再是最合适的场景。
所以,一个更稳妥的使用原则是:
- 想快速试验某个点子,用 REPL;
- 想保留代码、反复执行、组织结构,回到脚本文件。
直接执行脚本文件
最常见的 Node 启动方式,仍然是直接执行一个 .js 文件:
node index.js这一方式的优点非常直观:
- 命令简单;
- 结果直接;
- 很适合学习初期和单文件脚本场景。
但从工程视角看,你要逐渐意识到:你并不是“把一个文件扔给系统直接运行”,而是在调用 Node 运行时,让它读取这个文件、解析其中的代码、建立模块上下文,然后再执行。
换句话说,Node.js 不是一个“执行纯文本文件”的壳,而是一个真正的 JavaScript 运行时。它会为你的代码提供一整套浏览器里没有的能力,比如文件系统访问、进程信息、命令行参数、环境变量和模块加载机制。
也正因为如此,Node 程序的执行方式会直接影响后面的很多问题,比如:
- 当前工作目录到底是谁;
- 脚本里能拿到哪些全局信息;
- 路径和模块导入会如何解析;
- 文件在运行时究竟处于怎样的作用域中。
Node.js 提供了哪些浏览器里没有的全局能力?
很多前端开发者在第一次用 Node.js 时,会产生一种熟悉又陌生的感觉。熟悉的是语言本身仍然是 JavaScript;陌生的是环境突然多出了一大批浏览器里没有的能力。
这也是理解 Node.js 的关键:你写的还是 JavaScript,但它现在运行在一个完全不同的宿主环境里。
globalThis 仍然存在,但宿主能力已经变了
在现代 JavaScript 里,globalThis 仍然是统一的全局对象入口。也就是说,无论在浏览器还是在 Node 中,你都可以通过它访问全局作用域上的一些能力。
但真正重要的不是“有没有全局对象”,而是 全局对象上挂了什么。
在 Node 里,你会频繁接触这些运行时能力:
process:当前进程的信息与控制入口;Buffer:二进制数据处理能力;setTimeout、setInterval:定时调度;console:标准输出与调试打印;- 某些模块系统下的
__dirname、__filename; - 以及一整套通过模块导入获得的系统级能力,比如
fs、path、url。
你可以把它理解成:Node.js 把 JavaScript 从“页面逻辑环境”带到了“操作系统与服务端环境”。
process 是理解 Node 的关键入口
如果说浏览器中的 window 很有代表性,那么 Node.js 里最值得尽早建立熟悉感的对象通常就是 process。
它承担了很多和“当前运行实例”有关的信息,比如:
- 命令行参数;
- 环境变量;
- 当前工作目录;
- 平台信息;
- 退出码;
- 标准输入输出流。
也就是说,当你开始写脚本、CLI 或服务端程序时,很多与“程序是如何被启动、当前处于什么环境”相关的问题,最后都会回到 process 这个入口上。
__dirname 和 __filename 不是所有时候都存在
很多初学者第一次看到 __dirname 和 __filename 时,会把它们当作 Node.js 永远可用的内建变量。但更准确一点说,它们是 特定模块上下文里的能力。在传统的 CommonJS 环境里,它们非常常见;但在原生 ESM 中,这两个变量并不会直接存在。
这一点现在先有个印象就够了。你不需要一开始就把所有模块差异都掌握,但要先知道:Node.js 中很多“看起来像全局变量”的东西,其实都和当前采用的模块系统有关。
一个 Node 文件为什么不是简单“顺着跑完”?
很多人学 Node.js 时,会自然地把脚本文件理解成“从上到下执行的一段代码”。这个理解不能算错,但它不完整。真正重要的是:一个 Node 文件并不是裸奔在全局环境里的,它实际上运行在受控的模块上下文中。
模块作用域意味着什么
如果你在浏览器里直接写一个 <script> 标签,某些变量可能会意外挂到全局环境上;但在 Node 里,一个文件通常会被当作独立模块处理。这意味着:
- 你在文件顶层声明的变量,默认不会自动变成整个应用的全局变量;
- 每个文件都有自己的局部模块边界;
- 文件之间的协作通常依赖导入导出,而不是彼此偷用全局状态。
这种设计对工程化非常重要。因为只要项目开始变大,你就会发现,“每个文件都是一个受控模块”比“所有文件共享一个松散全局空间”稳定得多。
CommonJS 中最直观的理解方式
在传统 CommonJS 模型里,一个文件之所以看起来有自己的模块边界,是因为 Node 会把它放进一个受控的包装函数环境中。你不需要一开始就记住具体实现细节,但可以先建立这样一个心智:
- 当前文件不是直接摊在全局上执行;
- 它是在 Node 提供的模块上下文里执行;
- 所以你才能拿到
module、exports、require以及某些路径变量。
这个认知非常重要。因为它解释了很多看起来“奇怪”的行为,比如:
- 为什么文件顶层变量不会自动泄漏到别的文件;
- 为什么模块可以拥有自己的导入导出能力;
- 为什么路径和模块系统会紧密绑定在一起。
下一篇讲模块系统时,我们会把这件事彻底展开。
入口文件与执行边界是什么关系?
一旦你开始写不止一个文件的 Node 项目,就会遇到一个问题:到底哪个文件才是真正的入口?
在最简单的场景里,答案就是你在终端里执行的那个文件,比如:
node index.js这意味着 index.js 是当前程序的启动入口,它会去导入别的模块、组装配置、启动服务或者执行任务。
但随着项目规模扩大,入口文件的意义会越来越重要。因为它决定了:
- 程序从哪里开始;
- 哪些初始化逻辑应该最先发生;
- 哪些模块只是被依赖,哪些模块真正负责启动系统。
这也是为什么很多项目都会显式保留一个很薄的入口层。它本身不承载复杂业务,而是负责:
- 读取配置;
- 组装依赖;
- 注册全局能力;
- 启动真正的服务或任务流程。
你可以把它理解成“应用启动边界”。这条边界越清楚,后面的模块组织就越清楚。
总结
这一篇我们先把 Node.js 的运行入口世界梳理了一遍,包括 REPL、脚本执行、运行时提供的全局能力,以及模块作用域与入口边界这些高频但很容易被忽略的基础认知。
真正重要的不是记住某个对象名,而是建立这样一个心智:Node.js 不是“在终端里跑 JavaScript 文件”这么简单,它是一个会为你的代码提供运行时上下文、模块边界和系统级能力的完整环境。
下一篇我们会继续往前走,把 Node.js 中最容易踩坑也最值得尽早搞懂的话题彻底展开,也就是模块系统:CommonJS、ESM 和它们背后的加载规则。