JavaScript / TypeScript
目标:用 2–3 小时从零到能写一个可运行的小功能,并掌握最常用语法、API 与工程化最小配置。
结构:概念 → 示例 → 练习 → 自测
一、概念(80/20 清单)
1) 语言与运行时(JS 必修)
- 
变量与作用域(
let/const/var):默认用const;用let表示会变;避免var以防函数作用域与提升带来的坑。 - 
值与类型:基本类型(string/number/boolean/null/undefined/symbol/bigint)与引用类型(object/function);用
typeof与Array.isArray做快速判断。 - 
== 与 ===:总用
===与!==,避免隐式类型转换。 - 
对象与原型:对象通过原型链共享方法;
Object.create/class都依赖原型机制。 - 
函数与箭头函数:箭头函数没有自己的
this/arguments;用来保持外层this。 - 
闭包:函数“记住”其定义时的词法环境;常用于工厂函数与私有状态。
 - 
模块(ESM):用
import/export组织代码,浏览器与 Node 均支持 ESM。 - 
异步(Promise/async/await):
await语法糖基于 Promise;微任务(Promise)优先于宏任务(setTimeout)。 - 
错误处理:同步用
try/catch;异步await外层也要try/catch或在链末尾.catch。 - 
数组常用方法:
map/filter/reduce/some/every/find覆盖 80% 处理场景。 - 
对象操作:解构与展开(
{...obj})、Object.assign做浅拷贝与合并。 - 
JSON:
JSON.parse/JSON.stringify是前后端传输与持久化的基石。 - 
DOM 与事件:
querySelector/addEventListener;事件委托用e.target.closest()。 - 
网络请求(Fetch):
fetch(url).then(r=>r.json());注意非 2xx 不会抛错需手动判断。 - 
存储:
localStorage/sessionStorage保存小量数据;序列化与容量限制要注意。 - 
URL 与查询参数:
URL和URLSearchParams用于拼接与解析链接。 - 
定时器:
setTimeout/clearTimeout与setInterval/clearInterval;防抖/节流均基于它们。 - 
集合类型:
Map/Set/WeakMap/WeakSet适合键值映射与去重。 - 
国际化(Intl):
Intl.DateTimeFormat/NumberFormat本地化展示数字与日期。 
参考:MDN Web Docs(语言、Web API),ECMAScript 规范(语言语义)。
2) 类型系统(TS 必修)
- 
类型注解与类型推断:TS 会自动推断;只在边界(入参/返回值/公共接口)显式标注。
 - 
接口与类型别名(
interface/type):两者都可描述对象形状;需要交叉/联合/映射类型时更常用type。 - 
联合与交叉(
|/&):联合表示“可能是其中之一”,交叉表示“同时满足多个类型”。 - 
字面量与枚举:用字面量联合 +
as const代替enum,便于 tree-shaking。 - 
泛型(
<T>):把“类型参数化”以复用;为返回值与入参建立对应关系。 - 
类型守卫:
typeof、in、instanceof、自定义谓词(arg is X)用于在分支中缩窄类型。 - 
any与unknown:any放弃检查仅作逃生阀;unknown需先缩窄后使用更安全。 - 
never与void:never表示不可能到达的分支;void表示无返回值。 - 
可选链与空值合并(
?./??):安全访问与为null/undefined提供默认值。 - 
类型断言:
as是“我保证”;应作为最后手段。 
参考:TypeScript Handbook、tsconfig Reference。
3) 必备 API / 配置(≤2 句/条)
- 
DOM 选择/遍历:
document.querySelector(All),element.closest。 - 
事件:
addEventListener(type, handler, { capture: false });解绑用removeEventListener。 - 
Fetch:
fetch(url, { method, headers, body });先if(!res.ok) throw …再await res.json()。 - 
存储:
localStorage.getItem/setItem/removeItem/clear;注意都以字符串形式存储。 - 
URL:
new URL(url, base);new URLSearchParams(obj)。 - 
计时:
performance.now()高精度计时。 - 
控制台调试:
console.log/table/group/time与debugger。 - 
tsconfig 关键项:
"strict": true、"target": "ES2020"、"module": "ES2020"、"lib": ["ES2020","DOM"]、"sourceMap": true、"outDir": "dist"。 - 
包管理与脚本:
npm init -y、npx临时执行、npm run build定义编译脚本。 
4) 典型应用场景与设计取舍
- 
直接 DOM vs 框架:小功能/嵌入式页面优先原生 DOM;复杂状态/组件化考虑框架。
 - 
