JavaScript从入门到精通

教程简介

系统学习JavaScript的完整教程,从零基础到高级特性,涵盖ES6+、DOM操作、异步编程、闭包等核心概念,包含待办事项和天气查询两个实战项目,适合Web开发初学者。

JavaScript 从入门到精通 — 完整教程

本教程面向零基础到中高级开发者,涵盖 JavaScript 核心概念、ES6+ 新特性、DOM 操作、异步编程、实战项目等内容。全文超过两万字,配合大量代码示例与实际案例,助你系统掌握这门语言。


目录

  1. JavaScript 简介与历史
  2. 开发环境搭建
  3. 基本语法与数据类型
  4. 变量声明(var / let / const)
  5. 运算符与表达式
  6. 字符串方法详解
  7. 数组操作与常用方法
  8. 对象与 JSON
  9. 条件判断与循环
  10. 函数定义与箭头函数
  11. 闭包与作用域
  12. Promise 与 async/await
  13. DOM 操作详解
  14. 事件处理
  15. BOM 浏览器对象模型
  16. ES6+ 新特性
  17. 模块化(import / export)
  18. 错误处理与调试
  19. 正则表达式
  20. 本地存储
  21. Fetch API 与网络请求
  22. 面向对象编程(类与继承)
  23. 实战:待办事项应用
  24. 实战:天气查询应用
  25. 性能优化技巧
  26. 学习路线图

1. JavaScript 简介与历史

1.1 什么是 JavaScript

JavaScript(简称 JS)是一种轻量级、解释型的编程语言,最初被设计用于在浏览器中实现动态交互效果。如今它已经发展成为一门全栈语言,可以运行在浏览器、服务器(Node.js)、桌面应用(Electron)、移动端(React Native)甚至物联网设备上。

JavaScript 的核心特点:

  • 动态类型:变量不需要预先声明类型,运行时自动推断
  • 基于原型的面向对象:不同于 Java 的基于类的继承,JS 使用原型链实现继承
  • 事件驱动:天然支持事件回调,适合处理用户交互
  • 函数是一等公民:函数可以作为变量传递、作为参数、作为返回值
  • 单线程 + 事件循环:JS 引擎是单线程的,但通过事件循环机制实现异步

1.2 发展历史

年份 事件
1995 Brendan Eich 在 Netscape 用 10 天创造了 Mocha,后改名 LiveScript,最终定名 JavaScript
1997 ECMAScript 1 标准发布(ECMA-262)
1999 ECMAScript 3 发布,加入正则表达式、try/catch
2009 ECMAScript 5 发布,加入严格模式、JSON、数组方法
2009 Node.js 发布,JS 进入服务端
2015 ECMAScript 6(ES2015)发布,里程碑式更新:let/const、箭头函数、Promise、类、模块
2016+ 每年发布新版本(ES2016、ES2017...),持续迭代

1.3 JavaScript 的运行环境

JavaScript 代码可以在两种主要环境中运行:

浏览器环境:浏览器内置 JS 引擎(Chrome 的 V8、Firefox 的 SpiderMonkey、Safari 的 JavaScriptCore),通过 <script> 标签引入 JS 代码。

<!-- 内联脚本 -->
<script>
  console.log('Hello, Browser!');
</script>

<!-- 外部脚本 -->
<script src="app.js"></script>

Node.js 环境:将 Chrome V8 引擎独立出来,配合 libuv 实现了跨平台的 JS 运行时。

# 终端中运行
node app.js

1.4 第一个程序

// 在控制台输出文字
console.log('Hello, World!');

// 变量与运算
let name = 'JavaScript';
let year = 2025;
console.log(`${name} 已经 ${year - 1995} 岁了!`);

2. 开发环境搭建

2.1 浏览器(零配置起步)

所有现代浏览器都自带开发者工具,按 F12Ctrl+Shift+I(Mac 为 Cmd+Option+I)即可打开。

  • Console 面板:直接输入 JS 代码并执行
  • Sources 面板:查看和调试网页中的 JS 文件
  • Network 面板:监控网络请求

推荐使用 Google ChromeMicrosoft Edge,它们的 DevTools 功能最完善。

2.2 安装 Node.js

访问 nodejs.org 下载 LTS(长期支持)版本。安装完成后验证:

node -v      # 查看 Node.js 版本
npm -v       # 查看 npm 版本

提示:推荐使用 nvm(Node Version Manager)管理多版本 Node.js,便于切换。

2.3 代码编辑器

Visual Studio Code(强烈推荐)是目前最流行的 JS 开发编辑器,免费且开源。

推荐安装的 VS Code 扩展:

扩展名 用途
ESLint 代码风格检查与自动修复
Prettier 代码格式化
JavaScript (ES6) code snippets 代码片段快速输入
Live Server 本地热重载服务器
Auto Rename Tag 自动同步修改 HTML 标签

2.4 项目初始化

# 创建项目目录
mkdir my-js-project
cd my-js-project

# 初始化 npm 项目
npm init -y

# 创建入口文件
echo 'console.log("项目启动成功!");' > index.js

# 运行
node index.js

3. 基本语法与数据类型

3.1 语句与分号

JavaScript 语句通常以分号 ; 结尾。虽然 JS 引擎有自动分号插入(ASI)机制,但建议始终手动书写分号以避免歧义。

let a = 10;     // 赋值语句
console.log(a); // 输出语句

3.2 注释

// 单行注释

/*
  多行注释
  适合写较长的说明
*/

/**
 * 文档注释(JSDoc 格式)
 * @param {string} name - 用户名
 * @returns {string} 问候语
 */
function greet(name) {
  return `你好,${name}!`;
}

3.3 基本数据类型

JavaScript 有 8 种数据类型,分为两大类:

原始类型(Primitive):存储的是值本身

// 1. Number — 数值(整数和浮点数共用一种类型)
let age = 25;
let price = 99.9;
let hex = 0xff;        // 十六进制
let binary = 0b1010;   // 二进制
let octal = 0o77;      // 八进制
let infinity = Infinity;
let notANumber = NaN;   // "Not a Number",但 typeof 返回 'number'

console.log(typeof age);         // "number"
console.log(typeof NaN);         // "number"(注意!)
console.log(0.1 + 0.2);          // 0.30000000000000004(浮点精度问题)
console.log(0.1 + 0.2 === 0.3);  // false

// 2. String — 字符串
let single = '单引号';
let double = "双引号";
let template = `模板字符串,可以嵌入 ${single}`;
console.log(typeof single);      // "string"

// 3. Boolean — 布尔值
let isTrue = true;
let isFalse = false;
console.log(typeof isTrue);      // "boolean"

// 4. undefined — 变量已声明但未赋值
let notAssigned;
console.log(notAssigned);         // undefined
console.log(typeof notAssigned);  // "undefined"

// 5. null — 表示"空"或"无"
let empty = null;
console.log(empty);               // null
console.log(typeof empty);        // "object"(历史遗留 bug)

// 6. Symbol — 唯一标识符(ES6 新增)
let id1 = Symbol('id');
let id2 = Symbol('id');
console.log(id1 === id2);         // false,每个 Symbol 都是唯一的

// 7. BigInt — 大整数(ES2020 新增)
let bigNum = 9007199254740991n;   // 超过 Number.MAX_SAFE_INTEGER
console.log(typeof bigNum);       // "bigint"

引用类型(Reference):存储的是引用地址

// Object — 对象
let person = {
  name: '张三',
  age: 30
};

// Array — 数组(本质也是对象)
let fruits = ['苹果', '香蕉', '橘子'];

// Function — 函数(本质也是对象)
function sayHello() {
  console.log('Hello!');
}

console.log(typeof person);   // "object"
console.log(typeof fruits);   // "object"
console.log(typeof sayHello); // "function"
console.log(Array.isArray(fruits)); // true

3.4 类型转换

// 转为字符串
let num = 42;
String(num);           // "42"
num.toString();        // "42"
num + '';              // "42"(隐式转换)

// 转为数字
Number('123');         // 123
Number('abc');         // NaN
parseInt('42px');      // 42(解析到非数字字符为止)
parseFloat('3.14abc'); // 3.14
+'100';                // 100(一元加号隐式转换)

// 转为布尔值
Boolean(0);            // false
Boolean('');           // false
Boolean(null);         // false
Boolean(undefined);    // false
Boolean(NaN);          // false
Boolean(1);            // true
Boolean('hello');      // true
Boolean([]);           // true(空数组也是 true!)
Boolean({});           // true(空对象也是 true!)

// 以下值被称为"假值(Falsy)",其余都是"真值(Truthy)"
// false, 0, '', null, undefined, NaN

4. 变量声明(var / let / const)

4.1 var — 旧时代的变量声明

var name = '张三';
var age;         // 可以只声明不赋值
age = 25;        // 后续赋值

// var 的问题一:变量提升(Hoisting)
console.log(x); // undefined(不会报错!因为声明被提升到作用域顶部)
var x = 10;

// 等价于:
// var x;
// console.log(x);
// x = 10;

// var 的问题二:没有块级作用域
for (var i = 0; i < 3; i++) {
  // i 在这里可用
}
console.log(i); // 3 — i 泄漏到了外部!

// var 的问题三:可以重复声明
var a = 1;
var a = 2; // 不报错,容易引发 bug

4.2 let — 现代的变量声明

