> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# JavaScript 入门
- URL: https://blog.vercanti.com/javascript-ru-men/
- Published: 2026-08-28T14:35:16.000Z
- Updated: 2026-08-28T14:58:29.000Z
- Description: 1. 什么是 JavaScript(#%E4%BB%80%E4%B9%88%E6%98%AF-javascript) 2. 如何引入 JavaScript(#%E5%A6%82%E4%BD%95%E5%BC%95%E5%85%A5-javascript) 3. 变量与数据类型(#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) 4. 运算符(#%E8%BF%90%E7%AE%97%E7%AC%A6) 5. 字符串操作(#%E5%AD%97%E7%AC%A6%E4%B8%B2%
- Author: yellowdog
- Tags: 前端开发

> 官方文档：<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript>  
> 最后更新：2026-03-05

---

## 目录

1. [什么是 JavaScript](#%E4%BB%80%E4%B9%88%E6%98%AF-javascript)
2. [如何引入 JavaScript](#%E5%A6%82%E4%BD%95%E5%BC%95%E5%85%A5-javascript)
3. [变量与数据类型](#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B)
4. [运算符](#%E8%BF%90%E7%AE%97%E7%AC%A6)
5. [字符串操作](#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%93%8D%E4%BD%9C)
6. [数组方法](#%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95)
7. [对象](#%E5%AF%B9%E8%B1%A1)
8. [流程控制](#%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6)
9. [函数](#%E5%87%BD%E6%95%B0)
10. [DOM 操作](#dom-%E6%93%8D%E4%BD%9C)
11. [事件](#%E4%BA%8B%E4%BB%B6)
12. [异步编程](#%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B)
13. [ES6+ 常用特性](#es6-%E5%B8%B8%E7%94%A8%E7%89%B9%E6%80%A7)
14. [综合实战：Todo List](#%E7%BB%BC%E5%90%88%E5%AE%9E%E6%88%98%EF%BC%9Atodo-list)

---

## 什么是 JavaScript

JavaScript 是运行在浏览器中的编程语言，负责网页的"行为"部分：

- HTML 负责结构（内容是什么）
- CSS 负责样式（内容长什么样）
- JavaScript 负责交互（内容怎么响应用户操作）

JavaScript 也可以运行在服务器端（通过 Node.js），但本文聚焦于浏览器环境。

---

## 如何引入 JavaScript

### 方式一：内联写在 HTML 中

```html
<!DOCTYPE html>
<html>
<body>
  <h1>我的页面</h1>

  <!-- 推荐放在 body 末尾，确保 HTML 先加载完 -->
  <script>
    console.log("Hello, JavaScript!");
  </script>
</body>
</html>

```

### 方式二：外部文件引入（推荐）

```html
<!DOCTYPE html>
<html>
<body>
  <h1>我的页面</h1>

  <!-- src 指向外部 JS 文件，defer 表示等 HTML 解析完再执行 -->
  <script src="main.js" defer></script>
</body>
</html>

```

```js
// main.js
console.log("Hello from external file!");

```

### 查看输出

按 F12 打开浏览器开发者工具，切换到 Console（控制台）标签页，即可看到 `console.log()` 的输出。

---

## 变量与数据类型

### var / let / const

JavaScript 有三种声明变量的方式：

```js
// var：旧写法，有变量提升问题，不推荐使用
var oldWay = "旧方式";

// let：可以重新赋值的变量
let count = 0;
count = 1; // 允许

// const：常量，声明后不能重新赋值（推荐优先使用）
const PI = 3.14;
// PI = 3; // 报错：Assignment to constant variable.

```

推荐原则：**默认用 `const`，需要改变时再用 `let`，不要用 `var`**。

#### var / let / const 区别对比

| 特性         | var              | let       | const     |
| ---------- | ---------------- | --------- | --------- |
| 作用域        | 函数作用域            | 块级作用域     | 块级作用域     |
| 变量提升       | 有，初始值为 undefined | 有，但有暂时性死区 | 有，但有暂时性死区 |
| 重复声明       | 允许               | 不允许       | 不允许       |
| 重新赋值       | 允许               | 允许        | 不允许       |
| 挂载到 window | 是                | 否         | 否         |

### 命名规范

```js
// 小驼峰命名法（推荐用于变量和函数）
const userName = "张三";
const totalPrice = 99.9;

// 全大写加下划线（推荐用于真正的常量）
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = "https://api.example.com";

// 布尔值通常以 is / has / can 开头，更易读
const isLoggedIn = true;
const hasPermission = false;

// 不推荐的命名
const a = 1;        // 含义不明
const UserName = ""; // 首字母大写通常留给 class

```

### 数据类型

JavaScript 共 8 种数据类型，分为基本类型和引用类型两大类：

```js
// 基本类型（Primitive）

// 1. string：字符串
const name = "张三";
const greeting = '你好';

// 2. number：数字（整数和小数都是 number）
const age = 25;
const price = 9.99;
const infinity = Infinity;  // 无穷大
const notANumber = NaN;     // 非数字，但类型是 number

// 3. boolean：布尔值
const isActive = true;
const isDone = false;

// 4. null：表示"有意为空"，需要手动赋值
const selectedUser = null;

// 5. undefined：表示"未定义"，变量声明但未赋值时的默认值
let score;
console.log(score); // undefined

// 6. symbol：唯一标识符（ES6 新增，进阶内容）
const id = Symbol("id");

// 7. bigint：超大整数（ES2020 新增）
const bigNumber = 9007199254740991n; // 末尾加 n

// 引用类型

// 8. object：对象（数组、函数等也属于 object）
const person = { name: "张三", age: 25 };
const colors = ["红", "绿", "蓝"];

```

### typeof 检测类型

```js
console.log(typeof "hello");        // "string"
console.log(typeof 42);             // "number"
console.log(typeof true);           // "boolean"
console.log(typeof undefined);      // "undefined"
console.log(typeof null);           // "object"（历史遗留 bug，记住这个特例）
console.log(typeof {});             // "object"
console.log(typeof []);             // "object"（数组也是 object）
console.log(typeof function(){}); // "function"
console.log(typeof Symbol());       // "symbol"
console.log(typeof 42n);            // "bigint"

// 检测数组用 Array.isArray()
console.log(Array.isArray([]));     // true
console.log(Array.isArray({}));     // false

```

---

## 运算符

### 算术运算符

```js
const a = 10;
const b = 3;

console.log(a + b);  // 13，加
console.log(a - b);  // 7，减
console.log(a * b);  // 30，乘
console.log(a / b);  // 3.3333...，除
console.log(a % b);  // 1，取余（模运算）
console.log(a ** b); // 1000，幂运算（10 的 3 次方）

// 自增自减
let count = 5;
count++;    // count 变为 6
count--;    // count 变为 5
count += 2; // count 变为 7
count *= 3; // count 变为 21

```

### 比较运算符：== vs ===

```js
// == 宽松相等：会进行类型转换再比较（不推荐）
console.log(1 == "1");   // true（字符串"1"被转为数字1）
console.log(0 == false); // true（false 被转为 0）
console.log(null == undefined); // true

// === 严格相等：类型和值都必须相同（推荐）
console.log(1 === "1");  // false
console.log(1 === 1);    // true
console.log(null === undefined); // false

// 不等
console.log(1 != "1");  // false（宽松，类型转换后相等）
console.log(1 !== "1"); // true（严格，类型不同）

// 大小比较
console.log(5 > 3);   // true
console.log(5 >= 5);  // true
console.log(3 < 5);   // true
console.log(3 <= 2);  // false

```

### 逻辑运算符

```js
// && 逻辑与：两者都为真才为真
console.log(true && true);   // true
console.log(true && false);  // false

// || 逻辑或：至少一个为真则为真
console.log(false || true);  // true
console.log(false || false); // false

// ! 逻辑非：取反
console.log(!true);  // false
console.log(!false); // true

// 短路求值（非常重要）
// && 左边为假时，右边不执行，直接返回左边的值
const result1 = null && "永远不会到这里"; // null
const result2 = "有值" && "到这里了";     // "到这里了"

// || 左边为真时，右边不执行，直接返回左边的值
const result3 = "有值" || "不会到这里"; // "有值"
const result4 = null || "默认值";        // "默认值"

// 常见用法：提供默认值
const userName = inputValue || "匿名用户";

```

### 三元运算符

```js
// 语法：条件 ? 真时的值 : 假时的值
const age = 20;
const status = age >= 18 ? "成年人" : "未成年";
console.log(status); // "成年人"

// 等同于
let status2;
if (age >= 18) {
  status2 = "成年人";
} else {
  status2 = "未成年";
}

```

### 可选链运算符 ?.

```js
// 问题：深层访问对象属性时，中间某层为 null/undefined 会报错
const user = null;
// console.log(user.name); // 报错：Cannot read properties of null

// 解决：使用可选链 ?.，遇到 null/undefined 直接返回 undefined
console.log(user?.name);           // undefined，不报错
console.log(user?.address?.city);  // undefined

const obj = { a: { b: { c: 42 } } };
console.log(obj?.a?.b?.c);  // 42
console.log(obj?.x?.y?.z);  // undefined

```

### 空值合并运算符 ??

```js
// ?? 只在左边为 null 或 undefined 时才使用右边的值
// 与 || 的区别：|| 会对所有假值（0、""、false）使用右边

const score = 0;
console.log(score || 100);  // 100，因为 0 是假值（可能不是你想要的）
console.log(score ?? 100);  // 0，因为 0 不是 null/undefined

const name = "";
console.log(name || "匿名");  // "匿名"
console.log(name ?? "匿名");  // ""（空字符串不是 null/undefined）

```

---

## 字符串操作

### 模板字符串（Template Literals）

```js
const name = "张三";
const age = 25;

// 旧方式：字符串拼接，繁琐易出错
const msg1 = "你好，我叫" + name + "，今年" + age + "岁。";

// 新方式：模板字符串（反引号包裹，${}内写表达式）
const msg2 = `你好，我叫${name}，今年${age}岁。`;
console.log(msg2); // 你好，我叫张三，今年25岁。

// 可以在 ${} 中写任意表达式
const result = `1 + 1 = ${1 + 1}`;
const isAdult = `${age >= 18 ? "成年" : "未成年"}`;

// 模板字符串支持多行
const html = `
  <div>
    <h1>${name}</h1>
    <p>年龄：${age}</p>
  </div>
`;

```

### 常用字符串方法

```js
const str = "  Hello, JavaScript!  ";

// length：字符串长度（属性，不是方法）
console.log(str.length); // 22

// trim()：去除首尾空白
console.log(str.trim());       // "Hello, JavaScript!"
console.log(str.trimStart());  // "Hello, JavaScript!  "（仅去左边）
console.log(str.trimEnd());    // "  Hello, JavaScript!"（仅去右边）

const clean = "Hello, JavaScript!";

// indexOf(searchStr)：查找子字符串首次出现的位置，找不到返回 -1
console.log(clean.indexOf("Java"));   // 7
console.log(clean.indexOf("Python")); // -1

// includes(searchStr)：是否包含某子字符串，返回布尔值
console.log(clean.includes("Java"));   // true
console.log(clean.includes("Python")); // false

// startsWith / endsWith：判断开头/结尾
console.log(clean.startsWith("Hello"));       // true
console.log(clean.endsWith("!"));             // true
console.log(clean.startsWith("Java"));        // false

// slice(start, end)：截取子字符串，end 不包含
// start/end 可以是负数，表示从末尾倒数
console.log(clean.slice(7));      // "JavaScript!"
console.log(clean.slice(7, 17));  // "JavaScript"
console.log(clean.slice(-1));     // "!"

// split(separator)：按分隔符拆分为数组
console.log(clean.split(", "));   // ["Hello", "JavaScript!"]
console.log("a-b-c".split("-"));  // ["a", "b", "c"]
console.log("abc".split(""));     // ["a", "b", "c"]

// replace(search, replacement)：替换第一个匹配
console.log(clean.replace("Hello", "Hi")); // "Hi, JavaScript!"

// replaceAll(search, replacement)：替换所有匹配
console.log("a-b-a".replaceAll("a", "x")); // "x-b-x"

// toUpperCase / toLowerCase：大小写转换
console.log(clean.toUpperCase()); // "HELLO, JAVASCRIPT!"
console.log(clean.toLowerCase()); // "hello, javascript!"

// padStart / padEnd：补齐长度
console.log("5".padStart(3, "0")); // "005"
console.log("hi".padEnd(5, "."));  // "hi..."

// repeat：重复
console.log("ha".repeat(3)); // "hahaha"

```

---

## 数组方法

### 基础增删方法

```js
const fruits = ["苹果", "香蕉", "橙子"];

// push：在末尾添加，返回新长度
fruits.push("葡萄");
console.log(fruits); // ["苹果", "香蕉", "橙子", "葡萄"]

// pop：删除末尾元素，返回被删除的元素
const last = fruits.pop();
console.log(last);   // "葡萄"
console.log(fruits); // ["苹果", "香蕉", "橙子"]

// unshift：在开头添加，返回新长度
fruits.unshift("西瓜");
console.log(fruits); // ["西瓜", "苹果", "香蕉", "橙子"]

// shift：删除开头元素，返回被删除的元素
const first = fruits.shift();
console.log(first);  // "西瓜"
console.log(fruits); // ["苹果", "香蕉", "橙子"]

```

### 遍历方法

```js
const numbers = [1, 2, 3, 4, 5];

// forEach：遍历，无返回值（不能 break）
numbers.forEach((num, index) => {
  console.log(`第${index}项：${num}`);
});

// map：遍历并返回新数组（不修改原数组）
const doubled = numbers.map(num => num * 2);
console.log(doubled);  // [2, 4, 6, 8, 10]
console.log(numbers);  // [1, 2, 3, 4, 5]（原数组不变）

// filter：过滤，返回满足条件的元素组成的新数组
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]

// find：返回第一个满足条件的元素，找不到返回 undefined
const firstEven = numbers.find(num => num % 2 === 0);
console.log(firstEven); // 2

// findIndex：返回第一个满足条件的元素的索引，找不到返回 -1
const firstEvenIndex = numbers.findIndex(num => num % 2 === 0);
console.log(firstEvenIndex); // 1

// some：是否至少有一个元素满足条件，返回布尔值
console.log(numbers.some(num => num > 4));  // true
console.log(numbers.some(num => num > 10)); // false

// every：是否所有元素都满足条件，返回布尔值
console.log(numbers.every(num => num > 0)); // true
console.log(numbers.every(num => num > 3)); // false

// includes：是否包含某值，返回布尔值
console.log(numbers.includes(3)); // true
console.log(numbers.includes(9)); // false

```

### reduce：累积计算

```js
const numbers = [1, 2, 3, 4, 5];

// reduce(callback, initialValue)
// callback 接收 (accumulator, currentValue, index, array)
// accumulator：累积值（上一次回调的返回值）
// currentValue：当前元素

// 求和
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15

// 求最大值
const max = numbers.reduce((acc, num) => (num > acc ? num : acc), numbers[0]);
console.log(max); // 5

// 统计出现次数
const words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
const count = words.reduce((acc, word) => {
  acc[word] = (acc[word] || 0) + 1;
  return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, cherry: 1 }

```

### 其他常用方法

```js
// sort：排序（默认按字符串排序，数字排序需传比较函数）
const letters = ["c", "a", "b"];
letters.sort();
console.log(letters); // ["a", "b", "c"]

const nums = [10, 1, 5, 2];
nums.sort((a, b) => a - b); // 升序
console.log(nums); // [1, 2, 5, 10]
nums.sort((a, b) => b - a); // 降序
console.log(nums); // [10, 5, 2, 1]

// flat：展平嵌套数组
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat());    // [1, 2, 3, 4, [5, 6]]（只展平一层）
console.log(nested.flat(2));   // [1, 2, 3, 4, 5, 6]（展平两层）
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6]（展平所有层）

// slice：截取子数组（不修改原数组）
const arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 3)); // [2, 3]
console.log(arr.slice(-2));   // [4, 5]

// concat：合并数组
const a = [1, 2];
const b = [3, 4];
console.log(a.concat(b)); // [1, 2, 3, 4]

// join：将数组元素连接为字符串
console.log(["a", "b", "c"].join("-")); // "a-b-c"
console.log(["a", "b", "c"].join(""));  // "abc"

// 展开运算符（spread）合并数组
const merged = [...a, ...b, 5];
console.log(merged); // [1, 2, 3, 4, 5]

```

---

## 对象

### 创建与访问

```js
// 创建对象
const person = {
  name: "张三",
  age: 25,
  isStudent: false,
  address: {
    city: "北京",
    district: "海淀"
  },
  // 方法
  greet() {
    return `你好，我叫${this.name}`;
  }
};

// 访问属性：点语法
console.log(person.name);           // "张三"
console.log(person.address.city);   // "北京"

// 访问属性：方括号语法（当属性名是变量时使用）
const key = "age";
console.log(person[key]);           // 25

// 调用方法
console.log(person.greet());        // "你好，我叫张三"

// 修改属性
person.age = 26;

// 添加新属性
person.email = "zhangsan@example.com";

// 删除属性
delete person.isStudent;

```

### 对象解构

```js
const user = {
  name: "李四",
  age: 30,
  city: "上海",
  role: "admin"
};

// 基本解构
const { name, age } = user;
console.log(name); // "李四"
console.log(age);  // 30

// 重命名
const { name: userName, age: userAge } = user;
console.log(userName); // "李四"

// 默认值（属性不存在时使用默认值）
const { name: n, score = 100 } = user;
console.log(score); // 100（user 中没有 score，使用默认值）

// 剩余属性
const { name: theName, ...rest } = user;
console.log(theName); // "李四"
console.log(rest);    // { age: 30, city: "上海", role: "admin" }

// 函数参数中解构（非常常用）
function displayUser({ name, age, city = "未知" }) {
  console.log(`${name}，${age}岁，来自${city}`);
}
displayUser(user); // "李四，30岁，来自上海"

```

### 对象展开与合并

```js
const defaults = { color: "red", size: "medium", weight: 100 };
const custom = { color: "blue", weight: 200 };

// 展开合并（后面的属性覆盖前面的）
const config = { ...defaults, ...custom };
console.log(config);
// { color: "blue", size: "medium", weight: 200 }

// 创建对象副本（浅拷贝）
const original = { a: 1, b: 2 };
const copy = { ...original };
copy.a = 99;
console.log(original.a); // 1（原对象不受影响）

```

### Object 静态方法

```js
const obj = { a: 1, b: 2, c: 3 };

// Object.keys()：返回所有键名组成的数组
console.log(Object.keys(obj));   // ["a", "b", "c"]

// Object.values()：返回所有值组成的数组
console.log(Object.values(obj)); // [1, 2, 3]

// Object.entries()：返回 [键, 值] 对组成的数组
console.log(Object.entries(obj));
// [["a", 1], ["b", 2], ["c", 3]]

// 遍历对象（常用模式）
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

// Object.fromEntries()：将 [键, 值] 数组转为对象
const entries = [["x", 10], ["y", 20]];
console.log(Object.fromEntries(entries)); // { x: 10, y: 20 }

// 实用：将对象的所有值乘以2
const doubled = Object.fromEntries(
  Object.entries(obj).map(([key, value]) => [key, value * 2])
);
console.log(doubled); // { a: 2, b: 4, c: 6 }

```

---

## 流程控制

### if / else

```js
const score = 85;

if (score >= 90) {
  console.log("优秀");
} else if (score >= 80) {
  console.log("良好");
} else if (score >= 60) {
  console.log("及格");
} else {
  console.log("不及格");
}
// 输出："良好"

```

### switch / case

```js
const day = "Monday";

switch (day) {
  case "Monday":
  case "Tuesday":
  case "Wednesday":
  case "Thursday":
  case "Friday":
    console.log("工作日");
    break; // 不加 break 会继续执行下一个 case
  case "Saturday":
  case "Sunday":
    console.log("周末");
    break;
  default:
    console.log("无效日期");
}
// 输出："工作日"

```

### for 循环

```js
// 基础 for 循环
for (let i = 0; i < 5; i++) {
  console.log(i); // 0, 1, 2, 3, 4
}

// for...of：遍历可迭代对象（数组、字符串等）（推荐用于数组）
const fruits = ["苹果", "香蕉", "橙子"];
for (const fruit of fruits) {
  console.log(fruit);
}

// for...in：遍历对象的键名（主要用于对象）
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(`${key}: ${obj[key]}`);
}

// while 循环：先判断条件再执行
let count = 0;
while (count < 3) {
  console.log(count);
  count++;
}

// do...while：先执行一次再判断条件
let n = 0;
do {
  console.log(n);
  n++;
} while (n < 3);

```

### break 和 continue

```js
// break：跳出循环
for (let i = 0; i < 10; i++) {
  if (i === 5) break;
  console.log(i); // 0, 1, 2, 3, 4
}

// continue：跳过当前迭代，继续下一次
for (let i = 0; i < 5; i++) {
  if (i === 2) continue;
  console.log(i); // 0, 1, 3, 4
}

```

---

## 函数

### 三种定义方式

```js
// 1. 函数声明（Function Declaration）
// 特点：有变量提升，可在定义前调用
function add(a, b) {
  return a + b;
}
console.log(add(2, 3)); // 5

// 2. 函数表达式（Function Expression）
// 特点：没有提升，必须先定义再调用
const multiply = function(a, b) {
  return a * b;
};
console.log(multiply(2, 3)); // 6

// 3. 箭头函数（Arrow Function）（ES6 新增，推荐）
// 特点：语法简洁，没有自己的 this
const subtract = (a, b) => a - b;
console.log(subtract(5, 3)); // 2

// 箭头函数的多种写法
const square = x => x * x;           // 单参数可省略括号
const greet = () => "Hello!";         // 无参数用空括号
const getObj = () => ({ key: "val" }); // 返回对象字面量需加括号
const divide = (a, b) => {            // 多行需要花括号和 return
  if (b === 0) return null;
  return a / b;
};

```

### 参数默认值与 rest 参数

```js
// 默认参数值
function createUser(name, role = "普通用户", active = true) {
  return { name, role, active };
}
console.log(createUser("张三"));               // { name: "张三", role: "普通用户", active: true }
console.log(createUser("李四", "管理员"));    // { name: "李四", role: "管理员", active: true }
console.log(createUser("王五", "游客", false)); // { name: "王五", role: "游客", active: false }

// rest 参数：将剩余参数收集为数组（必须放在最后）
function sum(first, ...rest) {
  return rest.reduce((acc, num) => acc + num, first);
}
console.log(sum(1, 2, 3, 4, 5)); // 15

```

### 作用域与闭包

```js
// 全局作用域：在任何地方都能访问
const globalVar = "我是全局变量";

function outer() {
  // 函数作用域
  const outerVar = "我是外层变量";

  function inner() {
    // 块级作用域（可以访问外层变量）
    const innerVar = "我是内层变量";
    console.log(globalVar);  // 可以访问
    console.log(outerVar);   // 可以访问
    console.log(innerVar);   // 可以访问
  }

  inner();
  // console.log(innerVar); // 报错：innerVar 在这里不可见
}

// 变量提升（Hoisting）
console.log(hoisted); // undefined（var 声明提升，但赋值不提升）
var hoisted = "我被提升了";

// console.log(notHoisted); // 报错：不能在声明前访问 let/const
let notHoisted = "我没有提升";

// 闭包：函数记住了它被创建时的词法作用域
function makeCounter() {
  let count = 0; // 这个变量被闭包"记住"了

  return {
    increment() { count++; },
    decrement() { count--; },
    getCount() { return count; }
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 2
// count 变量在外部无法直接访问，被封装在闭包内

```

### 高阶函数

接受函数作为参数，或者返回函数的函数，称为高阶函数。

```js
// 接受函数作为参数（map/filter/forEach 都是高阶函数）
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(num => num % 2 === 0); // filter 接收一个函数

// 返回函数（工厂函数）
function createMultiplier(multiplier) {
  // 返回一个新函数
  return (number) => number * multiplier;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// 函数组合
function pipe(...fns) {
  return (value) => fns.reduce((acc, fn) => fn(acc), value);
}

const process = pipe(
  x => x * 2,
  x => x + 1,
  x => x ** 2
);
console.log(process(3)); // ((3 * 2) + 1)² = 49

```

---

## DOM 操作

DOM（Document Object Model，文档对象模型）是浏览器将 HTML 解析为的树形结构，JavaScript 通过操作 DOM 来动态修改页面。

### 获取元素

```js
// getElementById：通过 id 获取单个元素
const title = document.getElementById("main-title");

// querySelector：通过 CSS 选择器获取第一个匹配元素
const firstButton = document.querySelector("button");
const submitBtn = document.querySelector("#submit-btn");
const firstCard = document.querySelector(".card");
const input = document.querySelector("input[type='text']");

// querySelectorAll：获取所有匹配元素（返回 NodeList，可用 forEach 遍历）
const allButtons = document.querySelectorAll("button");
const allCards = document.querySelectorAll(".card");

allButtons.forEach(btn => {
  console.log(btn.textContent);
});

// 检查是否找到了元素
if (title) {
  console.log("找到了标题");
} else {
  console.log("没有找到 id 为 main-title 的元素");
}

```

### 修改内容

```js
const element = document.querySelector("#content");

// innerHTML：读写元素的 HTML 内容（可以包含 HTML 标签）
element.innerHTML = "<strong>加粗文字</strong>";
console.log(element.innerHTML); // "<strong>加粗文字</strong>"

// textContent：读写元素的纯文本内容（不解析 HTML 标签，更安全）
element.textContent = "<strong>这会显示为普通文字</strong>";
// 页面上显示的就是 "<strong>这会显示为普通文字</strong>" 这段文字本身

// innerText：类似 textContent，但会考虑 CSS 样式（比如隐藏元素不会包含）
element.innerText = "普通文字";

// 注意：使用 innerHTML 插入用户输入的内容存在 XSS 安全风险
// 如果只是设置文字，优先用 textContent

```

### 修改样式

```js
const box = document.querySelector(".box");

// 通过 style 直接设置行内样式
box.style.color = "red";
box.style.fontSize = "20px";         // CSS 属性名用驼峰命名
box.style.backgroundColor = "blue";
box.style.display = "none";          // 隐藏元素
box.style.display = "";              // 清除行内样式，恢复 CSS 默认值

// 通过 classList 操作 class（推荐，将样式放在 CSS 中管理）
box.classList.add("active");         // 添加 class
box.classList.remove("active");      // 移除 class
box.classList.toggle("active");      // 切换 class（有就删，没有就加）
box.classList.contains("active");    // 检查是否有某 class，返回布尔值
box.classList.replace("old", "new"); // 替换 class

// 读取计算后的样式（考虑了所有 CSS 规则）
const computedStyle = window.getComputedStyle(box);
console.log(computedStyle.fontSize);     // 实际字体大小
console.log(computedStyle.display);      // 实际显示方式

```

### 创建、插入、删除元素

```js
// createElement：创建新元素
const newDiv = document.createElement("div");
newDiv.textContent = "我是新创建的 div";
newDiv.classList.add("card");

const container = document.querySelector("#container");

// appendChild：在末尾插入子元素
container.appendChild(newDiv);

// insertBefore(newElement, referenceElement)：在某元素前插入
const firstChild = container.firstElementChild;
container.insertBefore(newDiv, firstChild);

// 更现代的方法（推荐）
container.prepend(newDiv);   // 在开头插入
container.append(newDiv);    // 在末尾插入（可以插入字符串）
newDiv.before(container);    // 在 container 前插入 newDiv
newDiv.after(container);     // 在 container 后插入 newDiv

// insertAdjacentHTML：插入 HTML 字符串（效率更高）
container.insertAdjacentHTML("beforeend", "<p>末尾新段落</p>");
container.insertAdjacentHTML("afterbegin", "<p>开头新段落</p>");

// 删除元素
const toRemove = document.querySelector(".old-element");
if (toRemove) {
  toRemove.remove(); // 直接删除自身
}

```

### 属性操作

```js
const img = document.querySelector("img");
const input = document.querySelector("input");

// getAttribute：获取属性值
console.log(img.getAttribute("src"));   // 图片路径
console.log(img.getAttribute("alt"));   // 替代文字

// setAttribute：设置属性值
img.setAttribute("src", "/new-image.jpg");
img.setAttribute("alt", "新图片描述");

// removeAttribute：删除属性
input.removeAttribute("disabled");

// 直接通过属性名访问（更简洁，适用于常见属性）
img.src = "/another-image.jpg";
img.alt = "另一张图片";
input.value = "默认文字";
input.disabled = true;

// data-* 自定义属性（用于存储与元素相关的数据）
// HTML: <div data-user-id="123" data-role="admin">
const div = document.querySelector("div");
console.log(div.dataset.userId); // "123"（data-user-id 转为驼峰）
console.log(div.dataset.role);   // "admin"
div.dataset.newProp = "value";   // 动态添加 data 属性

```

---

## 事件

### addEventListener

```js
const button = document.querySelector("#my-button");

// 基本用法
button.addEventListener("click", function(event) {
  console.log("按钮被点击了！");
  console.log(event); // 事件对象，包含事件相关信息
});

// 箭头函数写法
button.addEventListener("click", (event) => {
  console.log("点击位置：", event.clientX, event.clientY);
});

```

#### addEventListener 参数说明

| 参数       | 类型               | 必填 | 说明                                             |
| -------- | ---------------- | -- | ---------------------------------------------- |
| type     | string           | 是  | 事件类型，如 "click"、"input"、"keydown"               |
| callback | Function         | 是  | 事件触发时执行的函数，接收 event 对象为参数                      |
| options  | boolean 或 object | 否  | true 表示在捕获阶段触发；object 可设置 capture、once、passive |

```js
// options 对象的常用配置
button.addEventListener("click", handler, {
  capture: false,  // 是否在捕获阶段触发（默认 false，即冒泡阶段）
  once: true,      // 是否只触发一次，触发后自动移除监听器
  passive: true    // 是否不调用 preventDefault()（用于滚动优化）
});

// 移除事件监听器（需要保存函数引用）
function handleClick() {
  console.log("点击");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // 移除

```

### 常用事件类型

```js
const input = document.querySelector("input");
const form = document.querySelector("form");

// 鼠标事件
element.addEventListener("click", handler);       // 鼠标单击
element.addEventListener("dblclick", handler);    // 双击
element.addEventListener("mouseover", handler);   // 鼠标移入（包含子元素）
element.addEventListener("mouseout", handler);    // 鼠标移出
element.addEventListener("mouseenter", handler);  // 鼠标进入（不包含子元素）
element.addEventListener("mouseleave", handler);  // 鼠标离开

// 键盘事件
document.addEventListener("keydown", (e) => {
  console.log(e.key);     // 按键名称，如 "Enter"、"Escape"、"a"
  console.log(e.code);    // 物理按键代码，如 "KeyA"、"Enter"
  console.log(e.ctrlKey); // 是否按住 Ctrl
  console.log(e.shiftKey);// 是否按住 Shift
});
document.addEventListener("keyup", handler);    // 按键松开

// 表单事件
input.addEventListener("input", (e) => {
  console.log("当前值：", e.target.value); // 每次输入都触发
});
input.addEventListener("change", (e) => {
  console.log("值改变了：", e.target.value); // 失焦后才触发
});
input.addEventListener("focus", handler);  // 获得焦点
input.addEventListener("blur", handler);   // 失去焦点

form.addEventListener("submit", (e) => {
  e.preventDefault(); // 阻止表单默认提交（页面刷新）
  // 处理表单数据
});

// 页面加载事件
window.addEventListener("load", handler);             // 页面及所有资源加载完成
document.addEventListener("DOMContentLoaded", handler); // HTML 解析完成（推荐）
window.addEventListener("resize", handler);           // 窗口大小变化
window.addEventListener("scroll", handler);           // 页面滚动

```

### 事件对象

```js
document.addEventListener("click", (event) => {
  // 触发事件的原始元素
  console.log(event.target);

  // 绑定事件监听器的元素
  console.log(event.currentTarget);

  // 阻止默认行为（如阻止链接跳转、表单提交）
  event.preventDefault();

  // 阻止事件冒泡（阻止事件继续向父元素传播）
  event.stopPropagation();

  // 鼠标位置
  console.log(event.clientX, event.clientY); // 相对于视口
  console.log(event.pageX, event.pageY);     // 相对于文档
});

```

### 事件委托

将事件监听器绑定到父元素，利用事件冒泡处理子元素的事件。适合动态添加的元素。

```js
// 问题：列表项是动态添加的，无法提前为每个 li 绑定事件
const list = document.querySelector("#todo-list");

// 不用为每个 li 单独绑定，只在父元素上绑定一次
list.addEventListener("click", (event) => {
  // 判断点击的是不是 li 元素
  if (event.target.tagName === "LI") {
    event.target.classList.toggle("done");
  }

  // 更精确的判断（支持嵌套元素）
  const li = event.target.closest("li");
  if (li) {
    li.classList.toggle("done");
  }
});

```

---

## 异步编程

JavaScript 是单线程的，异步操作（如网络请求、定时器）通过事件循环机制处理。

### 回调函数

```js
// 回调：将函数作为参数传入，等操作完成后调用
setTimeout(() => {
  console.log("1秒后执行");
}, 1000);

// 回调地狱（Callback Hell）：多层嵌套导致代码难以维护
fetchUser(userId, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      // 越来越深，难以阅读和维护
    });
  });
});

```

### Promise

Promise 是异步操作的封装，代表一个未来会完成（或失败）的操作。

```js
// 创建 Promise
const promise = new Promise((resolve, reject) => {
  // 执行异步操作
  const success = true;

  if (success) {
    resolve("操作成功！"); // 成功时调用 resolve
  } else {
    reject(new Error("操作失败！")); // 失败时调用 reject
  }
});

// 使用 Promise
promise
  .then((result) => {
    console.log("成功：", result); // "操作成功！"
  })
  .catch((error) => {
    console.log("失败：", error.message);
  })
  .finally(() => {
    console.log("无论成功失败都会执行"); // 常用于关闭 loading 状态
  });

// Promise 链式调用
fetch("/api/users")
  .then((response) => response.json()) // 解析 JSON，返回新 Promise
  .then((users) => {
    console.log(users);
    return users[0]; // 返回值传给下一个 .then
  })
  .then((firstUser) => {
    console.log(firstUser.name);
  })
  .catch((error) => {
    // 任何一步出错都会跳到这里
    console.error("出错了：", error);
  });

```

#### Promise 静态方法

```js
const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");

// Promise.all：所有 Promise 都成功才成功，有一个失败就失败
Promise.all([p1, p2, p3])
  .then(([users, posts, comments]) => {
    console.log("三个请求都成功了");
  })
  .catch((error) => {
    console.log("至少一个请求失败了");
  });

// Promise.allSettled：等所有 Promise 结束，不管成功还是失败
Promise.allSettled([p1, p2, p3])
  .then((results) => {
    results.forEach((result) => {
      if (result.status === "fulfilled") {
        console.log("成功：", result.value);
      } else {
        console.log("失败：", result.reason);
      }
    });
  });

// Promise.race：第一个完成的 Promise 的结果（不管成功失败）
Promise.race([p1, p2, p3])
  .then((firstResult) => {
    console.log("最快的请求结果：", firstResult);
  });

```

### async / await

async/await 是 Promise 的语法糖，让异步代码看起来像同步代码，更易读。

```js
// async 函数：始终返回 Promise
async function fetchUserData(userId) {
  // await 等待 Promise 完成，暂停函数执行
  // await 只能在 async 函数内使用
  const response = await fetch(`/api/users/${userId}`);
  const user = await response.json();
  return user; // 等同于 Promise.resolve(user)
}

// 调用 async 函数（它返回 Promise，可以用 .then 或 await）
fetchUserData(1).then(user => console.log(user));

// 更推荐的方式：在另一个 async 函数中 await
async function main() {
  const user = await fetchUserData(1);
  console.log(user.name);
}

// 错误处理：用 try/catch
async function safeRequest() {
  try {
    const response = await fetch("/api/data");

    // 检查 HTTP 状态码
    if (!response.ok) {
      throw new Error(`HTTP 错误：${response.status}`);
    }

    const data = await response.json();
    console.log("数据：", data);
    return data;
  } catch (error) {
    // 网络错误或手动 throw 的错误都会在这里捕获
    console.error("请求失败：", error.message);
    return null; // 返回 null 作为错误时的默认值
  } finally {
    console.log("请求结束，关闭 loading");
  }
}

```

### fetch API

fetch 是浏览器内置的 HTTP 请求函数，返回 Promise。

#### fetch 参数说明

| 参数      | 类型     | 必填 | 说明       |
| ------- | ------ | -- | -------- |
| url     | string | 是  | 请求的 URL  |
| options | object | 否  | 请求配置，见下表 |

#### options 配置说明

| 字段          | 类型                | 默认值           | 说明                                   |
| ----------- | ----------------- | ------------- | ------------------------------------ |
| method      | string            | "GET"         | 请求方法：GET、POST、PUT、DELETE 等           |
| headers     | object            | {}            | 请求头                                  |
| body        | string 或 FormData | 无             | 请求体（GET 请求不能有 body）                  |
| mode        | string            | "cors"        | 跨域模式：cors、no-cors、same-origin        |
| credentials | string            | "same-origin" | 是否发送 Cookie：omit、same-origin、include |

```js
// GET 请求
async function getUsers() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const users = await response.json(); // 解析 JSON 响应体
  return users;
}

// POST 请求（发送 JSON 数据）
async function createUser(userData) {
  const response = await fetch("https://jsonplaceholder.typicode.com/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json", // 告诉服务器发送的是 JSON
      "Authorization": "Bearer token123"  // 鉴权 token（如果需要）
    },
    body: JSON.stringify(userData) // 将对象转为 JSON 字符串
  });

  const newUser = await response.json();
  return newUser;
}

// 使用示例
async function demo() {
  try {
    const users = await getUsers();
    console.log("所有用户：", users);

    const newUser = await createUser({ name: "新用户", email: "new@example.com" });
    console.log("创建的用户：", newUser);
  } catch (error) {
    console.error("操作失败：", error.message);
  }
}

demo();

```

---

## ES6+ 常用特性

### 解构赋值

```js
// 数组解构
const [first, second, third] = [1, 2, 3];
console.log(first);  // 1
console.log(second); // 2

// 跳过某些元素
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3

// 默认值
const [x = 10, y = 20] = [5];
console.log(x, y); // 5 20

// 剩余元素
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

// 交换变量（无需临时变量）
let p = 1, q = 2;
[p, q] = [q, p];
console.log(p, q); // 2 1

// 对象解构（见"对象"章节）
const { name, age } = { name: "张三", age: 25 };

```

### 展开运算符（Spread Operator）

```js
// 展开数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];       // [1, 2, 3, 4, 5, 6]
const withExtra = [...arr1, 99, ...arr2]; // [1, 2, 3, 99, 4, 5, 6]

// 复制数组（浅拷贝）
const copy = [...arr1];
copy.push(100);
console.log(arr1); // [1, 2, 3]（原数组不变）

// 展开对象
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 }（obj2 的 b 覆盖 obj1 的 b）

// 将 arguments/NodeList 转为真正的数组
function test() {
  const argsArray = [...arguments];
  console.log(argsArray.map(x => x * 2));
}
test(1, 2, 3); // [2, 4, 6]

```

### 模块：import / export

```js
// utils.js（导出模块）

// 命名导出
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export class Calculator {
  multiply(a, b) {
    return a * b;
  }
}

// 默认导出（每个文件只能有一个）
export default function main() {
  console.log("主函数");
}

```

```js
// main.js（导入模块）

// 导入命名导出
import { PI, add, Calculator } from "./utils.js";

// 重命名导入
import { add as sum } from "./utils.js";

// 导入所有命名导出到一个命名空间
import * as utils from "./utils.js";
console.log(utils.PI);
console.log(utils.add(1, 2));

// 导入默认导出（名称可以自定义）
import mainFunction from "./utils.js";

// 同时导入默认导出和命名导出
import mainFunction, { PI, add } from "./utils.js";

```

### class

```js
// 定义类
class Animal {
  // constructor：构造函数，创建实例时自动调用
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  // 方法
  speak() {
    return `${this.name} 说：${this.sound}`;
  }

  // 静态方法：通过类调用，不能通过实例调用
  static create(name, sound) {
    return new Animal(name, sound);
  }

  // getter
  get info() {
    return `动物：${this.name}`;
  }
}

// 创建实例
const dog = new Animal("小狗", "汪汪");
console.log(dog.speak());   // "小狗 说：汪汪"
console.log(dog.info);      // "动物：小狗"（getter 当属性访问）
console.log(Animal.create("小猫", "喵喵").speak()); // 静态方法

// 继承
class Dog extends Animal {
  constructor(name, breed) {
    super(name, "汪汪"); // 必须先调用 super() 调用父类构造函数
    this.breed = breed;
  }

  // 重写父类方法
  speak() {
    return `${super.speak()}（品种：${this.breed}）`; // super 调用父类方法
  }

  fetch() {
    return `${this.name} 去捡球了！`;
  }
}

const myDog = new Dog("旺财", "金毛");
console.log(myDog.speak());  // "旺财 说：汪汪（品种：金毛）"
console.log(myDog.fetch());  // "旺财 去捡球了！"
console.log(myDog instanceof Dog);    // true
console.log(myDog instanceof Animal); // true（继承关系）

```

### Symbol、WeakMap、WeakSet 简介

```js
// Symbol：唯一标识符，常用于对象属性键，避免命名冲突
const id = Symbol("id");
const anotherId = Symbol("id");
console.log(id === anotherId); // false（每个 Symbol 都是唯一的）

const user = {
  name: "张三",
  [id]: 12345 // 以 Symbol 为键
};
console.log(user[id]); // 12345

// WeakMap：键必须是对象，不阻止垃圾回收（用于存储私有数据或缓存）
const cache = new WeakMap();
const obj = {};
cache.set(obj, { computedValue: 42 });
console.log(cache.get(obj)); // { computedValue: 42 }
// 当 obj 被垃圾回收后，cache 中对应的条目也会自动清理

// WeakSet：存储对象引用，不阻止垃圾回收（用于标记对象）
const seen = new WeakSet();
const element = document.querySelector("div");
seen.add(element);
console.log(seen.has(element)); // true

```

---

## 综合实战：Todo List

一个完整的待办事项应用，涵盖：DOM 操作、事件处理、本地存储。

### HTML 结构

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Todo List</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 0 16px; }
    h1 { margin-bottom: 20px; }
    .input-area { display: flex; gap: 8px; margin-bottom: 20px; }
    .input-area input { flex: 1; padding: 8px 12px; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; }
    .input-area button { padding: 8px 16px; background: #333; color: white; border: none; border-radius: 4px; cursor: pointer; }
    .todo-list { list-style: none; }
    .todo-item { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid #eee; }
    .todo-item.done .todo-text { text-decoration: line-through; color: #aaa; }
    .todo-text { flex: 1; font-size: 16px; }
    .delete-btn { background: none; border: none; color: #f44; cursor: pointer; font-size: 18px; }
    .stats { margin-top: 16px; color: #666; font-size: 14px; }
    .filter-area { display: flex; gap: 8px; margin-bottom: 16px; }
    .filter-area button { padding: 4px 12px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: white; }
    .filter-area button.active { background: #333; color: white; border-color: #333; }
  </style>
</head>
<body>
  <h1>待办事项</h1>

  <div class="input-area">
    <input type="text" id="todo-input" placeholder="输入待办事项..." />
    <button id="add-btn">添加</button>
  </div>

  <div class="filter-area">
    <button class="filter-btn active" data-filter="all">全部</button>
    <button class="filter-btn" data-filter="active">未完成</button>
    <button class="filter-btn" data-filter="done">已完成</button>
  </div>

  <ul class="todo-list" id="todo-list"></ul>

  <div class="stats" id="stats"></div>

  <script src="todo.js"></script>
</body>
</html>

```

### JavaScript 逻辑

```js
// todo.js

// ------- 状态管理 -------

// 从 localStorage 读取数据，如果没有则初始化为空数组
let todos = loadFromStorage();
let currentFilter = "all"; // 当前筛选状态

// ------- localStorage 操作 -------

function loadFromStorage() {
  try {
    const stored = localStorage.getItem("todos");
    // 如果 stored 为 null（首次访问），返回空数组；否则解析 JSON
    return stored ? JSON.parse(stored) : [];
  } catch (error) {
    // JSON.parse 出错时（数据损坏），返回空数组
    console.error("读取本地数据失败：", error);
    return [];
  }
}

function saveToStorage() {
  // 将 todos 数组转为 JSON 字符串保存
  localStorage.setItem("todos", JSON.stringify(todos));
}

// ------- 核心功能 -------

// 生成唯一 ID（用当前时间戳）
function generateId() {
  return Date.now().toString();
}

// 添加待办事项
function addTodo(text) {
  const trimmed = text.trim();
  if (!trimmed) return; // 不允许添加空内容

  const todo = {
    id: generateId(),
    text: trimmed,
    done: false,
    createdAt: new Date().toISOString()
  };

  todos.push(todo);
  saveToStorage();
  render();
}

// 切换完成状态
function toggleTodo(id) {
  todos = todos.map(todo =>
    todo.id === id ? { ...todo, done: !todo.done } : todo
  );
  saveToStorage();
  render();
}

// 删除待办事项
function deleteTodo(id) {
  todos = todos.filter(todo => todo.id !== id);
  saveToStorage();
  render();
}

// ------- 渲染 -------

// 根据当前筛选条件过滤数据
function getFilteredTodos() {
  switch (currentFilter) {
    case "active":
      return todos.filter(todo => !todo.done);
    case "done":
      return todos.filter(todo => todo.done);
    default: // "all"
      return todos;
  }
}

// 渲染单个 todo 项的 HTML
function createTodoHTML(todo) {
  return `
    <li class="todo-item ${todo.done ? "done" : ""}" data-id="${todo.id}">
      <input type="checkbox" class="todo-check" ${todo.done ? "checked" : ""} />
      <span class="todo-text">${escapeHTML(todo.text)}</span>
      <button class="delete-btn" title="删除">×</button>
    </li>
  `;
}

// 防止 XSS：将用户输入的特殊字符转义
function escapeHTML(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

// 渲染统计信息
function renderStats() {
  const total = todos.length;
  const done = todos.filter(t => t.done).length;
  const active = total - done;

  const statsEl = document.getElementById("stats");
  statsEl.textContent = `共 ${total} 项，已完成 ${done} 项，未完成 ${active} 项`;
}

// 主渲染函数
function render() {
  const listEl = document.getElementById("todo-list");
  const filtered = getFilteredTodos();

  if (filtered.length === 0) {
    listEl.innerHTML = "<li style='padding:12px;color:#aaa;'>暂无待办事项</li>";
  } else {
    listEl.innerHTML = filtered.map(createTodoHTML).join("");
  }

  renderStats();
}

// ------- 事件绑定 -------

// 添加按钮点击
document.getElementById("add-btn").addEventListener("click", () => {
  const input = document.getElementById("todo-input");
  addTodo(input.value);
  input.value = ""; // 清空输入框
  input.focus();    // 焦点回到输入框
});

// 按 Enter 键添加
document.getElementById("todo-input").addEventListener("keydown", (e) => {
  if (e.key === "Enter") {
    const input = e.target;
    addTodo(input.value);
    input.value = "";
  }
});

// 列表事件委托：处理复选框和删除按钮
document.getElementById("todo-list").addEventListener("click", (e) => {
  // 获取最近的 li 元素，取出其 data-id
  const li = e.target.closest(".todo-item");
  if (!li) return;

  const id = li.dataset.id;

  if (e.target.classList.contains("todo-check")) {
    // 点击了复选框
    toggleTodo(id);
  } else if (e.target.classList.contains("delete-btn")) {
    // 点击了删除按钮
    deleteTodo(id);
  }
});

// 筛选按钮
document.querySelector(".filter-area").addEventListener("click", (e) => {
  const btn = e.target.closest(".filter-btn");
  if (!btn) return;

  // 更新当前筛选状态
  currentFilter = btn.dataset.filter;

  // 更新按钮激活样式
  document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
  btn.classList.add("active");

  render();
});

// ------- 初始化 -------
render();

```

### 要点总结

这个 Todo List 综合了以下知识点：

- `localStorage.getItem` / `setItem`：持久化存储数据，页面刷新后数据不丢失
- `JSON.stringify` / `JSON.parse`：在字符串和对象之间转换（localStorage 只能存字符串）
- `Array.map`：更新数组中某个元素（配合展开运算符创建新对象）
- `Array.filter`：删除元素或筛选展示
- `insertAdjacentHTML` / `innerHTML`：高效渲染列表
- 事件委托：在父元素监听子元素事件，支持动态添加的元素
- `e.target.closest(selector)`：向上查找最近的匹配元素
- `dataset`：读取 `data-*` 属性
- XSS 防护：使用 `textContent` 转义用户输入

---

## 学习路线建议

掌握本文内容后，建议按以下顺序继续学习：

1. [HTML与CSS入门](https://blog.vercanti.com/html-yu-css-ru-men/) — 巩固 HTML/CSS 基础，理解 DOM 的基础
2. 深入学习 TypeScript — 为 JavaScript 添加类型系统
3. [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/) — 学习主流前端框架，进一步简化 DOM 操作
4. Node.js — 在服务端运行 JavaScript
5. 浏览器 API — Web Storage、Web Workers、WebSocket 等

---

## 最佳实践

**始终使用 `const` 和 `let`，不用 `var`**：`var` 存在变量提升和函数作用域问题，容易产生难以追踪的 bug。`const` 声明不可重新赋值的变量，`let` 声明可变量，两者都是块级作用域，语义更清晰。

**用 `===` 严格相等而非 `==` 宽松相等**：`==` 会进行隐式类型转换（如 `0 == false` 为 `true`），产生意料之外的结果。始终用 `===` 和 `!==` 进行比较，避免隐式转换陷阱。

**异步代码使用 `async/await`，而非裸 Promise 链**：`async/await` 使异步代码读起来像同步，错误处理用 `try/catch` 而非 `.catch()`，代码结构更清晰，调试更容易。

**不要用 `innerHTML` 插入用户输入的内容**：`innerHTML` 会执行 HTML 中的脚本，直接插入用户输入会导致 XSS 攻击。使用 `textContent` 插入纯文本，或对 HTML 进行转义后再插入。

**用 `for...of` 替代 `forEach` 进行需要 `await` 的异步循环**：`forEach` 不支持 async 回调等待，`for...of` 配合 `await` 才能顺序执行异步操作。

---

## 常见陷阱

### 陷阱：`this` 在箭头函数和普通函数中行为不同

**现象：** 在类方法内用箭头函数的 `setTimeout` 回调，`this` 意外丢失；或在对象字面量中用箭头函数，`this` 指向了 `window`。

**原因：** 普通函数的 `this` 在调用时动态绑定；箭头函数的 `this` 在定义时捕获外层 `this`，不会动态改变。

**解决：** 类方法中需要访问 `this` 的回调用箭头函数；对象字面量的方法用普通函数以保持正确绑定。

```javascript
class Timer {
  start() {
    setTimeout(() => {
      this.tick()  // 箭头函数：this 正确指向 Timer 实例
    }, 1000)
  }
}

```

### 陷阱：`forEach` 中用 `await` 无法等待异步操作

**现象：** `forEach` 中的 `await` 不阻塞循环，所有异步操作同时触发，后续代码在操作完成前就已执行。

**原因：** `forEach` 不支持 async 回调的返回 Promise，内部 `await` 只影响回调本身，`forEach` 不等待它们。

**解决：** 用 `for...of` \+ `await` 顺序执行，或 `Promise.all` \+ `map` 并行执行。

```javascript
// 错误：不等待
items.forEach(async item => { await save(item) })

// 正确：顺序等待
for (const item of items) { await save(item) }

// 正确：并行等待
await Promise.all(items.map(item => save(item)))

```

### 陷阱：`==` 宽松比较产生意外结果

**现象：** `if (value == false)` 对 `0`、`""`、`null`、`undefined` 都成立，逻辑判断出现意外分支。

**原因：** `==` 比较不同类型时进行隐式类型转换，规则复杂且违反直觉（`null == undefined` 为 `true`，但 `null == 0` 为 `false`）。

**解决：** 始终使用 `===` 严格相等；需要 null/undefined 兼容检查时用 `value == null`（这是唯一合理的 `==` 使用场景）。

---

## 参见

- [HTML与CSS入门](https://blog.vercanti.com/html-yu-css-ru-men/)
- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)
- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)
- [JavaScript Promise 完全指南](https://blog.vercanti.com/javascript-promise-wan-quan-zhi-nan/)