Fetch vs 第三方库:浏览器端优先原生 Fetch;需要拦截器/重试/超时可加轻量封装。
 - 
本地存储 vs 服务端:配置/小缓存用
localStorage;可变共享数据走接口并考虑缓存失效。 - 
any速度 vs 类型安全:开发快可以局部放宽,但在模块边界恢复严格类型。 - 
事件直绑 vs 委托:大量子元素动态增删时用事件委托(绑定在父元素上)。
 - 
ESM 原生加载 vs 打包:学习与演示用原生 ESM;上线常配合打包/压缩以兼容与性能。
 
5) 内部规范(建议)
- 
默认
strict+ 禁止any(必要时集中隔离);导出 API 必须有类型。 - 
代码风格:
const优先、不可变更新({...obj}/arr.map)、统一使用===。 - 
错误与返回约定:有可能失败的函数返回
Result<T> = {ok:true,data}|{ok:false,error}或抛出明确错误。 - 
- *
 
 
二、最小可运行示例(从零开始)
技术点覆盖:模块、DOM、事件委托、
localStorage、async/await、基本类型与类型缩窄。
效果:一个极简 Todo 应用(可添加/勾选/删除,自动持久化)。
0) 环境准备
- 
安装 Node.js ≥ 18 与一款现代浏览器(Chrome/Edge/Firefox/Safari)。
 - 
