04 — JavaScript 基础回顾

你说你有 JavaScript 基础。本章不重复教基础语法,而是把项目里频繁用到的、可能你不熟的现代 JS 语法挑出来讲。
这一章对读懂 React 代码非常关键


1. 解构赋值(Destructuring) — 项目里到处都用

1.1 对象解构

1
2
3
4
5
6
7
// 你可能熟悉的写法:
const props = someObject;
const name = props.name;
const age = props.age;

// 解构写法(等价但更简洁):
const { name, age } = someObject;

项目里到处都这么用

1
2
3
4
5
// ChatItem.tsx
export function ChatItem({ chat, active, onSelect, onDelete }: Props) {
// ↑ 解构 Props,props.chat → chat
return <div onClick={() => onSelect(chat.id)}>...</div>;
}

1.2 嵌套解构 + 重命名

1
2
3
// 没用到,但要知道可以这样:
const { address: { city }, name: userName } = user;
// ↑ 重命名 ↑ 把 name 改成 userName

1.3 默认值

1
2
3
// 对象解构时可以给默认值
const { collapsed = false } = someProps;
// 如果 someProps.collapsed 是 undefined,就用 false

项目里的例子

1
2
3
4
// Sidebar.tsx
const [collapsed, setCollapsed] = useState<boolean>(() => {
// ... 从 localStorage 读 ...
});

1.4 数组解构

1
2
const [first, second] = [1, 2, 3];    // first=1, second=2
const [matches, setMatches] = useState(false); // ⭐ React 里全是数组解构

为什么 useState 返回的是数组而不是对象?
因为如果返回对象,你必须知道字段名:

1
const { value, setValue } = useState();  // 字段名固定

返回数组可以随便命名:

1
const [collapsed, setCollapsed] = useState(false);  // 你想叫啥叫啥

2. 展开运算符(Spread)... — 项目里常用

2.1 展开数组

1
2
3
4
5
const a = [1, 2];
const b = [...a, 3, 4]; // [1, 2, 3, 4](展开 a 后追加)

// React 中常用:复制数组并追加新元素
const newList = [...oldList, newItem];

2.2 展开对象

1
2
const user = { name: 'Alice', age: 30 };
const updated = { ...user, age: 31 }; // { name: 'Alice', age: 31 }(覆盖 age)

2.3 复制对象

1
2
3
4
5
6
const a = { x: 1 };
const b = { ...a }; // 浅拷贝(不是深拷贝)

// React 中:触发重新渲染的常见模式
set({ chatList: [...chatList] });
// ↑ 创建一个新数组(即使内容一样),React 才会重新渲染

2.4 项目里出现的地方

1
2
3
4
5
6
7
8
9
// 展开 CSS Modules 的样式
className={`${styles.item} ${active ? styles.active : ''}`}
// ↑ 不是展开,是模板字符串拼接

// 展开 props
<MyComponent {...someObject} /> // 把对象的所有字段作为 props

// 复制数组
chatList: [...get().chatList] // 创建新数组触发 React 更新

3. 模板字符串(Template Literals)

1
2
3
4
5
6
7
8
9
const name = '豆豆';
const greeting = `你好,${name}!`; // 用反引号 + ${}

// 多行字符串
const html = `
<div>
<p>${name}</p>
</div>
`;

项目里

1
2
3
4
5
6
7
8
// 组合 CSS Module 类名
<div className={`${styles.item} ${active ? styles.active : ''}`}>
// ↑ 模板字符串,${} 里写表达式

// 多行 CSS 字符串
className={`glass-panel ${styles.sidebar} ${collapsed ? styles.collapsed : ''} ${
isDrawerOpen ? styles.drawerOpen : ''
}`}

4. 可选链 ?. 与空值合并 ??

4.1 可选链 ?.

项目里频繁出现

1
2
3
4
5
6
// 以前:层层判断
const title = user && user.profile && user.profile.title;

// 现在:
const title = user?.profile?.title;
// ↑ 如果 user 为 null/undefined,立即返回 undefined,不报错

项目里的例子

1
2
3
4
5
6
7
8
// Message.tsx
{message.timeline && message.timeline.length > 0 && (
<Timeline items={message.timeline} open={!!message.showTimeline} onToggle={...} />
)}

// InputArea.tsx
<button onClick={() => fileInputRef.current?.click()}>
// ↑ fileInputRef.current 可能为 null

4.2 空值合并 ??

1
2
3
4
5
6
const name = input ?? '默认值';
// 等价于:input !== null && input !== undefined ? input : '默认值'

