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):语言语义权威来源(用于深入理解事件循环、抽象操作等)。