随机本地静态服务器任选其一:
npx http-server/npx serve/python3 -m http.server(后续给出命令)。 
1) 初始化项目
mkdir mini-ts-todo && cd mini-ts-todo
npm init -y
npm i -D typescript
npx tsc --init
2) 替换 tsconfig.json(最小可用严格配置)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "noEmitOnError": true
  },
  "include": ["src"]
}
3) 新建页面 index.html
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>TS 最小示例:Todo</title>
  <style>
    html,body{font-family:system-ui,-apple-system,Segoe UI,Roboto,'Noto Sans SC',sans-serif;margin:0}
    .container{max-width:720px;margin:40px auto;padding:0 16px}
    h1{font-size:1.2rem}
    form{display:flex;gap:8px;margin:12px 0}
    input[type="text"]{flex:1;padding:8px;border:1px solid #ccc;border-radius:6px}
    button{padding:8px 12px;border-radius:6px;border:1px solid #ccc;background:#f7f7f7;cursor:pointer}
    ul{list-style:none;padding:0}
    li{display:flex;align-items:center;gap:8px;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee}
    label{display:flex;align-items:center;gap:8px;flex:1}
    .delete{background:transparent;border:none;font-size:1rem}
  </style>
</head>
<body>
  <div class="container">
    <h1>Todo(TypeScript + DOM + 本地存储)</h1>
    <form id="todo-form" autocomplete="off">
      <input id="todo-input" type="text" placeholder="输入任务并回车…" />
      <button type="submit">添加</button>
    </form>
    <p id="stats" aria-live="polite"></p>
    <ul id="todo-list"></ul>
  </div>
  <!-- 使用编译后的 ESM 输出 -->
  <script type="module" src="./dist/main.js"></script>
</body>
</html>
4) 新建入口 src/main.ts(含详细注释)
// --- 类型定义:每条 todo 的形状(TypeScript 核心:为边界定义类型) ---
type Todo = {
  id: string;
  title: string;
  done: boolean;
  createdAt: number;
};
// 生成稳定的 id(优先用 Web Crypto;不支持时回退到时间戳+随机)
const newId = () =>
  (globalThis.crypto && "randomUUID" in globalThis.crypto)
    ? (globalThis.crypto as any).randomUUID()
    : Date.now().toString(36) + Math.random().toString(36).slice(2);
// --- DOM 查询助手:失败即抛错,避免后续空指针 ---
const qs = <T extends Element>(sel: string, root: Document | Element = document) => {
  const el = root.querySelector(sel);
  if (!el) throw new Error(`Not found: ${sel}`);
  return el as T;
};
const $list = qs<HTMLUListElement>('#todo-list');
const $form = qs<HTMLFormElement>('#todo-form');
const $input = qs<HTMLInputElement>('#todo-input');
const $stats = qs<HTMLParagraphElement>('#stats');
// --- 本地存储 ---
function load(): Todo[] {
  try {
    const raw = localStorage.getItem('todos');
    if (!raw) return [];
    const data: unknown = JSON.parse(raw);
    if (!Array.isArray(data)) return [];
    // 轻量“校验/清洗”,将未知转为我们需要的形状
    return (data as any[]).map(x => ({
      id: String(x?.id ?? newId()),
      title: String(x?.title ?? ''),
      done: Boolean(x?.done),
      createdAt: Number(x?.createdAt ?? Date.now())
    })).filter(t => t.title !== '');
  } catch (e) {
    console.warn('Failed to read todos:', e);
    return [];
  }
}
function save(todos: Todo[]) {
  localStorage.setItem('todos', JSON.stringify(todos));
}
// --- 应用状态(单一真相源) ---
let todos: Todo[] = load();
render();
// --- 事件:提交表单添加 todo(阻止默认提交刷新) ---
$form.addEventListener('submit', ev => {
  ev.preventDefault();
  const title = $input.value.trim();
  if (!title) return;
  todos = [{ id: newId(), title, done: false, createdAt: Date.now() }, ...todos];
  save(todos);
  $input.value = '';
  render();
});
// --- 事件委托:在 ul 上监听,处理复选与删除 ---
$list.addEventListener('click', ev => {
  const target = ev.target as HTMLElement;
  const li = target.closest('li[data-id]') as HTMLLIElement | null;
  if (!li) return;
  const id = li.dataset.id!;
  if (target.matches('input[type="checkbox"]')) {
    toggle(id);
  } else if (target.matches('button.delete')) {
    remove(id);
  }
});
// --- 操作 ---
function toggle(id: string) {
  todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
  save(todos);
  render();
}
function remove(id: string) {
  todos = todos.filter(t => t.id !== id);
  save(todos);
  render();
}
// --- 渲染:最小化 DOM 操作(一次性 innerHTML 更新) ---
function render() {
  $list.innerHTML = todos.map(t => `
    <li data-id="${t.id}">
      <label>
        <input type="checkbox" ${t.done ? 'checked' : ''}>
        <span ${t.done ? 'style="text-decoration: line-through; opacity:.7"' : ''}>
          ${escapeHtml(t.title)}
        </span>
      </label>
      <button class="delete" aria-label="delete">🗑️</button>
    </li>
  `).join('');
  const total = todos.length;
  const done = todos.filter(t => t.done).length;
  $stats.textContent = `总数:${total},已完成:${done}`;
}
// --- 防 XSS:简单转义(插入 innerHTML 前务必转义外部字符串) ---
function escapeHtml(s: string) {
  const map: Record<string, string> = {
    '&': '&', '<': '<', '>': '>', '"': '"', "'": '''
  };
  return s.replace(/[&<>"']/g, ch => map[ch]);
}
// --- 异步演示(事件循环微任务/宏任务):控制台会看到顺序 ---
async function boot() {
  console.log('应用启动');
  setTimeout(() => console.log('宏任务:setTimeout'), 0);
  queueMicrotask(() => console.log('微任务:queueMicrotask'));
  await delay('TS 已就绪 ✅(查看控制台输出)', 200);
  console.log('异步:', 'TS 已就绪 ✅(查看控制台输出)');
}
function delay<T>(v: T, ms: number) {
  return new Promise<T>(resolve => setTimeout(() => resolve(v), ms));
}
boot();
5) 编译与运行
# 终端 A:持续编译
npx tsc -w
# 终端 B:启本地静态服务器(任选其一)
npx http-server -c-1 .
# 或:npx serve .
# 或:python3 -m http.server
打开控制台观察日志,尝试添加/勾选/删除 Todo,刷新后数据仍在(localStorage 持久化)。
6) 常见坑与调试技巧(与示例相关)
- 
await+ 循环:forEach中的await不生效;改用for...of或Promise.all。 - 
请求错误不抛:
fetch对 4xx/5xx 不抛错,需if (!res.ok) throw。 - 
事件目标:事件委托时用
e.target.closest('选择器')而不是只看e.target。 - 
this绑定:把方法解构出来后this丢失会报错;用箭头函数或fn.bind(obj)。 - 
源映射调试:
"sourceMap": true后在 DevTools 的 “Sources” 直接断在.ts。 - 
日志可读性:
console.group/console.table;必要时放debugger做断点。 - 
性能小贴士:合并 DOM 更新(如一次性
innerHTML),避免在循环里读写触发布局抖动。 
查阅:MDN(Console、DOM、Fetch)、TypeScript Handbook(Narrowing、Generics)、tsconfig Reference。
三、分层练习清单
入门(3 项)
- 
输入校验:禁止添加空白标题(已经有),并限制标题长度(超出弹
alert)。 - 
过滤视图:增加“全部 / 未完成 / 已完成”三段切换按钮(基于内存状态过滤)。
 - 
本地化显示:用
Intl.DateTimeFormat在每条 Todo 后显示创建时间(相对或本地化日期)。 
巩固(3 项)
- 
搜索:加一个输入框,实时过滤显示标题包含关键字的 todo(防抖 300ms)。
 - 
批量操作:增加“全选/全不选/清除已完成”按钮,注意状态与 UI 的一致性。
 - 
模块化:把
storage、view、types拆成多个文件并使用 ESMimport/export。 
进阶(3 项)
- 
网络边界:新建
api.ts,封装Result<T>返回风格与错误处理,写一个模拟接口(delay+ 随机失败)。 - 
更强类型:为
filter的选项使用字面量联合type Filter = 'all' | 'active' | 'done'+as const,让 UI 与逻辑强绑定。 - 
构建与部署:尝试用 Vite 初始化同等功能并比较原生 ESM 与打包的差异,开启 TS 严格模式。
 
四、自测(Checklist + 闭卷题)
A. 自测清单(能做到即达标)
- 
我能用
import/export组织最小项目并在浏览器运行。 - 
我知道何时用
let/const,避免var,且默认使用===。 - 
我能写出一个返回
Promise<T>的异步函数,并在调用处用try/catch处理错误。 - 
我能用
map/filter/reduce完成列表大多数数据处理。 - 
我能用
addEventListener+ 事件委托管理动态元素的交互。 - 
我能解释微任务与宏任务的执行顺序,并预测一个简单片段的输出。
 - 
我能在
tsconfig中开启strict并解释target/module/lib/sourceMap/outDir的含义。 - 
我能写一个带泛型的工具函数(如
head<T>(arr:T[])),并在边界处标注类型。 - 
我知道
any与unknown的差别,并尽量使用unknown+ 类型缩窄。 - 
我能在 DevTools 中设置断点与查看网络与存储面板。
 
B. 闭卷题(3 题,含标准答案)
1) 事件循环顺序
题:下面代码输出顺序是?
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
queueMicrotask(() => console.log(4));
console.log(5);
答案:1, 5, 3, 4, 2。
解析:同步先执行(1、5);同一轮事件循环清空微任务队列(按入队顺序 Promise 回调 3 再 queueMicrotask 的 4);然后执行宏任务队列(setTimeout 的 2)。
2) 用泛型修正不安全的函数
题:指出问题并重写:
function head(arr: any[]): any { return arr[0]; }
const a: number = head([1,2,3]);
const b: number = head(['x','y']);
答案(安全写法):
function head<T>(arr: T[]): T | undefined { return arr[0]; }
// 推断:a: number | undefined, b: string | undefined
解析:用 泛型 把入参与返回值“绑定”;避免 any;空数组时返回 undefined 更贴近真实世界。
3) this 绑定
题:严格模式/ESM 下输出/行为?
const obj = { x: 42, getX() { return this.x; } };
const { getX } = obj;
console.log(getX());
答案:抛出 TypeError: Cannot read properties of undefined。
解析:解构后独立调用 getX(),this 为 undefined(严格模式);访问 this.x 直接报错。解决:const getX = obj.getX.bind(obj) 或 const getX = () => obj.getX()。
附:常见坑一页纸(速记)
- 
forEach里return只是跳过当前回调,不会中断循环;需要中断用some/every或普通for。 - 
Number('') === 0而parseInt('')为NaN;数值转换建议Number()并结合Number.isNaN。 - 
||会把0/''/false视为假值;只为null/undefined兜底请用??。 - 
JSON 反序列化得到的类型是
any,需要手动校验/缩窄(本示例做了轻校验)。 - 
localStorage有配额限制(~5MB),不适合存大型列表与二进制。 - 
Date默认构造本地时区;跨时区显示用Intl.DateTimeFormat。 - 
- *
 
 
官方文档指引(建议收藏):
MDN Web Docs:语言核心、DOM、Fetch、URL、Storage、Console。
TypeScript Handbook & tsconfig Reference:类型系统、类型缩窄、泛型、编译选项。
ECMAScript(ECMA‑262):语言语义权威来源(用于深入理解事件循环、抽象操作等)。