// 对比 || :
const a = 0 || 'default'; // 'default' (因为 0 是 falsy)
const b = 0 ?? 'default'; // 0 (只有 null/undefined 才用默认值)

5. 箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 普通函数
function add(a, b) { return a + b; }

// 箭头函数
const add = (a, b) => a + b; // 单行直接 return

// 多个语句需要 {}
const add = (a, b) => {
console.log('计算中');
return a + b;
};

// 单个参数可省略括号
const double = x => x * 2;

项目里几乎全是箭头函数

1
2
3
4
5
6
7
8
9
10
// 事件处理
onClick={() => selectAgent(a.id)}
onChange={(e) => setInput(e.target.value)}

// 数组方法
chatList.map((c) => <ChatItem key={c.id} chat={c} ... />)
chatList.find((c) => c.id === id)

// 简单操作
const canSend = !isSending && !isUploading && (inputMessage.trim().length > 0 || selectedFile);

6. 数组方法(必须熟练)

6.1 .map() — 转换每个元素,返回新数组

1
[1, 2, 3].map(x => x * 2)        // [2, 4, 6]

项目里 90% 的列表渲染都用 map

1
2
3
4
// ChatItem.tsx 里没用 map,但 Sidebar.tsx 用了:
{chatList.map((c) => (
<ChatItem key={c.id} chat={c} active={c.id === currentChatId} onSelect={selectChat} onDelete={deleteChat} />
))}

⚠️ 列表中每个元素必须有 key 属性(看 11-事件处理与条件列表渲染

6.2 .filter() — 过滤

1
2
3
4
[1, 2, 3, 4].filter(x => x > 2)   // [3, 4]

// 项目里
const remaining = chatList.filter(c => c.id !== id);

6.3 .find() — 找一个(找不到返回 undefined)

1
2
3
4
[1, 2, 3].find(x => x > 1)       // 2

// 项目里
const chat = chatList.find(c => c.id === currentChatId);

6.4 .findIndex() — 找索引(找不到返回 -1)

1
2
3
4
5
6
7
[1, 2, 3].findIndex(x => x > 1)   // 1

// 项目里
const idx = aiMsg.timeline.findIndex(
(t) => t.type === 'tool' && t.toolCallId === event.toolCallId && t.status === 'running'
);
if (idx >= 0) { ... }

6.5 .some() / .every() — 判断

1
2
3
4
5
[1, 2, 3].some(x => x > 2)       // true(至少一个满足)
[1, 2, 3].every(x => x > 0) // true(所有都满足)

// 项目里
const hasError = aiMsg.timeline.some(t => t.type === 'error');

6.6 .reduce() — 累积

项目里没怎么用,但你可能会碰到。

6.7 .includes() — 包含

1
2
3
4
[1, 2, 3].includes(2)             // true

// 字符串也有
'hello'.includes('ell') // true

7. 对象方法(常用)

7.1 Object.keys() / Object.values() / Object.entries()

1
2
3
4
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj) // ['a', 'b', 'c']
Object.values(obj) // [1, 2, 3]
Object.entries(obj) // [['a', 1], ['b', 2], ['c', 3]]

7.2 ?. 可选链调用

1
const result = obj.method?.();     // 如果 method 不存在,不报错,返回 undefined

7.3 in 操作符

1
2
3
4
5
6
if ('data' in someObj) { ... }    // 判断对象是否包含某字段

// 项目里
if (data && typeof data === 'object' && 'data' in data) {
// ...
}

8. 异步:Promise、async/await、fetch

8.1 Promise 三种状态

1
2
3
4
5
const p = new Promise((resolve, reject) => {
// 异步操作
if (成功) resolve(data);
else reject(error);
});

8.2 async/await — 项目里到处都是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 旧写法(回调地狱)
fetch(url)
.then(res => res.json())
.then(data => render(data))
.catch(err => console.error(err));

// ✅ 新写法
async function loadData() {
try {
const res = await fetch(url);
const data = await res.json();
render(data);
} catch (err) {
console.error(err);
}
}

项目里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.tsx
useEffect(() => {
(async () => {
await testConnection();
await loadChats();
createNewChat();
})();
}, [...]);

// chatStore.ts
async loadChats() {
try {
const data = await sessionApi.list();
set({ chatList: data.records.map(mapSessionListItem) });
} catch (e) {
console.error('loadChats failed', e);
set({ chatList: [] });
}
}

8.3 fetch — 调用后端 API