// 块级作用域
for (let i = 0; i < 3; i++) {
  // i 仅在此代码块内有效
}
// console.log(i); // ReferenceError! i 未定义

// 不存在变量提升(存在暂时性死区 TDZ)
// console.log(y); // ReferenceError!
let y = 20;

// 不允许重复声明
let b = 1;
// let b = 2; // SyntaxError!

// 可以重新赋值
let count = 0;
count = 1; // 合法

4.3 const — 常量声明

// 声明时必须赋值
const PI = 3.14159;
// PI = 3; // TypeError! 常量不可重新赋值

// const 对于引用类型:引用地址不可变,但内容可变
const user = { name: '李四' };
user.name = '王五';     // 合法!修改的是属性值
// user = {};           // TypeError! 不能重新赋值整个对象

// 如果需要完全冻结对象
const frozen = Object.freeze({ name: '赵六' });
frozen.name = '钱七';  // 静默失败(严格模式下报错)
console.log(frozen.name); // '赵六'

4.4 最佳实践

// 优先级:const > let > 不用 var
// 原则:如果值不会变,用 const;如果会变,用 let

const MAX_SIZE = 100;       // 常量用大写下划线命名
let currentUser = null;     // 会变的用 let
// var 已经不推荐在现代代码中使用

5. 运算符与表达式

5.1 算术运算符

let a = 10, b = 3;

console.log(a + b);  // 13  加法
console.log(a - b);  // 7   减法
console.log(a * b);  // 30  乘法
console.log(a / b);  // 3.333...  除法
console.log(a % b);  // 1   取余
console.log(a ** b); // 1000 幂运算(ES2016)

// 自增与自减
let count = 0;
count++;    // count = count + 1
count--;    // count = count - 1
++count;    // 前置自增(先加后用)
count++;    // 后置自增(先用后加)

5.2 比较运算符

// 相等与不等(会进行类型转换)
console.log(1 == '1');    // true(宽松相等)
console.log(1 != '1');    // false

// 严格相等(推荐使用,不会进行类型转换)
console.log(1 === '1');   // false
console.log(1 !== '1');   // true

// 大小比较
console.log(5 > 3);       // true
console.log(5 >= 5);      // true
console.log('apple' < 'banana'); // true(按字典序比较)

5.3 逻辑运算符

// 与、或、非
console.log(true && false);  // false(与)
console.log(true || false);  // true(或)
console.log(!true);          // false(非)

// 短路求值 — 常用于设置默认值
let name = user && user.name;    // 如果 user 存在,取 user.name
let port = config.port || 3000;  // 如果 port 未定义,使用默认值 3000

// 空值合并运算符(ES2020)— 只在 null/undefined 时才取默认值
let value = 0 ?? '默认值';       // 0(0 不是 null/undefined)
let value2 = null ?? '默认值';   // '默认值'

5.4 赋值运算符

let x = 10;
x += 5;   // x = x + 5 → 15
x -= 3;   // x = x - 3 → 12
x *= 2;   // x = x * 2 → 24
x /= 4;   // x = x / 4 → 6
x %= 4;   // x = x % 4 → 2
x **= 3;  // x = x ** 3 → 8

5.5 三元运算符

// 条件 ? 值1 : 值2
let age = 20;
let status = age >= 18 ? '成年' : '未成年';
console.log(status); // '成年'

// 可以嵌套(但不建议过深嵌套)
let grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'D';

5.6 解构赋值中的运算符

// 展开运算符(Spread)
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]

let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }

// 剩余运算符(Rest)
let [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
let { name, ...others } = { name: '张三', age: 30, city: '北京' };
// name='张三', others={age:30, city:'北京'}

6. 字符串方法详解

6.1 创建与基本操作

let str = 'Hello, JavaScript!';

// 获取长度
console.log(str.length); // 18

// 获取指定位置的字符
console.log(str.charAt(0));    // 'H'
console.log(str[0]);           // 'H'(更常用)
console.log(str.at(-1));       // '!'(ES2022,支持负索引)

// 查找子串
console.log(str.indexOf('Java'));       // 7(首次出现的位置)
console.log(str.lastIndexOf('a'));      // 14(最后一次出现的位置)
console.log(str.includes('Script'));    // true
console.log(str.startsWith('Hello'));   // true
console.log(str.endsWith('!'));         // true
console.log(str.search(/java/i));       // 7(支持正则表达式)

6.2 截取与替换

let str = 'Hello, World!';

// 截取子串
console.log(str.slice(0, 5));       // 'Hello'(推荐)
console.log(str.slice(7));          // 'World!'
console.log(str.slice(-6, -1));     // 'World'(支持负索引)
console.log(str.substring(0, 5));   // 'Hello'(不支持负索引)

// 替换
console.log(str.replace('World', 'JS'));  // 'Hello, JS!'
console.log(str.replaceAll('l', 'L'));    // 'HeLLo, WorLd!'(ES2021)
// 使用正则进行全局替换
console.log(str.replace(/l/g, 'L'));      // 'HeLLo, WorLd!'

// 分割
console.log(str.split(', '));  // ['Hello', 'World!']
console.log('a-b-c'.split('-')); // ['a', 'b', 'c']

// 去除空白
let padded = '  hello  ';
console.log(padded.trim());      // 'hello'
console.log(padded.trimStart()); // 'hello  '
console.log(padded.trimEnd());   // '  hello'

6.3 模板字符串(ES6)

let name = '小明';
let age = 18;

// 多行字符串
let html = `
  <div class="card">
    <h2>${name}</h2>
    <p>年龄:${age}</p>
  </div>
`;

// 表达式嵌入
let price = 99.9;
let quantity = 3;
console.log(`总价:¥${(price * quantity).toFixed(2)}`);
// 输出:总价:¥299.70

// 标签模板字符串(高级用法)
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] !== undefined ? `<mark>${values[i]}</mark>` : '');
  }, '');
}
let highlighted = highlight`姓名:${name},年龄:${age}`;
// '姓名:<mark>小明</mark>,年龄:<mark>18</mark>'

6.4 常用转换方法

let str = 'Hello World';

console.log(str.toUpperCase()); // 'HELLO WORLD'
console.log(str.toLowerCase()); // 'hello world'
console.log(str.padStart(20, '-')); // '---------Hello World'
console.log(str.padEnd(20, '-'));   // 'Hello World---------'
console.log('abc'.repeat(3));       // 'abcabcabc'

// 类型转换
console.log(String(123));       // '123'
console.log((123).toString());  // '123'
console.log((255).toString(16)); // 'ff'(转为十六进制字符串)

7. 数组操作与常用方法

7.1 创建数组

// 字面量创建(推荐)
let fruits = ['苹果', '香蕉', '橘子'];

// 构造函数创建
let numbers = new Array(5);        // 创建长度为 5 的空数组
let values = new Array(1, 2, 3);   // [1, 2, 3]

// Array.from — 从类数组或可迭代对象创建
let chars = Array.from('hello');   // ['h', 'e', 'l', 'l', 'o']
let range = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]

// Array.of — 将参数作为数组元素
let single = Array.of(5);          // [5](注意:不是 [undefined x 5])

7.2 增删元素

let arr = [1, 2, 3];

// 末尾操作
arr.push(4, 5);    // 末尾添加 → [1, 2, 3, 4, 5],返回新长度
arr.pop();         // 末尾删除 → [1, 2, 3, 4],返回被删除的元素

// 开头操作
arr.unshift(0);    // 开头添加 → [0, 1, 2, 3, 4],返回新长度
arr.shift();       // 开头删除 → [1, 2, 3, 4],返回被删除的元素

// 任意位置操作
arr.splice(1, 1);       // 从索引 1 开始删除 1 个元素 → [1, 3, 4]
arr.splice(1, 0, 2);    // 在索引 1 处插入 2,不删除 → [1, 2, 3, 4]
arr.splice(1, 1, 9, 8); // 替换索引 1 的元素为 9 和 8 → [1, 9, 8, 3, 4]

7.3 遍历方法

let numbers = [1, 2, 3, 4, 5];

// forEach — 遍历(无返回值)
numbers.forEach((num, index) => {
  console.log(`索引 ${index}:${num}`);
});

// map — 映射(返回新数组)
let doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]

// filter — 过滤(返回满足条件的新数组)
let evens = numbers.filter(n => n % 2 === 0); // [2, 4]

// find — 查找第一个满足条件的元素
let found = numbers.find(n => n > 3); // 4

// findIndex — 查找第一个满足条件的索引
let idx = numbers.findIndex(n => n > 3); // 3

// some — 至少有一个满足条件
console.log(numbers.some(n => n > 4)); // true

// every — 全部满足条件
console.log(numbers.every(n => n > 0)); // true

7.4 归约与排序

let numbers = [1, 2, 3, 4, 5];

// reduce — 归约(累加、统计等)
let sum = numbers.reduce((acc, cur) => acc + cur, 0); // 15
let max = numbers.reduce((max, cur) => cur > max ? cur : max, -Infinity); // 5

// 排序
let arr = [3, 1, 4, 1, 5, 9, 2, 6];
arr.sort();  // [1, 1, 2, 3, 4, 5, 6, 9](默认按字符串排序,可能出错!)

// 正确的数值排序
arr.sort((a, b) => a - b); // 升序:[1, 1, 2, 3, 4, 5, 6, 9]
arr.sort((a, b) => b - a); // 降序:[9, 6, 5, 4, 3, 2, 1, 1]

