Chapter 01

JavaScript 核心特性回顾

TypeScript 是 JavaScript 的超集——打好 JS 基础,才能真正驾驭 TypeScript 和 React

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 变量,在块外仍然可以访问。letconst 使用块作用域,只在声明所在的 {} 内有效。

// 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)

letconst 也会被提升,但进入暂时性死区——从块的开始到声明语句之前,变量存在但无法访问。试图访问会抛出 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 是终态)。

// 创建 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)

访问深层属性时,如果链上某个值是 nullundefined,不会抛出错误,而是短路返回 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)

当左侧值是 nullundefined 时,返回右侧默认值。与 || 的区别:|| 对所有假值(包括 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 和数组高阶方法——如果不熟悉,建议在进入下一章之前多练习几遍。