先把 JavaScript 练顺,再学 TypeScript:一条 Node.js 学习路线

#JavaScript #TypeScript #Node.js #学习路线

Table of Contents

学 JavaScript 时,很多人会遇到同一类卡点:代码能照着写,却说不清变量为什么在这里访问不到;回调里的 this 为什么变了;异步错误该在哪里处理;几层循环能不能换成更清楚的数据处理流程。

这些问题通常不是多背几条语法就能解决的。更顺的学习顺序是:先理解 JavaScript 如何管理变量、函数和数据,再掌握异步编程;接着用 TypeScript 描述数据结构和函数边界;最后把它们用在 Node.js 后端项目里。

这篇文章会按这条顺序展开。刚开始学编程也可以读;如果变量、条件、循环和函数还不熟,就把每一阶段的练习时间拉长,不必追赶日历上的天数。

先分清三个名字:JavaScript 是编程语言;Node.js 让 JavaScript 能在浏览器之外运行,因此可以用它写后端服务;TypeScript 在 JavaScript 上增加类型检查,开发时帮助发现一部分错误,最终运行的通常仍是生成的 JavaScript。

先看完整路线

这条路线分成四段:

阶段时间参考学习重点阶段目标
现代 JavaScript3 天作用域、箭头函数、异步、数组方法能读懂并写出清楚的现代 JS 代码
TypeScript3 天基础类型、对象类型、泛型、类能给函数和数据结构加上合适的类型
Node.js 与 Koa4 天模块、事件循环、文件与 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 可以放到应用本身能够运行之后学习,这样更容易分辨问题来自代码、配置还是运行环境。

一个可完成的小项目,可以包含这些部分:

  1. 设计几个 REST API 路由,并说明每个请求接收什么、返回什么。
  2. 把请求参数、数据库记录和响应结果分别定义成类型。
  3. 加入必要的输入校验和统一错误处理。
  4. 将配置放入环境变量,记录必要的运行日志。
  5. 本地运行并验证,再学习如何部署。

类型定义能帮助代码保持一致;处理外部输入时,仍要在运行时校验。客户端发来的 JSON 不会因为你写了一个 TypeScript 类型就自动变得可信。

怎样判断自己学会了

不要只用“看完了几节”衡量进度。可以用下面这些任务自测:

  • 能说明 let、const 和 var 的主要区别,并读懂一个闭包计数器。
  • 能解释箭头函数的 this 从哪里来,知道什么时候该用普通对象方法。
  • 能用 async/await 写异步流程,并处理失败情况。
  • 能用 map、filter、find 和 reduce 完成简单的数据转换与统计。
  • 能给函数参数、返回值、对象和 API 数据定义类型。
  • 能看懂 Promise<User> 这样的类型,并写出一个简单泛型函数。
  • 能把这些知识组合成一个小 API,并说明数据从请求到响应经过了什么步骤。

学习时可以重复一个简单循环:先读一个例子,猜它会输出什么;再亲手运行,修改输入,观察差异;遇到错误时读错误信息并定位原因;最后用自己的话记下“我原来误解了什么”。每天写几行真正运行过的代码,比一次读完很多说明更容易留下长期记忆。

延伸阅读

从 JavaScript 到 TypeScript,再到 Node.js 后端,不必试图一次学完所有内容。先把变量、函数、异步和数组处理练熟,再用类型把代码边界说明白,最后通过一个小项目把知识连起来。能解释自己的代码、能定位它的问题,才是这条路线真正要到达的地方。