// 反转
arr.reverse(); // [1, 1, 2, 3, 4, 5, 6, 9]

7.5 数组转换

let arr = [1, 2, 3, 4, 5];

// slice — 截取(不改变原数组)
let sliced = arr.slice(1, 3); // [2, 3]
let copy = arr.slice();        // 浅拷贝

// concat — 合并
let merged = arr.concat([6, 7, 8]); // [1,2,3,4,5,6,7,8]

// flat — 扁平化嵌套数组(ES2019)
let nested = [1, [2, [3, [4]]]];
console.log(nested.flat());    // [1, 2, [3, [4]]]
console.log(nested.flat(2));   // [1, 2, 3, [4]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4]

// join — 连接为字符串
console.log(arr.join('-')); // '1-2-3-4-5'

// Array.isArray — 判断是否为数组
console.log(Array.isArray(arr)); // true
console.log(Array.isArray('abc')); // false

// includes — 是否包含某元素
console.log(arr.includes(3)); // true

8. 对象与 JSON

8.1 对象基础

// 对象字面量
let person = {
  name: '张三',
  age: 30,
  hobbies: ['读书', '编程', '跑步'],
  address: {
    city: '北京',
    district: '海淀区'
  },
  // 方法
  greet() {
    return `大家好,我是${this.name},今年${this.age}岁`;
  }
};

// 访问属性
console.log(person.name);           // 点号访问
console.log(person['age']);         // 方括号访问(适用于动态 key)
console.log(person.address.city);   // 链式访问

// 动态属性名
let key = 'email';
person[key] = 'zhangsan@example.com';

// 可选链操作符(ES2020)— 安全访问深层属性
console.log(person.address?.zipCode); // undefined(不会报错)
console.log(person.job?.company);     // undefined

8.2 对象操作

let user = { name: '李四', age: 25 };

// 添加/修改属性
user.email = 'lisi@example.com';
user.age = 26;

// 删除属性
delete user.email;

// 检查属性
console.log('name' in user);              // true
console.log(user.hasOwnProperty('age'));   // true
console.log(Object.hasOwn(user, 'age'));  // true(ES2022,推荐)

// 遍历属性
for (let key in user) {
  console.log(`${key}: ${user[key]}`);
}

// 获取键、值、键值对
console.log(Object.keys(user));    // ['name', 'age']
console.log(Object.values(user));  // ['李四', 26]
console.log(Object.entries(user)); // [['name', '李四'], ['age', 26]]

// 从键值对创建对象
let entries = [['a', 1], ['b', 2]];
let obj = Object.fromEntries(entries); // { a: 1, b: 2 }

8.3 对象合并与拷贝

let defaults = { theme: 'light', lang: 'zh', fontSize: 14 };
let userConfig = { theme: 'dark', fontSize: 16 };

// Object.assign — 浅拷贝合并
let config = Object.assign({}, defaults, userConfig);
// { theme: 'dark', lang: 'zh', fontSize: 16 }

// 展开运算符(更常用)
let config2 = { ...defaults, ...userConfig };

// 深拷贝(ES2024 正式支持,老版本用 JSON 方式)
let original = { a: 1, b: { c: 2 } };
let deepCopy = structuredClone(original); // 深拷贝
deepCopy.b.c = 99;
console.log(original.b.c); // 2(原对象不受影响)

// JSON 方式深拷贝(有限制:不支持函数、undefined、Symbol、循环引用)
let jsonCopy = JSON.parse(JSON.stringify(original));

8.4 JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。

// JSON.stringify — 对象转 JSON 字符串
let data = { name: '张三', age: 30, scores: [90, 85, 92] };
let jsonStr = JSON.stringify(data);
// '{"name":"张三","age":30,"scores":[90,85,92]}'

// 格式化输出
let pretty = JSON.stringify(data, null, 2);

// 自定义转换(replacer 参数)
let filtered = JSON.stringify(data, ['name', 'scores']);
// '{"name":"张三","scores":[90,85,92]}'

// JSON.parse — JSON 字符串转对象
let parsed = JSON.parse(jsonStr);
console.log(parsed.name); // '张三'

// 带 reviver 函数
let dateStr = '{"name":"会议","time":"2025-01-01T00:00:00Z"}';
let withDate = JSON.parse(dateStr, (key, value) => {
  if (key === 'time') return new Date(value);
  return value;
});

9. 条件判断与循环

9.1 if / else if / else

let score = 85;

if (score >= 90) {
  console.log('优秀');
} else if (score >= 80) {
  console.log('良好'); // 执行这行
} else if (score >= 60) {
  console.log('及格');
} else {
  console.log('不及格');
}

9.2 switch

let day = new Date().getDay(); // 0=周日, 1=周一...

switch (day) {
  case 0:
  case 6:
    console.log('周末');
    break;
  case 1:
    console.log('周一');
    break;
  default:
    console.log('工作日');
}

// 注意:switch 使用严格相等(===)比较
// 不写 break 会导致"穿透"到下一个 case

9.3 for 循环

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

// for...of — 遍历可迭代对象(数组、字符串、Map、Set 等)
let fruits = ['苹果', '香蕉', '橘子'];
for (let fruit of fruits) {
  console.log(fruit);
}

// for...in — 遍历对象的可枚举属性
let person = { name: '张三', age: 30 };
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}

9.4 while 与 do...while

// while — 先判断后执行
let n = 0;
while (n < 5) {
  console.log(n);
  n++;
}

// do...while — 先执行后判断(至少执行一次)
let input;
do {
  input = 'yes'; // 模拟用户输入
  console.log('处理输入...');
} while (input !== 'yes');

9.5 break 与 continue

// 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 < 10; i++) {
  if (i % 2 === 0) continue;
  console.log(i); // 1, 3, 5, 7, 9
}

// 标签(Label)— 跳出多层循环
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) break outer;
    console.log(i, j);
  }
}

10. 函数定义与箭头函数

10.1 函数声明

// 函数声明(有提升,可以在声明前调用)
function add(a, b) {
  return a + b;
}

// 函数表达式(无提升)
const subtract = function(a, b) {
  return a - b;
};

// 默认参数(ES6)
function greet(name = '访客') {
  return `你好,${name}!`;
}
console.log(greet());       // '你好,访客!'
console.log(greet('小明')); // '你好,小明!'

10.2 箭头函数(ES6)

// 基本语法
const multiply = (a, b) => a * b;

// 单参数可省略括号
const square = x => x * x;

// 无参数需要空括号
const sayHi = () => 'Hi!';

// 多条语句需要花括号和 return
const calculate = (a, b) => {
  let sum = a + b;
  let product = a * b;
  return { sum, product };
};

// 返回对象字面量需要用括号包裹
const createUser = (name, age) => ({ name, age });

10.3 箭头函数与普通函数的区别

// 区别一:this 绑定
const obj = {
  name: '张三',

  // 普通函数:this 指向调用者
  sayName() {
    console.log(this.name); // '张三'
  },

  // 箭头函数:this 继承外层作用域
  delayedSay() {
    setTimeout(() => {
      console.log(this.name); // '张三'(箭头函数没有自己的 this)
    }, 100);
  }
};

// 区别二:没有 arguments 对象
function regular() {
  console.log(arguments); // Arguments [1, 2, 3]
}
regular(1, 2, 3);

const arrow = (...args) => {
  console.log(args); // [1, 2, 3](用 rest 参数代替)
};
arrow(1, 2, 3);

// 区别三:不能作为构造函数
// const Person = (name) => { this.name = name; };
// new Person('张三'); // TypeError!

10.4 高阶函数

高阶函数是接受函数作为参数或返回函数的函数。

// 接受函数作为参数
function withLogging(fn) {
  return function(...args) {
    console.log(`调用函数,参数:${args}`);
    const result = fn(...args);
    console.log(`返回结果:${result}`);
    return result;
  };
}

const loggedAdd = withLogging((a, b) => a + b);
loggedAdd(3, 5);
// 调用函数,参数:3,5
// 返回结果:8

// 返回函数(函数工厂)
function multiplier(factor) {
  return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

// 实际应用:数组方法都是高阶函数
[1, 2, 3, 4, 5]
  .filter(n => n % 2 !== 0)   // [1, 3, 5]
  .map(n => n ** 2)            // [1, 9, 25]
  .reduce((sum, n) => sum + n, 0); // 35

11. 闭包与作用域

11.1 作用域链

let global = '全局变量';

function outer() {
  let outerVar = '外层变量';

  function inner() {
    let innerVar = '内层变量';
    console.log(innerVar);  // ✅ 自己的变量
    console.log(outerVar);  // ✅ 外层变量(向上查找)
    console.log(global);    // ✅ 全局变量(继续向上查找)
  }

  inner();
}

outer();
// 作用域链:inner → outer → 全局

11.2 闭包

闭包是指函数能够"记住"并访问它创建时所在的作用域,即使该函数在其他地方执行。

function createCounter() {
  let count = 0; // 这个变量被"封闭"在闭包中

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

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount());  // 1
// count 变量无法从外部直接访问,实现了"私有变量"

11.3 闭包的实际应用

// 应用一:防抖(Debounce)
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const search = debounce((keyword) => {
  console.log(`搜索:${keyword}`);
}, 300);

// 应用二:缓存(Memoization)
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n - 1));
console.log(factorial(10)); // 3628800(第二次调用相同参数会直接返回缓存)

