JavaScript 从入门到精通 — 完整教程
本教程面向零基础到中高级开发者,涵盖 JavaScript 核心概念、ES6+ 新特性、DOM 操作、异步编程、实战项目等内容。全文超过两万字,配合大量代码示例与实际案例,助你系统掌握这门语言。
目录
- JavaScript 简介与历史
- 开发环境搭建
- 基本语法与数据类型
- 变量声明(var / let / const)
- 运算符与表达式
- 字符串方法详解
- 数组操作与常用方法
- 对象与 JSON
- 条件判断与循环
- 函数定义与箭头函数
- 闭包与作用域
- Promise 与 async/await
- DOM 操作详解
- 事件处理
- BOM 浏览器对象模型
- ES6+ 新特性
- 模块化(import / export)
- 错误处理与调试
- 正则表达式
- 本地存储
- Fetch API 与网络请求
- 面向对象编程(类与继承)
- 实战:待办事项应用
- 实战:天气查询应用
- 性能优化技巧
- 学习路线图
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 浏览器(零配置起步)
所有现代浏览器都自带开发者工具,按 F12 或 Ctrl+Shift+I(Mac 为 Cmd+Option+I)即可打开。
- Console 面板:直接输入 JS 代码并执行
- Sources 面板:查看和调试网页中的 JS 文件
- Network 面板:监控网络请求
推荐使用 Google Chrome 或 Microsoft 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 是一门博大精深的语言,从最初的浏览器脚本语言发展为如今的全栈开发利器。本教程涵盖了从基础语法到高级特性、从理论知识到实战项目的完整内容。
学习编程最重要的是动手实践。建议你:
- 跟着代码敲一遍 — 不要只是看,每个示例都亲手运行
- 做项目 — 完成本教程的两个实战项目,然后自己设计新项目
- 读源码 — 阅读优秀的开源项目代码
- 写博客/笔记 — 教是最好的学
- 参与社区 — Stack Overflow、GitHub、掘金等
记住:每一个大师都曾经是一个初学者。 坚持学习,不断实践,你一定能精通 JavaScript!
📚 本教程持续更新中。如有疑问或建议,欢迎反馈交流。