第三章 JavaScript
# 第三章 JavaScript
# 一、JavaScript 简介
# 1.1 JavaScript 概述
JavaScript 是一种轻量级、解释型的编程语言,最初由 Netscape 公司开发,用于解决网页的动态交互问题。现在它已成为现代 Web 开发不可或缺的核心技术。
核心特点:
- 脚本语言:JavaScript 是解释型语言,无需编译即可运行
- 基于对象:支持面向对象编程,通过原型链实现继承,支持封装等特性
- 弱类型:变量类型在运行时动态确定,具有自动类型转换能力
- 事件驱动:通过事件响应用户操作,实现交互功能
- 跨平台性:只需浏览器支持即可运行,具备良好的跨平台兼容性
应用领域:
- 网页交互效果(表单验证、动画效果)
- 前端框架开发(React、Vue、Angular)
- 后端开发(Node.js)
- 移动应用开发(React Native、Ionic)
- 桌面应用开发(Electron)
# 1.2 JavaScript 组成结构
JavaScript 由三个核心部分组成:
# 1.2.1 ECMAScript(语言核心)
ECMAScript 是 JavaScript 的标准化规范,定义了语法、类型、语句、关键字、对象等基础内容。
ECMAScript 版本里程碑:
ECMAScript 是由 ECMA-262 标准定义的语言规范(语法、类型、语句、关键字、内置对象等)。自 2015 年后采用“年度发布”策略(按年份:ES2015、ES2016 ...),每版只收敛通过正式阶段的提案,保持增量演进。
| 版本 | 发布时间 | 关键增量 | 影响与实践 |
|---|---|---|---|
| ES3 | 1999 | 正则表达式、try/catch、更完善字符串与数值处理 | 奠定现代 JavaScript 基本语法结构。 |
| ES5 | 2009 | strict mode、Object.defineProperty、JSON、改进继承 | 为库/框架提供更细粒度的属性控制与安全模式。 |
| ES2015 (ES6) | 2015 | class、模块(import/export)、let/const、箭头函数、Promise、迭代器/生成器、Map/Set、Proxy、Reflect | 现代语法基线;大量代码需通过 Babel/TypeScript 转译以兼容旧浏览器。 |
| ES2016 | 2016 | 指数运算符 **、Array.prototype.includes | 小增量,语义更直观。 |
| ES2017 | 2017 | async/await、Object.values/entries、字符串填充 | 异步写法简化,Promise 链可读性大幅提升。 |
| ES2018 | 2018 | 异步迭代、Rest/Spread 对象支持、Promise.finally、正则改进 | 异步遍历与资源释放模式更统一。 |
| ES2019 | 2019 | Array.flat/flatMap、Object.fromEntries、trimStart/End、可选 catch 绑定 | 数组与对象转换更便捷。 |
| ES2020 | 2020 | BigInt、可选链 ?.、空值合并 ??、Promise.allSettled、import.meta、globalThis | 数值边界与空值处理更安全,模块与跨环境全局访问统一。 |
| ES2021+ | 2021 以后 | 逻辑赋值运算符、String.prototype.replaceAll、WeakRef 等逐年小增量 | 持续聚焦可读性与性能优化。 |
# 1.2.2 BOM(浏览器对象模型)
Browser Object Model(BOM)提供操作浏览器窗口及其环境的接口集合,未被 ECMA-262 完全标准化,实际由各浏览器在 WHATWG 等规范推动下趋于一致。
常用对象与典型用途:
window:全局顶层,计时器(setTimeout/setInterval)、全局变量、事件绑定、打开/关闭窗口(受安全策略限制)。location:读写 URL、跳转、replace无历史记录跳转、search解析查询参数。history:back/forward/go导航;单页应用配合 History API(pushState/replaceState+popstate)实现路由。navigator:获取 UA、onLine状态、clipboard、geolocation等(注意权限与隐私)。screen:分辨率信息,响应布局场景较少直接使用,更多使用 CSS 媒体查询。storage:localStorage(持久)与sessionStorage(会话);键值对 ~5MB 左右容量,易阻塞主线程,避免频繁写入。
# 1.2.3 DOM(文档对象模型)
DOM 将 HTML 文档表示为树形结构,提供动态修改网页内容的能力。
DOM 编程
- DOM 编程就是使用 document 对象的 API 完成对网页 HTML 文档进行动态修改,以实现网页数据和样式动态变化效果的编程。
- document 对象代表整个 html 文档,可用来访问页面中的所有元素,是最复杂的一个 dom 对象,可以说是学习好 dom 编程的关键所在。
- 根据 HTML 代码结构特点,document 对象本身是一种树形结构的文档对象。

DOM 树结构示例:

DOM 编程本质是通过 JavaScript API 动态操作 HTML 元素,实现:
- 内容修改(文本、HTML)
- 样式变更(CSS 属性)
- 结构调整(增加、删除、移动元素)
- 事件处理(用户交互响应)
# 1.3 JavaScript 引入方式
# 1.3.1 内联脚本方式
特点:
- 直接在 HTML 中使用
<script>标签编写 JavaScript 代码 - 适用于简单逻辑或页面特定功能
- 代码量小时使用,避免额外 HTTP 请求
语法:
<script type="text/javascript">
// JavaScript 代码
</script>
2
3
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript 内联示例</title>
<style>
.btn1 {
display: block;
width: 150px;
height: 40px;
background-color: rgb(245, 241, 129);
color: rgb(238, 31, 31);
border: 3px solid rgb(238, 23, 66);
font-size: 22px;
font-family: '隶书';
line-height: 30px;
border-radius: 5px;
cursor: pointer;
}
</style>
<script>
function surprise() {
alert("Hello,我是惊喜!");
}
</script>
</head>
<body>
<button class="btn1" onclick="surprise()">点我有惊喜</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 1.3.2 外部脚本方式
特点:
- 将 JavaScript 代码保存在独立的
.js文件中 - 通过
<script>标签的src属性引入 - 提高代码复用性和可维护性
最佳实践:
- ✅ 推荐使用,便于代码管理和缓存
- ✅ 支持模块化开发
- ✅ 便于团队协作和版本控制
步骤:
- 创建外部 JS 文件(
js/button.js):
function surprise() {
alert("Hello,我是惊喜!");
}
2
3
- 在 HTML 中引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript 外部引入示例</title>
<style>
.btn1 {
display: block;
width: 150px;
height: 40px;
background-color: rgb(245, 241, 129);
color: rgb(238, 31, 31);
border: 3px solid rgb(238, 23, 66);
font-size: 22px;
font-family: '隶书';
line-height: 30px;
border-radius: 5px;
cursor: pointer;
}
</style>
<!-- 引入外部 JavaScript 文件 -->
<script src="js/button.js"></script>
</head>
<body>
<button class="btn1" onclick="surprise()">点我有惊喜</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
引入方式注意事项
- 一个
<script>标签只能用于一种方式:要么定义内联脚本,要么引入外部文件,不能混用 - 多个脚本标签:一个 HTML 页面可以包含多个
<script>标签 - 加载顺序:脚本按照在 HTML 中出现的顺序执行
- type 属性:现代浏览器中
type="text/javascript"是默认值,可以省略
# 1.3.3 脚本加载位置优化
推荐做法:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>优化的脚本加载</title>
<!-- CSS 放在 head 中 -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- 页面内容 -->
<div id="content">...</div>
<!-- JavaScript 放在 body 底部,提高页面加载速度 -->
<script src="js/main.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现代加载方式:
<!-- 异步加载(推荐) -->
<script src="js/main.js" defer></script>
<!-- 或者使用 async(适用于独立脚本) -->
<script src="js/analytics.js" async></script>
2
3
4
5
# 二、JavaScript 数据类型与运算符
JavaScript 是动态弱类型语言,变量的类型在运行时确定。现代 JavaScript 共有 8 种数据类型:Number(数值类型)、String(字符串类型)、Boolean(布尔类型)、Undefined(未定义)、Null(空值)、Symbol(符号类型)- ES6+、BigInt(大整数类型)- ES2020、Object(对象类型)。
# 2.1 数据类型概述
# 2.1.1 基本数据类型(原始类型)
1. Number(数值类型)
let integer = 42; // 整数
let decimal = 3.14; // 浮点数
let negative = -100; // 负数
let infinity = Infinity; // 无穷大
let nan = NaN; // 不是数字(Not a Number)
// 特殊值检测
console.log(Number.isFinite(42)); // true
console.log(Number.isNaN(NaN)); // true
console.log(Number.isInteger(42)); // true
2
3
4
5
6
7
8
9
10
2. String(字符串类型)
let str1 = 'Hello World'; // 单引号
let str2 = "JavaScript"; // 双引号
let str3 = `Template ${str2}`; // 模板字符串(ES6+)
let str4 = 'It\'s a "quoted" text'; // 转义字符
// 常用方法
console.log(str1.length); // 11
console.log(str1.toUpperCase()); // "HELLO WORLD"
console.log(str2.includes('Script')); // true
2
3
4
5
6
7
8
9
3. Boolean(布尔类型)
let isTrue = true;
let isFalse = false;
// 类型转换(重要)
console.log(Boolean('')); // false(空字符串)
console.log(Boolean('hello')); // true(非空字符串)
console.log(Boolean(0)); // false(数字0)
console.log(Boolean(42)); // true(非零数字)
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
2
3
4
5
6
7
8
9
10
4. Undefined(未定义)
let undefVar; // 声明但未赋值
console.log(undefVar); // undefined
console.log(typeof undefVar); // "undefined"
2
3
5. Null(空值):
let nullVar = null; // 明确赋值为空
console.log(nullVar); // null
console.log(typeof nullVar); // "object"(历史遗留问题)
2
3
6. Symbol(符号类型)- ES6+
// 创建唯一标识符
let sym1 = Symbol('id');
let sym2 = Symbol('id');
console.log(sym1 === sym2); // false(每个Symbol都是唯一的)
// 应用场景:对象属性键
const obj = {
[sym1]: 'value1',
[sym2]: 'value2'
};
2
3
4
5
6
7
8
9
10
7. BigInt(大整数类型)- ES2020
// 处理超出 Number.MAX_SAFE_INTEGER 的大整数
let bigInt1 = 123456789012345678901234567890n;
let bigInt2 = BigInt('123456789012345678901234567890');
console.log(typeof bigInt1); // "bigint"
console.log(bigInt1 + 10n); // 运算需要同为BigInt类型
2
3
4
5
6
# 2.1.2 引用数据类型
8. Object(对象类型)
// 包括对象、数组、函数、Date等
let obj = { name: 'Alice', age: 25 };
let arr = [1, 2, 3, 4, 5];
let func = function() { return 'Hello'; };
let date = new Date();
console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof func); // "function"(特殊的对象)
console.log(typeof date); // "object"
2
3
4
5
6
7
8
9
10
# 2.1.3 类型检测方法
// typeof 操作符
console.log(typeof 42); // "number"
console.log(typeof 'hello'); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(注意:这是历史bug)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function(){}); // "function"
// 更精确的类型检测
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
// Array.isArray() - 专门检测数组
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.2 变量声明与最佳实践
# 2.2.1 变量声明方式
1. var(ES5 及之前)
var name = 'Alice';
var age = 25;
var age = 30; // 可以重复声明
// var 的特点和问题
function example() {
if (true) {
var x = 1; // 函数作用域
}
console.log(x); // 1(可以访问)
}
// 变量提升问题
console.log(hoisted); // undefined(不是报错)
var hoisted = 'I am hoisted';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2. let(ES6+)- 推荐
let userName = 'Bob';
// let userName = 'Charlie'; // ❌ 错误:不能重复声明
// 块级作用域
function example() {
if (true) {
let y = 1; // 块级作用域
}
// console.log(y); // ❌ 错误:y 未定义
}
// 暂时性死区
// console.log(temporal); // ❌ 错误:在初始化前使用
let temporal = 'Temporal Dead Zone';
2
3
4
5
6
7
8
9
10
11
12
13
14
3. const(ES6+)- 常量声明
const PI = 3.14159;
// PI = 3.14; // ❌ 错误:不能重新赋值
// 对象和数组的特殊情况
const user = { name: 'Alice', age: 25 };
user.age = 26; // ✅ 允许:修改对象属性
user.city = 'NYC'; // ✅ 允许:添加属性
// user = {}; // ❌ 错误:不能重新赋值整个对象
const numbers = [1, 2, 3];
numbers.push(4); // ✅ 允许:修改数组内容
// numbers = []; // ❌ 错误:不能重新赋值整个数组
2
3
4
5
6
7
8
9
10
11
12
# 2.2.2 现代 JavaScript 最佳实践
变量声明建议
- 优先使用
const:对于不会重新赋值的变量 - 需要重新赋值时使用
let:替代var - 避免使用
var:现代代码中几乎不需要 - 声明时尽量初始化:提高代码可读性
// ✅ 推荐的写法
const APP_NAME = 'MyApp'; // 常量用大写
const users = []; // 不会重新赋值的集合
let currentUser = null; // 可能会重新赋值
let index = 0; // 循环计数器
// ❌ 不推荐的写法
var appName = 'MyApp'; // 使用var
var users; // 声明但不初始化
users = [];
// 解构赋值(ES6+)
const person = { name: 'Alice', age: 25, city: 'NYC' };
const { name, age } = person; // 提取对象属性
const [first, second] = [1, 2, 3]; // 提取数组元素
// 模板字符串(ES6+)
const greeting = `Hello, ${name}! You are ${age} years old.`;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.2.3 变量命名规范
// ✅ 推荐的命名方式
const MAX_RETRY_COUNT = 3; // 常量:大写+下划线
let userName = 'alice'; // 驼峰命名
let isLoggedIn = false; // 布尔值用is/has/can等前缀
let userList = []; // 数组用复数或List后缀
let userInfo = {}; // 对象用Info/Data/Config等后缀
// ❌ 不推荐的命名方式
let a = 'alice'; // 单字母变量名(除循环外)
let UserName = 'alice'; // 变量不用大写开头
let user_name = 'alice'; // 不使用下划线(非常量)
let 123name = 'alice'; // ❌ 数字开头(语法错误)
2
3
4
5
6
7
8
9
10
11
12
# 2.2.4 类型转换详解
自动类型转换(隐式转换)
// 字符串转换
console.log(5 + '3'); // "53"(数字转字符串)
console.log('5' + 3); // "53"
console.log('5' - 3); // 2(字符串转数字)
console.log('5' * 3); // 15
// 布尔转换
console.log(!!'hello'); // true(双重否定转布尔)
console.log(+true); // 1(布尔转数字)
console.log(+'42'); // 42(字符串转数字)
// 假值(Falsy values)
console.log(Boolean(false)); // false
console.log(Boolean(0)); // false
console.log(Boolean(-0)); // false
console.log(Boolean(0n)); // false (BigInt)
console.log(Boolean('')); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
手动类型转换(显式转换)
// 转为数字
console.log(Number('42')); // 42
console.log(parseInt('42px')); // 42(解析整数)
console.log(parseFloat('3.14')); // 3.14(解析浮点数)
console.log(+'42'); // 42(一元加号)
// 转为字符串
console.log(String(42)); // "42"
console.log((42).toString()); // "42"
console.log(42 + ''); // "42"(简便方法)
// 转为布尔值
console.log(Boolean(1)); // true
console.log(!!1); // true(双重否定)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2.3 运算符详解
# 2.3.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.3333...(除法)
console.log(a % b); // 1(取余)
console.log(a ** b); // 1000(指数运算,ES2016)
// 特殊情况
console.log(10 / 0); // Infinity(不报错)
console.log(10 % 0); // NaN(不报错)
console.log(0 / 0); // NaN
2
3
4
5
6
7
8
9
10
11
12
13
赋值运算符
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 %= 5; // x = x % 5 → 1
x **= 3; // x = x ** 3 → 1(ES2016)
// 自增自减
let y = 5;
console.log(++y); // 6(前置:先增后用)
console.log(y++); // 6(后置:先用后增)
console.log(y); // 7
2
3
4
5
6
7
8
9
10
11
12
13
# 2.3.2 比较运算符
基本比较
console.log(5 > 3); // true
console.log(5 < 3); // false
console.log(5 >= 5); // true
console.log(5 <= 4); // false
2
3
4
相等性比较(重要)
// == 相等(会进行类型转换)
console.log(5 == '5'); // true(字符串转数字)
console.log(true == 1); // true(布尔转数字)
console.log(false == 0); // true
console.log(null == undefined); // true(特殊规则)
// === 严格相等(不进行类型转换)
console.log(5 === '5'); // false(类型不同)
console.log(true === 1); // false
console.log(null === undefined); // false
// != 和 !== 类似
console.log(5 != '5'); // false
console.log(5 !== '5'); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
最佳实践
推荐始终使用 === 和 !== 进行比较,避免意外的类型转换。
# 2.3.3 逻辑运算符
基本逻辑运算
let a = true, b = false;
console.log(a && b); // false(与运算)
console.log(a || b); // true(或运算)
console.log(!a); // false(非运算)
2
3
4
5
短路求值(重要特性)
// && 短路:第一个为假时不执行后面的
false && console.log('不会执行'); // 不输出
true && console.log('会执行'); // 输出:会执行
// || 短路:第一个为真时不执行后面的
true || console.log('不会执行'); // 不输出
false || console.log('会执行'); // 输出:会执行
// 实际应用:设置默认值
function greet(name) {
name = name || 'Guest'; // 传统方式
console.log('Hello, ' + name);
}
// ES2020 空值合并运算符(推荐)
function greetModern(name) {
name = name ?? 'Guest'; // 只有null/undefined时使用默认值
console.log(`Hello, ${name}`);
}
greet(''); // "Hello, Guest"(空字符串被认为是假值)
greetModern(''); // "Hello, "(空字符串不会触发默认值)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.3.4 现代运算符(ES6+)
解构赋值
// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [3, 4, 5]
// 对象解构
const person = { name: 'Alice', age: 25, city: 'NYC' };
const { name, age, country = 'USA' } = person; // 设置默认值
console.log(name); // "Alice"
console.log(country); // "USA"(默认值)
2
3
4
5
6
7
8
9
10
展开运算符(Spread Operator)
// 数组展开
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 对象展开
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// 函数参数展开
function sum(a, b, c) {
return a + b + c;
}
console.log(sum(...[1, 2, 3])); // 6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可选链操作符(ES2020)
const user = {
name: 'Alice',
address: {
street: '123 Main St',
city: 'NYC'
}
};
// 传统方式(冗长且易错)
const street = user && user.address && user.address.street;
// 可选链(简洁安全)
const streetModern = user?.address?.street;
const zipCode = user?.address?.zipCode ?? 'N/A'; // 结合空值合并
console.log(streetModern); // "123 Main St"
console.log(zipCode); // "N/A"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3.5 条件(三元)运算符
// 基本语法:condition ? valueIfTrue : valueIfFalse
const age = 18;
const status = age >= 18 ? 'adult' : 'minor';
console.log(status); // "adult"
// 嵌套使用(不推荐过度嵌套)
const score = 85;
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'F';
// 更复杂的条件建议使用if-else或switch
function getGrade(score) {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
return 'F';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.3.6 位运算符(了解)
let a = 5; // 101(二进制)
let b = 3; // 011(二进制)
console.log(a & b); // 1(按位与)
console.log(a | b); // 7(按位或)
console.log(a ^ b); // 6(按位异或)
console.log(~a); // -6(按位非)
console.log(a << 1); // 10(左移)
console.log(a >> 1); // 2(右移)
console.log(a >>> 1); // 2(无符号右移)
// 实际应用:权限系统
const PERMISSIONS = {
READ: 1, // 001
WRITE: 2, // 010
EXECUTE: 4 // 100
};
let userPermissions = PERMISSIONS.READ | PERMISSIONS.WRITE; // 011
console.log(userPermissions & PERMISSIONS.READ); // 1(有读权限)
console.log(userPermissions & PERMISSIONS.EXECUTE); // 0(无执行权限)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 三、JavaScript 流程控制与函数
# 3.1 条件语句
# 3.1.1 if 语句
JavaScript 的 if 语句与其他编程语言类似,但在类型转换方面有其特殊性。
基本语法:
if (condition) {
// 条件为真时执行
} else if (anotherCondition) {
// 另一个条件为真时执行
} else {
// 所有条件都为假时执行
}
2
3
4
5
6
7
真值与假值转换:
// 假值(Falsy values)- 会被转换为 false
if (false) console.log('不会执行');
if (0) console.log('不会执行');
if (-0) console.log('不会执行');
if (0n) console.log('不会执行'); // BigInt 零
if ('') console.log('不会执行'); // 空字符串
if (null) console.log('不会执行');
if (undefined) console.log('不会执行');
if (NaN) console.log('不会执行');
// 真值(Truthy values)- 会被转换为 true
if ('false') console.log('会执行'); // 非空字符串
if ('0') console.log('会执行'); // 非空字符串
if (1) console.log('会执行'); // 非零数字
if (-1) console.log('会执行'); // 非零数字
if ([]) console.log('会执行'); // 空数组
if ({}) console.log('会执行'); // 空对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分支编写最佳实践:
使用严格比较:
value === 0/value === ''明确语义。避免将
[]与{}当作“无值”:它们是 truthy,需根据长度或键数判定:if (arr.length === 0)。可选链结合空值合并:
user?.profile?.name ?? '匿名'简化嵌套判断。Guard Clause(卫语句)提升可读性:
function save(data) { if (!data || !data.id) return; // 早退出 // 后续逻辑... }1
2
3
4避免多层嵌套 if:使用卫语句或拆分函数。
表达复杂条件时提取命名变量:
const isExpired = now > token.expireAt; const needRefresh = isExpired && token.refreshable; if (needRefresh) refreshToken();1
2
3
实际应用示例:
// 用户输入验证
function validateUser(user) {
if (!user) {
console.log('用户对象不存在');
return false;
}
if (!user.name || user.name.trim() === '') {
console.log('用户名不能为空');
return false;
}
if (!user.age || user.age < 0 || user.age > 150) {
console.log('年龄无效');
return false;
}
console.log('用户信息有效');
return true;
}
// 测试
validateUser({ name: '张三', age: 25 }); // 用户信息有效
validateUser({ name: '', age: 25 }); // 用户名不能为空
validateUser(null); // 用户对象不存在
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3.1.2 switch 语句
基本语法:
switch (expression) {
case value1:
// 执行代码
break;
case value2:
case value3: // 多个case可以共享代码
// 执行代码
break;
default:
// 默认代码
}
2
3
4
5
6
7
8
9
10
11
现代用法示例:
// 传统写法
function getSeasonTraditional(month) {
switch (month) {
case 3:
case 4:
case 5:
return '春季';
case 6:
case 7:
case 8:
return '夏季';
case 9:
case 10:
case 11:
return '秋季';
case 12:
case 1:
case 2:
return '冬季';
default:
return '无效月份';
}
}
// 现代写法:使用对象映射(推荐)
function getSeason(month) {
const seasons = {
3: '春季', 4: '春季', 5: '春季',
6: '夏季', 7: '夏季', 8: '夏季',
9: '秋季', 10: '秋季', 11: '秋季',
12: '冬季', 1: '冬季', 2: '冬季'
};
return seasons[month] || '无效月份';
}
// 或使用数组索引
function getSeasonModern(month) {
const seasonMap = [
'', '冬季', '冬季', '春季', '春季', '春季',
'夏季', '夏季', '夏季', '秋季', '秋季', '秋季', '冬季'
];
return seasonMap[month] || '无效月份';
}
console.log(getSeason(4)); // "春季"
console.log(getSeasonModern(8)); // "夏季"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 3.2 循环语句
循环与遍历方式应根据数据结构与语义选择,避免误用 for...in 遍历数组导致枚举原型或非数字键。下面对常见方式进行对比与改进。
| 形式 | 是否遍历原型附加属性 | 返回值 | 适用场景 | 是否可中途 break |
|---|---|---|---|---|
for 索引 | 否 | 无 | 需索引/性能敏感 | 可以 |
for...of | 否 | 无 | 遍历可迭代(数组/Map/Set/字符串) | 可以 |
for...in | 可能(枚举可枚举属性) | 属性名字符串 | 遍历对象键(非数组) | 可以 |
Array.prototype.forEach | 否 | 无 | 简单对每项执行副作用 | 不可直接 break(可用 return 跳过) |
map | 否 | 新数组 | 转换数组元素 | 不可直接 break |
filter | 否 | 新数组 | 条件筛选 | 不可直接 break |
reduce | 否 | 聚合结果 | 汇总/折叠 | 不可直接 break |
some / every | 否 | 布尔 | 条件存在性 / 全量检查 | 早停(内部返回) |
entries() + for...of | 否 | [index,value] | 需要索引与值同时 | 可以 |
数组不要使用 for...in:它遍历可枚举键,顺序与整数索引迭代不同,且可能包含自定义属性。
# 3.2.1 while 和 do-while 循环
while 循环:
// 基本语法
while (condition) {
// 循环体
}
// 示例:计算 1 到 100 的和
let sum = 0;
let i = 1;
while (i <= 100) {
sum += i;
i++;
}
console.log(sum); // 5050
2
3
4
5
6
7
8
9
10
11
12
13
do-while 循环:
// 至少执行一次
let input;
do {
input = prompt('请输入一个正数(输入0退出):');
console.log('您输入的是:', input);
} while (input !== '0' && input !== null);
2
3
4
5
6
# 3.2.2 for 循环
传统 for 循环:
// 基本语法
for (initialization; condition; increment) {
// 循环体
}
// 九九乘法表示例
function multiplicationTable() {
let result = '';
for (let i = 1; i <= 9; i++) {
let row = '';
for (let j = 1; j <= i; j++) {
row += `${j}×${i}=${i * j} `;
}
result += row + '\n';
}
console.log(result);
}
multiplicationTable();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for...in 循环(遍历对象属性):
const person = {
name: '张三',
age: 25,
city: '北京'
};
// 遍历对象属性
for (const key in person) {
console.log(`${key}: ${person[key]}`);
}
// 输出:
// name: 张三
// age: 25
// city: 北京
// 遍历数组索引(不推荐)
const cities = ['北京', '上海', '深圳', '武汉', '西安', '成都'];
for (const index in cities) {
console.log(`${index}: ${cities[index]}`);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for...of 循环(ES6+ 推荐):
const cities = ['北京', '上海', '深圳', '武汉', '西安', '成都'];
// 遍历数组元素(推荐)
for (const city of cities) {
console.log(city);
}
// 同时获取索引和值
for (const [index, city] of cities.entries()) {
console.log(`${index}: ${city}`);
}
// 遍历字符串
for (const char of 'Hello') {
console.log(char); // H e l l o
}
// 遍历 Set
const uniqueNumbers = new Set([1, 2, 3, 2, 1]);
for (const num of uniqueNumbers) {
console.log(num); // 1 2 3
}
// 遍历 Map
const map = new Map([['name', '张三'], ['age', 25]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3.2.3 现代数组遍历方法(推荐)
const numbers = [1, 2, 3, 4, 5];
// forEach - 遍历每个元素
numbers.forEach((num, index) => {
console.log(`Index ${index}: ${num}`);
});
// map - 转换数组元素
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter - 过滤数组元素
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
// find - 查找第一个匹配的元素
const found = numbers.find(num => num > 3);
console.log(found); // 4
// reduce - 累积计算
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
// some - 是否有元素满足条件
const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true
// every - 是否所有元素都满足条件
const allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 3.2.4 循环控制语句
// break - 跳出循环
for (let i = 0; i < 10; i++) {
if (i === 5) break;
console.log(i); // 0 1 2 3 4
}
// continue - 跳过当前迭代
for (let i = 0; i < 5; i++) {
if (i === 2) continue;
console.log(i); // 0 1 3 4
}
// 标签语句 - 控制嵌套循环
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=${i}, j=${j}`);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2.5 循环选择建议
- 数组遍历:优先使用
for...of或数组方法(forEach,map,filter等) - 对象遍历:使用
for...in或Object.keys(),Object.entries() - 需要索引:使用传统
for循环或Array.entries() - 性能要求高:传统
for循环通常最快 - 小批量简单遍历:
for...of可读性高;极端性能热点可用传统for(引擎差异已很小) - 避免在循环体频繁 DOM 操作:先收集结果,用
DocumentFragment或字符串拼接一次性插入。 - 不要滥用
reduce实现多步逻辑;分拆为多个明确步骤提升可维护性。 - 长链式
map().filter().reduce()可在性能敏感场景合并为单次for,但需权衡可读性。 - 使用迭代器时注意惰性消费:自定义生成器可减少中间数组开销。
# 3.2.6 异步循环
for...of 可与 await 组合顺序执行:
for (const url of urls) {
const data = await fetch(url).then(r => r.json());
console.log(data.id);
}
2
3
4
并发场景使用 Promise.all:
const results = await Promise.all(urls.map(u => fetch(u).then(r => r.json())));
# 3.2.7 构建批量 HTML 示例(避免多次回流)
const list = document.createElement('ul');
const frag = document.createDocumentFragment();
for (const city of cities) {
const li = document.createElement('li');
li.textContent = city;
frag.appendChild(li);
}
list.appendChild(frag);
document.body.appendChild(list);
2
3
4
5
6
7
8
9
# 3.2.8 实际应用示例
// 生成指定范围的随机数
function getRandomNumbers(count, min, max) {
const numbers = [];
for (let i = 0; i < count; i++) {
const random = Math.floor(Math.random() * (max - min + 1)) + min;
numbers.push(random);
}
return numbers;
}
// 查找数组中的重复元素
function findDuplicates(arr) {
const seen = new Set();
const duplicates = new Set();
for (const item of arr) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return Array.from(duplicates);
}
// 测试
console.log(getRandomNumbers(5, 1, 10)); // [3, 7, 1, 9, 5]
console.log(findDuplicates([1, 2, 3, 2, 4, 1])); // [2, 1]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 3.3 函数详解
# 3.3.1 函数声明方式
1. 函数声明(Function Declaration)
// 基本语法
function functionName(parameters) {
// 函数体
return value; // 可选
}
// 示例
function add(a, b) {
return a + b;
}
// 特点:存在函数提升(Hoisting)
console.log(multiply(3, 4)); // 12 - 可以在声明前调用
function multiply(x, y) {
return x * y;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2. 函数表达式(Function Expression)
// 匿名函数表达式
const subtract = function(a, b) {
return a - b;
};
// 命名函数表达式
const divide = function divideNumbers(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
};
// 特点:不存在函数提升
// console.log(subtract(5, 3)); // ❌ 错误:在声明前调用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 箭头函数(Arrow Function)- ES6+
// 基本语法
const functionName = (parameters) => {
// 函数体
return value;
};
// 简写形式
const square = x => x * x; // 单参数,单表达式
const greet = () => 'Hello World'; // 无参数
const getMax = (a, b) => a > b ? a : b; // 多参数,单表达式
// 复杂函数体
const processArray = (arr) => {
const result = arr
.filter(x => x > 0)
.map(x => x * 2)
.reduce((sum, x) => sum + x, 0);
return result;
};
// 实际应用示例
const users = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 },
{ name: '王五', age: 20 }
];
// 传统写法
const adultUsers1 = users.filter(function(user) {
return user.age >= 25;
});
// 箭头函数写法(推荐)
const adultUsers2 = users.filter(user => user.age >= 25);
const userNames = users.map(user => user.name);
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
console.log(adultUsers2); // [{ name: '张三', age: 25 }, { name: '李四', age: 30 }]
console.log(userNames); // ['张三', '李四', '王五']
console.log(totalAge); // 75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
函数提升(Function Hoisting)与变量提升(Variable Hoisting)
- 函数提升
- 函数声明:具有提升特性,JavaScript 引擎会将函数声明提升到作用域顶部,因此可以在函数声明前调用。
- 函数表达式和箭头函数:不具有提升特性,必须先声明后使用,否则会报错。
- 变量提升(Variable Hoisting)
- var 声明:存在变量提升,只有声明被提升到作用域顶部,初始化(赋值)不会被提升,提升后的值为
undefined。 - let 和 const 声明:不存在传统意义上的变量提升,存在"暂时性死区"(TDZ),在声明前访问会导致
ReferenceError。
- var 声明:存在变量提升,只有声明被提升到作用域顶部,初始化(赋值)不会被提升,提升后的值为
- 提升机制的区别
- 函数提升:提升整个函数定义
- 变量提升:只提升声明,不提升初始化
- 提升顺序:函数声明优先于变量声明
- 最佳实践
- 优先使用 const 和 let,避免使用 var
- 先声明后使用,不依赖提升机制
- 函数表达式和箭头函数更符合现代 JavaScript 开发习惯
# 3.3.2 现代函数特性(ES6+)
1. 默认参数
// ES5 传统写法
function greetOld(name, greeting) {
name = name || 'Guest';
greeting = greeting || 'Hello';
return greeting + ', ' + name + '!';
}
// ES6+ 默认参数(推荐)
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
// 复杂默认值
function createUser(name, options = {}) {
const {
age = 18,
city = '北京',
isActive = true
} = options;
return { name, age, city, isActive };
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('张三')); // "Hello, 张三!"
console.log(greet('李四', '您好')); // "您好, 李四!"
console.log(createUser('王五', { age: 25 })); // { name: '王五', age: 25, city: '北京', isActive: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2. 剩余参数(Rest Parameters)
// 收集多余参数
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
// 混合使用
function introduce(name, age, ...hobbies) {
console.log(`我是${name},今年${age}岁`);
if (hobbies.length > 0) {
console.log(`我的爱好有:${hobbies.join(', ')}`);
}
}
console.log(sum(1, 2, 3, 4, 5)); // 15
introduce('张三', 25, '读书', '游泳', '编程');
// 我是张三,今年25岁
// 我的爱好有:读书, 游泳, 编程
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3. 参数解构
// 对象参数解构
function createCard({ title, content, author = '匿名' }) {
return `
<div class="card">
<h3>${title}</h3>
<p>${content}</p>
<small>作者:${author}</small>
</div>
`;
}
// 数组参数解构
function getCoordinates([x, y, z = 0]) {
return { x, y, z };
}
// 使用示例
const cardHtml = createCard({
title: 'JavaScript 学习',
content: '今天学习了函数的高级用法'
});
const coords = getCoordinates([10, 20]);
console.log(coords); // { x: 10, y: 20, z: 0 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 3.3.3 函数作用域与闭包
作用域链
const globalVar = 'global';
function outerFunction(x) {
const outerVar = 'outer';
function innerFunction(y) {
const innerVar = 'inner';
// 可以访问所有外层作用域的变量
console.log(globalVar); // 'global'
console.log(outerVar); // 'outer'
console.log(innerVar); // 'inner'
console.log(x, y); // 参数也在作用域内
}
return innerFunction;
}
const myFunction = outerFunction('outer-param');
myFunction('inner-param');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
闭包(Closure)
// 基本闭包示例
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1(独立的计数器)
// 实际应用:模块模式
const Calculator = (function() {
let history = [];
return {
add: function(a, b) {
const result = a + b;
history.push(`${a} + ${b} = ${result}`);
return result;
},
subtract: function(a, b) {
const result = a - b;
history.push(`${a} - ${b} = ${result}`);
return result;
},
getHistory: function() {
return [...history]; // 返回副本,防止外部修改
},
clearHistory: function() {
history = [];
}
};
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
console.log(Calculator.getHistory()); // ['5 + 3 = 8', '10 - 4 = 6']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 3.3.4 高阶函数
// 函数作为参数
function processArray(arr, callback) {
const result = [];
for (const item of arr) {
result.push(callback(item));
}
return result;
}
// 函数作为返回值
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
// 使用示例
const numbers = [1, 2, 3, 4, 5];
const doubled = processArray(numbers, x => x * 2);
const squared = processArray(numbers, x => x * x);
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(squared); // [1, 4, 9, 16, 25]
console.log(double(5)); // 10
console.log(triple(4)); // 12
// 实际应用:防抖函数
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 使用防抖
const handleInput = debounce(function(event) {
console.log('搜索:', event.target.value);
}, 300);
// document.getElementById('search').addEventListener('input', handleInput);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 3.3.5 异步函数基础
回调函数
// 模拟异步操作
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: '张三', age: 25 };
callback(null, data); // 第一个参数是错误,第二个是数据
}, 1000);
}
// 使用回调
fetchData((error, data) => {
if (error) {
console.error('获取数据失败:', error);
} else {
console.log('获取数据成功:', data);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise(ES6+)
// 创建 Promise
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: '用户' + userId });
} else {
reject(new Error('无效的用户ID'));
}
}, 1000);
});
}
// 使用 Promise
fetchUserData(1)
.then(user => console.log('用户信息:', user))
.catch(error => console.error('错误:', error));
// Promise 链式调用
fetchUserData(1)
.then(user => {
console.log('获取用户:', user);
return fetchUserData(2); // 返回另一个 Promise
})
.then(user2 => {
console.log('获取用户2:', user2);
})
.catch(error => {
console.error('发生错误:', error);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async/await(ES2017+)
// async 函数自动返回 Promise
async function getUserInfo(userId) {
try {
const user = await fetchUserData(userId);
console.log('用户信息:', user);
return user;
} catch (error) {
console.error('获取用户失败:', error);
throw error;
}
}
// 使用 async/await
async function main() {
try {
const user1 = await getUserInfo(1);
const user2 = await getUserInfo(2);
console.log('所有用户信息获取完成');
return [user1, user2];
} catch (error) {
console.error('主函数执行失败:', error);
}
}
// 调用
main().then(users => {
console.log('最终结果:', users);
});
// 并行执行
async function getAllUsers() {
try {
const [user1, user2, user3] = await Promise.all([
fetchUserData(1),
fetchUserData(2),
fetchUserData(3)
]);
return [user1, user2, user3];
} catch (error) {
console.error('获取用户列表失败:', error);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
函数最佳实践
- 优先使用箭头函数:简洁且避免
this绑定问题 - 使用默认参数:提高函数的健壮性
- 保持函数纯净:相同输入应产生相同输出,避免副作用
- 单一职责:每个函数应该只做一件事
- 使用 async/await:比 Promise 链更易读
- 合理使用闭包:避免内存泄漏
# 3.3.6 实际应用示例
// 工具函数集合
const Utils = {
// 格式化日期
formatDate: (date = new Date(), format = 'YYYY-MM-DD') => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day);
},
// 深拷贝
deepClone: (obj) => {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(Utils.deepClone);
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = Utils.deepClone(obj[key]);
}
}
return cloned;
},
// 节流函数
throttle: (func, limit) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
};
// 使用示例
console.log(Utils.formatDate()); // "2025-10-21"
console.log(Utils.formatDate(new Date(), 'MM/DD/YYYY')); // "10/21/2025"
const original = { a: 1, b: { c: 2 } };
const cloned = Utils.deepClone(original);
cloned.b.c = 3;
console.log(original.b.c); // 2(原对象未被修改)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 3.3.7 小结
1. 常见定义方式对比
JavaScript 函数形态多样:函数声明、函数表达式、箭头函数、生成器、异步函数等。理解差异(提升、this 绑定、参数处理)有助于正确选型与避免陷阱。
| 形式 | 示例 | 提升 (Hoisting) | this 绑定 | 是否有 arguments | 适用场景 |
|---|---|---|---|---|---|
| 函数声明 | function foo(x){} | 整个函数体提升 | 动态(调用时决定) | 有 | 公共 API、需提升使用 |
| 函数表达式 | const foo = function(x){} | 仅变量提升(TDZ) | 动态 | 有 | 需封装在块内、避免过度提升 |
| 箭头函数 | const foo = (x) => x*2 | 变量提升(TDZ) | 词法捕获外层 this | 无(可用剩余参数) | 回调、简写纯函数 |
| 生成器 | function* gen(){yield 1} | 声明提升 | 动态 | 有 | 惰性序列 / 自定义迭代协议 |
| 异步函数 | async function f(){} | 声明提升 | 动态 | 有 | 简化 Promise 链 |
2. 参数特性
- 默认参数:
function greet(name='匿名'){}避免在函数体内手动name = name || '匿名'误覆盖 falsy 值。 - 剩余参数:
function sum(...nums){ return nums.reduce((a,b)=>a+b,0); }。 - 解构参数:
function init({url, timeout=3000}){}增强可读性;注意避免过度深层解构导致签名复杂。 - 可变参数与箭头函数:箭头没有
arguments,使用剩余参数替代。
3. this 与箭头函数
this 由调用位置决定(严格模式下默认 undefined;非严格为 window)。箭头函数不绑定自己的 this、arguments、super,而是捕获定义时的外层上下文:
const obj = {
id: 1,
normal() { console.log(this.id); },
arrow: () => console.log(this.id)
};
obj.normal(); // 1
obj.arrow(); // undefined(箭头的 this 指向外层:全局/模块)
2
3
4
5
6
7
不使用箭头函数定义对象方法(除非刻意避免动态 this)。在类中使用普通方法而非箭头(箭头作为实例字段可能影响性能与原型共享)。
4. IIFE 与模块化
旧写法:立即执行函数表达式(IIFE)用于创建私有作用域:
(function(){ const secret = 42; console.log('init'); })();
现代:ES Module 自带文件级作用域 + 默认严格模式,不再需要 IIFE 隔离;使用模块导出即可:
// utils.js
const secret = 42;
export function calc(x){ return x + secret; }
2
3
5. 纯函数与副作用
- 纯函数:输出只依赖输入,无可观察副作用(不修改外部、无随机/时间)。
- 副作用示例:修改全局变量、操作 DOM、网络请求、日志。将副作用分离,利于测试与复用。
// 纯函数
const toUpperList = arr => arr.map(s => s.toUpperCase());
// 副作用包装
function renderCities(cities){ document.body.textContent = cities.join(','); }
2
3
4
6. 参数个数不匹配
多余实参被忽略;缺失形参为 undefined。通过默认参数或解构给出安全值:function build(port = 8080){}。
7. 防御性与最佳实践
| 建议 | 说明 |
|---|---|
| 避免过度嵌套回调 | 用 Promise / async/await 或事件驱动拆分 |
| 使用箭头简化短纯函数 | 映射、筛选、简单计算场景 |
| 对公共 API 使用函数声明 | 便于提升与阅读(顶层工具函数) |
避免使用 arguments | 剩余参数更显式类型安全(与 TS 协同) |
| 明确返回意图 | 尽量单一出口;复杂条件用卫语句 |
| 命名表达式而非匿名 | 便于栈追踪与调试(const parseUser = (raw)=>{}) |
| 为异步函数处理错误 | try/catch 或在调用方集中捕获 |
# 四、JavaScript 对象和 JSON
# 4.1 对象创建与操作
# 4.1.1 对象创建方式
1. 对象字面量(推荐)
// 基本语法
const person = {
name: '张三',
age: 25,
city: '北京',
// 方法简写(ES6+)
greet() {
return `Hello, I'm ${this.name}`;
},
// 传统方法定义
introduce: function() {
return `我是${this.name},今年${this.age}岁,来自${this.city}`;
}
};
// 计算属性名(ES6+)
const key = 'dynamicKey';
const obj = {
[key]: 'dynamic value',
[`${key}_suffix`]: 'another value'
};
console.log(obj.dynamicKey); // "dynamic value"
console.log(obj.dynamicKey_suffix); // "another value"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2. 构造函数方式
// 使用 new Object()
const person1 = new Object();
person1.name = '李四';
person1.age = 30;
person1.greet = function() {
return `Hello, I'm ${this.name}`;
};
// 自定义构造函数
function Person(name, age, city) {
this.name = name;
this.age = age;
this.city = city;
this.greet = function() {
return `Hello, I'm ${this.name}`;
};
}
const person2 = new Person('王五', 28, '上海');
console.log(person2.greet()); // "Hello, I'm 王五"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
3. 类语法(ES6+)
class Person {
constructor(name, age, city) {
this.name = name;
this.age = age;
this.city = city;
}
greet() {
return `Hello, I'm ${this.name}`;
}
introduce() {
return `我是${this.name},今年${this.age}岁,来自${this.city}`;
}
// 静态方法
static createAnonymous() {
return new Person('匿名', 0, '未知');
}
// getter/setter
get info() {
return `${this.name} (${this.age})`;
}
set info(value) {
const [name, age] = value.split(' ');
this.name = name;
this.age = parseInt(age);
}
}
const person3 = new Person('赵六', 35, '深圳');
const anonymous = Person.createAnonymous();
console.log(person3.info); // "赵六 (35)"
person3.info = "新名字 40";
console.log(person3.name); // "新名字"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 4.1.2 现代对象操作语法
1. 属性访问
const user = {
name: '张三',
age: 25,
'full-name': '张三丰',
address: {
city: '北京',
street: '长安街'
}
};
// 点号访问
console.log(user.name); // "张三"
// 方括号访问
console.log(user['full-name']); // "张三丰"
console.log(user['age']); // 25
// 动态属性访问
const prop = 'name';
console.log(user[prop]); // "张三"
// 可选链访问(ES2020)
console.log(user?.address?.city); // "北京"
console.log(user?.contact?.phone); // undefined(不会报错)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2. 对象解构(ES6+)
const user = {
name: '张三',
age: 25,
city: '北京',
hobbies: ['读书', '游泳', '编程']
};
// 基本解构
const { name, age } = user;
console.log(name, age); // "张三" 25
// 重命名
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // "张三" 25
// 默认值
const { name, age, country = '中国' } = user;
console.log(country); // "中国"
// 嵌套解构
const { address: { city, street = '未知' } = {} } = user;
// 剩余属性
const { name, ...otherInfo } = user;
console.log(otherInfo); // { age: 25, city: '北京', hobbies: [...] }
// 函数参数解构
function greetUser({ name, age, city = '未知' }) {
return `Hello ${name}, 你${age}岁,来自${city}`;
}
console.log(greetUser(user)); // "Hello 张三, 你25岁,来自北京"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3. 展开运算符(ES2018)
const user1 = { name: '张三', age: 25 };
const user2 = { city: '北京', country: '中国' };
// 对象合并
const fullUser = { ...user1, ...user2 };
console.log(fullUser); // { name: '张三', age: 25, city: '北京', country: '中国' }
// 覆盖属性
const updatedUser = { ...user1, age: 26, city: '上海' };
console.log(updatedUser); // { name: '张三', age: 26, city: '上海' }
// 浅拷贝
const userCopy = { ...user1 };
// 添加新属性
const userWithId = { id: 1, ...user1, createdAt: new Date() };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4.1.3 对象实用方法
Object 静态方法
const user = {
name: '张三',
age: 25,
city: '北京'
};
// 获取属性名数组
const keys = Object.keys(user);
console.log(keys); // ['name', 'age', 'city']
// 获取属性值数组
const values = Object.values(user);
console.log(values); // ['张三', 25, '北京']
// 获取键值对数组
const entries = Object.entries(user);
console.log(entries); // [['name', '张三'], ['age', 25], ['city', '北京']]
// 从键值对创建对象
const newUser = Object.fromEntries([
['name', '李四'],
['age', 30]
]);
console.log(newUser); // { name: '李四', age: 30 }
// 对象合并(浅拷贝,嵌套对象仍共享引用)
const merged = Object.assign({}, user, { country: '中国' });
// 深拷贝(结构化数据),支持 Map/Set/Date/循环引用;现代浏览器/Node18+
const copy = structuredClone(user)
// 浅冻结对象(不可添加/删除/修改(值若是对象仍可内部改)),
const frozenUser = Object.freeze({ ...user });
// frozenUser.age = 30; // 严格模式下会报错
// 深冻结(递归 `freeze`)可能影响性能:仅在配置常量、不可变模型初始构建时使用。业务中推荐通过不可变数据模式(复制 + 修改)或借助库( immer / immutable-js )在复杂状态下管理变更。
// 检查对象是否被冻结
console.log(Object.isFrozen(frozenUser)); // true
// 密封对象(不能添加/删除属性,但可以修改现有属性),多用于锁定结构但允许状态变化
const sealedUser = Object.seal({ ...user });
// 精细控制单个属性特征。默认 writable:false, enumerable:false, configurable:false,需显式设置
// Object.defineProperty(obj,'x',{ value:1, writable:false, enumerable:false })
// 禁止再添加新属性,不影响修改现有属性
// Object.preventExtensions(obj)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
提示
- 不在热路径重复创建大型对象或深拷贝;优先更新必要字段。
- 避免在构造函数中使用箭头定义方法(实例方法应共享原型)。
- 使用工厂函数时把复杂逻辑与纯数据分层;方法放在单独模块避免重复引用(或用闭包缓存)。
- 使用
structuredClone替代 JSON 序列化深拷贝复杂数据(保留 Date / Map / Set)。 - 判断属性存在性优先
Object.hasOwn,减少被覆盖风险。 - 继承层级保持浅;多行为组合使用对象组合而非深继承(Composition over Inheritance)。
- 需要严格不可变时对外暴露冻结副本:
return Object.freeze({...config}),内部保留可变版本。
# 4.1.4 原型与继承基础
原型的基本概念
- 原型(Prototype)是 JavaScript 中实现对象间继承和共享属性的核心机制。每个 JavaScript 对象都有一个内部属性
[[Prototype]](在大多数浏览器中可以通过__proto__访问),它指向另一个对象,这个对象就是该对象的原型。 - 属性访问沿链向上查找:实例自身 → 原型 → 原型的原型 ... → 直到
Object.prototype或null。 constructor指回构造函数(可被覆写,不是可靠的类型判定方式)。
为什么需要原型?
- 内存效率:如果没有原型,每个对象都需要拥有自己的方法副本,这会浪费大量内存。通过原型,多个对象可以共享同一组方法。
- 动态继承:JavaScript 是动态语言,原型链允许在运行时修改对象的行为,可以随时添加或修改原型上的属性和方法。
- 代码复用:原型机制使得对象可以继承其他对象的属性和方法,实现了代码复用。
- 模拟类继承:在 ES6 class 出现之前,原型链是 JavaScript 实现面向对象编程的主要方式。
原型的本质
- 原型本质上就是一个普通的对象
- 当你试图访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找
- 这个过程会一直持续,直到找到该属性或者到达原型链的末端(通常是
Object.prototype,其原型是null)
简单示例理解原型
// 构造函数
function Person(name) {
this.name = name;
}
// 在原型上添加方法(所有实例共享)
Person.prototype.sayHello = function() {
return `你好,我是${this.name}`;
};
// 创建两个实例
const person1 = new Person('张三');
const person2 = new Person('李四');
// 两个实例共享同一个方法
console.log(person1.sayHello === person2.sayHello); // true
console.log(person1.sayHello()); // "你好,我是张三"
console.log(person2.sayHello()); // "你好,我是李四"
// 查看原型链
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null(原型链的末端)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
原型链的工作原理
// 原型链示例
function Animal(species) {
this.species = species;
}
Animal.prototype.eat = function() {
return `${this.species} 正在吃东西`;
};
function Dog(name, breed) {
Animal.call(this, '犬科'); // 调用父类构造函数
this.name = name;
this.breed = breed;
}
// 设置原型链,让Dog继承Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向
Dog.prototype.bark = function() {
return `${this.name} 正在叫`;
};
const myDog = new Dog('旺财', '金毛');
// 属性查找过程:
// 1. 首先在myDog对象自身查找
// 2. 如果没找到,去Dog.prototype查找
// 3. 如果还没找到,去Animal.prototype查找
// 4. 如果还没找到,去Object.prototype查找
// 5. 如果还没找到,返回undefined
console.log(myDog.name); // "旺财"(来自实例自身)
console.log(myDog.bark()); // "旺财 正在叫"(来自Dog.prototype)
console.log(myDog.eat()); // "犬科 正在吃东西"(来自Animal.prototype)
console.log(myDog.toString()); // "[object Object]"(来自Object.prototype)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
原型与类的关系
ES6 的 class 语法本质上是原型机制的语法糖:
// ES6 class语法
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `你好,我是${this.name}`;
}
}
// 等价的原型写法
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return `你好,我是${this.name}`;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
原型的重要性
- JavaScript 的核心特性:原型是 JavaScript 语言的核心特性,理解原型是深入理解 JavaScript 的关键。
- 性能优化:通过原型共享方法,可以显著减少内存使用,提高性能。
- 灵活性:原型机制提供了极大的灵活性,可以在运行时动态修改对象的行为。
- 生态系统基础:许多 JavaScript 库和框架都基于原型机制构建,如 jQuery、React 等。
类继承(ES6+)
// 基类
class Animal {
constructor(name, type) {
this.name = name;
this.type = type;
}
speak() {
return `${this.name} makes a sound`;
}
getInfo() {
return `${this.name} is a ${this.type}`;
}
}
// 派生类
class Dog extends Animal {
constructor(name, breed) {
super(name, 'dog'); // 调用父类构造函数
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
wagTail() {
return `${this.name} wags tail`;
}
}
class Cat extends Animal {
constructor(name, color) {
super(name, 'cat');
this.color = color;
}
speak() {
return `${this.name} meows`;
}
purr() {
return `${this.name} purrs`;
}
}
// 使用示例
const dog = new Dog('旺财', '金毛');
const cat = new Cat('咪咪', '橘色');
console.log(dog.speak()); // "旺财 barks"
console.log(dog.getInfo()); // "旺财 is a dog"
console.log(dog.wagTail()); // "旺财 wags tail"
console.log(cat.speak()); // "咪咪 meows"
console.log(cat.purr()); // "咪咪 purrs"
// 多态性
const animals = [dog, cat];
animals.forEach(animal => {
console.log(animal.speak()); // 调用各自的 speak 方法
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
构造函数和 class 差异要点:
class默认严格模式;不能被提升完整使用(仅名称提升到 TDZ)。- 私有字段
#field仅运行时访问,被反射枚举时不可见(不是安全沙箱,仅语法层面)。 - 实例方法定义在原型上(共享),避免每次构造创建新函数引用;若使用箭头函数做类字段(
method = () => {})会在每个实例生成副本,慎用。 - 继承:
class Sub extends Base {}自动设置原型链;旧式需手动Sub.prototype = Object.create(Base.prototype)。
原型操作实用方法
// 检查实例关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
// 检查原型链
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
// 检查属性是否为自有属性
console.log(dog.hasOwnProperty('name')); // true
console.log(dog.hasOwnProperty('speak')); // false(在原型上)
// Object.hasOwn() 与 hasOwnProperty() 的区别
// 1. Object.hasOwn() 是ES2022新增的静态方法,而 hasOwnProperty() 是实例方法
// 2. Object.hasOwn() 更安全,可以处理对象没有继承自 Object.prototype 的情况
// 3. Object.hasOwn() 可以避免对象被覆盖 hasOwnProperty 方法时的问题
// 示例1:基本用法相同
console.log(Object.hasOwn(dog, 'name')); // true
console.log(Object.hasOwn(dog, 'speak')); // false(在原型上)
// 示例2:处理没有原型链的对象
const objWithoutPrototype = Object.create(null);
objWithoutPrototype.prop = 'value';
// hasOwnProperty 会报错,因为对象没有继承 Object.prototype
// console.log(objWithoutPrototype.hasOwnProperty('prop')); // TypeError
// Object.hasOwn() 可以正常工作
console.log(Object.hasOwn(objWithoutPrototype, 'prop')); // true
// 示例3:处理被覆盖的 hasOwnProperty 方法
const weirdObject = {
name: 'test',
hasOwnProperty: function() {
return false; // 总是返回 false
}
};
console.log(weirdObject.hasOwnProperty('name')); // false(错误结果)
console.log(Object.hasOwn(weirdObject, 'name')); // true(正确结果)
// 推荐使用 Object.hasOwn() 替代 hasOwnProperty(),因为它更安全、更可靠
// 获取属性描述符
const descriptor = Object.getOwnPropertyDescriptor(dog, 'name');
console.log(descriptor); // { value: '旺财', writable: true, enumerable: true, configurable: true }
// 定义属性
Object.defineProperty(dog, 'id', {
value: 1,
writable: false,
enumerable: false,
configurable: false
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 4.2 JSON(JavaScript Object Notation)
# 4.2.1 JSON 概述
JSON 是一种轻量级的数据交换格式,具有以下特点:
- 语言无关:虽然源于 JavaScript,但几乎所有编程语言都支持
- 易读易写:对人类和机器都友好的格式
- 轻量级:相比 XML 更简洁
- 广泛应用:Web API、配置文件、数据存储等场景
# 4.2.2 JSON 语法规则
基本语法:
// 有效的 JSON 格式
{
"name": "张三",
"age": 25,
"isStudent": false,
"address": {
"city": "北京",
"zipCode": "100000"
},
"hobbies": ["读书", "游泳", "编程"],
"spouse": null
}
2
3
4
5
6
7
8
9
10
11
12
语法要求:
| 项 | JSON | JS 对象字面量 |
|---|---|---|
| 引号 | 键与字符串值必须使用双引号 " | 键可不带引号(满足标识符规则),字符串可用单/双/反引号 |
| 支持类型 | string / number / boolean / null / object / array | 还包括 undefined / Symbol / BigInt / 函数等 |
| 末尾逗号 | 不允许 | 允许(部分环境 / 解析器容忍) |
| 注释 | 不允许 | 可在源文件出现(非 JSON 解析) |
| 转义 | 必须对 "、控制字符等进行标准转义 | JS 字符串同理;模板字符串支持多行 |
| 日期表示 | 无专用类型(通常用 ISO 字符串) | 可用 Date 实例 |
# 4.2.3 JSON 操作方法
| 方法 | 作用 | 关键参数 |
|---|---|---|
JSON.stringify(value, replacer?, space?) | JS 值序列化为 JSON 字符串 | replacer 过滤/转换;space 美化输出(数字或字符串) |
JSON.parse(text, reviver?) | 将 JSON 字符串解析为 JS 值 | reviver 二次转换(如日期、枚举恢复) |
1. JSON.stringify() - 序列化
const user = {
name: '张三',
age: 25,
city: '北京',
hobbies: ['读书', '游泳'],
createdAt: new Date(),
greet: function() { return 'hello'; }, // 函数会被忽略
secret: undefined // undefined 会被忽略
};
// 基本序列化
const jsonString = JSON.stringify(user);
console.log(jsonString);
// {"name":"张三","age":25,"city":"北京","hobbies":["读书","游泳"],"createdAt":"2025-10-21T..."}
// 格式化输出(美化)
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
/*
{
"name": "张三",
"age": 25,
"city": "北京",
"hobbies": [
"读书",
"游泳"
],
"createdAt": "2025-10-21T..."
}
*/
// 选择性序列化
const selectedJson = JSON.stringify(user, ['name', 'age']);
console.log(selectedJson); // {"name":"张三","age":25}
// 自定义序列化
const customJson = JSON.stringify(user, (key, value) => {
if (key === 'age') return value + '岁';
if (key === 'createdAt') return value.toISOString();
return value;
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2. JSON.parse() - 反序列化
const jsonString = '{"name":"张三","age":25,"city":"北京","hobbies":["读书","游泳"]}';
// 基本解析
const userData = JSON.parse(jsonString);
console.log(userData.name); // "张三"
// 自定义解析
const customData = JSON.parse(jsonString, (key, value) => {
if (key === 'age') return `${value}岁`;
if (key === 'hobbies') return value.join(', ');
return value;
});
console.log(customData.age); // "25岁"
console.log(customData.hobbies); // "读书, 游泳"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. Replacer / Reviver 高级用法
replacer可为数组:JSON.stringify(obj, ['id','name'])仅保留列出的键。reviver深度遍历解析树(自底向上)可用于:恢复类型、数据清洗、字段重命名。- BigInt 不能直接序列化:需转换为字符串:
JSON.stringify({ big: bigInt.toString() });解析后再BigInt(str)。
# 4.2.4 错误处理与验证
安全的 JSON 操作
// 安全的 JSON 解析
function safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('JSON 解析失败:', error.message);
return defaultValue;
}
}
// 安全的 JSON 序列化
function safeJsonStringify(obj, defaultValue = '{}') {
try {
return JSON.stringify(obj);
} catch (error) {
console.error('JSON 序列化失败:', error.message);
return defaultValue;
}
}
// 使用示例
const invalidJson = '{"name": "张三", "age": }'; // 无效 JSON
const result = safeJsonParse(invalidJson, { name: '默认用户' });
console.log(result); // { name: '默认用户' }
// 循环引用处理
const circularObj = { name: '张三' };
circularObj.self = circularObj;
const safeResult = safeJsonStringify(circularObj, '{"error": "circular reference"}');
console.log(safeResult); // '{"error": "circular reference"}'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
JSON 验证
// 验证 JSON 格式
function isValidJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
// 验证 JSON 结构
function validateUserJson(jsonStr) {
const user = safeJsonParse(jsonStr);
if (!user) return { valid: false, errors: ['无效的 JSON 格式'] };
const errors = [];
if (!user.name || typeof user.name !== 'string') {
errors.push('name 字段必须是非空字符串');
}
if (!user.age || typeof user.age !== 'number' || user.age < 0) {
errors.push('age 字段必须是正数');
}
return {
valid: errors.length === 0,
errors,
data: errors.length === 0 ? user : null
};
}
// 使用示例
const userJson = '{"name":"张三","age":25}';
const validation = validateUserJson(userJson);
if (validation.valid) {
console.log('用户数据有效:', validation.data);
} else {
console.log('验证失败:', validation.errors);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 4.2.5 实际应用场景
1. AJAX 数据交换
// 发送 JSON 数据
async function createUser(userData) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('创建用户失败:', error);
throw error;
}
}
// 使用示例
createUser({
name: '张三',
email: 'zhangsan@example.com',
age: 25
}).then(user => {
console.log('用户创建成功:', user);
}).catch(error => {
console.error('操作失败:', error);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2. 本地存储
// 本地存储工具类
class LocalStorage {
static setItem(key, value) {
try {
const jsonValue = JSON.stringify(value);
localStorage.setItem(key, jsonValue);
return true;
} catch (error) {
console.error('存储失败:', error);
return false;
}
}
static getItem(key, defaultValue = null) {
try {
const jsonValue = localStorage.getItem(key);
return jsonValue ? JSON.parse(jsonValue) : defaultValue;
} catch (error) {
console.error('读取失败:', error);
return defaultValue;
}
}
static removeItem(key) {
localStorage.removeItem(key);
}
}
// 使用示例
const userPreferences = {
theme: 'dark',
language: 'zh-CN',
notifications: true
};
LocalStorage.setItem('userPrefs', userPreferences);
const prefs = LocalStorage.getItem('userPrefs', {});
console.log(prefs); // { theme: 'dark', language: 'zh-CN', notifications: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
3. 配置文件处理
// 应用配置管理
class Config {
constructor() {
this.config = {};
this.loadFromStorage();
}
loadFromStorage() {
const savedConfig = LocalStorage.getItem('appConfig', {});
this.config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryCount: 3,
debug: false,
...savedConfig
};
}
get(key, defaultValue = undefined) {
return this.config[key] ?? defaultValue;
}
set(key, value) {
this.config[key] = value;
this.save();
}
save() {
LocalStorage.setItem('appConfig', this.config);
}
export() {
return JSON.stringify(this.config, null, 2);
}
import(jsonString) {
const validation = this.validateConfig(jsonString);
if (validation.valid) {
this.config = { ...this.config, ...validation.data };
this.save();
return true;
}
return false;
}
validateConfig(jsonString) {
const config = safeJsonParse(jsonString);
if (!config) return { valid: false, errors: ['无效的 JSON 格式'] };
// 这里可以添加更多验证逻辑
return { valid: true, data: config };
}
}
// 使用示例
const appConfig = new Config();
appConfig.set('debug', true);
console.log(appConfig.get('apiUrl')); // "https://api.example.com"
const exportedConfig = appConfig.export();
console.log(exportedConfig); // 格式化的 JSON 字符串
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 4.2.6 JSON 性能优化
大对象处理
// 分批处理大数组
function processLargeArray(largeArray, batchSize = 1000) {
const results = [];
for (let i = 0; i < largeArray.length; i += batchSize) {
const batch = largeArray.slice(i, i + batchSize);
const batchResult = JSON.stringify(batch);
results.push(batchResult);
// 给浏览器喘息的机会
if (i % (batchSize * 10) === 0) {
setTimeout(() => {}, 0);
}
}
return results;
}
// 流式处理
async function streamProcessJson(data) {
return new Promise((resolve) => {
setTimeout(() => {
const result = JSON.stringify(data);
resolve(result);
}, 0);
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
JSON 使用注意事项
- 安全性:不要解析来自不可信源的 JSON,可能存在安全风险
- 性能:大对象的序列化/反序列化会影响性能
- 数据丢失:函数、undefined、Symbol 会在序列化时丢失
- 循环引用:会导致序列化失败
- 精度问题:大整数可能丢失精度
# 4.2.7 常见问题解决
// 处理特殊值
const specialValues = {
date: new Date(),
func: () => 'hello',
undef: undefined,
sym: Symbol('test'),
regex: /test/g,
bigint: 123456789012345678901234567890n
};
// 自定义 toJSON 方法
const customObject = {
name: '张三',
createdAt: new Date(),
toJSON() {
return {
name: this.name,
createdAt: this.createdAt.toISOString(),
type: 'user'
};
}
};
console.log(JSON.stringify(customObject));
// {"name":"张三","createdAt":"2025-10-21T...","type":"user"}
// 深拷贝(简单版本,不处理特殊对象)
function deepClone(obj) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error('深拷贝失败:', error);
return obj;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 4.2.8 常见陷阱
| 场景 | 问题 | 方案 |
|---|---|---|
| 序列化函数/undefined/Symbol | 自动被忽略或转换为 null(数组中 undefined 变 null) | 预先过滤或转成占位字符串 |
| 日期对象 | 输出 ISO 字符串,读取时仍是普通字符串 | reviver 恢复或显式 new Date(str) |
| 非安全整数(> Number.MAX_SAFE_INTEGER) | 精度丢失 | 转为字符串或使用 BigInt(自行封装) |
| 大对象美化输出 | 使用 space 导致体积膨胀 | 仅在调试环境使用缩进,美化禁用于生产 |
| 循环引用 | 抛错 | 自定义 replacer / 预处理 / 使用专用库 (flatted) |
| JSON 中注释 | 解析失败 | 若需“可读配置”使用 JSONC(由工具转换) |
# 4.2.9 安全与防护
- 不信任来源的 JSON:解析前校验 Content-Type 与大小限制(防止资源耗尽)。
- 禁止直接
evalJSON 字符串,必须使用JSON.parse(eval存在代码注入风险)。 - 响应嵌入
<script>时避免 JSONP 注入,使用纯application/json+ CORS;弃用老式 JSONP 方案。 - 后端输出时为防 XSSI(跨站脚本包含),可在 JSON 前添加不可执行前缀:
)]}',\n(客户端需去除再解析)。 - 过滤敏感字段:token、密码哈希、内部 ID;使用
replacer/ 显式构建白名单对象。 - 避免深层递归对象序列化造成 CPU 峰值,设置最大深度或字段裁剪策略。
# 4.2.10 与其他格式对比(何时选 JSON)
| 格式 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| JSON | 人类可读、原生支持广、解析快 | 无二进制、无模式约束 | Web API / 配置 |
| YAML | 可读性强、支持注释 | 解析更复杂、缩进敏感 | 人工维护配置文件 |
| Protocol Buffers | 紧凑、高速、强 schema | 需编译描述文件 | 服务间高性能通信 |
| MessagePack | 二进制紧凑 | 需额外库 | 移动/物联网传输 |
# 4.2.11 最佳实践清单
- 保持键与字符串值使用双引号;不要混用单引号(解析失败)。
- 利用
replacer/ 白名单对象构建输出,避免“黑名单遗漏”。 - 接口对超大 JSON 响应分页 / 按需拉取,降低首次解析开销。
- 日期统一 ISO8601;客户端解析后再格式化展示。
- 遇大整数/精度敏感:使用字符串承载,附带字段后缀(如
amountMinorUnits)。 - 使用
structuredClone深复制复杂对象后再序列化,保留 Map/Set。旧环境使用 polyfill 或库。 - 不在生产开启
space缩进;仅调试或日志需要时使用。 - 预防循环引用导致崩溃:检测并替换标记,或清理双向引用。
- 不要把函数或
undefined放入需要序列化的数据结构;先转换成纯数据模型。 - 安全场景采用
Content-Type: application/json;禁用 JSONP unless legacy fallback。
# 4.3 JS 常见对象
更多细节参见 MDN (opens new window)
# 4.3.1 数组
创建:[]、Array.of(1,2)、Array.from(iterable/mapFn);避免使用 new Array(len) 再填充造成“空槽”迭代差异(map 不访问空槽)。
高频方法分类:
| 类别 | 方法 | 要点 |
|---|---|---|
| 遍历/筛选 | forEach / map / filter / some / every | map 返回新数组;forEach 不返回;some/every 可早停 |
| 聚合 | reduce / reduceRight | 复杂逻辑拆分避免“大而全” reduce |
| 位置 | find / findIndex / indexOf / includes / at | includes 对 NaN 友好;at(-1) 取最后元素 |
| 结构变换 | slice / splice / flat / flatMap | slice 不变原数组;splice 变原数组;flat 默认 1 层 |
| 增删尾首 | push / pop / shift / unshift | 频繁首部操作可考虑 deque 数据结构(库) |
| 合并 | concat / 展开 ...arr | 展开结合解构更常用 |
| 其他 | sort / join / Array.isArray | sort 默认按字符串排序需提供比较函数 |
易错点:
sort()默认比较字符串:[10,2].sort()→[10,2];数字排序:arr.sort((a,b)=>a-b)。- 稀疏数组:
Array(5).map(()=>0)得[empty × 5];先填充fill(0)。 - 引用复制与浅拷贝:
const copy=[...arr]仅一层。 - 使用
reduce累加时初始值必须明确:reduce((acc,v)=>acc+v,0);否则空数组报错。
# 4.3.2 String
高频:includes、startsWith、endsWith、slice、split、replace / replaceAll、padStart/padEnd、trim/trimStart/trimEnd、模板字符串 `${var}`。
易错点:
- 正则替换多个出现:旧版用
/pattern/g+replace;现代直接replaceAll('x','y')。 - 编码处理:表情符长度 > 1(UTF-16 代理对)
'😀'.length === 2,用Array.from(str)获得实际字符数组。 - 大量拼接考虑使用数组 +
join('')(旧浏览器性能差异,现代已优化但仍可读性更好)。
# 4.3.3 Number / Math
Number 侧:Number.isNaN(取代全局 isNaN)、Number.isFinite、Number.parseInt(str,10)、toFixed(仅显示用途可能有四舍五入偏差)。
Math 常用:Math.max/min(...vals)、Math.floor/ceil/round/trunc、Math.random()(非安全;密码令牌请使用 crypto.getRandomValues)、Math.pow(已用 ** 代替)、Math.abs。
易错点:
- 浮点误差:使用
Number.EPSILON:Math.abs(a-b) < Number.EPSILON。 - 随机整数:
Math.floor(Math.random()*n)范围[0,n);避免parseInt(Math.random()*n)(语义不清)。 - 安全整数范围:
Number.isSafeInteger(num)确保不超 2^53-1。
# 4.3.4 Date
首选 ISO 字符串/时间戳存储;避免依赖本地时区隐式转换。
常用:new Date()、Date.now()、date.getTime()、toISOString()、getUTC* 系列(跨时区计算更稳定)。
易错点:
- 月份从 0 开始:
new Date(2025, 0, 1)是 1 月份。 - 直接字符串解析非标准格式可能实现差异;统一使用 ISO 8601。
- 时区转换最好用库(dayjs / date-fns / luxon),避免 DST(夏令时)手写 Bug。
# 4.3.5 Map / Set
| 结构 | 关键方法 | 优势 | 注意 |
|---|---|---|---|
| Map | set/get/has/delete/forEach/entries | 键可为任意对象 | 与对象对比不会自动字符串化键 |
| Set | add/has/delete/forEach | 去重集合 | 对象去重需自定义序列化 |
使用示例:
const set = new Set([1,2,2]); // {1,2}
const map = new Map();
map.set({id:1}, 'data');
2
3
易错点:new Set([ [1,2],[1,2] ]) 保留两个不同引用;需转换统一标识。
# 4.3.6 Promise
常用:Promise.resolve、Promise.reject、Promise.all、Promise.allSettled、Promise.race、Promise.any。
易错点:
all失败短路;需要收集全部结果(含失败)用allSettled。race第一 settle(成功或失败)即返回;避免误用期望“最快成功”。- 未处理拒绝会出现 UnhandledRejection;始终在链尾
catch或使用顶层监听。
# 4.3.7 实用片段
数组去重:const unique = [...new Set(arr)];
对象键值翻转:const flipped = Object.fromEntries(Object.entries(obj).map(([k,v])=>[v,k]));
安全取最后元素:arr.at(-1) 替代 arr[arr.length-1]。
不可变更新:const next = {...state, list: state.list.map(x=>x.id===id? {...x,done:true}:x) };
# 4.3.8 总结与推荐
- 掌握高频方法语义与副作用(是否修改原数组)。
- 避免背诵全部 API;熟悉分类后查文档更高效。
- 数值与日期超出基础场景使用第三方库降低边界错误风险。
- 利用
Map/Set替代对象键值去重与频次统计场景。 - Promise 并发控制可用
Promise.all+ 分批或库(p-limit)避免过载。
# 五、JavaScript 事件处理
# 5.1 事件驱动编程
事件概念:事件是用户与页面交互或浏览器自身行为产生的信号。JavaScript 事件驱动编程是指通过监听和响应这些事件来实现程序功能的编程模式。
事件的生命周期:
- 事件触发:用户操作或浏览器行为发生
- 事件捕获:事件从根节点向目标元素传播
- 事件处理:执行绑定的事件处理函数
- 事件冒泡:事件从目标元素向根节点传播
# 5.2 事件模型概述
事件是“用户或系统行为”触发的通知,通过事件循环调度回调。浏览器 DOM 事件传播由三个阶段:捕获 (capture) → 目标 (target) → 冒泡 (bubble)。
核心概念:
- 事件对象:
event(或形参名称自定义)包含类型、目标、坐标、键值等信息。 event.target与event.currentTarget区分:委托时target是实际触发元素,currentTarget是绑定监听的元素。- 传播控制:
event.stopPropagation()阻止继续传播;event.stopImmediatePropagation()还阻止同元素后续监听;event.preventDefault()阻止默认行为(如提交、跳转)。 - Passive 监听:在滚动/触摸事件使用
{ passive: true }提示不会调用preventDefault(),提升性能。
# 5.3 事件分类与实际应用
鼠标事件
| 事件类型 | 触发时机 | 应用场景 | 现代用法 |
|---|---|---|---|
click | 单击元素 | 按钮点击、链接跳转 | 优先使用,支持键盘访问 |
dblclick | 双击元素 | 文件打开、文本选择 | 注意与 click 事件冲突 |
mousedown | 鼠标按下 | 拖拽开始、自定义按钮 | 配合 mouseup 使用 |
mouseup | 鼠标松开 | 拖拽结束、绘图应用 | 处理拖拽逻辑 |
mouseenter | 鼠标进入 | 悬停效果、提示显示 | 不冒泡,性能更好 |
mouseleave | 鼠标离开 | 隐藏提示、恢复状态 | 不冒泡,性能更好 |
mouseover | 鼠标悬停 | 兼容性要求高的场景 | 会冒泡,注意事件委托 |
mouseout | 鼠标移出 | 兼容性要求高的场景 | 会冒泡,注意事件委托 |
mousemove | 鼠标移动 | 拖拽、绘图、跟随效果 | 频繁触发,注意性能 |
contextmenu | 右键菜单 | 自定义右键菜单 | 通常阻止默认行为 |
键盘事件
| 事件类型 | 触发时机 | 应用场景 | 注意事项 |
|---|---|---|---|
keydown | 按键按下 | 实时响应、组合键 | 连续触发,支持所有键 |
keyup | 按键松开 | 防止重复触发 | 单次触发,处理释放逻辑 |
keypress | 字符键按下 | 字符输入处理 | 已废弃,不推荐使用 |
表单事件
| 事件类型 | 触发时机 | 应用场景 | 最佳实践 |
|---|---|---|---|
input | 输入内容改变 | 实时验证、搜索建议 | 推荐替代 keyup |
change | 失焦且值改变 | 表单验证、数据保存 | 适用于 select、checkbox |
focus | 获得焦点 | 显示提示、清空 placeholder | 用户体验优化 |
blur | 失去焦点 | 验证输入、保存数据 | 表单验证关键点 |
submit | 表单提交 | 数据验证、阻止提交 | 必须验证数据完整性 |
reset | 表单重置 | 清空状态、恢复默认值 | 同步清空自定义状态 |
现代 Web 事件
| 事件类型 | 触发时机 | 应用场景 | 兼容性 |
|---|---|---|---|
wheel | 鼠标滚轮 | 自定义滚动、缩放 | 现代浏览器 |
touchstart | 触摸开始 | 移动端交互 | 移动设备 |
touchmove | 触摸移动 | 滑动手势 | 移动设备 |
touchend | 触摸结束 | 手势完成 | 移动设备 |
resize | 窗口尺寸变化 | 响应式布局 | 频繁触发,需防抖 |
scroll | 页面滚动 | 懒加载、导航高亮 | 频繁触发,需节流 |
# 5.4 现代事件绑定方式
# 5.4.1 HTML 属性绑定(不推荐)
<!-- 传统方式:HTML中直接绑定 -->
<button onclick="handleClick()">点击我</button>
<input onchange="handleChange(this)">
<script>
function handleClick() {
console.log('按钮被点击');
}
function handleChange(element) {
console.log('输入值:', element.value);
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
缺点:
- HTML 和 JavaScript 耦合严重
- 难以维护和调试
- 不支持多个事件处理函数
- 违反内容与行为分离原则
# 5.4.2 DOM 属性绑定(基础方式)
// DOM属性绑定示例
// 注意:此代码需要放在HTML文档底部或确保DOM已加载
// 直接获取元素并绑定事件
const button = document.getElementById('myButton');
const input = document.getElementById('myInput');
// 绑定单个事件 - DOM属性方式
button.onclick = function(event) {
console.log('按钮被点击', event);
};
// 绑定事件,支持箭头函数
input.onchange = (event) => {
console.log('输入值改变:', event.target.value);
};
// 注意:只能绑定一个处理函数,后绑定的会覆盖前面的
button.onclick = function() {
console.log('这会覆盖上面的处理函数');
};
// 如果需要确保DOM加载完成,可以使用以下方式之一:
// 方式1:将脚本放在</body>前
// 方式2:使用DOMContentLoaded事件(推荐)
document.addEventListener('DOMContentLoaded', function() {
// 在这里使用DOM属性绑定
const button = document.getElementById('myButton');
button.onclick = function() {
console.log('DOM加载完成后的按钮点击');
};
});
// 方式3:使用window.onload(不推荐用于DOM操作)
window.onload = function() {
// 页面完全加载后执行(包括图片、样式表等)
const button = document.getElementById('myButton');
button.onclick = function() {
console.log('页面完全加载后的按钮点击');
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 5.4.3 addEventListener(推荐方式)
// 现代事件绑定 - 最佳实践
document.addEventListener('DOMContentLoaded', function() {
const button = document.getElementById('myButton');
const input = document.getElementById('myInput');
// 基本用法
button.addEventListener('click', function(event) {
console.log('点击事件1', event);
});
// 可以绑定多个处理函数
button.addEventListener('click', function(event) {
console.log('点击事件2', event);
});
// 使用箭头函数(注意 this 指向)
button.addEventListener('click', (event) => {
console.log('箭头函数处理', event.target);
});
// 配置选项
button.addEventListener('click', handleClick, {
once: true, // 自动执行一次后移除
passive: true, // 不会调用 preventDefault(提升滚动性能)
capture: true // 在捕获阶段执行 false, 在冒泡阶段执行
});
// 移除事件监听器
function handleClick(event) {
console.log('可移除的事件处理');
}
button.addEventListener('click', handleClick);
// 移除事件
// button.removeEventListener('click', handleClick);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 5.5 事件对象详解
# 5.5.1 Event 对象属性
button.addEventListener('click', function(event) {
// 基本属性
console.log('事件类型:', event.type); // "click"
console.log('目标元素:', event.target); // 实际触发事件的元素
console.log('当前元素:', event.currentTarget); // 绑定事件的元素
console.log('时间戳:', event.timeStamp); // 事件发生时间
// 鼠标相关属性
console.log('鼠标位置:', {
clientX: event.clientX, // 相对于视口的X坐标
clientY: event.clientY, // 相对于视口的Y坐标
pageX: event.pageX, // 相对于页面的X坐标
pageY: event.pageY, // 相对于页面的Y坐标
screenX: event.screenX, // 相对于屏幕的X坐标
screenY: event.screenY // 相对于屏幕的Y坐标
});
// 键盘修饰键
console.log('修饰键状态:', {
ctrlKey: event.ctrlKey, // Ctrl键是否按下
shiftKey: event.shiftKey, // Shift键是否按下
altKey: event.altKey, // Alt键是否按下
metaKey: event.metaKey // Meta键(Mac的Cmd,Win的Win键)
});
// 鼠标按键信息
console.log('鼠标按键:', event.button); // 0:左键 1:中键 2:右键
console.log('按键状态:', event.buttons); // 按位表示按下的按键
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 5.5.2 键盘事件对象
input.addEventListener('keydown', function(event) {
console.log('键盘事件信息:', {
key: event.key, // 按键值(推荐)
code: event.code, // 物理按键码
keyCode: event.keyCode, // 数字键码(已废弃)
which: event.which // 键码(已废弃)
});
// 判断特殊按键
if (event.key === 'Enter') {
console.log('按下回车键');
}
if (event.key === 'Escape') {
console.log('按下ESC键');
}
// 组合键检测
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // 阻止浏览器默认保存
console.log('Ctrl+S 保存快捷键');
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.5.3 表单事件对象
// input 事件 - 实时响应输入
input.addEventListener('input', function(event) {
const value = event.target.value;
console.log('实时输入值:', value);
// 实时验证示例
if (value.length < 3) {
event.target.style.borderColor = 'red';
} else {
event.target.style.borderColor = 'green';
}
});
// change 事件 - 值改变且失焦
input.addEventListener('change', function(event) {
console.log('最终输入值:', event.target.value);
});
// 表单提交事件
form.addEventListener('submit', function(event) {
const formData = new FormData(event.target);
// 验证表单数据
let isValid = true;
for (let [key, value] of formData.entries()) {
if (!value.trim()) {
isValid = false;
console.log(`${key} 不能为空`);
}
}
if (!isValid) {
event.preventDefault(); // 阻止表单提交
return false;
}
console.log('表单验证通过,准备提交');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 5.6 事件传播机制
# 5.6.1 事件流的三个阶段
// HTML 结构
/*
<div id="outer">
<div id="middle">
<button id="inner">点击我</button>
</div>
</div>
*/
const outer = document.getElementById('outer');
const middle = document.getElementById('middle');
const inner = document.getElementById('inner');
// 捕获阶段(从根到目标)
outer.addEventListener('click', function() {
console.log('外层 - 捕获阶段');
}, true); // 第三个参数为 true 表示捕获阶段
middle.addEventListener('click', function() {
console.log('中层 - 捕获阶段');
}, true);
inner.addEventListener('click', function() {
console.log('内层 - 捕获阶段');
}, true);
// 冒泡阶段(从目标到根)
inner.addEventListener('click', function() {
console.log('内层 - 冒泡阶段');
});
middle.addEventListener('click', function() {
console.log('中层 - 冒泡阶段');
});
outer.addEventListener('click', function() {
console.log('外层 - 冒泡阶段');
});
// 点击内层按钮的输出顺序:
// 外层 - 捕获阶段
// 中层 - 捕获阶段
// 内层 - 捕获阶段
// 内层 - 冒泡阶段
// 中层 - 冒泡阶段
// 外层 - 冒泡阶段
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 5.6.2 控制事件传播
button.addEventListener('click', function(event) {
console.log('按钮被点击');
// 阻止事件冒泡
event.stopPropagation();
// 阻止默认行为
event.preventDefault();
// 立即停止事件传播(包括同一元素的其他监听器)
// event.stopImmediatePropagation();
});
// 阻止默认行为的实际应用
document.getElementById('myLink').addEventListener('click', function(event) {
const userConfirmed = confirm('确定要跳转到外部链接吗?');
if (!userConfirmed) {
event.preventDefault(); // 阻止链接跳转
}
});
// 阻止表单默认提交,使用AJAX
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault(); // 阻止默认提交
// 使用 fetch API 提交表单
const formData = new FormData(event.target);
fetch('/api/submit', {
method: 'POST',
body: formData
}).then(response => {
console.log('表单提交成功');
}).catch(error => {
console.error('提交失败:', error);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 5.7 事件委托(Event Delegation)
基本概念:事件委托是利用事件冒泡机制,将子元素的事件处理委托给父元素的技术。
优点:
- 减少内存占用(少量事件监听器)
- 动态元素自动获得事件处理
- 提高性能,特别是大量元素时
// 传统方式 - 为每个按钮单独绑定事件(不推荐)
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', function() {
console.log('按钮被点击:', this.textContent);
});
});
// 事件委托方式 - 推荐
const container = document.getElementById('buttonContainer');
container.addEventListener('click', function(event) {
// 检查点击的是否是按钮
if (event.target.classList.contains('btn')) {
console.log('按钮被点击:', event.target.textContent);
// 根据按钮类型执行不同操作
if (event.target.classList.contains('delete-btn')) {
handleDelete(event.target);
} else if (event.target.classList.contains('edit-btn')) {
handleEdit(event.target);
}
}
});
function handleDelete(button) {
const item = button.closest('.item');
if (confirm('确定要删除这个项目吗?')) {
item.remove();
}
}
function handleEdit(button) {
const item = button.closest('.item');
const text = item.querySelector('.text');
const newText = prompt('请输入新内容:', text.textContent);
if (newText) {
text.textContent = newText;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
动态内容处理
// 动态添加的元素自动获得事件处理
const todoList = document.getElementById('todoList');
// 事件委托处理所有待办事项操作
todoList.addEventListener('click', function(event) {
const target = event.target;
if (target.classList.contains('complete-btn')) {
// 标记完成
const todoItem = target.closest('.todo-item');
todoItem.classList.toggle('completed');
} else if (target.classList.contains('delete-btn')) {
// 删除项目
const todoItem = target.closest('.todo-item');
todoItem.remove();
} else if (target.classList.contains('edit-btn')) {
// 编辑项目
const todoItem = target.closest('.todo-item');
const textElement = todoItem.querySelector('.todo-text');
const currentText = textElement.textContent;
const newText = prompt('编辑待办事项:', currentText);
if (newText && newText.trim()) {
textElement.textContent = newText.trim();
}
}
});
// 动态添加新的待办事项
function addTodoItem(text) {
const todoHTML = `
<div class="todo-item">
<span class="todo-text">${text}</span>
<button class="complete-btn">完成</button>
<button class="edit-btn">编辑</button>
<button class="delete-btn">删除</button>
</div>
`;
todoList.insertAdjacentHTML('beforeend', todoHTML);
}
// 新添加的元素自动具有事件处理能力
document.getElementById('addTodo').addEventListener('click', function() {
const input = document.getElementById('todoInput');
const text = input.value.trim();
if (text) {
addTodoItem(text);
input.value = '';
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 5.8 性能优化技巧
# 5.8.1 事件节流(Throttle)
// 节流函数 - 限制执行频率
function throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
// 应用场景:滚动事件优化
window.addEventListener('scroll', throttle(function(event) {
const scrollTop = window.pageYOffset;
console.log('当前滚动位置:', scrollTop);
// 根据滚动位置显示/隐藏导航栏
const navbar = document.getElementById('navbar');
if (scrollTop > 100) {
navbar.classList.add('fixed');
} else {
navbar.classList.remove('fixed');
}
}, 100)); // 每100ms最多执行一次
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 5.8.2 事件防抖(Debounce)
// 防抖函数 - 延迟执行,如果再次触发则重置
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 应用场景:搜索建议
const searchInput = document.getElementById('searchInput');
const suggestionsList = document.getElementById('suggestions');
const handleSearch = debounce(async function(event) {
const query = event.target.value.trim();
if (query.length < 2) {
suggestionsList.innerHTML = '';
return;
}
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const suggestions = await response.json();
suggestionsList.innerHTML = suggestions
.map(item => `<li onclick="selectSuggestion('${item}')">${item}</li>`)
.join('');
} catch (error) {
console.error('搜索建议获取失败:', error);
}
}, 300); // 停止输入300ms后执行搜索
searchInput.addEventListener('input', handleSearch);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 5.8.3 被动事件监听器
// 被动监听器 - 承诺不会调用preventDefault,提升性能
document.addEventListener('touchmove', function(event) {
// 处理触摸移动,但不阻止默认行为
console.log('触摸移动');
}, { passive: true });
// 滚动优化
window.addEventListener('scroll', function(event) {
// 滚动处理逻辑
updateScrollIndicator();
}, { passive: true });
function updateScrollIndicator() {
const scrollPercent = (window.pageYOffset / (document.body.scrollHeight - window.innerHeight)) * 100;
document.getElementById('scrollIndicator').style.width = scrollPercent + '%';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
事件处理最佳实践
- 优先使用 addEventListener:支持多个监听器,功能更强大
- 合理使用事件委托:特别是动态内容和大量元素
- 注意性能优化:使用节流、防抖处理高频事件
- 正确处理事件对象:充分利用事件信息
- 适当阻止默认行为:根据需求决定是否阻止
- 使用现代事件:优先使用 input 而非 keyup
- 考虑可访问性:确保键盘用户也能触发事件
# 5.9 实际应用案例
# 5.9.1 图片懒加载
// 使用 Intersection Observer API 实现懒加载
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
// 观察所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.9.2 拖拽功能实现
let isDragging = false;
let currentElement = null;
let offset = { x: 0, y: 0 };
document.addEventListener('mousedown', function(event) {
if (event.target.classList.contains('draggable')) {
isDragging = true;
currentElement = event.target;
const rect = currentElement.getBoundingClientRect();
offset.x = event.clientX - rect.left;
offset.y = event.clientY - rect.top;
currentElement.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', function(event) {
if (isDragging && currentElement) {
event.preventDefault();
currentElement.style.position = 'absolute';
currentElement.style.left = (event.clientX - offset.x) + 'px';
currentElement.style.top = (event.clientY - offset.y) + 'px';
}
});
document.addEventListener('mouseup', function() {
if (isDragging) {
isDragging = false;
if (currentElement) {
currentElement.style.cursor = 'grab';
currentElement = null;
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 5.10 常见易错与最佳实践
| 场景 | 问题 | 解决 |
|---|---|---|
| 过多监听器 | 大量子元素各自绑定,内存与性能下降 | 使用委托在父级处理 |
| 重复添加 | 多次 addEventListener 导致执行重复 | 在初始化阶段集中绑定或使用 once |
| 匿名回调无法解绑 | removeEventListener 需要同函数引用 | 抽取具名函数或使用对象存储引用 |
| 没有阻止默认提交 | 表单校验后仍提交 | form.addEventListener('submit', e=>{e.preventDefault(); ...}) |
| 阻止可访问性键行为 | 过度 preventDefault() 键盘失效 | 仅在必要时阻止;保留基本导航键 |
| 未使用 passive | 滚动监听影响流畅度 | { passive:true } 优化滚动/触摸 |
| 冒泡干扰 | 嵌套组件事件互相影响 | 合理使用 stopPropagation 或局部标记判断 |
现代事件处理应:使用 addEventListener + options、通过委托减少绑定数量、使用防抖/节流优化高频事件、明确传播与默认行为控制、保持可访问性与性能平衡。弃用内联与 DOM0 模式作为主实现方式。
# 六、浏览器对象模型(BOM)编程
# 6.1 BOM 概述与核心概念
# 6.1.1 什么是 BOM
BOM(Browser Object Model,浏览器对象模型)是 JavaScript 与浏览器交互的接口,它提供了访问和控制浏览器窗口、导航、历史记录等功能的对象和方法。
BOM 的特点:
- 无标准规范:BOM 没有官方的 W3C 标准,各浏览器实现略有差异
- 以 window 为核心:所有 BOM 对象都是 window 对象的属性
- 提供浏览器功能:控制窗口、获取浏览器信息、管理历史等
# 6.1.2 BOM 对象层次结构
window (全局对象,代表浏览器窗口)
├── document (DOM 根对象,操作页面内容)
├── location (地址栏对象,URL 操作)
├── history (历史记录对象,导航控制)
├── navigator (浏览器信息对象,环境检测)
├── screen (屏幕对象,显示器信息)
├── localStorage (本地存储,持久化数据)
├── sessionStorage (会话存储,临时数据)
└── console (控制台对象,调试输出)
2
3
4
5
6
7
8
9
# 6.2 Window 对象核心功能
# 6.2.1 全局作用域与对象
// window 是全局对象,所有全局变量和函数都是其属性
var globalVar = 'Hello World';
function globalFunction() {
console.log('This is a global function');
}
// 等价于
window.globalVar = 'Hello World';
window.globalFunction = function() {
console.log('This is a global function');
};
console.log(window.globalVar); // "Hello World"
window.globalFunction(); // "This is a global function"
// 检查全局变量是否存在
if ('someVariable' in window) {
console.log('someVariable exists');
}
// 安全访问可能不存在的对象
const result = window.optionalAPI || 'API not available';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 6.2.2 窗口信息与操作
// 获取窗口尺寸信息
const windowInfo = {
// 视口尺寸(不包括滚动条)
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
// 浏览器窗口外部尺寸
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
// 屏幕相对位置
screenX: window.screenX,
screenY: window.screenY,
// 滚动位置
scrollX: window.scrollX,
scrollY: window.scrollY
};
console.log('窗口信息:', windowInfo);
// 响应窗口尺寸变化
window.addEventListener('resize', function() {
console.log(`窗口尺寸变化: ${window.innerWidth} x ${window.innerHeight}`);
// 响应式布局处理
if (window.innerWidth < 768) {
document.body.classList.add('mobile-layout');
} else {
document.body.classList.remove('mobile-layout');
}
});
// 滚动事件处理
window.addEventListener('scroll', function() {
const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
// 更新滚动进度条
document.getElementById('scrollProgress').style.width = scrollPercent + '%';
// 显示/隐藏回到顶部按钮
const backToTop = document.getElementById('backToTop');
if (window.scrollY > 300) {
backToTop.style.display = 'block';
} else {
backToTop.style.display = 'none';
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 6.2.3 定时器管理
| 需求 | API | 特点 | 避坑 |
|---|---|---|---|
| 延时执行 | setTimeout(fn, ms) | 最小延迟常被 clamped(后台标签可能 >=1000ms) | 存引用清理:const id=setTimeout(...); clearTimeout(id) |
| 周期执行 | setInterval(fn, ms) | 间隔不精确,受执行时间与事件循环影响 | 推荐用递归 setTimeout 精准控制与动态调整 |
| 帧动画 | requestAnimationFrame(fn) | ~16.6ms(60fps),页面隐藏暂停 | UI 渲染/动画首选(避免无限循环) |
| 闲时执行 | requestIdleCallback(fn) | 浏览器空闲时回调(非所有环境支持) | 低优任务,可设 timeout 兜底 |
避免:在高频 UI 更新用 setInterval 而非 requestAnimationFrame,会出现掉帧;忘记清理计时器导致页面卸载后仍执行(内存泄漏)。
// setTimeout - 单次延时执行
const timeoutId = setTimeout(() => {
console.log('3秒后执行');
}, 3000);
// 清除定时器
// clearTimeout(timeoutId);
// setInterval - 周期性执行
let counter = 0;
const intervalId = setInterval(() => {
counter++;
console.log(`定时器执行第 ${counter} 次`);
// 执行10次后停止
if (counter >= 10) {
clearInterval(intervalId);
console.log('定时器已停止');
}
}, 1000);
// 更精确的定时器替代方案
class PreciseTimer {
constructor(callback, interval) {
this.callback = callback;
this.interval = interval;
this.startTime = 0;
this.count = 0;
this.active = false;
}
start() {
this.active = true;
this.startTime = performance.now();
this.tick();
}
tick() {
if (!this.active) return;
this.count++;
const targetTime = this.startTime + (this.count * this.interval);
const currentTime = performance.now();
const delay = Math.max(0, targetTime - currentTime);
setTimeout(() => {
if (this.active) {
this.callback();
this.tick();
}
}, delay);
}
stop() {
this.active = false;
}
}
// 使用精确定时器
const preciseTimer = new PreciseTimer(() => {
console.log('精确定时器执行:', new Date().toLocaleTimeString());
}, 1000);
// preciseTimer.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 6.2.4 窗口与通信
| 场景 | API | 关键注意 |
|---|---|---|
| 弹出新窗口 | window.open(url, name, features) | 受浏览器策略/拦截;尽量基于用户主动交互触发 |
| 跨窗口通信 | otherWindow.postMessage(data, targetOrigin) | 校验 event.origin;限制可接受来源 |
| 广播同源多标签/iframe | new BroadcastChannel('channel') | 小数据广播;关闭页面需 channel.close() |
| 办公/后台同步 | Service Worker + MessageChannel | 复杂离线/推送场景 |
监听 message:
window.addEventListener('message', e => {
if (e.origin !== 'https://trusted.example') return; // 安全过滤
// 处理 e.data
});
2
3
4
数据结构嵌套可使用 structuredClone(obj) 做安全深拷贝后再传递(多数现代浏览器内建)。
# 6.3 Location 对象与导航控制
# 6.3.1 常用操作
| 需求 | 推荐方式 | 说明 |
|---|---|---|
| 获取当前完整 URL | location.href 或 new URL(location.href).href | URL 对象利于安全修改部分字段 |
| 页面跳转(保留历史) | location.assign(url) 或 location.href = url | 用户可后退 |
| 页面跳转(不保留历史) | location.replace(url) | 登录/重定向场景 |
| 刷新页面 | location.reload() | 可传 true 强制重新请求(部分浏览器) |
| 解析查询参数 | new URLSearchParams(location.search) | 避免手写正则 |
| 修改查询参数再跳转 | const u=new URL(location); u.searchParams.set('q','js'); location.href=u; | 保持其他部分不变 |
注意:避免直接拼接字符串构造 URL,使用 URL 与 URLSearchParams 自动编码;跨域跳转注意协议与端口。
# 6.3.2 URL 信息获取与解析
// 完整的 URL 信息
const locationInfo = {
href: location.href, // 完整 URL
protocol: location.protocol, // 协议 (http: 或 https:)
hostname: location.hostname, // 主机名
port: location.port, // 端口号
pathname: location.pathname, // 路径
search: location.search, // 查询字符串
hash: location.hash // 锚点
};
console.log('页面 URL 信息:', locationInfo);
// URL 参数解析工具类
class URLParams {
constructor(url = location.search) {
this.params = new URLSearchParams(url);
}
get(key) {
return this.params.get(key);
}
getAll(key) {
return this.params.getAll(key);
}
set(key, value) {
this.params.set(key, value);
return this;
}
delete(key) {
this.params.delete(key);
return this;
}
toString() {
return this.params.toString();
}
toObject() {
const obj = {};
for (const [key, value] of this.params) {
obj[key] = value;
}
return obj;
}
// 更新浏览器 URL(不刷新页面)
updateURL() {
const newURL = `${location.pathname}?${this.toString()}${location.hash}`;
history.pushState(null, '', newURL);
}
}
// 使用示例
const urlParams = new URLParams();
console.log('用户ID:', urlParams.get('userId'));
console.log('所有参数:', urlParams.toObject());
// 动态更新 URL 参数
urlParams.set('page', '2').set('size', '20').updateURL();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 6.3.3 页面导航与跳转
// 安全的页面跳转
class Navigation {
static navigate(url, newTab = false) {
if (newTab) {
// 安全地打开新标签页
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (!newWindow) {
alert('弹窗被阻止,请允许弹窗后重试');
}
} else {
// 在当前页面跳转
location.href = url;
}
}
static reload(forceReload = false) {
location.reload(forceReload);
}
static replace(url) {
location.replace(url); // 不在历史记录中留下记录
}
// 智能返回
static goBack() {
if (history.length > 1) {
history.back();
} else {
// 没有历史记录时跳转到首页
this.navigate('/');
}
}
// 确认跳转
static confirmNavigate(url, message = '确定要离开当前页面吗?') {
if (confirm(message)) {
this.navigate(url);
}
}
}
// 使用示例
document.getElementById('homeButton').addEventListener('click', () => {
Navigation.navigate('/');
});
document.getElementById('newTabButton').addEventListener('click', () => {
Navigation.navigate('https://www.example.com', true);
});
// 页面离开确认
window.addEventListener('beforeunload', function(event) {
if (hasUnsavedChanges()) {
event.preventDefault();
event.returnValue = ''; // 现代浏览器要求
return ''; // 兼容旧浏览器
}
});
function hasUnsavedChanges() {
// 检查是否有未保存的更改
return document.querySelector('.dirty-form') !== null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 6.4 History 对象与历史管理
关键方法:history.back()、history.forward()、history.go(n)、history.pushState(state, '', url)、history.replaceState(state, '', url)。
单页应用(SPA)利用 pushState/replaceState 更新地址栏不刷新页面,并监听 popstate:
window.addEventListener('popstate', e => {
// 根据 location.pathname 渲染对应视图
});
history.pushState({ view:'detail' }, '', '/detail/42');
2
3
4
实践要点:
- 保存最少必要 state(可还原视图)避免内存膨胀。
- 处理返回:滚动位置还原与焦点管理提升体验。
- SSR/预渲染场景确保服务器也能处理新路径(避免 404)。
# 6.4.1 现代历史导航
// 历史记录管理类
class HistoryManager {
static pushState(data, title, url) {
history.pushState(data, title, url);
this.fireHistoryChange('push', data, url);
}
static replaceState(data, title, url) {
history.replaceState(data, title, url);
this.fireHistoryChange('replace', data, url);
}
static back() {
history.back();
}
static forward() {
history.forward();
}
static go(delta) {
history.go(delta);
}
// 自定义历史变化事件
static fireHistoryChange(type, data, url) {
window.dispatchEvent(new CustomEvent('historychange', {
detail: { type, data, url }
}));
}
// 初始化历史监听
static init() {
window.addEventListener('popstate', (event) => {
console.log('历史状态变化:', event.state);
this.fireHistoryChange('pop', event.state, location.href);
});
window.addEventListener('historychange', (event) => {
console.log('自定义历史事件:', event.detail);
});
}
}
// 单页应用路由管理
class SimpleRouter {
constructor() {
this.routes = new Map();
this.init();
}
init() {
HistoryManager.init();
window.addEventListener('historychange', (event) => {
this.handleRoute();
});
// 处理初始路由
this.handleRoute();
}
addRoute(path, handler) {
this.routes.set(path, handler);
}
navigate(path, data = null) {
HistoryManager.pushState(data, '', path);
}
handleRoute() {
const path = location.pathname;
const handler = this.routes.get(path);
if (handler) {
handler(history.state);
} else {
console.log('路由未找到:', path);
}
}
}
// 使用示例
const router = new SimpleRouter();
router.addRoute('/', (data) => {
console.log('首页');
document.getElementById('content').innerHTML = '<h1>首页</h1>';
});
router.addRoute('/about', (data) => {
console.log('关于页面');
document.getElementById('content').innerHTML = '<h1>关于我们</h1>';
});
// 导航示例
document.getElementById('homeLink').addEventListener('click', (e) => {
e.preventDefault();
router.navigate('/');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 6.5 现代用户交互
# 6.5.1 替代传统弹窗的现代方案
// 现代提示框类
class ModernDialog {
static createDialog(content, options = {}) {
const dialog = document.createElement('div');
dialog.className = 'modern-dialog';
dialog.innerHTML = `
<div class="dialog-overlay">
<div class="dialog-content">
<div class="dialog-header">
${options.title || '提示'}
<button class="dialog-close">×</button>
</div>
<div class="dialog-body">
${content}
</div>
<div class="dialog-footer">
${this.createButtons(options.buttons)}
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
return dialog;
}
static createButtons(buttons = [{ text: '确定', action: () => {} }]) {
return buttons.map(btn =>
`<button class="dialog-btn" data-action="${btn.action.name}">${btn.text}</button>`
).join('');
}
static alert(message, title = '提示') {
return new Promise((resolve) => {
const dialog = this.createDialog(message, {
title,
buttons: [{ text: '确定', action: resolve }]
});
this.bindEvents(dialog, { '确定': resolve });
});
}
static confirm(message, title = '确认') {
return new Promise((resolve) => {
const dialog = this.createDialog(message, {
title,
buttons: [
{ text: '取消', action: () => resolve(false) },
{ text: '确定', action: () => resolve(true) }
]
});
this.bindEvents(dialog, {
'取消': () => resolve(false),
'确定': () => resolve(true)
});
});
}
static prompt(message, defaultValue = '', title = '输入') {
const inputHTML = `
<p>${message}</p>
<input type="text" class="dialog-input" value="${defaultValue}" autofocus>
`;
return new Promise((resolve) => {
const dialog = this.createDialog(inputHTML, {
title,
buttons: [
{ text: '取消', action: () => resolve(null) },
{ text: '确定', action: () => {
const input = dialog.querySelector('.dialog-input');
resolve(input.value);
}}
]
});
this.bindEvents(dialog, {
'取消': () => resolve(null),
'确定': () => {
const input = dialog.querySelector('.dialog-input');
resolve(input.value);
}
});
});
}
static bindEvents(dialog, actions) {
// 关闭按钮
dialog.querySelector('.dialog-close').addEventListener('click', () => {
document.body.removeChild(dialog);
});
// 按钮事件
dialog.querySelectorAll('.dialog-btn').forEach(btn => {
btn.addEventListener('click', () => {
const action = actions[btn.textContent];
if (action) action();
document.body.removeChild(dialog);
});
});
// ESC 键关闭
const escHandler = (e) => {
if (e.key === 'Escape') {
document.body.removeChild(dialog);
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
}
// 使用现代弹窗
async function modernInteraction() {
await ModernDialog.alert('这是一个现代化的提示框');
const confirmed = await ModernDialog.confirm('确定要删除这个项目吗?');
if (confirmed) {
const name = await ModernDialog.prompt('请输入您的姓名:', '张三');
if (name) {
console.log('用户输入的姓名:', name);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# 6.6 现代存储解决方案
# 6.6.1 Storage 概览
| 类型 | 生命周期 | 容量(约) | 作用域 | 典型用途 | 注意 |
|---|---|---|---|---|---|
| localStorage | 永久(除非手动清除) | ~5MB | 同源 | 用户设置、缓存轻量数据 | 同步 API,频繁写入阻塞;避免存敏感信息 |
| sessionStorage | 会话(标签页/窗口关闭失效) | ~5MB | 同源单标签页 | 临时状态、一次性表单缓存 | 不跨标签页共享 |
| Cookie | 可设过期 | 4KB 左右单条 | 发送到同源服务器 | 会话标识、少量跨请求状态 | 每次请求携带影响性能;加 HttpOnly/Secure 属性 |
| IndexedDB | 持久 | 上限远高于 5MB | 同源 | 大量结构化离线数据 | 异步;版本/事务处理复杂 |
建议:敏感数据(令牌)优先 HttpOnly Cookie + 服务端会话;大量结构化使用 IndexedDB;避免 localStorage 存 JWT(易被脚本窃取)。
# 6.6.2 增强的本地存储
// 增强的存储管理类
class StorageManager {
constructor(storage = localStorage) {
this.storage = storage;
}
// 设置数据(支持过期时间)
setItem(key, value, expireHours = null) {
const data = {
value,
timestamp: Date.now(),
expire: expireHours ? Date.now() + (expireHours * 60 * 60 * 1000) : null
};
try {
this.storage.setItem(key, JSON.stringify(data));
return true;
} catch (error) {
console.error('存储失败:', error);
return false;
}
}
// 获取数据(自动检查过期)
getItem(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
if (!item) return defaultValue;
const data = JSON.parse(item);
// 检查是否过期
if (data.expire && Date.now() > data.expire) {
this.removeItem(key);
return defaultValue;
}
return data.value;
} catch (error) {
console.error('读取失败:', error);
return defaultValue;
}
}
// 删除数据
removeItem(key) {
this.storage.removeItem(key);
}
// 清空所有数据
clear() {
this.storage.clear();
}
// 获取所有键
keys() {
return Object.keys(this.storage);
}
// 检查键是否存在
hasItem(key) {
return this.storage.getItem(key) !== null;
}
// 获取存储大小
getSize() {
let total = 0;
for (let key in this.storage) {
if (this.storage.hasOwnProperty(key)) {
total += this.storage[key].length + key.length;
}
}
return total;
}
// 清理过期数据
cleanExpired() {
const keys = this.keys();
keys.forEach(key => {
this.getItem(key); // 触发过期检查
});
}
}
// 使用示例
const localStore = new StorageManager(localStorage);
const sessionStore = new StorageManager(sessionStorage);
// 存储用户设置(24小时过期)
localStore.setItem('userSettings', {
theme: 'dark',
language: 'zh-CN',
notifications: true
}, 24);
// 存储临时数据
sessionStore.setItem('currentPage', 'dashboard');
// 读取数据
const settings = localStore.getItem('userSettings', {});
console.log('用户设置:', settings);
// 定期清理过期数据
setInterval(() => {
localStore.cleanExpired();
}, 60 * 60 * 1000); // 每小时清理一次
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# 6.6.3 现代存储 API
// IndexedDB 包装器(简化使用)
class IndexedDBManager {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async init(stores = []) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
stores.forEach(store => {
if (!db.objectStoreNames.contains(store.name)) {
const objectStore = db.createObjectStore(store.name, {
keyPath: store.keyPath || 'id',
autoIncrement: store.autoIncrement !== false
});
// 创建索引
if (store.indexes) {
store.indexes.forEach(index => {
objectStore.createIndex(index.name, index.keyPath, {
unique: index.unique || false
});
});
}
}
});
};
});
}
async add(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return store.add(data);
}
async get(storeName, key) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return store.get(key);
}
async getAll(storeName) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return store.getAll();
}
async update(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return store.put(data);
}
async delete(storeName, key) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return store.delete(key);
}
}
// 使用示例
const dbManager = new IndexedDBManager('MyApp', 1);
async function initDatabase() {
await dbManager.init([
{
name: 'users',
keyPath: 'id',
autoIncrement: true,
indexes: [
{ name: 'email', keyPath: 'email', unique: true },
{ name: 'name', keyPath: 'name' }
]
},
{
name: 'posts',
keyPath: 'id',
autoIncrement: true
}
]);
console.log('数据库初始化完成');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 6.7 设备与环境检测
# 6.7.1 navigator 常见能力
| 能力 | 示例 | 注意 |
|---|---|---|
| 在线状态 | navigator.onLine | 不可靠,仅表示网络栈是否在线;需实际请求验证 |
| 剪贴板 | navigator.clipboard.writeText(str) | 需 HTTPS + 用户交互上下文 |
| 地理定位 | navigator.geolocation.getCurrentPosition | 权限弹窗;回退策略与错误处理 |
| UA/平台信息 | navigator.userAgent | 只做特性检测,避免 UA 字符串分支(易碎) |
特性检测优先:
if ('serviceWorker' in navigator) { /* 注册 SW */ }
# 6.7.2 Navigator 对象应用
// 现代设备检测类
class DeviceDetector {
static getDeviceInfo() {
return {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages,
online: navigator.onLine,
cookieEnabled: navigator.cookieEnabled,
hardwareConcurrency: navigator.hardwareConcurrency,
maxTouchPoints: navigator.maxTouchPoints
};
}
static isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test(navigator.userAgent);
}
static isTablet() {
return /iPad|Android(?!.*Mobile)/i.test(navigator.userAgent);
}
static getNetworkInfo() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
return {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData
};
}
return null;
}
static async getPermissions() {
const permissions = {};
const permissionList = ['camera', 'microphone', 'geolocation', 'notifications'];
for (const permission of permissionList) {
try {
const result = await navigator.permissions.query({ name: permission });
permissions[permission] = result.state;
} catch (error) {
permissions[permission] = 'unknown';
}
}
return permissions;
}
// 地理位置获取
static getCurrentPosition(options = {}) {
const defaultOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
};
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('地理位置API不支持'));
return;
}
navigator.geolocation.getCurrentPosition(
resolve,
reject,
{ ...defaultOptions, ...options }
);
});
}
}
// 使用示例
console.log('设备信息:', DeviceDetector.getDeviceInfo());
console.log('是否移动设备:', DeviceDetector.isMobile());
console.log('网络信息:', DeviceDetector.getNetworkInfo());
// 获取权限状态
DeviceDetector.getPermissions().then(permissions => {
console.log('权限状态:', permissions);
});
// 监听网络状态变化
window.addEventListener('online', () => {
console.log('网络已连接');
});
window.addEventListener('offline', () => {
console.log('网络已断开');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
BOM 编程最佳实践
- 优先使用现代 API:避免使用 alert/confirm/prompt,使用自定义弹窗
- 合理使用存储:根据数据特性选择合适的存储方式
- 处理兼容性:检测功能支持性,提供降级方案
- 注意安全性:特别是跨域和权限相关操作
- 优化性能:避免过度监听窗口事件,使用防抖节流
- 用户体验:提供加载状态、错误处理和用户反馈
# 6.8 实际应用案例
# 6.8.1 响应式页面适配
// 响应式管理器
class ResponsiveManager {
constructor() {
this.breakpoints = {
mobile: 768,
tablet: 1024,
desktop: 1200
};
this.init();
}
init() {
this.updateLayout();
window.addEventListener('resize', this.debounce(() => {
this.updateLayout();
}, 250));
}
updateLayout() {
const width = window.innerWidth;
const body = document.body;
// 移除所有断点类
body.classList.remove('mobile', 'tablet', 'desktop');
// 添加当前断点类
if (width < this.breakpoints.mobile) {
body.classList.add('mobile');
} else if (width < this.breakpoints.tablet) {
body.classList.add('tablet');
} else {
body.classList.add('desktop');
}
// 触发自定义事件
window.dispatchEvent(new CustomEvent('breakpointchange', {
detail: { width, breakpoint: this.getCurrentBreakpoint() }
}));
}
getCurrentBreakpoint() {
const width = window.innerWidth;
if (width < this.breakpoints.mobile) return 'mobile';
if (width < this.breakpoints.tablet) return 'tablet';
return 'desktop';
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// 页面性能监控
class PerformanceMonitor {
static init() {
// 页面加载性能
window.addEventListener('load', () => {
setTimeout(() => {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('页面加载性能:', {
DNS查询: perfData.domainLookupEnd - perfData.domainLookupStart,
TCP连接: perfData.connectEnd - perfData.connectStart,
首字节时间: perfData.responseStart - perfData.navigationStart,
DOM解析: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
页面完全加载: perfData.loadEventEnd - perfData.navigationStart
});
}, 0);
});
// 资源加载监控
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 1000) {
console.warn(`慢资源警告: ${entry.name} 耗时 ${entry.duration.toFixed(2)}ms`);
}
});
});
observer.observe({ entryTypes: ['resource'] });
}
}
// 初始化应用
const responsiveManager = new ResponsiveManager();
PerformanceMonitor.init();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 6.9 常见易错与安全要点
| 场景 | 问题 | 解决 |
|---|---|---|
| 直接字符串拼接查询参数 | 编码缺失、XSS 风险 | 使用 URLSearchParams 或 new URL() 修改参数 |
| 未校验 postMessage 来源 | 数据泄漏 | 校验 event.origin 与设置 targetOrigin 精确值(不要 *) |
| 使用 setInterval 做动画 | 掉帧/不均匀 | 改用 requestAnimationFrame |
| localStorage 存大量 JSON | 主线程阻塞 | 批量写入拆分 + 尽量使用 IndexedDB |
| 盲目 UA 判断 | 分支错误、维护成本高 | 特性检测(如 'clipboard' in navigator) |
| 未清理计时器/频道 | 内存泄漏 | 在组件卸载/页面关闭前 clearTimeout/clearInterval/close() |
| HTTP 非安全上下文使用敏感 API | 权限失败或报错 | 确保 HTTPS + 有用户交互触发 |
# 6.10 推荐实践清单
- URL 操作统一使用
URL与URLSearchParams,避免手写解析。 - 路由变更保持滚动位置与焦点管理,提升可访问性。
- 高频 UI 更新使用
requestAnimationFrame;复杂调度用单一调度器集中管理计时器。 - 跨窗口通信严格限制来源;结构化数据传递前可
structuredClone副本。 - 存储分层:偏持久(localStorage) vs 临时(sessionStorage) vs 会话鉴权(Cookie) vs 大量结构化(IndexedDB)。
- 特性检测替代 UA/平台分支,构建渐进增强策略。
- 避免在同步加载阶段执行大量 storage 读写(影响首屏)。
# 七、文档对象模型(DOM)编程
# 7.1 DOM 核心概念与现代理解
# 7.1.1 DOM 概述
DOM(Document Object Model,文档对象模型)是 HTML 和 XML 文档的编程接口。它将整个页面表示为节点树,使 JavaScript 能够动态访问和修改文档的内容、结构和样式。
DOM 的核心特点:
- 树形结构:文档被解析为节点树,每个 HTML 元素都是一个节点
- 动态性:可以实时修改页面内容而无需刷新
- 标准化:遵循 W3C 标准,跨浏览器兼容
- 事件驱动:支持用户交互和动态响应
# 7.1.2 DOM 节点类型详解
// DOM 节点类型常量
const NODE_TYPES = {
ELEMENT_NODE: 1, // 元素节点 <div>, <p> 等
ATTRIBUTE_NODE: 2, // 属性节点(已废弃)
TEXT_NODE: 3, // 文本节点
COMMENT_NODE: 8, // 注释节点
DOCUMENT_NODE: 9, // 文档节点
DOCUMENT_FRAGMENT_NODE: 11 // 文档片段节点
};
// 节点分析工具
class NodeAnalyzer {
// 分析单个节点的详细信息
static analyzeNode(node) {
const info = {
nodeName: node.nodeName,
nodeType: node.nodeType,
nodeValue: node.nodeValue,
textContent: node.textContent?.slice(0, 50)
};
switch(node.nodeType) {
case NODE_TYPES.ELEMENT_NODE:
info.tagName = node.tagName;
info.className = node.className;
info.id = node.id;
info.attributes = [...node.attributes].map(attr => ({
name: attr.name,
value: attr.value
}));
break;
case NODE_TYPES.TEXT_NODE:
info.isWhitespace = /^\s*$/.test(node.textContent);
break;
case NODE_TYPES.COMMENT_NODE:
info.comment = node.textContent;
break;
}
return info;
}
// 递归遍历DOM树,对每个节点执行回调函数
static traverseNode(node, callback, depth = 0) {
callback(node, depth);
for (const child of node.childNodes) {
this.traverseNode(child, callback, depth + 1);
}
}
// 获取节点树结构信息
static getNodeTreeInfo(rootNode) {
const nodeInfo = [];
this.traverseNode(rootNode, (node, depth) => {
const analysis = this.analyzeNode(node);
nodeInfo.push({
depth,
...analysis
});
});
return nodeInfo;
}
// 统计节点类型数量
static countNodeTypes(rootNode) {
const counts = {};
this.traverseNode(rootNode, (node) => {
const typeName = this.getNodeTypeName(node.nodeType);
counts[typeName] = (counts[typeName] || 0) + 1;
});
return counts;
}
// 获取节点类型名称
static getNodeTypeName(nodeType) {
for (const [name, type] of Object.entries(NODE_TYPES)) {
if (type === nodeType) {
return name;
}
}
return 'UNKNOWN';
}
// 查找特定类型的节点
static findNodesByType(rootNode, targetType) {
const foundNodes = [];
this.traverseNode(rootNode, (node) => {
if (node.nodeType === targetType) {
foundNodes.push(node);
}
});
return foundNodes;
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', function() {
// 获取要分析的根节点
const rootElement = document.getElementById('complex-example');
// 1. 分析单个节点
console.log('=== 单个节点分析 ===');
const singleNodeInfo = NodeAnalyzer.analyzeNode(rootElement);
console.log('根节点信息:', singleNodeInfo);
// 2. 获取节点树结构
console.log('\n=== 节点树结构 ===');
const nodeTree = NodeAnalyzer.getNodeTreeInfo(rootElement);
nodeTree.forEach(node => {
const indent = ' '.repeat(node.depth);
const nodeName = node.nodeType === NODE_TYPES.TEXT_NODE ?
`"${node.textContent.trim().slice(0, 20)}"` : node.nodeName;
console.log(`${indent}${nodeName} (类型: ${NodeAnalyzer.getNodeTypeName(node.nodeType)})`);
});
// 3. 统计节点类型
console.log('\n=== 节点类型统计 ===');
const nodeCounts = NodeAnalyzer.countNodeTypes(rootElement);
Object.entries(nodeCounts).forEach(([type, count]) => {
console.log(`${type}: ${count}个`);
});
// 4. 查找特定类型节点
console.log('\n=== 查找文本节点 ===');
const textNodes = NodeAnalyzer.findNodesByType(rootElement, NODE_TYPES.TEXT_NODE);
textNodes.forEach((node, index) => {
const content = node.textContent.trim();
if (content) { // 忽略空白文本节点
console.log(`文本节点${index + 1}: "${content}"`);
}
});
// 5. 查找注释节点
console.log('\n=== 查找注释节点 ===');
const commentNodes = NodeAnalyzer.findNodesByType(rootElement, NODE_TYPES.COMMENT_NODE);
commentNodes.forEach((node, index) => {
console.log(`注释节点${index + 1}: "${node.nodeValue}"`);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
复杂 HTML 示例:
<div id="complex-example" class="container">
<header class="header">
<h1>标题</h1>
<!-- 这是头部注释 -->
</header>
<main class="content">
<section>
<h2>小标题</h2>
<p>这是第一段文本。</p>
<p>这是第二段文本。</p>
<!-- 这是内容注释 -->
</section>
<aside class="sidebar">
<ul>
<li>列表项1</li>
<li>列表项2</li>
</ul>
</aside>
</main>
<footer class="footer">
<p>页脚文本</p>
<!-- 这是页脚注释 -->
</footer>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NodeAnalyzer 类在实际开发中有多种应用场景:
- 调试 DOM 结构:快速了解复杂页面的节点结构
- 性能优化:统计节点数量,识别可能的性能问题
- 内容提取:从复杂页面中提取特定类型的节点
- 自动化测试:验证 DOM 结构的正确性
- 内容分析:分析页面内容的组成和结构
# 7.2 获取页面元素的几种方式
# 7.2.1 在整个文档范围内查找元素结点
| 功能 | API | 返回值 |
|---|---|---|
| 根据 id 值查询 | document.getElementById(“id 值”) | 一个具体的元素节点对象 |
| 根据标签名查询 | document.getElementsByTagName(“标签名”) | 元素节点数组 |
| 根据 name 属性值查询 | document.getElementsByName(“name 值”) | 元素节点数组 |
| 根据类名查询 | document.getElementsByClassName("类名") | 元素节点数组 |
# 7.2.2 在具体元素节点范围内查找子节点
| 功能 | API | 返回值 |
|---|---|---|
| 查找子标签 | element.children | 返回子标签数组 |
| 查找第一个子标签 | element.firstElementChild | 第一个子元素节点对象 |
| 查找最后一个子标签 | element.lastElementChild | 最后一个子元素节点对象 |
# 7.2.3 查找指定子元素节点的父节点
| 功能 | API | 返回值 |
|---|---|---|
| 查找指定元素节点的父标签 | element.parentElement | 父元素节点对象 |
# 7.2.4 查找指定元素节点的兄弟节点
| 功能 | API | 返回值 |
|---|---|---|
| 查找前一个兄弟标签 | node.previousElementSibling | 前一个兄弟元素节点对象 |
| 查找后一个兄弟标签 | node.nextElementSibling | 后一个兄弟元素节点对象 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
/*
1 获得document dom树
window.document
2 从document中获取要操作的元素
1. 直接获取
var el1 =document.getElementById("username") // 根据元素的id值获取页面上唯一的一个元素
var els =document.getElementsByTagName("input") // 根据元素的标签名获取多个同名元素
var els =document.getElementsByName("aaa") // 根据元素的name属性值获得多个元素
var els =document.getElementsByClassName("a") // 根据元素的class属性值获得多个元素
2. 间接获取
var cs=div01.children // 通过父元素获取全部的子元素
var firstChild =div01.firstElementChild // 通过父元素获取第一个子元素
var lastChild = div01.lastElementChild // 通过父元素获取最后一个子元素
var parent = pinput.parentElement // 通过子元素获取父元素
var pElement = pinput.previousElementSibling // 获取前面的第一个元素
var nElement = pinput.nextElementSibling // 获取后面的第一个元素
3 对元素进行操作
1. 操作元素的属性
2. 操作元素的样式
3. 操作元素的文本
4. 增删元素
*/
function fun1(){
//1 获得document
//2 通过document获得元素
var el1 =document.getElementById("username") // 根据元素的id值获取页面上唯一的一个元素
console.log(el1)
}
function fun2(){
var els =document.getElementsByTagName("input") // 根据元素的标签名获取多个同名元素
for(var i = 0 ;i<els.length;i++){
console.log(els[i])
}
}
function fun3(){
var els =document.getElementsByName("aaa") // 根据元素的name属性值获得多个元素
console.log(els)
for(var i =0;i< els.length;i++){
console.log(els[i])
}
}
function fun4(){
var els =document.getElementsByClassName("a") // 根据元素的class属性值获得多个元素
for(var i =0;i< els.length;i++){
console.log(els[i])
}
}
function fun5(){
// 先获取父元素
var div01 = document.getElementById("div01")
// 获取所有子元素
var cs=div01.children // 通过父元素获取全部的子元素
for(var i =0;i< cs.length;i++){
console.log(cs[i])
}
console.log(div01.firstElementChild) // 通过父元素获取第一个子元素
console.log(div01.lastElementChild) // 通过父元素获取最后一个子元素
}
function fun6(){
// 获取子元素
var pinput =document.getElementById("password")
console.log(pinput.parentElement) // 通过子元素获取父元素
}
function fun7(){
// 获取子元素
var pinput =document.getElementById("password")
console.log(pinput.previousElementSibling) // 获取前面的第一个元素
console.log(pinput.nextElementSibling) // 获取后面的第一个元素
}
</script>
</head>
<body>
<div id="div01">
<input type="text" class="a" id="username" name="aaa"/>
<input type="text" class="b" id="password" name="aaa"/>
<input type="text" class="a" id="email"/>
<input type="text" class="b" id="address"/>
</div>
<input type="text" class="a"/><br>
<hr>
<input type="button" value="通过父元素获取子元素" onclick="fun5()" id="btn05"/>
<input type="button" value="通过子元素获取父元素" onclick="fun6()" id="btn06"/>
<input type="button" value="通过当前元素获取兄弟元素" onclick="fun7()" id="btn07"/>
<hr>
<input type="button" value="根据id获取指定元素" onclick="fun1()" id="btn01"/>
<input type="button" value="根据标签名获取多个元素" onclick="fun2()" id="btn02"/>
<input type="button" value="根据name属性值获取多个元素" onclick="fun3()" id="btn03"/>
<input type="button" value="根据class属性值获得多个元素" onclick="fun4()" id="btn04"/>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 7.3 操作元素属性值
# 7.3.1 属性操作
| 需求 | 操作方式 |
|---|---|
| 读取属性值 | 元素对象.属性名 |
| 修改属性值 | 元素对象.属性名=新的属性值 |
# 7.3.2 内部文本操作
| 需求 | 操作方式 |
|---|---|
| 获取或者设置标签体的文本内容 | element.innerText |
| 获取或者设置标签体的内容 | element.innerHTML |
# 7.3.3 格式操作
| 需求 | 操作方式 |
|---|---|
| 设置标签体的格式 | element.style.样式名="" |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
/*
1 获得document dom树
window.document
2 从document中获取要操作的元素
1. 直接获取
var el1 =document.getElementById("username") // 根据元素的id值获取页面上唯一的一个元素
var els =document.getElementsByTagName("input") // 根据元素的标签名获取多个同名元素
var els =document.getElementsByName("aaa") // 根据元素的name属性值获得多个元素
var els =document.getElementsByClassName("a") // 根据元素的class属性值获得多个元素
2. 间接获取
var cs=div01.children // 通过父元素获取全部的子元素
var firstChild =div01.firstElementChild // 通过父元素获取第一个子元素
var lastChild = div01.lastElementChild // 通过父元素获取最后一个子元素
var parent = pinput.parentElement // 通过子元素获取父元素
var pElement = pinput.previousElementSibling // 获取前面的第一个元素
var nElement = pinput.nextElementSibling // 获取后面的第一个元素
3 对元素进行操作
1. 操作元素的属性 元素名.属性名=""
2. 操作元素的样式 元素名.style.样式名="" 样式名"-" 要进行驼峰转换
3. 操作元素的文本 元素名.innerText 只识别文本
元素名.innerHTML 同时可以识别html代码
4. 增删元素
*/
function changeAttribute(){
var in1 =document.getElementById("in1")
// 语法 元素.属性名=""
// 获得属性值
console.log(in1.type)
console.log(in1.value)
// 修改属性值
in1.type="button"
in1.value="嗨"
}
function changeStyle(){
var in1 =document.getElementById("in1")
// 语法 元素.style.样式名="" 原始样式名中的"-"符号 要转换驼峰式 background-color > backgroundColor
in1.style.color="green"
in1.style.borderRadius="5px"
}
function changeText(){
var div01 =document.getElementById("div01")
/*
语法 元素名.innerText 只识别文本
元素名.innerHTML 同时可以识别html代码
*/
console.log(div01.innerText)
div01.innerHTML="<h1>嗨</h1>"
}
</script>
<style>
#in1{
color: red;
}
</style>
</head>
<body>
<input id="in1" type="text" value="hello">
<div id="div01">
hello
</div>
<hr>
<button onclick="changeAttribute()">操作属性</button>
<button onclick="changeStyle()">操作样式</button>
<button onclick="changeText()">操作文本</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 7.4 增删元素
# 7.4.1 对页面的元素进行增删操作
| API | 功能 |
|---|---|
| document.createElement(“标签名”) | 创建元素节点并返回,但不会自动添加到文档中 |
| document.createTextNode(“文本值”) | 创建文本节点并返回,但不会自动添加到文档中 |
| element.appendChild(ele) | 将 ele 添加到 element 所有子节点后面 |
| parentEle.insertBefore(newEle,targetEle) | 将 newEle 插入到 targetEle 前面 |
| parentEle.replaceChild(newEle, oldEle) | 用新节点替换原有的旧子节点 |
| element.remove() | 删除某个标签 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
/*
1 获得document dom树
window.document
2 从document中获取要操作的元素
1. 直接获取
var el1 =document.getElementById("username") // 根据元素的id值获取页面上唯一的一个元素
var els =document.getElementsByTagName("input") // 根据元素的标签名获取多个同名元素
var els =document.getElementsByName("aaa") // 根据元素的name属性值获得多个元素
var els =document.getElementsByClassName("a") // 根据元素的class属性值获得多个元素
2. 间接获取
var cs=div01.children // 通过父元素获取全部的子元素
var firstChild =div01.firstElementChild // 通过父元素获取第一个子元素
var lastChild = div01.lastElementChild // 通过父元素获取最后一个子元素
var parent = pinput.parentElement // 通过子元素获取父元素
var pElement = pinput.previousElementSibling // 获取前面的第一个元素
var nElement = pinput.nextElementSibling // 获取后面的第一个元素
3 对元素进行操作
1. 操作元素的属性 元素名.属性名=""
2. 操作元素的样式 元素名.style.样式名="" 样式名"-" 要进行驼峰转换
3. 操作元素的文本 元素名.innerText 只识别文本
元素名.innerHTML 同时可以识别html代码
4. 增删元素
var element =document.createElement("元素名") // 创建元素
父元素.appendChild(子元素) // 在父元素中追加子元素
父元素.insertBefore(新元素,参照元素) // 在某个元素前增加元素
父元素.replaceChild(新元素,被替换的元素) // 用新的元素替换某个子子元素
元素.remove() // 删除当前元素
*/
function addCs(){
// 创建一个新的元素
// 创建元素
var csli =document.createElement("li") // <li></li>
// 设置子元素的属性和文本 <li id="cs">长沙</li>
csli.id="cs"
csli.innerText="长沙"
// 将子元素放入父元素中
var cityul =document.getElementById("city")
// 在父元素中追加子元素
cityul.appendChild(csli)
}
function addCsBeforeSz(){
// 创建一个新的元素
// 创建元素
var csli =document.createElement("li") // <li></li>
// 设置子元素的属性和文本 <li id="cs">长沙</li>
csli.id="cs"
csli.innerText="长沙"
// 将子元素放入父元素中
var cityul =document.getElementById("city")
// 在父元素中追加子元素
//cityul.insertBefore(新元素,参照元素)
var szli =document.getElementById("sz")
cityul.insertBefore(csli,szli)
}
function replaceSz(){
// 创建一个新的元素
// 创建元素
var csli =document.createElement("li") // <li></li>
// 设置子元素的属性和文本 <li id="cs">长沙</li>
csli.id="cs"
csli.innerText="长沙"
// 将子元素放入父元素中
var cityul =document.getElementById("city")
// 在父元素中追加子元素
//cityul.replaceChild(新元素,被替换的元素)
var szli =document.getElementById("sz")
cityul.replaceChild(csli,szli)
}
function removeSz(){
var szli =document.getElementById("sz")
// 哪个元素调用了remove该元素就会从dom树中移除
szli.remove()
}
function clearCity(){
var cityul =document.getElementById("city")
/* var fc =cityul.firstChild
while(fc != null ){
fc.remove()
fc =cityul.firstChild
} */
cityul.innerHTML=""
//cityul.remove()
}
</script>
</head>
<body>
<ul id="city">
<li id="bj">北京</li>
<li id="sh">上海</li>
<li id="sz">深圳</li>
<li id="gz">广州</li>
</ul>
<hr>
<!-- 目标1 在城市列表的最后添加一个子标签 <li id="cs">长沙</li> -->
<button onclick="addCs()">增加长沙</button>
<!-- 目标2 在城市列表的深圳前添加一个子标签 <li id="cs">长沙</li> -->
<button onclick="addCsBeforeSz()">在深圳前插入长沙</button>
<!-- 目标3 将城市列表的深圳替换为 <li id="cs">长沙</li> -->
<button onclick="replaceSz()">替换深圳</button>
<!-- 目标4 城市列表中删除深圳 -->
<button onclick="removeSz()">删除深圳</button>
<!-- 目标5 清空城市列表 -->
<button onclick="clearCity()">清空</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 八、正则表达式
# 8.1 正则表达式简介
正则表达式是描述字符模式的对象。正则表达式用于对字符串模式匹配及检索替换,是对字符串执行模式匹配的强大工具。
- 语法
var patt=new RegExp(pattern,modifiers);
或者更简单的方式:
var patt=/pattern/modifiers;
2
3
修饰符
| 修饰符 | 描述 |
|---|---|
| i (opens new window) | 执行对大小写不敏感的匹配。 |
| g (opens new window) | 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。 |
| m | 执行多行匹配。 |
方括号
| 表达式 | 描述 |
|---|---|
| [abc] (opens new window) | 查找方括号之间的任何字符。 |
| [^abc] (opens new window) | 查找任何不在方括号之间的字符。 |
| [0-9] | 查找任何从 0 至 9 的数字。 |
| [a-z] | 查找任何从小写 a 到小写 z 的字符。 |
| [A-Z] | 查找任何从大写 A 到大写 Z 的字符。 |
| [A-z] | 查找任何从大写 A 到小写 z 的字符。 |
| [adgk] | 查找给定集合内的任何字符。 |
| [^adgk] | 查找给定集合外的任何字符。 |
(red | blue |green) | 查找任何指定的选项。 |
元字符
| 元字符 | 描述 |
|---|---|
| . (opens new window) | 查找单个字符,除了换行和行结束符。 |
| \w (opens new window) | 查找数字、字母及下划线。 |
| \W (opens new window) | 查找非单词字符。 |
| \d (opens new window) | 查找数字。 |
| \D (opens new window) | 查找非数字字符。 |
| \s (opens new window) | 查找空白字符。 |
| \S (opens new window) | 查找非空白字符。 |
| \b (opens new window) | 匹配单词边界。 |
| \B (opens new window) | 匹配非单词边界。 |
| \0 | 查找 NULL 字符。 |
| \n (opens new window) | 查找换行符。 |
| \f | 查找换页符。 |
| \r | 查找回车符。 |
| \t | 查找制表符。 |
| \v | 查找垂直制表符。 |
| \xxx (opens new window) | 查找以八进制数 xxx 规定的字符。 |
| \xdd (opens new window) | 查找以十六进制数 dd 规定的字符。 |
| \uxxxx (opens new window) | 查找以十六进制数 xxxx 规定的 Unicode 字符。 |
量词
| 量词 | 描述 |
|---|---|
| n+ (opens new window) | 匹配任何包含至少一个 n 的字符串。例如,/a+/ 匹配 "candy" 中的 "a","caaaaaaandy" 中所有的 "a"。 |
| n* (opens new window) | 匹配任何包含零个或多个 n 的字符串。例如,/bo*/ 匹配 "A ghost booooed" 中的 "boooo","A bird warbled" 中的 "b",但是不匹配 "A goat grunted"。 |
| n? (opens new window) | 匹配任何包含零个或一个 n 的字符串。例如,/e?le?/ 匹配 "angel" 中的 "el","angle" 中的 "le"。 |
| n{X} (opens new window) | 匹配包含 X 个 n 的序列的字符串。例如,/a{2}/ 不匹配 "candy," 中的 "a",但是匹配 "caandy," 中的两个 "a",且匹配 "caaandy." 中的前两个 "a"。 |
| n{X,} (opens new window) | X 是一个正整数。前面的模式 n 连续出现至少 X 次时匹配。例如,/a{2,}/ 不匹配 "candy" 中的 "a",但是匹配 "caandy" 和 "caaaaaaandy." 中所有的 "a"。 |
| n{X,Y} (opens new window) | X 和 Y 为正整数。前面的模式 n 连续出现至少 X 次,至多 Y 次时匹配。例如,/a{1,3}/ 不匹配 "cndy",匹配 "candy," 中的 "a","caandy," 中的两个 "a",匹配 "caaaaaaandy" 中的前面三个 "a"。注意,当匹配 "caaaaaaandy" 时,即使原始字符串拥有更多的 "a",匹配项也是 "aaa"。 |
| n$ (opens new window) | 匹配任何结尾为 n 的字符串。 |
| ^n (opens new window) | 匹配任何开头为 n 的字符串。 |
| ?=n (opens new window) | 匹配任何其后紧接指定字符串 n 的字符串。 |
| ?!n (opens new window) | 匹配任何其后没有紧接指定字符串 n 的字符串。 |
RegExp 对象方法
| 方法 | 描述 |
|---|---|
| compile (opens new window) | 在 1.5 版本中已废弃。 编译正则表达式。 |
| exec (opens new window) | 检索字符串中指定的值。返回找到的值,并确定其位置。 |
| test (opens new window) | 检索字符串中指定的值。返回 true 或 false。 |
| toString (opens new window) | 返回正则表达式的字符串。 |
支持正则的 String 的方法
| 方法 | 描述 |
|---|---|
| search (opens new window) | 检索与正则表达式相匹配的值。 |
| match (opens new window) | 找到一个或多个正则表达式的匹配。 |
| replace (opens new window) | 替换与正则表达式匹配的子串。 |
| split (opens new window) | 把字符串分割为字符串数组。 |
# 8.2 正则表达式体验
# 8.2.1 验证
注意:这里是使用正则表达式对象来调用方法。
// 创建一个最简单的正则表达式对象
var reg = /o/;
// 创建一个字符串对象作为目标字符串
var str = 'Hello World!';
// 调用正则表达式对象的test()方法验证目标字符串是否满足我们指定的这个模式,返回结果true
console.log("/o/.test('Hello World!')="+reg.test(str));
2
3
4
5
6
# 8.2.2 匹配
// 创建一个最简单的正则表达式对象
var reg = /o/;
// 创建一个字符串对象作为目标字符串
var str = 'Hello World!';
// 在目标字符串中查找匹配的字符,返回匹配结果组成的数组
var resultArr = str.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);
// 数组内容是o
console.log("resultArr[0]="+resultArr[0]);
2
3
4
5
6
7
8
9
10
11
# 8.2.3 替换
注意:这里是使用字符串对象来调用方法。
// 创建一个最简单的正则表达式对象
var reg = /o/;
// 创建一个字符串对象作为目标字符串
var str = 'Hello World!';
var newStr = str.replace(reg,'@');
// 只有第一个o被替换了,说明我们这个正则表达式只能匹配第一个满足的字符串
console.log("str.replace(reg)="+newStr);//Hell@ World!
// 原字符串并没有变化,只是返回了一个新字符串
console.log("str="+str);//str=Hello World!
2
3
4
5
6
7
8
9
10
# 8.2.4 全文查找
如果不使用 g 对正则表达式对象进行修饰,则使用正则表达式进行查找时,仅返回第一个匹配;使用 g 后,返回所有匹配。
// 目标字符串
var targetStr = 'Hello World!';
// 没有使用全局匹配的正则表达式
var reg = /[A-Z]/;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);
// 遍历数组,发现只能得到'H'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
2
3
4
5
6
7
8
9
10
11
12
13
对比
// 目标字符串
var targetStr = 'Hello World!';
// 使用了全局匹配的正则表达式
var reg = /[A-Z]/g;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为2
console.log("resultArr.length="+resultArr.length);
// 遍历数组,发现可以获取到“H”和“W”
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
2
3
4
5
6
7
8
9
10
11
12
# 8.2.5 忽略大小写
//目标字符串
var targetStr = 'Hello WORLD!';
//没有使用忽略大小写的正则表达式
var reg = /o/g;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为1
console.log("resultArr.length="+resultArr.length);
//遍历数组,仅得到'o'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
2
3
4
5
6
7
8
9
10
11
12
13
对比
//目标字符串
var targetStr = 'Hello WORLD!';
//使用了忽略大小写的正则表达式
var reg = /o/gi;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为2
console.log("resultArr.length="+resultArr.length);
//遍历数组,得到'o'和'O'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
2
3
4
5
6
7
8
9
10
11
12
# 8.2.6 元字符使用
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java开头
var reg = /^Java/g;
console.log('reg.test(str01)='+reg.test(str01)); // false
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // true
2
3
4
5
6
7
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java结尾
var reg = /Java$/g;
console.log('reg.test(str01)='+reg.test(str01)); // true
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // false
2
3
4
5
6
7
# 8.2.7 字符集合的使用
//n位数字的正则
var targetStr="123456789";
var reg=/^[0-9]{0,}$/;
//或者 : var reg=/^\d*$/;
var b = reg.test(targetStr);//true
2
3
4
5
//数字+字母+下划线,6-16位
var targetStr="HelloWorld";
var reg=/^[a-z0-9A-Z_]{6,16}$/;
var b = reg.test(targetStr);//true
2
3
4
# 8.2.8 常用正则表达式
| 需求 | 正则表达式 |
|---|---|
| 用户名 | /^[a-zA-Z][a-zA-Z-0-9]{5,9}$/ |
| 密码 | /^[a-zA-Z0-9_-@#&*]{6,12}$/ |
| 前后空格 | /^\s+ |\s+$/g |
| 电子邮箱 | /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,}$/ |
# 8.3 编写策略与误区
| 场景 | 错误示例 | 问题 | 改进 |
|---|---|---|---|
| 贪婪匹配过度 | /.+<\/div>/ | 回溯过多/跨越意外区域 | 使用惰性 .+? 或边界限定 [^<]+ |
| 过度使用分支 | /(cat | dog | pig |cow|horse)/ | 模式可读性差 | 转为数据驱动:animals.some(a=>str.includes(a)) |
| 直接验证 URL | 巨型正则 | 维护困难且仍有漏网 | 使用 try { new URL(str) } 基础校验 + 额外规则 |
| 邮箱正则过度精细 | RFC 全量匹配 | 性能差/复杂 | 简化常用格式 + 后端二次验证 |
| 未加长度限制 | /^\d+$/ | ReDoS 风险(超长输入) | 预先截断或 /^\d{1,20}$/ |
| 忽略 Unicode | /^[A-Za-z]+$/ | 无法支持中文、重音字符 | /^\p{L}+$/u 语言字符类别 |
原则:先用普通字符串/内置方法预过滤(长度/前缀),再用简洁正则;超复杂协议格式交给专门库。
# 8.4 常用模式速查
| 需求 | 正则 | 说明 | 注意 |
|---|---|---|---|
| 去除字符串首尾空白 | /^\s+ | \s+$/g 替换为空 | 双 replace 或使用 trim() | 优先内置 trim() |
| 简化连续空白为单空格 | /\s+/g | 标准化文本 | 不处理换行可用 [ ]+ |
| 纯数字(1-20 位) | /^\d{1,20}$/ | 限制长度防超长 | 超长先截断再测 |
| 用户名(字母数字下划线 5-16) | /^[A-Za-z0-9_]{5,16}$/ | 简单社交/登录名 | 不允许开头数字可用 ^[A-Za-z][A-Za-z0-9_]{4,15}$ |
| 强密码(至少 1 大小写 1 数字 1 特殊,8-32) | /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[~!@#$%^&*])[A-Za-z\d~!@#$%^&*]{8,32}$/ | 前瞻组合 | 仍需黑名单策略 |
| 中国大陆手机号(简单) | /^1[3-9]\d{9}$/ | 不区分运营商细则 | 号段更新需维护 |
| 邮箱(常用) | /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ | 简化版本 | 复杂邮箱交给后端 |
| IPv4 | /^(?:25[0-5] | 2[0-4]\d | 1\d{2} |[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/ | 每段 0-255 | 不包含前导零规则细化可再加条件 |
| 日期(YYYY-MM-DD 基础) | /^(\d{4})-(0[1-9] | 1[0-2])-(0[1-9] | [12]\d |3[01])$/ | 格式校验 | 闰年/月份天数需逻辑验证 |
| 去除脚本标签 | /<\/?script[^>]*>/gi | 初步剥离 | 不够安全:应 DOM 解析或白名单过滤 |
| 中文姓名(2-10 汉字) | /^[\u4e00-\u9fa5]{2,10}$/ | 基础匹配 | 少数民族/复姓扩展需定制 |
| Unicode 单词 | /^\p{L}+[\p{L}\p{Mn}\p{Pd}'’]*$/u | 包含重音/连字符 | 需 u 标志 |
| 十六进制色值 | /^#(?:[0-9A-Fa-f]{3} | [0-9A-Fa-f]{6})$/ | 3 或 6 位 | 不含 8 位透明度 |
| 简单 URL (http/https) | /^(https?:\/\/)[\w.-]+(?:\:[0-9]{2,5})?(?:\/[\w./%-]*)?$/ | 基础结构 | 复杂场景用 new URL() |
# 8.5 命名捕获与分组解析
const re = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
const m = '2025-10-21'.match(re);
if(m){ console.log(m.groups.year, m.groups.month, m.groups.day); }
2
3
非捕获组减少不必要的分组:/(?:http|https):\/\//。
前瞻示例:强密码前瞻组合(见上)避免回溯复杂嵌套。
# 8.6 正则替换与清洗
多步清洗示例:
function normalizeInput(str){
return str
.trim() // 去首尾空白
.replace(/\s+/g,' ') // 合并连续空白
.replace(/[\u200B\uFEFF]/g,''); // 去零宽字符
}
2
3
4
5
6
批量提取:
const tagRe = /#(\w+)/g;
const tags = [...'今天学习 #JS 和 #regex'.matchAll(tagRe)].map(m=>m[1]);
2
# 8.7 防止 ReDoS(正则拒绝服务)
ReDoS 原因:灾难性回溯(如 /(a+)+$/ 匹配超长 aaaaab)。
防护策略:
- 限制输入长度(前置裁剪)。
- 避免嵌套可变量词:
(.*)+、(\w+)*。 - 使用原子组 / possessive 量词(JS 暂无原生 possessive,改用结构限制)。
- 明确字符类范围:
[^\n]{0,200}而不是.*。 - 分段匹配:先切片再对局部执行复杂正则。
风险示例:
// 灾难性回溯示例(勿用在不受控长输入)
const bad = /(a+)+$/;
2
改进:
const safe = /^a{1,100}$/; // 上限限制
# 8.8 正则调试助手
function testRegex(re, str){
const matches = [...str.matchAll(re)];
return {
pattern: re.toString(),
count: matches.length,
items: matches.map(m=>({ match:m[0], index:m.index, groups:m.groups || {} }))
};
}
// 使用:testRegex(/\b\w+\b/g, 'Hello JS 正则');
2
3
4
5
6
7
8
9
浏览器推荐使用开发者工具 / 在线工具(regex101)查看回溯与分组;加 u 标志处理 Unicode;复杂表达式分行:
const emailRe = new RegExp([
'^',
'[A-Za-z0-9._%+-]+', // local part
'@',
'[A-Za-z0-9.-]+',
'\\.[A-Za-z]{2,}$'
].join('')); // 组合构造避免多行字面量转义困扰
2
3
4
5
6
7
# 8.9 与字符串方法的取舍
| 需求 | 优先方法 | 原因 | 正则替代何时使用 |
|---|---|---|---|
| 前缀/后缀判断 | startsWith/endsWith | 语义清晰/性能好 | 多条件或需忽略大小写时使用 ^foo+i |
| 包含子串 | includes | 简单存在性 | 复杂模式(边界、多字符类) |
| 简单替换 | replaceAll('a','b') | 清晰 | 条件/分组替换 (带捕获) |
| 拆分逗号列表 | split(',') | 固定分隔符 | 多种分隔符或忽略空白:/\s*,\s*/ |
| 数字格式判断 | Number()/isFinite() | 转换即验证 | 精确模式(前导零、有符号等) |
# 8.10 常见易错与最佳实践
| 问题 | 说明 | 建议 |
|---|---|---|
使用 [A-z] | 包含非字母字符(中间 ASCII) | 用 [A-Za-z] 或 \p{L}+u |
忽略 u 标志 | Unicode 分段错误 | 处理多语言时加 u 及 \p{…} |
| 贪婪量词跨边界 | .* 吃掉全部 | 使用惰性 .*? 或显式类 |
| 验证 URL 用巨型模式 | 维护困难 | 使用 new URL() + 轻量正则补充 |
| 超长输入直接 test | 回溯占用 CPU | 预限长度 + 分段处理 |
| 正则写死需求变化 | 缺乏可读性 | 拆分注释、分组命名、封装构建函数 |
| 过度正则化 | 简单逻辑硬套正则 | 先尝试字符串/内置 API |
# 8.11 小结
现代正则实践强调:限制输入长度、防回溯爆炸、命名捕获提升可读性、Unicode 支持、与内置字符串 API 协作(分层校验)。放弃死记海量符号表,聚焦问题场景与安全性能。复杂协议 / 邮箱 RFC / URL 验证交由专门库或后端二次校验;前端负责“快速、合理、可维护”的第一层过滤。
# 九 案例开发-日程管理-第一期
# 9.1 登录页及校验

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.ht{
text-align: center;
color: cadetblue;
font-family: 幼圆;
}
.tab{
width: 500px;
border: 5px solid cadetblue;
margin: 0px auto;
border-radius: 5px;
font-family: 幼圆;
}
.ltr td{
border: 1px solid powderblue;
}
.ipt{
border: 0px;
width: 50%;
}
.btn1{
border: 2px solid powderblue;
border-radius: 4px;
width:60px;
background-color: antiquewhite;
}
#usernameMsg , #userPwdMsg {
color: rgb(230, 87, 51);
}
.buttonContainer{
text-align: center;
}
</style>
<script>
// 检验用户名格式是否合法的函数
function checkUsername(){
// 定义正则表示字符串的规则
var usernameReg= /^[a-zA-Z0-9]{5,10}$/
// 获得用户在页面上输入的信息
var usernameInput =document.getElementById("usernameInput")
var username = usernameInput.value
// 获得格式提示的框
var usernameMsg =document.getElementById("usernameMsg")
// 格式有误时,返回false,在页面上提示
if(!usernameReg.test(username)){
usernameMsg.innerText="用户名格式有误"
return false
}
// 格式OK,返回true 在页面上提示OK
usernameMsg.innerText="OK"
return true
}
// 检验密码格式是否合法的函数
function checkUserPwd(){
// 定义正则表示字符串的规则
var userPwdReg= /^[0-9]{6}$/
// 获得用户在页面上输入的信息
var userPwdInput =document.getElementById("userPwdInput")
var userPwd = userPwdInput.value
// 获得格式提示的框
var userPwdMsg =document.getElementById("userPwdMsg")
// 格式有误时,返回false,在页面上提示
if(!userPwdReg.test(userPwd)){
userPwdMsg.innerText="密码必须是6位数字"
return false
}
// 格式OK,返回true 在页面上提示OK
userPwdMsg.innerText="OK"
return true
}
// 表单在提交时,校验用户名和密码格式,格式OK才会提交
function checkForm(){
var flag1 =checkUsername()
var flag2 =checkUserPwd()
return flag1&&flag2
}
</script>
</head>
<body>
<h1 class="ht">欢迎使用日程管理系统</h1>
<h3 class="ht">请登录</h3>
<form method="post" action="/user/login" onsubmit="return checkForm()">
<table class="tab" cellspacing="0px">
<tr class="ltr">
<td>请输入账号</td>
<td>
<input class="ipt" type="text" id="usernameInput" name="username" onblur="checkUsername()">
<span id="usernameMsg"></span>
</td>
</tr>
<tr class="ltr">
<td>请输入密码</td>
<td>
<input class="ipt" type="password" id="userPwdInput" name="userPwd" onblur="checkUserPwd()">
<span id="userPwdMsg"></span>
</td>
</tr>
<tr class="ltr">
<td colspan="2" class="buttonContainer">
<input class="btn1" type="submit" value="登录">
<input class="btn1" type="reset" value="重置">
<button class="btn1"><a href="regist.html">去注册</a></button>
</td>
</tr>
</table>
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 9.2 注册页及校验

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.ht{
text-align: center;
color: cadetblue;
font-family: 幼圆;
}
.tab{
width: 500px;
border: 5px solid cadetblue;
margin: 0px auto;
border-radius: 5px;
font-family: 幼圆;
}
.ltr td{
border: 1px solid powderblue;
}
.ipt{
border: 0px;
width: 50%;
}
.btn1{
border: 2px solid powderblue;
border-radius: 4px;
width:60px;
background-color: antiquewhite;
}
.msg {
color: gold;
}
.buttonContainer{
text-align: center;
}
</style>
<script>
function checkUsername(){
var usernameReg = /^[a-zA-Z0-9]{5,10}$/
var usernameInput = document.getElementById("usernameInput")
var username = usernameInput.value
var usernameMsg = document.getElementById("usernameMsg")
if(!usernameReg.test(username)){
usernameMsg.innerText="格式有误"
return false
}
usernameMsg.innerText="OK"
return true
}
function checkUserPwd(){
var userPwdReg = /^\d{6}$/
var userPwdInput = document.getElementById("userPwdInput")
var userPwd = userPwdInput.value
var userPwdMsg = document.getElementById("userPwdMsg")
if(!userPwdReg.test(userPwd)){
userPwdMsg.innerText="格式有误"
return false
}
userPwdMsg.innerText="OK"
return true
}
function checkReUserPwd(){
var userPwdReg = /^\d{6}$/
// 再次输入的密码的格式
var reUserPwdInput = document.getElementById("reUserPwdInput")
var reUserPwd = reUserPwdInput.value
var reUserPwdMsg = document.getElementById("reUserPwdMsg")
if(!userPwdReg.test(reUserPwd)){
reUserPwdMsg.innerText="格式有误"
return false
}
// 获得上次密码,对比两次密码是否一致
var userPwdInput = document.getElementById("userPwdInput")
var userPwd = userPwdInput.value
if(reUserPwd != userPwd){
reUserPwdMsg.innerText="两次密码不一致"
return false
}
reUserPwdMsg.innerText="OK"
return true
}
function checkForm(){
var flag1 = checkUsername()
var flag2 = checkUserPwd()
var flag3 = checkReUserPwd()
return flag1 && flag2 && flag3
}
</script>
</head>
<body>
<h1 class="ht">欢迎使用日程管理系统</h1>
<h3 class="ht">请注册</h3>
<form method="post" action="/user/regist" onsubmit="return checkForm()">
<table class="tab" cellspacing="0px">
<tr class="ltr">
<td>请输入账号</td>
<td>
<input class="ipt" id="usernameInput" type="text" name="username" onblur="checkUsername()">
<span id="usernameMsg" class="msg"></span>
</td>
</tr>
<tr class="ltr">
<td>请输入密码</td>
<td>
<input class="ipt" id="userPwdInput" type="password" name="userPwd" onblur="checkUserPwd()">
<span id="userPwdMsg" class="msg"></span>
</td>
</tr>
<tr class="ltr">
<td>确认密码</td>
<td>
<input class="ipt" id="reUserPwdInput" type="password" onblur="checkReUserPwd()">
<span id="reUserPwdMsg" class="msg"></span>
</td>
</tr>
<tr class="ltr">
<td colspan="2" class="buttonContainer">
<input class="btn1" type="submit" value="注册">
<input class="btn1" type="reset" value="重置">
<button class="btn1"><a href="login.html">去登录</a></button>
</td>
</tr>
</table>
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131