// 应用三:私有变量模拟
function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > 0 && amount <= balance) balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// balance 变量外部无法直接修改,保证了数据安全

11.4 经典陷阱:循环中的闭包

// ❌ 错误写法
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(因为 var 没有块级作用域,i 被共享)

// ✅ 修复方法一:使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2

// ✅ 修复方法二:使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// 输出:0, 1, 2

12. Promise 与 async/await

12.1 异步编程概述

JavaScript 是单线程语言,但通过事件循环(Event Loop)机制实现异步操作。常见的异步操作包括:网络请求、定时器、文件读写等。

12.2 Promise 基础

Promise 是异步操作的容器,代表一个最终会完成(或失败)的操作。

// 创建 Promise
const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功!');  // 状态变为 fulfilled
    } else {
      reject(new Error('操作失败'));  // 状态变为 rejected
    }
  }, 1000);
});

// 使用 Promise
myPromise
  .then(result => {
    console.log(result); // '操作成功!'
  })
  .catch(error => {
    console.error(error.message);
  })
  .finally(() => {
    console.log('无论成功失败都会执行');
  });

12.3 Promise 链式调用

function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: '张三' }), 100);
  });
}

function fetchOrders(userId) {
  return new Promise(resolve => {
    setTimeout(() => resolve([
      { id: 1, product: '手机', price: 5999 },
      { id: 2, product: '耳机', price: 299 }
    ]), 100);
  });
}

// 链式调用
fetchUser(1)
  .then(user => {
    console.log('用户:', user.name);
    return fetchOrders(user.id); // 返回新的 Promise
  })
  .then(orders => {
    console.log('订单:', orders);
  })
  .catch(error => {
    console.error('出错了:', error);
  });

12.4 Promise 静态方法

// Promise.all — 全部成功才成功,一个失败就失败
const p1 = fetch('/api/users');
const p2 = fetch('/api/orders');
const p3 = fetch('/api/products');

Promise.all([p1, p2, p3])
  .then(([users, orders, products]) => {
    console.log('全部加载完成');
  })
  .catch(error => {
    console.error('有一个请求失败了');
  });

// Promise.allSettled — 等待全部完成(不管成功失败)(ES2020)
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.race([p1, p2, p3])
  .then(first => console.log('最快返回:', first));

// Promise.any — 第一个成功的决定结果(ES2021)
Promise.any([p1, p2, p3])
  .then(first => console.log('第一个成功:', first));

12.5 async/await(ES2017)

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

// 基本用法
async function getUserData(userId) {
  try {
    const user = await fetchUser(userId);      // 等待 Promise 完成
    const orders = await fetchOrders(user.id);  // 再等待下一个
    return { user, orders };
  } catch (error) {
    console.error('获取数据失败:', error);
    throw error; // 可以继续向上抛出
  }
}

// 调用 async 函数
getUserData(1).then(data => console.log(data));

// 并发执行(提高性能)
async function getAllData() {
  // 同时发起请求,而不是依次等待
  const [users, orders, products] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/orders').then(r => r.json()),
    fetch('/api/products').then(r => r.json())
  ]);
  return { users, orders, products };
}

// 顶层 await(ES2022,仅在模块中可用)
// const data = await fetch('/api/data').then(r => r.json());

13. DOM 操作详解

13.1 什么是 DOM

DOM(Document Object Model,文档对象模型)是浏览器将 HTML 文档解析后生成的树状结构,JavaScript 通过 DOM API 可以动态地读取和修改页面内容。

13.2 获取元素

// 通过 ID 获取(返回单个元素)
const header = document.getElementById('header');

// 通过 CSS 选择器获取(返回第一个匹配元素)
const firstCard = document.querySelector('.card');

// 通过 CSS 选择器获取所有匹配元素(返回 NodeList)
const allCards = document.querySelectorAll('.card');

// 其他方法(较少使用)
const divs = document.getElementsByTagName('div');      // HTMLCollection
const items = document.getElementsByClassName('item');  // HTMLCollection

// NodeList 与 HTMLCollection 的区别
// NodeList 支持 forEach,HTMLCollection 不支持(需要 Array.from 转换)
allCards.forEach(card => console.log(card));

13.3 修改内容

const element = document.querySelector('#demo');

// innerHTML — 获取/设置 HTML 内容(有 XSS 风险)
element.innerHTML = '<strong>加粗</strong>文字';

// textContent — 获取/设置纯文本内容(安全)
element.textContent = '<strong>不会解析标签</strong>';

// innerText — 类似 textContent,但会考虑 CSS 隐藏
element.innerText = '可见文本';

13.4 修改样式与类名

const box = document.querySelector('.box');

// 修改内联样式
box.style.backgroundColor = '#ff6b6b';
box.style.padding = '20px';
box.style.borderRadius = '8px';

// 使用 cssText 批量设置
box.style.cssText = 'background: #ff6b6b; padding: 20px; border-radius: 8px;';

// 类名操作(推荐使用 classList)
box.classList.add('active');           // 添加类
box.classList.remove('hidden');        // 移除类
box.classList.toggle('expanded');      // 切换类
box.classList.contains('active');      // 检查是否包含
box.classList.replace('old', 'new');   // 替换类

13.5 创建与删除元素

// 创建元素
const newDiv = document.createElement('div');
newDiv.textContent = '我是新元素';
newDiv.classList.add('new-item');

// 插入到页面
const parent = document.querySelector('#container');
parent.appendChild(newDiv);                    // 末尾追加
parent.insertBefore(newDiv, parent.firstChild); // 插入到最前面

// 更灵活的插入方法(推荐)
parent.append(newDiv, '文本节点');              // 可追加多个节点和文本
parent.prepend(newDiv);                        // 插入到最前面
parent.before(newDiv);                         // 在 parent 前面插入
parent.after(newDiv);                          // 在 parent 后面插入

// 删除元素
newDiv.remove();                               // 直接删除自己
parent.removeChild(newDiv);                    // 从父元素中删除

13.6 属性操作

const link = document.querySelector('a');

// 获取/设置属性
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
console.log(link.getAttribute('href'));

// 直接访问属性
link.href = 'https://example.com';
link.target = '_blank';

// 移除属性
link.removeAttribute('target');

// 数据属性(data-*)
const item = document.querySelector('.item');
item.dataset.id = '123';           // 设置 data-id="123"
item.dataset.category = 'books';   // 设置 data-category="books"
console.log(item.dataset.id);      // '123'

14. 事件处理

14.1 事件监听

const button = document.querySelector('#myButton');

// 推荐方式:addEventListener
button.addEventListener('click', function(event) {
  console.log('按钮被点击了!');
  console.log('目标元素:', event.target);
  console.log('鼠标坐标:', event.clientX, event.clientY);
});

// 移除事件监听
function handleClick() {
  console.log('点击');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // 必须传入同一个函数引用

// 也可以用 on 属性(但只能绑一个处理函数)
button.onclick = function() { console.log('点击'); };

14.2 常见事件类型

// 鼠标事件
element.addEventListener('click', handler);       // 单击
element.addEventListener('dblclick', handler);    // 双击
element.addEventListener('mouseenter', handler);  // 鼠标进入
element.addEventListener('mouseleave', handler);  // 鼠标离开
element.addEventListener('mousemove', handler);   // 鼠标移动

// 键盘事件
document.addEventListener('keydown', (e) => {
  console.log(e.key);     // 按键名称
  console.log(e.code);    // 物理按键码
  if (e.key === 'Enter') { /* 回车 */ }
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault(); // 阻止默认行为
    // 自定义保存逻辑
  }
});

// 表单事件
const input = document.querySelector('input');
input.addEventListener('input', (e) => {
  console.log('实时输入:', e.target.value);
});
input.addEventListener('change', (e) => {
  console.log('值改变并失焦:', e.target.value);
});

const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
  e.preventDefault(); // 阻止表单默认提交
  const formData = new FormData(form);
  console.log(Object.fromEntries(formData));
});

// 滚动事件
window.addEventListener('scroll', () => {
  console.log('滚动位置:', window.scrollY);
});

14.3 事件委托

事件委托利用事件冒泡机制,将子元素的事件处理绑定到父元素上,提高性能。

// ❌ 不推荐:给每个按钮都绑定事件
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick);
});

// ✅ 推荐:事件委托
document.querySelector('.btn-container').addEventListener('click', (e) => {
  if (e.target.classList.contains('btn')) {
    console.log('点击了:', e.target.textContent);
  }
  // 更精确的匹配(处理子元素内部点击的情况)
  const btn = e.target.closest('.btn');
  if (btn) {
    console.log('点击了:', btn.textContent);
  }
});

// 动态添加的元素也能自动生效!

14.4 事件对象

element.addEventListener('click', (event) => {
  event.preventDefault();    // 阻止默认行为
  event.stopPropagation();   // 阻止事件冒泡

  event.target;              // 触发事件的元素
  event.currentTarget;       // 绑定事件监听器的元素
  event.type;                // 事件类型('click')
  event.timeStamp;           // 事件发生的时间戳
});

15. BOM 浏览器对象模型

15.1 window 对象

// 窗口尺寸
console.log(window.innerWidth);   // 视口宽度
console.log(window.innerHeight);  // 视口高度

