1. 为什么需要回顾 JavaScript
TypeScript 是 JavaScript 的超集(superset):每一行合法的 JS 都是合法的 TS。TypeScript 只在 JS 之上增加了类型注解,最终仍然编译成普通 JavaScript 在浏览器或 Node.js 中运行。
React 开发中大量依赖现代 JS 特性——解构赋值、展开运算符、箭头函数、Promise/async-await……这些不是 React 独有的语法,而是 ES6+ 的核心能力。如果对这些不熟悉,看 React 源码和教程时会频繁卡住。
本章目标:快速过一遍 React 开发中最常用的 JS 特性,重点关注容易混淆的概念(如 this 绑定、作用域、闭包、异步模型),为后续 TypeScript 和 React 章节打好基础。
2. var / let / const 区别
这三个关键字都用于声明变量,但它们的作用域规则和可变性完全不同,是 JS 基础中最容易出错的地方之一。
作用域差异:函数作用域 vs 块作用域
var 使用函数作用域,在块(如 if / for)内声明的 var 变量,在块外仍然可以访问。let 和 const 使用块作用域,只在声明所在的 {} 内有效。
// var —— 函数作用域,泄漏到块外
function testVar() {
if (true) {
var x = 10;
}
console.log(x); // 10 —— 块外依然能访问!
}
// let —— 块作用域,块外不可见
function testLet() {
if (true) {
let y = 20;
}
console.log(y); // ReferenceError: y is not defined
}
变量提升(Hoisting)
var 声明会被提升到函数顶部,但赋值不会提升,所以在声明前使用 var 变量得到 undefined 而不是报错。
console.log(a); // undefined(提升了声明,但未赋值)
var a = 5;
console.log(b); // ReferenceError(TDZ,见下)
let b = 5;
暂时性死区(TDZ — Temporal Dead Zone)
let 和 const 也会被提升,但进入暂时性死区——从块的开始到声明语句之前,变量存在但无法访问。试图访问会抛出 ReferenceError。这是比 var 更安全的行为,能帮你尽早发现错误。
何时用 const,何时用 let
最佳实践:默认用 const,只有当变量需要重新赋值时才改用 let。永远不要用 var。
注意:const 只保证变量绑定不变,对象/数组的内容仍然可以修改。
const user = { name: 'Alice', age: 25 };
user.age = 26; // ✅ 合法 —— 修改对象属性
user = {}; // ❌ TypeError —— 不能重新赋值绑定
const items = [1, 2, 3];
items.push(4); // ✅ 合法 —— 修改数组内容
3. 箭头函数 vs 普通函数
箭头函数(Arrow Function)是 ES6 引入的简洁函数写法,但它不只是语法糖——this 的绑定方式与普通函数根本不同。
this 绑定差异
普通函数的 this 由调用时的上下文决定(动态绑定)。箭头函数没有自己的 this,它捕获定义时外层作用域的 this(词法绑定)。
const obj = {
name: 'React',
// 普通函数 —— this 指向调用者 obj
sayHello: function() {
console.log(`Hello from ${this.name}`); // 'Hello from React'
},
// 箭头函数 —— this 继承自外层(此处是全局/undefined)
sayHelloArrow: () => {
console.log(`Hello from ${this?.name}`); // undefined
},
// 实际用途:在回调中保持 this
startTimer: function() {
setTimeout(() => {
console.log(this.name); // ✅ 'React' —— 箭头函数继承了 startTimer 的 this
}, 1000);
}
};
隐式返回
当函数体只有一个表达式时,可以省略 {} 和 return,这在 React 的 .map() 回调中非常常见。
// 普通写法
const double = (n) => { return n * 2; };
// 隐式返回
const double = n => n * 2;
// 隐式返回对象字面量 —— 需要加括号!
const getUser = id => ({ id, name: 'Alice' });
// React 中常见用法
const doubled = [1, 2, 3].map(n => n * 2); // [2, 4, 6]
无 arguments 对象
箭头函数没有 arguments 对象。如果需要收集不定参数,使用剩余参数(rest parameters)语法 ...args。
// 普通函数有 arguments
function sum() {
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
// 箭头函数用 rest 参数
const sum = (...args) => args.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6
4. 解构赋值
解构赋值让你从数组或对象中提取值并赋给变量,写法更简洁,React 的 Props 处理中随处可见。
数组解构
const colors = ['red', 'green', 'blue'];
// 基本解构
const [first, second, third] = colors;
// first = 'red', second = 'green', third = 'blue'
// 跳过元素
const [, , last] = colors;
// last = 'blue'
// 默认值(元素不存在时使用)
const [a = 'default', b = 'fallback'] = ['hello'];
// a = 'hello', b = 'fallback'
// 剩余元素
const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4]
// React useState 的经典用法
const [count, setCount] = useState(0);
对象解构
const user = { name: 'Alice', age: 25, city: 'Beijing' };
// 基本解构(变量名必须与属性名一致)
const { name, age } = user;
// 重命名(属性名 : 新变量名)
const { name: userName, age: userAge } = user;
// userName = 'Alice', userAge = 25
// 默认值
const { name, role = 'guest' } = user;
// role = 'guest'(user 中没有 role 属性)
// 嵌套解构
const profile = {
user: { name: 'Bob', address: { city: 'Shanghai' } }
};
const { user: { name, address: { city } } } = profile;
// 函数参数解构(React Props 常用!)
function UserCard({ name, age, role = 'user' }) {
return <div>{name} - {age}</div>;
}
5. 展开运算符 ...
展开运算符(Spread Operator)... 将数组或对象"展开"成独立元素,是创建不可变数据(immutable data)的核心工具,在 React 状态更新中非常重要。
数组展开与合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 合并数组
const combined = [...arr1, ...arr2]; // [1,2,3,4,5,6]
// 在任意位置插入
const withNew = [...arr1, 99, ...arr2]; // [1,2,3,99,4,5,6]
// 浅拷贝数组
const copy = [...arr1]; // 新数组,修改 copy 不影响 arr1
对象展开与合并
const defaults = { theme: 'dark', lang: 'zh', debug: false };
const userPrefs = { lang: 'en', fontSize: 16 };
// 合并对象(后面的属性覆盖前面的)
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'en', debug: false, fontSize: 16 }
// React 中更新状态的不可变写法
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ 错误:直接修改对象(React 不会检测到变化)
user.age = 26;
setUser(user);
// ✅ 正确:展开创建新对象
setUser({ ...user, age: 26 });
浅拷贝警告:展开运算符只进行浅拷贝(shallow copy)。嵌套对象的引用仍然共享,需要深层更新时要逐层展开,或使用 immer 等工具。
6. 模板字符串
模板字符串(Template Literals)用反引号 ` 包裹,支持多行字符串和表达式嵌入,比字符串拼接更清晰。
const name = 'Alice';
const score = 95;
// 嵌入表达式 ${ }
const msg = `你好,${name}!你的分数是 ${score} 分(${score >= 90 ? '优秀' : '良好'})`;
// 多行字符串(不需要 \n)
const html = `
<div class="card">
<h1>${name}</h1>
</div>
`;
// 标签模板(Tagged Template)—— 函数处理模板字符串
function highlight(strings, ...values) {
return strings.reduce((result, str, i) =>
result + str + (`<b>${values[i] ?? ''}</b>`), ''
);
}
const result = highlight`Name: ${name} Score: ${score}`;
// 'Name: <b>Alice</b> Score: <b>95</b>'
7. Promise 与异步编程
JavaScript 是单线程语言,同一时刻只能执行一段代码。为了处理耗时操作(网络请求、文件读取),JS 使用异步模型:发起操作后继续执行其他代码,操作完成后通过回调通知结果。
Promise 是对"未来值"的封装——它代表一个最终会有结果的异步操作。
Promise 三种状态
- pending(等待中):初始状态,操作尚未完成
- fulfilled(已完成):操作成功,有一个结果值
- rejected(已拒绝):操作失败,有一个错误原因
状态一旦从 pending 变化,就不可再改变(fulfilled 或 rejected 是终态)。
// 创建 Promise
const fetchUser = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({ name: 'Alice' }); // 成功 → fulfilled
} else {
reject(new Error('Network error')); // 失败 → rejected
}
}, 1000);
});
// .then() 处理成功,.catch() 处理失败,.finally() 总是执行
fetchUser
.then(user => console.log('User:', user.name))
.catch(err => console.error('Error:', err.message))
.finally(() => console.log('请求结束'));
Promise 组合方法
// Promise.all —— 所有 Promise 都成功才成功,有一个失败就失败
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
// Promise.allSettled —— 等所有 Promise 完成,不管成功还是失败
const results = await Promise.allSettled([req1, req2, req3]);
results.forEach(result => {
if (result.status === 'fulfilled') console.log(result.value);
else console.error(result.reason);
});
// Promise.race —— 第一个完成的 Promise(成功或失败)决定结果
const result = await Promise.race([fetchData(), timeout(5000)]);
8. async / await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码,大幅提升可读性。
基本用法
// async 函数:总是返回 Promise
async function loadUserData(userId: string) {
// await 挂起当前函数,等待 Promise 完成,然后返回结果值
const user = await fetchUser(userId); // 等待完成
const posts = await fetchPosts(user.id); // 再等待
return { user, posts };
}
// 调用 async 函数 —— 返回的是 Promise
loadUserData('123').then(data => console.log(data));
try-catch 错误处理
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
// fetch 不会对 HTTP 错误抛出异常,需要手动检查
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('请求失败:', error);
throw error; // 重新抛出,让调用者决定如何处理
}
}
并行执行(避免串行浪费)
// ❌ 串行执行 —— 总耗时 = 请求1 + 请求2(慢)
const users = await fetchUsers();
const posts = await fetchPosts();
// ✅ 并行执行 —— 总耗时 = max(请求1, 请求2)(快)
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
9. 模块系统(ESM)
现代 JavaScript 使用 ES Modules(ESM) 来组织代码。每个文件就是一个模块,需要显式 export 才能被外部使用,需要显式 import 才能使用其他模块。
具名导出(Named Export)
// math.ts —— 可以有多个具名导出
export const PI = 3.14159;
export function add(a: number, b: number): number { return a + b; }
export interface Point { x: number; y: number; }
// 导入(花括号,名称必须匹配)
import { PI, add, Point } from './math';
// 重命名导入
import { add as sum } from './math';
// 导入全部
import * as MathUtils from './math';
MathUtils.add(1, 2);
默认导出(Default Export)
// Button.tsx —— 一个文件只能有一个 default export
export default function Button({ label }: { label: string }) {
return <button>{label}</button>;
}
// 导入(不用花括号,可以用任意名称)
import Button from './Button';
import MyButton from './Button'; // 也可以
// 混合导出(React 组件文件的常见形式)
export { helperFn }; // 具名
export default MyComponent; // 默认
动态导入 import()
// 动态导入 —— 返回 Promise,实现代码分割(Code Splitting)
const loadChart = async () => {
const { default: ChartLib } = await import('./heavy-chart-lib');
ChartLib.render();
};
// React 懒加载(基于动态 import)
const LazyPage = React.lazy(() => import('./pages/Dashboard'));
ESM vs CommonJS:Node.js 传统上使用 require()(CommonJS),而现代前端(Vite/webpack)使用 ESM(import/export)。两者最大区别是:ESM 是静态分析的(支持 Tree Shaking),CJS 是动态的(运行时解析)。
10. 数组高阶方法
这些方法在 React 中极其常用:列表渲染用 map,过滤数据用 filter,计算汇总用 reduce。它们都是纯函数——不修改原数组,返回新数组。
map — 变换每个元素
const numbers = [1, 2, 3, 4];
// 将每个元素翻倍
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8]
// React 中渲染列表
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const listItems = users.map(user => (
<li key={user.id}>{user.name}</li>
));
filter — 过滤元素
const products = [
{ name: 'iPhone', price: 999, inStock: true },
{ name: 'Pixel', price: 799, inStock: false },
{ name: 'Galaxy', price: 899, inStock: true },
];
// 只保留有库存的商品
const available = products.filter(p => p.inStock);
// 链式调用:先过滤,再变换
const names = products
.filter(p => p.price < 900)
.map(p => p.name); // ['Pixel', 'Galaxy']
reduce — 归并为单一值
const orders = [
{ product: 'A', amount: 100 },
{ product: 'B', amount: 200 },
{ product: 'C', amount: 50 },
];
// 计算总金额
const total = orders.reduce((sum, order) => sum + order.amount, 0);
// 350
// 将数组转为对象(按 id 索引)
const usersById = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {} as Record<string, User>);
find / some / every
const nums = [10, 20, 30, 40];
// find —— 返回第一个匹配的元素(没找到返回 undefined)
const found = nums.find(n => n > 15); // 20
// findIndex —— 返回第一个匹配的索引
const idx = nums.findIndex(n => n > 25); // 2
// some —— 至少有一个满足条件返回 true
const hasLarge = nums.some(n => n > 35); // true
// every —— 所有元素都满足条件返回 true
const allPositive = nums.every(n => n > 0); // true
// includes —— 判断是否包含某值
['a', 'b', 'c'].includes('b'); // true
11. 可选链 ?. 与空值合并 ??
这两个运算符是处理 null / undefined 的利器,在 React 中与 TypeScript 配合能大幅减少 null 检查的冗余代码。
可选链 ?.(Optional Chaining)
访问深层属性时,如果链上某个值是 null 或 undefined,不会抛出错误,而是短路返回 undefined。
const user = {
profile: {
address: { city: 'Beijing' }
}
};
// 没有可选链 —— 可能 TypeError: Cannot read property of undefined
const city1 = user && user.profile && user.profile.address && user.profile.address.city;
// 有可选链 —— 优雅简洁
const city2 = user?.profile?.address?.city; // 'Beijing'
const zip = user?.profile?.address?.zip; // undefined(不报错)
// 也可用于方法调用和数组访问
user?.profile?.getAvatar?.(); // 方法可能不存在时
arr?.[0]?.name; // 数组可能为 null 时
空值合并 ??(Nullish Coalescing)
当左侧值是 null 或 undefined 时,返回右侧默认值。与 || 的区别:|| 对所有假值(包括 0、''、false)都会取右值,?? 只对 null/undefined 取右值。
const score = 0;
// || 的陷阱 —— 0 是假值,会错误地取默认值
const display1 = score || 'No score'; // 'No score' ← 错误!
// ?? 正确处理 —— 只有 null/undefined 才取默认值
const display2 = score ?? 'No score'; // 0 ← 正确
// 与可选链组合使用
const username = user?.profile?.name ?? 'Anonymous';
// 空值合并赋值 ??=(ES2021)
user.name ??= 'Guest'; // 只在 user.name 为 null/undefined 时赋值
本章小结:这些 JS 特性在 React 开发中每天都会用到。特别是解构赋值、展开运算符、箭头函数、async/await 和数组高阶方法——如果不熟悉,建议在进入下一章之前多练习几遍。