返回专题首页

Node.js 专题

进入 Node 的世界:REPL、脚本执行、全局对象与运行上下文

很多人第一次接触 Node.js,就是在终端里敲下一行 `node index.js`。从结果上看,这件事似乎非常直接:写一个文件,跑一个文件,终端给你打印结果。但如果只停留在“能跑起来”这个层次,后面你很快就会在 REPL、模块执行、全局变量、路径信息和运行上下文这些地方反复困

Node.js 专题第 03 篇 / 40 篇8 分钟

很多人第一次接触 Node.js,就是在终端里敲下一行 node index.js。从结果上看,这件事似乎非常直接:写一个文件,跑一个文件,终端给你打印结果。但如果只停留在“能跑起来”这个层次,后面你很快就会在 REPL、模块执行、全局变量、路径信息和运行上下文这些地方反复困惑。

这一篇我们会先把 Node.js 程序最基础的执行模型梳理清楚。它不会直接让你写出更复杂的业务代码,但它会帮助你理解:Node 程序到底是怎么开始跑起来的、哪些能力是运行时提供的、一个文件在 Node 里为什么不是简单地从上到下裸奔执行。

从 REPL 到脚本执行:Node 程序到底怎么开始?

学习 Node.js 时,最常见的三种使用方式分别是:

1. 进入交互式环境,也就是 REPL;2. 直接执行一个脚本文件;3. 把某个文件或模块作为项目入口来运行。

这三种方式看起来都在“运行 JavaScript”,但它们适合的场景并不一样。

什么是 REPL

REPLRead-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:二进制数据处理能力;
  • setTimeoutsetInterval:定时调度;
  • console:标准输出与调试打印;
  • 某些模块系统下的 __dirname__filename
  • 以及一整套通过模块导入获得的系统级能力,比如 fspathurl

你可以把它理解成: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 提供的模块上下文里执行;
  • 所以你才能拿到 moduleexportsrequire 以及某些路径变量。

这个认知非常重要。因为它解释了很多看起来“奇怪”的行为,比如:

  • 为什么文件顶层变量不会自动泄漏到别的文件;
  • 为什么模块可以拥有自己的导入导出能力;
  • 为什么路径和模块系统会紧密绑定在一起。

下一篇讲模块系统时,我们会把这件事彻底展开。

入口文件与执行边界是什么关系?

一旦你开始写不止一个文件的 Node 项目,就会遇到一个问题:到底哪个文件才是真正的入口?

在最简单的场景里,答案就是你在终端里执行的那个文件,比如:

node index.js

这意味着 index.js 是当前程序的启动入口,它会去导入别的模块、组装配置、启动服务或者执行任务。

但随着项目规模扩大,入口文件的意义会越来越重要。因为它决定了:

  • 程序从哪里开始;
  • 哪些初始化逻辑应该最先发生;
  • 哪些模块只是被依赖,哪些模块真正负责启动系统。

这也是为什么很多项目都会显式保留一个很薄的入口层。它本身不承载复杂业务,而是负责:

  • 读取配置;
  • 组装依赖;
  • 注册全局能力;
  • 启动真正的服务或任务流程。

你可以把它理解成“应用启动边界”。这条边界越清楚,后面的模块组织就越清楚。

总结

这一篇我们先把 Node.js 的运行入口世界梳理了一遍,包括 REPL、脚本执行、运行时提供的全局能力,以及模块作用域与入口边界这些高频但很容易被忽略的基础认知。

真正重要的不是记住某个对象名,而是建立这样一个心智:Node.js 不是“在终端里跑 JavaScript 文件”这么简单,它是一个会为你的代码提供运行时上下文、模块边界和系统级能力的完整环境。

下一篇我们会继续往前走,把 Node.js 中最容易踩坑也最值得尽早搞懂的话题彻底展开,也就是模块系统:CommonJSESM 和它们背后的加载规则。