1.1 JavaScript、TypeScript 与 Node.js
本章解决什么问题:JavaScript、TypeScript、Node.js 这三个名字经常被混在一起说。本章把它们各自是什么、彼此是什么关系一次讲清,并让你在自己的电脑上装好 Node.js、跑通第一条命令。
前置知识:无——这是全书第一章。如果你还没决定按什么顺序读本书,可以先看阅读指南与学习路线。
学习目标:读完本章后你能
- 用一句话说清 JavaScript、Node.js、TypeScript 各自指什么;
- 解释浏览器里的 JavaScript 和 Node.js 里的 JavaScript 差别在哪一层;
- 说出大型项目(比如 Pi)为什么选择 TypeScript;
- 理解
.ts文件必须先转换成 JavaScript 才能被执行;- 在自己电脑上安装 Node.js 并验证它能工作。
建立直觉
先给出一句话版本,后面每一节再展开:
- JavaScript 是一门编程语言,就像 Python、Java 是编程语言一样;
- Node.js 是一个能在浏览器之外执行 JavaScript 的程序,也就是一个「运行时」;
- TypeScript 是在 JavaScript 之上加了一层类型标注的「加强版」,执行之前要先把类型去掉、变回 JavaScript。
一个可能有帮助的类比:把 JavaScript 想成一份「乐谱」的记谱规则,浏览器和 Node.js 是两支都识谱、但配置不同的「乐队」——浏览器这支乐队擅长演「网页」这类曲目,Node.js 这支擅长演「操作文件、跑命令、连网络」这类曲目。TypeScript 则像是在乐谱上多写了一层校对批注:排练时用它挑错,正式演出前批注会被擦掉,乐队看到的仍然是标准乐谱。
JavaScript:从网页脚本到通用语言
JavaScript 诞生于 1995 年,最初是为网景(Netscape)浏览器设计的小脚本语言,用来让网页「动起来」:响应点击、校验表单、修改页面内容。1997 年起,这门语言以 ECMAScript 的名字被标准化——所以你以后看到「ES2022」这样的写法,指的就是 JavaScript 标准的某个年度版本。
三十年过去,JavaScript 早已不只属于网页。它有几个对本书重要的特点:
- 即写即跑、上手成本低:不需要开发者先手动编译出可执行文件,写完直接交给运行时执行(引擎内部如何加速执行,是运行时自己的事);
- 动态类型:变量不声明类型,一个变量这一刻装数字、下一刻可以装字符串——灵活,但也埋了坑(下文 TypeScript 一节会演示这个坑);
- 生态巨大:世界上最大的开源包仓库 npm 属于 JavaScript 生态(npm 是什么,1.3 npm、package.json 与项目结构专门讲)。
Pi 这样的 Agent 程序选择这个生态,看中的正是「同一门语言可以写命令行工具、写服务器、写界面」以及庞大的现成库。
Node.js:浏览器之外的 JavaScript 运行时
它是什么:Node.js 诞生于 2009 年,核心思路可以概括成一句话——把 Chrome 浏览器里负责执行 JavaScript 的 V8 引擎单独拿出来,包上一层操作系统能力,做成一个独立程序。装好之后,你在终端输入 node,就能在浏览器之外执行 JavaScript。
为什么需要它:浏览器出于安全考虑,严格限制网页里的 JavaScript——不允许它随意读写你电脑上的文件、不允许它启动别的程序。这对网页是合理的(你不会希望随便打开一个网站就能翻你的硬盘),但对 Pi 这样的程序是致命的:一个编程 Agent 的日常工作恰恰就是读写文件、执行命令、发起网络请求。所以 Pi 必须运行在浏览器之外,而 Node.js 正是让 JavaScript「走出浏览器」的运行时。
没有它会怎样:JavaScript 将只能困在网页里,Pi 要么换一门语言写,要么根本不存在这类用 JavaScript 生态构建的命令行工具。
浏览器 JS 和 Node.js JS 的区别在哪:语言本身是同一门(同样的语法、同样的 if/for/函数),差别在运行时各自附赠的 API(Application Programming Interface,应用编程接口——运行时提供给你的代码调用的一组现成能力)上:
| 浏览器运行时 | Node.js 运行时 | |
|---|---|---|
| 面向场景 | 网页 | 命令行工具、服务器 |
| 特有 API | DOM、window、页面事件 | 文件(node:fs)、进程(process)、网络服务(node:http) |
| 没有的东西 | 不能随意读写本地文件 | 没有 DOM,没有页面 |
这里的 DOM(Document Object Model,文档对象模型)指浏览器把网页内容组织成的一棵可以用代码增删改的树——它只在浏览器里存在,Node.js 里没有网页,自然也没有 DOM。
Node.js 的最小体验只需要一行。装好 Node.js 之后(安装步骤见本章末尾的实践任务),在终端执行:
node -e "console.log(1 + 1)"预期输出:
2-e 的意思是执行(evaluate)引号里的这段代码;console.log 是 JavaScript 里「向终端打印一行」的标准写法,相当于其他语言里的 print。
TypeScript:给 JavaScript 加一层类型
它是什么:TypeScript 是微软在 2012 年发布的语言,是 JavaScript 的超集——在语法层面,合法的 JavaScript 代码就是合法的 TypeScript 代码;TypeScript 在此基础上增加了一样核心能力:类型标注与编译期类型检查。文件后缀是 .ts。
为什么需要它:先看动态类型的坑。把下面内容存成 add.js:
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
console.log(add(1, "2"));用 node add.js 运行,真实输出是:
3
12第二行没有报错,而是悄悄输出了 12——因为 "2" 是字符串,JavaScript 遇到「数字 + 字符串」时会把数字也转成字符串再拼接,1 + "2" 变成了 "12"。程序继续带着这个错误的值往下跑,等你发现时,出错的地方可能离真正的原因已经很远了。项目只有几十行时你盯一眼就能看出来;但 Pi 的源码有九百多个文件(下文有统计),靠肉眼防这种错误不现实。
TypeScript 的做法是让你把「预期」写进代码。同样的函数,存成 add.ts:
function add(a: number, b: number): number {
return a + b;
}
console.log(add(1, 2));
console.log(add(1, "2"));a: number 读作「参数 a 必须是数字」,最后的 : number 表示「返回值是数字」。用 TypeScript 编译器 tsc 检查这个文件(先不必跟着做,1.2 会带你搭好环境):
tsc --noEmit add.ts真实输出:
add.ts(6,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.意思是:第 6 行第 20 列,把字符串传给了要求数字的参数。错误在程序运行之前就被抓住了,而不是等它变成运行结果里一个悄无声息的 "12"。删掉出错的那行后再运行,就能正常得到 3。
除了提前抓错,类型还有两个在大型项目里更重要的价值:
- 改代码有保障:改动一个函数的参数,所有传错的调用处会立刻全部报错——相当于免费的全量检查;
- 编辑器更聪明:类型让编辑器准确知道每个变量上有什么属性、每个函数要什么参数,自动补全和跳转定义都建立在类型之上。
没有它会怎样:几十万行的 JavaScript 项目并非不能写,但每次重构都像在没有安全网的地方走钢丝,大量本可编译期发现的错误被推迟到运行时。这也是 Pi 选择全程 TypeScript 的原因。
关键限制:Node.js 执行的是 JavaScript,所以 .ts 文件必须先经过 TypeScript 工具链处理——检查类型,然后把类型标注抹掉(这一步常叫「编译」或「转换」),产出普通 JavaScript,再交给 Node.js 执行:
图 1.1-1 一个 .ts 文件从源码到运行的完整路径
阅读顺序:从左到右。注意中间的分叉——类型检查不通过就到不了右边;检查通过后,产出的 add.js 里已经没有任何类型标注,Node.js 对「类型」一无所知。
这张图揭示了一个初学者常踩的认知误区:类型只活在编译期。
图解:一门语言、两种运行时、一层类型
把本章三个名字放进同一张图:
图 1.1-2 JavaScript、TypeScript 与两种运行时的关系
阅读顺序:从上往下。最上层是语言:TypeScript 编译成 JavaScript。中间是两种运行时:同一门语言,进了不同运行时就获得不同的 API。底部虚线表示 Chrome 与 Node.js 共用 V8 引擎。Pi 走的是右边这条路:TypeScript 写成,编译成 JavaScript,在 Node.js 里运行。
读这张图时抓一个主线:语言是同一门,环境决定能力。左下的浏览器路径本书基本不会再碰;右下「Node.js + 系统 API」这条路径就是 Pi 的立足之地,第 2.8 章会专门介绍这些系统 API。
Pi 中哪里用到了它
Pi 是一个纯 TypeScript 的 monorepo(单仓库多包:一个代码仓库里装了多个相互关联的 package,1.3 会正式介绍这个概念,4.3 monorepo 与 package 地图再看 Pi 的具体划分)。从源码结构看,统计本书锁定版本的 packages/ 目录(排除依赖与构建产物),.ts 源文件有九百多个,而 .js 文件只有个位数——「Pi 就是用 TypeScript 写成的」在源码里一目了然。
Pi 对运行时的要求也白纸黑字写在仓库根部的 package.json 里:
engines而整个仓库的 TypeScript 编译选项由根部的 tsconfig.base.json 统一规定,其中开启了最严格的 strict 模式:
这两个文件本身是什么、怎么读,分别是 1.3 和 2.1 之后章节的内容;现在你只需要知道:本章讲的三个名字,在 Pi 仓库的第一层目录里就全部出现了。
实践任务
目标:在你自己的电脑上装好 Node.js(版本 ≥ 20),并验证它能执行 JavaScript。本书后续所有实验都建立在这一步之上。
步骤:
打开 nodejs.org,下载 LTS(长期支持)版安装包并安装。写作本章时 LTS 主版本已 ≥ 22,正常下载即可满足要求;由于 Pi 自身要求 Node ≥ 22.19(见上一节的 SourceRef),建议直接装最新 LTS。macOS 用户也可以用
brew install node;需要多版本共存的读者可以了解 nvm,这里不展开。重新打开一个终端窗口(让安装程序对环境的修改生效),运行:
shnode --version再运行一行真正的 JavaScript:
shnode -e "console.log(1 + 1)"
预期现象:第 2 步输出一个以 v 开头的版本号。作者机器上的真实输出是 v26.5.0,你的数字几乎肯定不同——只要 v 后面的主版本号 ≥ 20 就合格。第 3 步输出:
2如何判断成功:两条命令都有输出、没有报错,且主版本号 ≥ 20。
常见错误:
command not found: node(Windows 上是'node' 不是内部或外部命令):Node.js 没装上,或者你还在用安装之前打开的旧终端窗口。先重开终端;仍不行就重装,Windows 安装时确认没有取消勾选「Add to PATH」。- 版本过旧(比如输出
v12.22.0):常见于用系统自带包管理器装出旧版的 Linux 用户。到官网重新下载,或用 nvm 安装:nvm install 22。 zsh: command not found: brew(macOS):你没有安装过 Homebrew,忽略这条路线,直接用官网安装包即可。
对应源码位置:Pi 对 Node 版本的要求见 package.json 的 engines 字段(上一节的 SourceRef)。
本章小结
- JavaScript 是一门起源于浏览器、如今通用的动态类型语言,标准名为 ECMAScript。
- 运行时决定代码的能力边界:浏览器给 JavaScript 配了 DOM 等页面 API;Node.js 基于 V8 引擎,给它配了文件、进程、网络等系统 API——Pi 需要的正是后者。
- TypeScript 是 JavaScript 的超集,价值在编译期类型检查;类型在编译时被抹掉,运行时并不存在。
.ts源码的路径是:TypeScript 工具检查并转换 → 纯 JavaScript → Node.js 执行。- Pi 是纯 TypeScript monorepo,要求 Node.js ≥ 22.19.0,全仓库开启 strict 严格模式。
关键术语:JavaScript / ECMAScript、运行时(Runtime)、Node.js、V8、DOM(文档对象模型)、TypeScript、超集、编译(类型擦除)、monorepo(单仓库多包)
关键源码索引:package.json 的 engines.node;tsconfig.base.json 的 strict
自测问题:
- 浏览器里的 JavaScript 和 Node.js 里的 JavaScript 是同一门语言吗?它们的差别出现在哪一层?
add(1, "2")在纯 JavaScript 里为什么不报错?TypeScript 是在什么时机、用什么方式抓住这个错误的?- 「TypeScript 的类型在运行时保护程序」这句话对吗?为什么?
- Pi 要求的最低 Node.js 版本是多少?写在仓库的哪个文件里?
下一章预告:1.2 运行第一个 TypeScript 程序——动手搭一个最小的 TypeScript 项目,认识 tsx,把本章「看懂」的例子亲手跑起来。