// 打开/关闭窗口
let newWindow = window.open('https://example.com', '_blank', 'width=800,height=600');
// newWindow.close();

// 滚动
window.scrollTo(0, 0);               // 滚动到顶部
window.scrollTo({ top: 100, behavior: 'smooth' }); // 平滑滚动

// 对话框(阻塞式,慎用)
// alert('提示信息');
// let confirmed = confirm('确定要删除吗?');
// let name = prompt('请输入姓名', '默认值');

15.2 location 对象

// 当前页面 URL 信息
console.log(location.href);       // 完整 URL
console.log(location.protocol);   // 'https:'
console.log(location.host);       // 'example.com:8080'
console.log(location.pathname);   // '/path'
console.log(location.search);     // '?key=value'
console.log(location.hash);       // '#section'

// URL 参数解析
const params = new URLSearchParams(location.search);
const id = params.get('id');
const page = params.get('page') || '1';

// 页面跳转
location.href = 'https://example.com';
location.assign('https://example.com');  // 同 href
location.replace('https://example.com'); // 不会留下历史记录
location.reload();                       // 刷新页面

15.3 history 对象

history.back();      // 后退
history.forward();   // 前进
history.go(-2);      // 后退两步

// HTML5 History API(SPA 路由基础)
history.pushState({ page: 'home' }, '', '/home');
history.replaceState({ page: 'about' }, '', '/about');

window.addEventListener('popstate', (event) => {
  console.log('状态变化:', event.state);
});

15.4 navigator 对象

console.log(navigator.userAgent);    // 浏览器 UA 字符串
console.log(navigator.language);     // 'zh-CN'
console.log(navigator.onLine);       // true/false(是否在线)
console.log(navigator.geolocation);  // 地理位置 API

// 检测设备类型
const isMobile = /Mobi|Android/i.test(navigator.userAgent);

// 剪贴板 API
async function copyText(text) {
  await navigator.clipboard.writeText(text);
  console.log('复制成功');
}

// 通知 API
if (Notification.permission === 'granted') {
  new Notification('标题', { body: '内容' });
} else {
  Notification.requestPermission();
}

16. ES6+ 新特性

16.1 解构赋值

// 数组解构
const [a, b, c] = [1, 2, 3];
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
const [, second] = [1, 2];              // 跳过元素,second=2
const [x = 10] = [];                    // 默认值,x=10

// 对象解构
const { name, age } = { name: '张三', age: 30 };
const { name: userName, age: userAge } = { name: '张三', age: 30 }; // 重命名
const { score = 0 } = {};               // 默认值
const { address: { city } } = { address: { city: '北京' } }; // 嵌套解构

// 函数参数解构
function printUser({ name, age, city = '未知' }) {
  console.log(`${name}, ${age}岁, ${city}`);
}
printUser({ name: '张三', age: 30 });

16.2 展开运算符与剩余参数

// 数组展开
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5]; // [1,2,3,4,5]

// 对象展开
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 }; // {a:1, b:2, c:3}

// 剩余参数
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4, 5); // 15

16.3 Map 与 Set

// Map — 键值对集合(键可以是任何类型)
const map = new Map();
map.set('name', '张三');
map.set(42, '数字键');
map.set(true, '布尔键');

console.log(map.get('name')); // '张三'
console.log(map.size);        // 3
console.log(map.has('name')); // true
map.delete(42);

// 遍历 Map
for (let [key, value] of map) {
  console.log(key, value);
}

// Set — 唯一值集合
const set = new Set([1, 2, 3, 2, 1]);
console.log(set); // Set {1, 2, 3}
console.log(set.size); // 3

set.add(4);
set.delete(1);
console.log(set.has(2)); // true

// 数组去重
let arr = [1, 1, 2, 3, 3, 4];
let unique = [...new Set(arr)]; // [1, 2, 3, 4]

16.4 Symbol 与迭代器

// Symbol — 唯一标识符
const SECRET = Symbol('secret');
const obj = {
  name: '张三',
  [SECRET]: '密码123'
};
console.log(obj[SECRET]); // '密码123'

// 自定义迭代器
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
};

for (let num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

16.5 可选链与空值合并

// 可选链(ES2020)
const user = { profile: { avatar: 'img.jpg' } };
console.log(user.profile?.avatar);      // 'img.jpg'
console.log(user.settings?.theme);      // undefined(不报错)
console.log(user.getName?.());           // undefined(方法也可能不存在)

// 空值合并(ES2020)
let value = null ?? '默认值';    // '默认值'
let value2 = 0 ?? '默认值';      // 0
let value3 = '' ?? '默认值';     // ''

// 与 || 的区别
let count = 0 || 10;  // 10(0 是假值)
let count2 = 0 ?? 10; // 0(只检查 null/undefined)

17. 模块化(import / export)

17.1 为什么需要模块化

模块化可以将代码拆分为独立的文件,实现代码复用、作用域隔离和按需加载。

17.2 导出(Export)

// math.js — 命名导出
export const PI = 3.14159;

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

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

// 也可以先定义再统一导出
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;
export { subtract, divide };
// user.js — 默认导出(每个模块只能有一个)
export default class User {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `你好,我是${this.name}`;
  }
}

17.3 导入(Import)

// 导入命名导出
import { add, multiply, PI } from './math.js';

// 导入默认导出
import User from './user.js';

// 混合导入
import User, { add, multiply } from './module.js';

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

// 导入所有
import * as math from './math.js';
math.add(1, 2);

// 动态导入(按需加载)
async function loadModule() {
  const module = await import('./heavy-module.js');
  module.doSomething();
}

17.4 在 HTML 中使用模块

<!-- 使用 type="module" -->
<script type="module" src="app.js"></script>

<!-- 模块默认是延迟加载的,等同于 defer -->
<!-- 模块自动使用严格模式 -->
<!-- 模块有独立的作用域 -->

18. 错误处理与调试

18.1 try...catch...finally

try {
  // 可能出错的代码
  let data = JSON.parse('invalid json');
} catch (error) {
  console.error('解析错误:', error.message);
  console.error('错误类型:', error.name);       // 'SyntaxError'
  console.error('错误堆栈:', error.stack);
} finally {
  // 无论是否出错都会执行
  console.log('清理完成');
}

18.2 自定义错误

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

function validateAge(age) {
  if (typeof age !== 'number' || age < 0 || age > 150) {
    throw new ValidationError('年龄必须是 0-150 之间的数字', 'age');
  }
  return true;
}

try {
  validateAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`字段 ${error.field} 验证失败: ${error.message}`);
  } else {
    throw error; // 不认识的错误继续抛出
  }
}

18.3 常见错误类型

// ReferenceError — 引用未定义的变量
// console.log(undefinedVar);

// TypeError — 类型错误
// null.property;
// 'abc'.push();

// SyntaxError — 语法错误
// eval('if (true) {');

// RangeError — 超出范围
// new Array(-1);

// URIError — URI 编码/解码错误
// decodeURI('%');

18.4 调试技巧

// console 方法
console.log('普通日志');
console.info('信息');
console.warn('警告');
console.error('错误');
console.table([{ name: '张三', age: 30 }, { name: '李四', age: 25 }]);
console.group('分组');
console.log('分组内容');
console.groupEnd();
console.time('计时器');
// 耗时操作
console.timeEnd('计时器'); // 输出耗时

// debugger 语句 — 手动断点
function complexLogic(data) {
  debugger; // 代码执行到这里会暂停(需要打开 DevTools)
  // ...
}

// 性能分析
console.profile('性能分析');
// 耗时代码
console.profileEnd('性能分析');

19. 正则表达式

19.1 创建正则

// 字面量方式
const regex1 = /hello/i;

// 构造函数方式
const regex2 = new RegExp('hello', 'i');

// 常用修饰符
// i — 忽略大小写
// g — 全局匹配
// m — 多行模式
// s — dotAll 模式(. 匹配换行符)
// u — Unicode 模式

19.2 常用语法

// 基本匹配
/hello/.test('hello world');     // true
/hello/.test('Hello World');     // false
/hello/i.test('Hello World');    // true

// 特殊字符
/\d/.test('123');     // true — \d 匹配数字
/\w/.test('abc');     // true — \w 匹配字母、数字、下划线
/\s/.test(' ');       // true — \s 匹配空白字符
/./.test('a');        // true — . 匹配任意字符(除换行符)

// 量词
/a*/.test('');         // true — 0 次或多次
/a+/.test('a');        // true — 1 次或多次
/a?/.test('');         // true — 0 次或 1 次
/a{3}/.test('aaa');    // true — 恰好 3 次
/a{2,4}/.test('aaa');  // true — 2 到 4 次

// 字符类
/[abc]/.test('a');     // true — 匹配 a、b 或 c
/[a-z]/.test('m');     // true — 匹配 a 到 z
/[0-9]/.test('5');     // true — 匹配 0 到 9
/[^abc]/.test('d');    // true — 匹配除 a、b、c 之外的字符

// 边界
/^hello/.test('hello world');  // true — 开头匹配
/world$/.test('hello world');  // true — 结尾匹配
/\bword\b/.test('a word b');  // true — 单词边界

19.3 常用方法

const str = '我的电话是 13812345678,邮箱是 test@example.com';

// test — 测试是否匹配
/\d{11}/.test(str); // true

// match — 提取匹配内容
str.match(/\d{11}/); // ['13812345678', index: 6, ...]
str.match(/\d+/g);   // ['13812345678']