1
2
3
4
5
6
7
8
const res = await fetch('https://example.com/api/data', {
method: 'GET', // 或 'POST'、'DELETE'
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ... }), // POST/PUT 的请求体
});

if (!res.ok) throw new Error('请求失败');
const json = await res.json();

项目里的封装

1
2
3
4
5
6
7
8
// api/client.ts
async function httpGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: 'GET',
headers: { Accept: 'application/json' },
});
return parseOrThrow<T>(res);
}

8.4 AbortController — 取消请求

1
2
3
4
5
6
7
8
9
10
11
12
const controller = new AbortController();

// 5 秒后取消
setTimeout(() => controller.abort(), 5000);

try {
const res = await fetch(url, { signal: controller.signal });
} catch (e) {
if (e.name === 'AbortError') {
console.log('请求被取消');
}
}

项目里

1
2
3
4
5
6
7
8
9
10
11
// chatStore.ts - 取消正在进行的 SSE 请求
abortController = new AbortController();
await parseSseStream({
url,
signal: abortController.signal,
...
});

async stopMessage() {
abortController?.abort(); // 用户点"停止"时调用
}

9. Set / Map 数据结构

9.1 Set — 值的集合(无重复)

1
2
3
4
5
const set = new Set();
set.add(1);
set.has(1); // true
set.delete(1);
set.size; // 元素个数

9.2 Map — 键值对(键可以是任意类型)

1
2
3
4
5
const map = new Map();
map.set('key', 'value');
map.get('key');
map.has('key');
map.delete('key');

项目里没怎么用,但 React 生态中常见。


10. typeof / instanceof / Array.isArray()

1
2
3
4
5
6
7
8
9
10
typeof 'hello'                   // 'string'
typeof 123 // 'number'
typeof undefined // 'undefined'
typeof null // 'object' ⚠️ null 的 typeof 是 object(历史 bug)

// 项目里
if (typeof data === 'object' && 'data' in data) { ... }

Array.isArray([1, 2]) // true
Array.isArray('hello') // false

11. JSON.parse / JSON.stringify

1
2
JSON.parse('{"name":"Alice"}')    // { name: 'Alice' }  字符串 → 对象
JSON.stringify({ name: 'A' }) // '{"name":"A"}' 对象 → 字符串

项目里频繁用到

1
2
3
4
5
6
// SSE 解析:每一行是 JSON 字符串
const event = JSON.parse(data);

// 构造 URL 参数
const params = new URLSearchParams({ agentType: 'chat', conversationId: 'abc' });
// URLSearchParams 会自动转义,无需手动 JSON.stringify

12. 类的简写语法

1
2
3
4
5
6
7
class ApiError extends Error {
constructor(message, code) {
super(message); // 调用父类 (Error) 的构造函数
this.name = 'ApiError';
this.code = code; // 在 this 上挂属性
}
}

项目里

1
2
3
4
5
6
7
// api/client.ts
export class ApiError extends Error {
constructor(message: string, public code?: number) {
super(message);
this.name = 'ApiError';
}
}

注意 public code?: number 是 TypeScript 简写,等价于在构造函数中赋值 this.code = code


13. 模块系统:import / export

13.1 命名导出/导入

1
2
3
4
5
6
// utils.js
export function add(a, b) { return a + b; }
export const PI = 3.14;

// 任意文件
import { add, PI } from './utils';

13.2 默认导出

1
2
3
4
5
// MyComponent.tsx
export default function MyComponent() { ... }

// 导入时可以随便命名
import MyComp from './MyComponent';

13.3 路径别名 @/

项目 vite.config.ts 配置了 @ 指向 src 目录:

1
2
3
4
5
import { useChatStore } from '@/store/chatStore';
import type { ChatMessage } from '@/store/chatStore';

// 等价于:
import { useChatStore } from '../../store/chatStore'; // 太长

不用 ../.../... 一路数层级,统一用 @/ 从 src 根开始算。

13.4 import type — TypeScript 专用

1
2
import { useChatStore } from '@/store/chatStore';         // 值(运行时需要)
import type { ChatMessage } from '@/store/chatStore'; // 只是类型(编译时删掉)

好处:编译后 import type 整行被删除,打包体积更小。


14. 一段话总结

本项目里 JS 的现代语法(解构、展开、箭头、map/filter、可选链、async/await)几乎每个文件都在用。
重点掌握:解构、展开运算符、可选链、数组方法、async/await、模块导入


接下来

JS 语法已对齐。继续阅读 05-TypeScript快速入门.md,理解 TypeScript 给 JS 加了什么。