先把 JavaScript 练顺,再学 TypeScript:一条 Node.js 学习路线
Table of Contents
学 JavaScript 时,很多人会遇到同一类卡点:代码能照着写,却说不清变量为什么在这里访问不到;回调里的 this 为什么变了;异步错误该在哪里处理;几层循环能不能换成更清楚的数据处理流程。
这些问题通常不是多背几条语法就能解决的。更顺的学习顺序是:先理解 JavaScript 如何管理变量、函数和数据,再掌握异步编程;接着用 TypeScript 描述数据结构和函数边界;最后把它们用在 Node.js 后端项目里。
这篇文章会按这条顺序展开。刚开始学编程也可以读;如果变量、条件、循环和函数还不熟,就把每一阶段的练习时间拉长,不必追赶日历上的天数。
先分清三个名字:JavaScript 是编程语言;Node.js 让 JavaScript 能在浏览器之外运行,因此可以用它写后端服务;TypeScript 在 JavaScript 上增加类型检查,开发时帮助发现一部分错误,最终运行的通常仍是生成的 JavaScript。
先看完整路线
这条路线分成四段:
| 阶段 | 时间参考 | 学习重点 | 阶段目标 |
|---|---|---|---|
| 现代 JavaScript | 3 天 | 作用域、箭头函数、异步、数组方法 | 能读懂并写出清楚的现代 JS 代码 |
| TypeScript | 3 天 | 基础类型、对象类型、泛型、类 | 能给函数和数据结构加上合适的类型 |
| Node.js 与 Koa | 4 天 | 模块、事件循环、文件与 HTTP、路由和中间件 | 能开发一个简单的后端 API |
| 项目与部署 | 4 天 | 数据库、验证、认证、日志和部署 | 完成并运行一个小型服务 |
这是一份约两周的高强度参考计划,适合已经会一点编程、每天能集中学习几小时的人。第一次接触编程时,按理解程度放慢更有效,计划本身不是考试期限。
第一段:先把现代 JavaScript 的基本功练扎实
变量、作用域与闭包
作用域决定变量在哪里能被访问。现代 JavaScript 里,通常优先用 const 声明不会重新赋值的变量,需要重新赋值时用 let。var 有函数作用域和变量提升等旧行为,初学时容易带来意外结果,读旧代码时再专门识别它。
const siteName = "Refrain";
let visitCount = 0;
visitCount += 1;
const 固定的是变量绑定,不代表对象内容冻结。例如 const user = { name: "小林" } 之后,user.name 仍可以修改,只是不能把 user 重新指向另一个对象。
闭包可以先理解为:函数记得它创建时周围的变量。它适合保存小段私有状态:
function createCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const nextCount = createCounter();
nextCount(); // 1
nextCount(); // 2
练习时,可以观察 let 和 var 在循环中的区别,再试着写一个计数器,确认外部代码只能通过返回的函数改变计数。
箭头函数与 this
箭头函数让短函数和回调更紧凑:
const square = number => number * number;
const labels = [1, 2, 3].map(number => `第 ${number} 项`);
它与普通函数有一个重要区别:箭头函数不会创建自己的 this,而是沿用外层的 this。因此,它很适合在回调里保留外层上下文;定义对象自身的方法时,通常使用方法简写,让 this 指向调用它的对象:
const user = {
name: "小林",
greet() {
return `你好,我是${this.name}`;
},
};
学习目标不是把所有函数都改成箭头函数,而是能根据 this 的需要选择写法。
Promise、async/await 与错误处理
读取文件、访问接口等操作不会立刻得到结果。Promise 用来表示异步操作最终成功或失败的结果。async/await 是组织这类流程的一种更直观的写法:
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error("用户信息读取失败");
}
return response.json();
}
loadUser(1).catch(error => {
console.error(error);
});
await 会暂停当前这个 async 函数,等待结果;它不会让整个 JavaScript 程序停下来。顺序依赖的操作可以一个接一个 await;彼此独立的操作则可以考虑用 Promise.all 并行等待。无论用哪种写法,都要想清楚失败时由谁处理。
用数组方法表达数据处理
数组方法能把“转换、筛选、查找、汇总”分别写清楚:
map:把每一项转换成新值。filter:留下符合条件的项。find:找出第一个符合条件的项;找不到时结果是undefined。reduce:把多项累积成一个结果,例如总数或分组对象。
比如,计算已付款订单的总额:
const paidTotal = orders
.filter(order => order.paid)
.reduce((sum, order) => sum + order.amount, 0);
链式调用能让数据流一眼可见,但不必为了“函数式”把简单逻辑绕复杂。把步骤命名清楚、方便检查,比少写几行更重要。
第一阶段可以用小任务收尾:学生成绩管理、商品库存检查、异步数据处理,以及对输入数据做验证和转换。每次练习先让程序跑起来,再修改输入、观察结果,并尝试处理边界情况。
第二段:用 TypeScript 把数据说清楚
TypeScript 在 JavaScript 的基础上加入类型检查。开发时,编辑器和编译器可以提前指出一部分类型不匹配的问题;最终运行的仍是 JavaScript。类型能帮助发现错误,但它不能代替测试,也不会自动验证来自网络的原始数据。
从基础类型开始
先认识常用类型:string、number、boolean、数组、联合类型和可选属性。下面的 User 描述了一个用户对象:
interface User {
id: number;
name: string;
email?: string;
}
function formatUser(user: User): string {
return `${user.name}(编号:${user.id})`;
}
问号表示 email 可以不存在。对确实允许多种形态的值,可以用联合类型,例如 number | string。拿到联合类型后,先判断它是哪一种,再使用对应操作。
刚开始不要把 any 当成省事的默认值:它会让类型检查失去作用。不确定数据是什么时,unknown 通常更合适,因为使用前还需要先检查它。
interface、type 与泛型
interface 常用于描述对象有哪些属性;type 可以给对象类型命名,也能表达联合类型等组合关系:
interface Product {
readonly id: number;
name: string;
price: number;
}
type ProductId = number | string;
readonly 表示这个属性创建后不应再被重新赋值。interface 和 type 有不少重叠用法,初学时先能读懂、能准确表达数据,再逐渐了解扩展方式和适用场景。
泛型让函数在复用时保留传入的数据类型。可以把它想成一个带类型标签的容器:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const firstName = first(["小林", "小周"]); // string | undefined
const firstScore = first([88, 95]); // number | undefined
同一个函数能处理不同类型,同时让调用者知道返回值是什么。像 Array<string> 和 Promise<User> 这样的写法,也是在说明容器里装着什么类型的数据。
类与综合练习
基础类型、对象类型和泛型掌握后,再学习类、构造函数、继承、public / private / protected 等访问修饰符。装饰器可以先作为选学内容,不必在入门时急着掌握。
最有用的综合练习,是把已经写过的 JavaScript 小程序逐步改成 TypeScript:给函数参数和返回值加类型,给 API 请求与响应定义结构,再用类型错误来检查数据是在哪里变得不一致。这样能看见类型系统怎样帮助组织代码,而不只是记住一串语法。
入门时可以安装 Node.js 的当前长期支持版本,然后在练习目录安装 TypeScript:
npm init -y
npm install --save-dev typescript
npx tsc --init
npx tsc
在编辑器里把鼠标停在变量上查看推导出的类型,再根据编译错误回到对应代码检查。先写一个能工作的 JavaScript 版本,再逐步补类型,通常比一开始就试图设计一套很复杂的类型更容易。
第三、四段:把语言知识放进后端项目
理解 JavaScript 和 TypeScript 的基础后,就可以继续学 Node.js。建议先弄清模块系统(CommonJS 与 ES Modules)、事件循环、文件系统和 HTTP 模块,再进入 Koa:路由如何接收请求、中间件如何串联、错误如何统一处理。
之后接上数据库,练习连接、增删改查和数据验证;可以用 MongoDB 与 Mongoose 作为练习组合。可以从 Todo API 或简单博客 API 开始,再逐步加入用户认证、日志、环境变量和部署。Docker 可以放到应用本身能够运行之后学习,这样更容易分辨问题来自代码、配置还是运行环境。
一个可完成的小项目,可以包含这些部分:
- 设计几个 REST API 路由,并说明每个请求接收什么、返回什么。
- 把请求参数、数据库记录和响应结果分别定义成类型。
- 加入必要的输入校验和统一错误处理。
- 将配置放入环境变量,记录必要的运行日志。
- 本地运行并验证,再学习如何部署。
类型定义能帮助代码保持一致;处理外部输入时,仍要在运行时校验。客户端发来的 JSON 不会因为你写了一个 TypeScript 类型就自动变得可信。
怎样判断自己学会了
不要只用“看完了几节”衡量进度。可以用下面这些任务自测:
- 能说明
let、const和var的主要区别,并读懂一个闭包计数器。 - 能解释箭头函数的
this从哪里来,知道什么时候该用普通对象方法。 - 能用
async/await写异步流程,并处理失败情况。 - 能用
map、filter、find和reduce完成简单的数据转换与统计。 - 能给函数参数、返回值、对象和 API 数据定义类型。
- 能看懂
Promise<User>这样的类型,并写出一个简单泛型函数。 - 能把这些知识组合成一个小 API,并说明数据从请求到响应经过了什么步骤。
学习时可以重复一个简单循环:先读一个例子,猜它会输出什么;再亲手运行,修改输入,观察差异;遇到错误时读错误信息并定位原因;最后用自己的话记下“我原来误解了什么”。每天写几行真正运行过的代码,比一次读完很多说明更容易留下长期记忆。
延伸阅读
从 JavaScript 到 TypeScript,再到 Node.js 后端,不必试图一次学完所有内容。先把变量、函数、异步和数组处理练熟,再用类型把代码边界说明白,最后通过一个小项目把知识连起来。能解释自己的代码、能定位它的问题,才是这条路线真正要到达的地方。