// replace — 替换匹配内容
str.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
// '我的电话是 138****5678,邮箱是 test@example.com'

// search — 查找匹配位置
str.search(/邮箱/); // 19

// split — 按正则分割
'a,b,,c'.split(/,+/); // ['a', 'b', 'c']

19.4 命名捕获组(ES2018)

const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-01-15'.match(dateRegex);

console.log(match.groups.year);  // '2025'
console.log(match.groups.month); // '01'
console.log(match.groups.day);   // '15'

// 在 replace 中使用命名组
'2025-01-15'.replace(dateRegex, '$<month>/$<day>/$<year>');
// '01/15/2025'

19.5 实用正则表达式

// 手机号验证(中国大陆)
const phoneRegex = /^1[3-9]\d{9}$/;

// 邮箱验证
const emailRegex = /^[\w.-]+@[\w-]+(\.\w+)+$/;

// 身份证号验证
const idCardRegex = /^\d{17}[\dXx]$/;

// URL 提取
const urlRegex = /https?:\/\/[\w.-]+(?:\/[\w./?%&=-]*)?/g;

// HTML 标签提取
const tagRegex = /<(\w+)[^>]*>.*?<\/\1>/g;

// 中文字符匹配
const chineseRegex = /[\u4e00-\u9fa5]+/g;

20. 本地存储

20.1 localStorage

localStorage 的数据永久存储,除非手动删除。

// 存储数据(只能存字符串)
localStorage.setItem('username', '张三');
localStorage.setItem('theme', 'dark');

// 读取数据
const name = localStorage.getItem('username'); // '张三'

// 删除数据
localStorage.removeItem('theme');

// 清空所有数据
localStorage.clear();

// 存储对象/数组(需要序列化)
const settings = { theme: 'dark', fontSize: 14 };
localStorage.setItem('settings', JSON.stringify(settings));

const loaded = JSON.parse(localStorage.getItem('settings'));
console.log(loaded.theme); // 'dark'

// 获取存储大小
let total = 0;
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  total += key.length + localStorage.getItem(key).length;
}
console.log(`已使用约 ${total * 2} 字节`); // UTF-16 编码,每字符 2 字节

20.2 sessionStorage

sessionStorage 的数据在标签页关闭后自动清除。

// 用法与 localStorage 完全相同
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');
sessionStorage.removeItem('token');

// 典型用法:保存表单草稿
function saveDraft() {
  const formData = {
    title: document.querySelector('#title').value,
    content: document.querySelector('#content').value
  };
  sessionStorage.setItem('draft', JSON.stringify(formData));
}

function loadDraft() {
  const draft = JSON.parse(sessionStorage.getItem('draft'));
  if (draft) {
    document.querySelector('#title').value = draft.title;
    document.querySelector('#content').value = draft.content;
  }
}

20.3 Cookie

// 设置 Cookie
document.cookie = 'username=张三; path=/; max-age=86400'; // 24小时过期
document.cookie = 'theme=dark; path=/; secure; samesite=strict';

// 读取 Cookie
function getCookie(name) {
  const cookies = document.cookie.split('; ');
  for (const cookie of cookies) {
    const [key, value] = cookie.split('=');
    if (key === name) return decodeURIComponent(value);
  }
  return null;
}

console.log(getCookie('username')); // '张三'

// 删除 Cookie(设置过期时间为过去的时间)
document.cookie = 'username=; path=/; max-age=0';

20.4 IndexedDB(简介)

IndexedDB 是浏览器内置的大型数据库,适合存储大量结构化数据。

// 打开数据库
const request = indexedDB.open('MyDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore('users', { keyPath: 'id' });
  store.createIndex('name', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');

  // 添加数据
  store.add({ id: 1, name: '张三', age: 30 });
  store.add({ id: 2, name: '李四', age: 25 });

  // 查询数据
  const getRequest = store.get(1);
  getRequest.onsuccess = () => {
    console.log(getRequest.result); // { id: 1, name: '张三', age: 30 }
  };
};

21. Fetch API 与网络请求

21.1 基本用法

// GET 请求
fetch('https://api.example.com/users')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP 错误: ${response.status}`);
    }
    return response.json(); // 解析 JSON
  })
  .then(data => {
    console.log('用户列表:', data);
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

// async/await 写法(推荐)
async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('获取用户失败:', error);
    throw error;
  }
}

21.2 POST 请求

async function createUser(userData) {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your-token-here'
    },
    body: JSON.stringify(userData)
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || '创建失败');
  }

  return response.json();
}

// 使用
createUser({ name: '张三', email: 'zhangsan@example.com' })
  .then(user => console.log('创建成功:', user));

21.3 其他 HTTP 方法

// PUT — 更新资源
async function updateUser(id, data) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  return response.json();
}

// DELETE — 删除资源
async function deleteUser(id) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'DELETE'
  });
  return response.ok;
}

// PATCH — 部分更新
async function patchUser(id, partialData) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(partialData)
  });
  return response.json();
}

21.4 请求配置与超时

// 带超时的请求
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('请求超时');
    }
    throw error;
  }
}

// 使用
fetchWithTimeout('https://api.example.com/data', {}, 3000)
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err.message));

// 上传进度(需要 XMLHttpRequest,fetch 不原生支持进度回调)
function uploadWithProgress(url, formData, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        onProgress((e.loaded / e.total) * 100);
      }
    });
    xhr.addEventListener('load', () => resolve(JSON.parse(xhr.responseText)));
    xhr.addEventListener('error', () => reject(new Error('上传失败')));
    xhr.open('POST', url);
    xhr.send(formData);
  });
}

22. 面向对象编程(类与继承)

22.1 类的基本语法

class Animal {
  // 构造函数
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

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

  // 静态方法(通过类名调用,实例无法调用)
  static isAnimal(obj) {
    return obj instanceof Animal;
  }
}

const dog = new Animal('旺财', '汪汪');
console.log(dog.speak());         // '旺财 说:汪汪'
console.log(Animal.isAnimal(dog)); // true
console.log(dog instanceof Animal); // true

22.2 继承(extends)

class Dog extends Animal {
  constructor(name, breed) {
    super(name, '汪汪'); // 调用父类构造函数
    this.breed = breed;
  }

  // 重写父类方法
  speak() {
    return `${super.speak()}!(${this.breed})`;
  }

  // 子类独有方法
  fetch(item) {
    return `${this.name} 捡回了 ${item}`;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name, '喵喵');
  }

  purr() {
    return `${this.name} 在打呼噜...`;
  }
}

const myDog = new Dog('小黑', '拉布拉多');
console.log(myDog.speak());      // '小黑 说:汪汪!(拉布拉多)'
console.log(myDog.fetch('球'));   // '小黑 捡回了 球'

const myCat = new Cat('咪咪');
console.log(myCat.speak());      // '咪咪 说:喵喵'
console.log(myCat.purr());       // '咪咪 在打呼噜...'

22.3 getter 与 setter

class Temperature {
  #celsius; // 私有属性(ES2022)

  constructor(celsius) {
    this.#celsius = celsius;
  }

  // getter
  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  // setter
  set fahrenheit(value) {
    this.#celsius = (value - 32) * 5 / 9;
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(value) {
    if (value < -273.15) {
      throw new Error('温度不能低于绝对零度');
    }
    this.#celsius = value;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212
temp.fahrenheit = 32;
console.log(temp.celsius);    // 0

22.4 私有属性与方法(ES2022)

class BankAccount {
  #balance;    // 私有属性
  #owner;

  constructor(owner, initialBalance) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  // 私有方法
  #validate(amount) {
    if (amount <= 0) throw new Error('金额必须大于零');
  }

  deposit(amount) {
    this.#validate(amount);
    this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount) {
    this.#validate(amount);
    if (amount > this.#balance) throw new Error('余额不足');
    this.#balance -= amount;
    return this.#balance;
  }

  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount('张三', 1000);
account.deposit(500);
console.log(account.balance); // 1500
// console.log(account.#balance); // SyntaxError! 私有属性无法从外部访问

22.5 Mixin 模式

JavaScript 单继承,但可以通过 Mixin 实现多继承的效果。

// Mixin:可复用的功能模块
const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
};

const Loggable = (Base) => class extends Base {
  log(message) {
    console.log(`[${this.constructor.name}] ${message}`);
  }
};

// 组合多个 Mixin
class User extends Loggable(Serializable(Object)) {
  constructor(name) {
    super();
    this.name = name;
  }
}

const user = new User('张三');
user.log('创建用户');          // '[User] 创建用户'
console.log(user.serialize()); // '{"name":"张三"}'

23. 实战:待办事项应用

这是一个完整的待办事项(Todo List)应用,综合运用了 DOM 操作、事件处理、本地存储等知识。

23.1 HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>待办事项</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #f0f2f5;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      padding: 40px 20px;
    }
    .container {
      width: 100%;
      max-width: 560px;
    }
    h1 {
      text-align: center;
      color: #1a1a2e;
      margin-bottom: 24px;
      font-size: 2rem;
    }
    .input-group {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }
    .input-group input {
      flex: 1;
      padding: 12px 16px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }
    .input-group input:focus {
      outline: none;
      border-color: #4a90d9;
    }
    .input-group button {
      padding: 12px 24px;
      background: #4a90d9;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.2s;
    }
    .input-group button:hover {
      background: #357abd;
    }
    .filters {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
      justify-content: center;
    }
    .filters button {
      padding: 6px 16px;
      border: 1px solid #d0d0d0;
      background: white;
      border-radius: 16px;
      cursor: pointer;
      font-size: 0.85rem;
      transition: all 0.2s;
    }
    .filters button.active {
      background: #4a90d9;
      color: white;
      border-color: #4a90d9;
    }
    .todo-list {
      list-style: none;
    }
    .todo-item {
      display: flex;
      align-items: center;
      padding: 12px 16px;
      background: white;
      border-radius: 8px;
      margin-bottom: 8px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      transition: transform 0.2s, opacity 0.2s;
    }
    .todo-item:hover {
      transform: translateY(-1px);
    }
    .todo-item.completed .todo-text {
      text-decoration: line-through;
      color: #999;
    }
    .todo-item input[type="checkbox"] {
      width: 20px;
      height: 20px;
      margin-right: 12px;
      cursor: pointer;
    }
    .todo-text {
      flex: 1;
      font-size: 1rem;
    }
    .todo-delete {
      background: none;
      border: none;
      color: #ff4757;
      font-size: 1.2rem;
      cursor: pointer;
      opacity: 0;
      transition: opacity 0.2s;
    }
    .todo-item:hover .todo-delete {
      opacity: 1;
    }
    .empty-state {
      text-align: center;
      color: #999;
      padding: 40px;
      font-size: 0.95rem;
    }
    .stats {
      text-align: center;
      color: #666;
      font-size: 0.85rem;
      margin-top: 16px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>📝 待办事项</h1>
    <div class="input-group">
      <input type="text" id="todoInput" placeholder="添加新的待办事项..." />
      <button id="addBtn">添加</button>
    </div>
    <div class="filters">
      <button class="active" data-filter="all">全部</button>
      <button data-filter="active">进行中</button>
      <button data-filter="completed">已完成</button>
    </div>
    <ul class="todo-list" id="todoList"></ul>
    <p class="stats" id="stats"></p>
  </div>
  <script src="todo.js"></script>
</body>
</html>

23.2 JavaScript 逻辑

// todo.js

// 数据管理
class TodoApp {
  constructor() {
    this.todos = this.loadFromStorage();
    this.currentFilter = 'all';

    // DOM 元素
    this.input = document.getElementById('todoInput');
    this.addBtn = document.getElementById('addBtn');
    this.list = document.getElementById('todoList');
    this.stats = document.getElementById('stats');
    this.filterBtns = document.querySelectorAll('.filters button');

    this.bindEvents();
    this.render();
  }

  // 从 localStorage 加载数据
  loadFromStorage() {
    try {
      return JSON.parse(localStorage.getItem('todos')) || [];
    } catch {
      return [];
    }
  }

  // 保存到 localStorage
  saveToStorage() {
    localStorage.setItem('todos', JSON.stringify(this.todos));
  }

  // 绑定事件
  bindEvents() {
    // 添加待办
    this.addBtn.addEventListener('click', () => this.addTodo());
    this.input.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') this.addTodo();
    });

    // 筛选按钮
    this.filterBtns.forEach(btn => {
      btn.addEventListener('click', () => {
        this.filterBtns.forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        this.currentFilter = btn.dataset.filter;
        this.render();
      });
    });

    // 事件委托:删除和切换完成状态
    this.list.addEventListener('click', (e) => {
      const item = e.target.closest('.todo-item');
      if (!item) return;

      const id = Number(item.dataset.id);

      if (e.target.classList.contains('todo-delete')) {
        this.deleteTodo(id);
      } else if (e.target.type === 'checkbox') {
        this.toggleTodo(id);
      }
    });
  }

  // 添加待办
  addTodo() {
    const text = this.input.value.trim();
    if (!text) return;

    this.todos.push({
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date().toISOString()
    });

    this.input.value = '';
    this.saveToStorage();
    this.render();
  }

  // 切换完成状态
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.saveToStorage();
      this.render();
    }
  }

  // 删除待办
  deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    this.saveToStorage();
    this.render();
  }

  // 获取筛选后的列表
  getFilteredTodos() {
    switch (this.currentFilter) {
      case 'active':
        return this.todos.filter(t => !t.completed);
      case 'completed':
        return this.todos.filter(t => t.completed);
      default:
        return this.todos;
    }
  }

  // 渲染列表
  render() {
    const filtered = this.getFilteredTodos();

    if (filtered.length === 0) {
      this.list.innerHTML = `
        <li class="empty-state">
          ${this.currentFilter === 'all' ? '🎉 暂无待办事项,享受轻松时光吧!' : '暂无匹配的待办事项'}
        </li>
      `;
    } else {
      this.list.innerHTML = filtered.map(todo => `
        <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
          <input type="checkbox" ${todo.completed ? 'checked' : ''} />
          <span class="todo-text">${this.escapeHtml(todo.text)}</span>
          <button class="todo-delete">✕</button>
        </li>
      `).join('');
    }

    // 更新统计
    const total = this.todos.length;
    const completed = this.todos.filter(t => t.completed).length;
    const active = total - completed;
    this.stats.textContent = `共 ${total} 项,${active} 项进行中,${completed} 项已完成`;
  }

  // 防 XSS:转义 HTML
  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
}

// 启动应用
new TodoApp();

这个待办事项应用涵盖了以下知识点:

  • 类与面向对象编程
  • DOM 操作与事件处理(含事件委托)
  • 本地存储(localStorage)
  • 数组方法(filter、find、map)
  • 模板字符串与 HTML 渲染
  • XSS 防护

24. 实战:天气查询应用

这个应用通过调用免费天气 API 获取实时天气数据,展示了 Fetch API、异步编程和错误处理的综合运用。

24.1 HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>天气查询</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
    }
    .container {
      width: 100%;
      max-width: 480px;
    }
    .card {
      background: rgba(255,255,255,0.95);
      border-radius: 20px;
      padding: 32px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.2);
    }
    h1 {
      text-align: center;
      color: #333;
      margin-bottom: 24px;
      font-size: 1.5rem;
    }
    .search-group {
      display: flex;
      gap: 8px;
      margin-bottom: 24px;
    }
    .search-group input {
      flex: 1;
      padding: 12px 16px;
      border: 2px solid #e0e0e0;
      border-radius: 12px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }
    .search-group input:focus {
      outline: none;
      border-color: #667eea;
    }
    .search-group button {
      padding: 12px 20px;
      background: linear-gradient(135deg, #667eea, #764ba2);
      color: white;
      border: none;
      border-radius: 12px;
      font-size: 1rem;
      cursor: pointer;
      transition: transform 0.2s;
    }
    .search-group button:hover {
      transform: scale(1.02);
    }
    .search-group button:disabled {
      opacity: 0.6;
      cursor: not-allowed;
      transform: none;
    }
    .weather-display {
      text-align: center;
    }
    .city-name {
      font-size: 1.5rem;
      color: #333;
      margin-bottom: 8px;
    }
    .temperature {
      font-size: 4rem;
      font-weight: 700;
      color: #667eea;
      line-height: 1;
    }
    .description {
      font-size: 1.1rem;
      color: #666;
      margin: 8px 0 24px;
      text-transform: capitalize;
    }
    .details {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 16px;
    }
    .detail-item {
      background: #f8f9fa;
      padding: 16px;
      border-radius: 12px;
      text-align: center;
    }
    .detail-label {
      font-size: 0.8rem;
      color: #999;
      margin-bottom: 4px;
    }
    .detail-value {
      font-size: 1.2rem;
      color: #333;
      font-weight: 600;
    }
    .error-message {
      text-align: center;
      color: #ff4757;
      padding: 20px;
    }
    .loading {
      text-align: center;
      color: #999;
      padding: 40px;
    }
    .history-section {
      margin-top: 24px;
    }
    .history-section h3 {
      color: #666;
      font-size: 0.9rem;
      margin-bottom: 8px;
    }
    .history-tags {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    .history-tag {
      padding: 4px 12px;
      background: #f0f0f0;
      border-radius: 16px;
      font-size: 0.85rem;
      cursor: pointer;
      transition: background 0.2s;
    }
    .history-tag:hover {
      background: #e0e0e0;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="card">
      <h1>🌤️ 天气查询</h1>
      <div class="search-group">
        <input type="text" id="cityInput" placeholder="输入城市名称..." />
        <button id="searchBtn">查询</button>
      </div>
      <div id="weatherContent">
        <p class="loading">请输入城市名称查询天气</p>
      </div>
      <div class="history-section" id="historySection" style="display:none">
        <h3>最近查询</h3>
        <div class="history-tags" id="historyTags"></div>
      </div>
    </div>
  </div>
  <script src="weather.js"></script>
</body>
</html>

24.2 JavaScript 逻辑

// weather.js

class WeatherApp {
  constructor() {
    this.API_KEY = ''; // 使用 wttr.in 免费 API,无需密钥
    this.BASE_URL = 'https://wttr.in';
    this.history = JSON.parse(localStorage.getItem('weatherHistory')) || [];

    this.input = document.getElementById('cityInput');
    this.searchBtn = document.getElementById('searchBtn');
    this.content = document.getElementById('weatherContent');
    this.historySection = document.getElementById('historySection');
    this.historyTags = document.getElementById('historyTags');

    this.bindEvents();
    this.renderHistory();
  }

  bindEvents() {
    this.searchBtn.addEventListener('click', () => this.search());
    this.input.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') this.search();
    });

    // 历史标签点击
    this.historyTags.addEventListener('click', (e) => {
      if (e.target.classList.contains('history-tag')) {
        this.input.value = e.target.textContent;
        this.search();
      }
    });
  }

  async search() {
    const city = this.input.value.trim();
    if (!city) return;

    this.searchBtn.disabled = true;
    this.content.innerHTML = '<p class="loading">正在查询...</p>';

    try {
      const weather = await this.fetchWeather(city);
      this.renderWeather(weather);
      this.addToHistory(city);
    } catch (error) {
      this.content.innerHTML = `
        <p class="error-message">
          ❌ 查询失败:${error.message}<br/>
          请检查城市名称是否正确
        </p>
      `;
    } finally {
      this.searchBtn.disabled = false;
    }
  }

  async fetchWeather(city) {
    const url = `${this.BASE_URL}/${encodeURIComponent(city)}?format=j1`;

    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`请求失败 (${response.status})`);
    }

    const data = await response.json();

    if (!data.current_condition || data.current_condition.length === 0) {
      throw new Error('未找到该城市的天气数据');
    }

    const current = data.current_condition[0];
    const area = data.nearest_area[0];

    return {
      city: area.areaName[0].value,
      country: area.country[0].value,
      temperature: current.temp_C,
      feelsLike: current.FeelsLikeC,
      humidity: current.humidity,
      windSpeed: current.windspeedKmph,
      description: current.lang_zh?.[0]?.value || current.weatherDesc[0].value,
      visibility: current.visibility,
      uvIndex: current.uvIndex
    };
  }

  renderWeather(weather) {
    this.content.innerHTML = `
      <div class="weather-display">
        <p class="city-name">${weather.city},${weather.country}</p>
        <p class="temperature">${weather.temperature}°C</p>
        <p class="description">${weather.description}</p>
        <div class="details">
          <div class="detail-item">
            <p class="detail-label">体感温度</p>
            <p class="detail-value">${weather.feelsLike}°C</p>
          </div>
          <div class="detail-item">
            <p class="detail-label">湿度</p>
            <p class="detail-value">${weather.humidity}%</p>
          </div>
          <div class="detail-item">
            <p class="detail-label">风速</p>
            <p class="detail-value">${weather.windSpeed} km/h</p>
          </div>
          <div class="detail-item">
            <p class="detail-label">能见度</p>
            <p class="detail-value">${weather.visibility} km</p>
          </div>
        </div>
      </div>
    `;
  }

  addToHistory(city) {
    // 去重,最新的放前面
    this.history = [city, ...this.history.filter(c => c !== city)].slice(0, 8);
    localStorage.setItem('weatherHistory', JSON.stringify(this.history));
    this.renderHistory();
  }

  renderHistory() {
    if (this.history.length === 0) {
      this.historySection.style.display = 'none';
      return;
    }

    this.historySection.style.display = 'block';
    this.historyTags.innerHTML = this.history
      .map(city => `<span class="history-tag">${city}</span>`)
      .join('');
  }
}

// 启动
new WeatherApp();

这个天气查询应用涵盖了:

  • Fetch API 异步请求
  • async/await 错误处理
  • 本地存储(搜索历史)
  • DOM 动态渲染
  • 用户体验优化(加载状态、错误提示)
  • 类与面向对象封装

25. 性能优化技巧

25.1 减少 DOM 操作

// ❌ 不推荐:频繁操作 DOM
for (let i = 0; i < 1000; i++) {
  document.body.innerHTML += `<p>第 ${i} 段</p>`; // 每次都触发重排
}

// ✅ 推荐:批量操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const p = document.createElement('p');
  p.textContent = `第 ${i} 段`;
  fragment.appendChild(p);
}
document.body.appendChild(fragment); // 只触发一次重排

// 使用 innerHTML 批量插入
let html = '';
for (let i = 0; i < 1000; i++) {
  html += `<p>第 ${i} 段</p>`;
}
document.body.innerHTML += html;

25.2 防抖与节流

// 防抖(Debounce):等待用户停止操作后才执行
function debounce(fn, delay = 300) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 场景:搜索框输入时自动搜索
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
  performSearch(e.target.value);
}, 300));

// 节流(Throttle):固定时间间隔内只执行一次
function throttle(fn, interval = 100) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 场景:滚动时的处理
window.addEventListener('scroll', throttle(() => {
  console.log('处理滚动事件');
}, 100));

25.3 懒加载

// 图片懒加载
const images = document.querySelectorAll('img[data-src]');

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;  // 设置真实图片地址
      img.removeAttribute('data-src');
      imageObserver.unobserve(img); // 停止观察
    }
  });
}, { rootMargin: '100px' }); // 提前 100px 开始加载

images.forEach(img => imageObserver.observe(img));
<!-- HTML 中使用 data-src 代替 src -->
<img data-src="real-image.jpg" src="placeholder.jpg" alt="懒加载图片" />

25.4 事件委托优化

// ❌ 不推荐:给每个元素都绑定事件
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', handleClick);
});

// ✅ 推荐:事件委托
document.querySelector('ul').addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') {
    handleClick(e);
  }
});

25.5 Web Workers

// 主线程
const worker = new Worker('worker.js');

worker.postMessage({ data: largeArray });

worker.onmessage = (e) => {
  console.log('处理结果:', e.data);
};

// worker.js — 在后台线程运行
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

25.6 内存管理

// 避免内存泄漏

// 1. 及时移除事件监听器
function setup() {
  const handler = () => console.log('click');
  button.addEventListener('click', handler);

  // 组件销毁时移除
  return () => {
    button.removeEventListener('click', handler);
  };
}

// 2. 清除定时器
const timer = setInterval(() => { /* ... */ }, 1000);
// 不需要时清除
clearInterval(timer);

// 3. 避免意外的全局变量
function leak() {
  // ❌ 意外全局变量
  accidental = '这是一个全局变量';

  // ✅ 正确做法
  const safe = '这是一个局部变量';
}

// 4. 使用 WeakMap 和 WeakSet(允许垃圾回收)
const weakMap = new WeakMap();
let obj = { data: 'heavy' };
weakMap.set(obj, 'metadata');
obj = null; // obj 可以被垃圾回收,weakMap 中的条目也会自动清除

26. 学习路线图

26.1 入门阶段(1-2 个月)

  • 变量与数据类型
  • 运算符与流程控制
  • 函数与作用域
  • 数组与对象操作
  • 字符串处理
  • 基础 DOM 操作
  • 完成待办事项项目

推荐练习:用原生 JS 实现计算器、倒计时器、轮播图

26.2 进阶阶段(2-3 个月)

  • 异步编程(Promise / async-await)
  • Fetch API 与 HTTP 请求
  • ES6+ 深入理解
  • 面向对象编程
  • 错误处理与调试
  • 正则表达式
  • 本地存储方案
  • 性能优化基础
  • 完成天气查询项目

推荐练习:实现分页组件、表单验证器、Markdown 编辑器

26.3 高级阶段(3-6 个月)

  • 设计模式(观察者、工厂、单例等)
  • 函数式编程(纯函数、柯里化、组合)
  • TypeScript 基础
  • 前端框架入门(Vue / React / Svelte)
  • Node.js 后端开发
  • 构建工具(Vite / Webpack)
  • 测试(Jest / Vitest)
  • 数据结构与算法

26.4 持续精进

  • 深入浏览器原理(渲染引擎、事件循环、V8 引擎)
  • 前端工程化(CI/CD、代码规范、自动化测试)
  • 全栈能力(数据库、API 设计、部署运维)
  • 开源贡献(阅读源码、提交 PR)
  • 性能监控与优化(Lighthouse、Core Web Vitals)

26.5 推荐资源

在线文档

  • MDN Web Docs(最权威的前端文档):https://developer.mozilla.org/zh-CN/
  • JavaScript.info(现代 JS 教程):https://zh.javascript.info/
  • ECMAScript 规范:https://tc39.es/ecma262/

交互式学习

  • freeCodeCamp:https://www.freecodecamp.org/
  • CodeWars(刷题平台):https://www.codewars.com/
  • LeetCode(算法练习):https://leetcode.cn/

经典书籍

  • 《JavaScript 高级程序设计》(红宝书)
  • 《你不知道的 JavaScript》(上中下)
  • 《JavaScript 忍者秘籍》
  • 《ECMAScript 6 入门》— 阮一峰

结语

JavaScript 是一门博大精深的语言,从最初的浏览器脚本语言发展为如今的全栈开发利器。本教程涵盖了从基础语法到高级特性、从理论知识到实战项目的完整内容。

学习编程最重要的是动手实践。建议你:

  1. 跟着代码敲一遍 — 不要只是看,每个示例都亲手运行
  2. 做项目 — 完成本教程的两个实战项目,然后自己设计新项目
  3. 读源码 — 阅读优秀的开源项目代码
  4. 写博客/笔记 — 教是最好的学
  5. 参与社区 — Stack Overflow、GitHub、掘金等

记住:每一个大师都曾经是一个初学者。 坚持学习,不断实践,你一定能精通 JavaScript!


📚 本教程持续更新中。如有疑问或建议,欢迎反馈交流。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录