0%

异常处理与调试

目标:掌握 JavaScript 错误处理机制、内存管理原理、性能调试技巧,以及使用 Chrome DevTools 进行问题排查。

目录

  1. 错误与异常基础
  2. 内存管理与垃圾回收
  3. 性能优化与调试
  4. 调试工具与技巧
  5. 实战案例
  6. 代码片段(直接可用)
  7. 速记与总结

1. 错误与异常基础

1.1 错误的几种来源

理解错误的来源是正确处理错误的第一步。JavaScript 中的错误可以分为三大类:

1.1.1 语法错误(SyntaxError)

语法错误发生在代码解析阶段,代码还没有执行就被发现了。这类错误通常由代码编辑器或浏览器控制台直接提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例1:缺少括号
function test() {
console.log('hello'; // ❌ SyntaxError: Unexpected token ';'
}

// 示例2:缺少引号
const str = 'hello; // ❌ SyntaxError: Unterminated string constant

// 示例3:错误的对象字面量
const obj = {
name: 'test',
age: 25, // ❌ SyntaxError: Unexpected token ','(旧版浏览器,ES5 不允许尾随逗号)
};

// 示例4:使用保留字
const class = 'test'; // ❌ SyntaxError: Unexpected token 'class'

// 示例5:箭头函数语法错误
const fn = => {}; // ❌ SyntaxError: Unexpected token '=>'

特点

  • 在代码执行前就会被发现
  • 浏览器无法运行包含语法错误的脚本
  • 通常有明确的错误位置提示
  • 修复后才能继续执行

1.1.2 运行时错误(Runtime Error)

运行时错误发生在代码执行过程中,语法正确但逻辑有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 示例1:TypeError - 类型错误
const obj = null;
console.log(obj.name); // ❌ TypeError: Cannot read property 'name' of null

const num = 123;
num(); // ❌ TypeError: num is not a function

// 示例2:ReferenceError - 引用错误
console.log(undefinedVar); // ❌ ReferenceError: undefinedVar is not defined

// 示例3:RangeError - 范围错误
const arr = new Array(-1); // ❌ RangeError: Invalid array length
function recurse() {
recurse(); // ❌ RangeError: Maximum call stack size exceeded(栈溢出)
}

// 示例4:URIError - URI 错误
decodeURIComponent('%'); // ❌ URIError: URI malformed

// 示例5:EvalError - eval 错误(现代 JavaScript 中很少见)

常见运行时错误类型

错误类型 触发场景 示例
TypeError 类型不符合预期 null.name123()
ReferenceError 引用未定义变量 console.log(undefinedVar)
RangeError 数值超出有效范围 new Array(-1)、栈溢出
URIError URI 编码/解码错误 decodeURIComponent('%')
EvalError eval 使用错误 现代 JS 中很少见

1.1.3 逻辑错误

逻辑错误是最难发现的,代码能正常运行但结果不符合预期。

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
30
31
32
// 示例1:条件判断错误
function isAdult(age) {
if (age >= 18) { // ❌ 应该是 > 18 还是 >= 18?
return true;
}
return false;
}

// 示例2:变量作用域错误
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
let item = items[i];
total += item.price; // ❌ 如果 item.price 是 undefined 会得到 NaN
}
return total;
}

// 示例3:异步逻辑错误
function fetchData() {
let result;
fetch('/api/data')
.then(res => res.json())
.then(data => {
result = data; // ❌ 异步操作,result 可能还是 undefined
});
return result; // ❌ 返回 undefined
}

// 示例4:类型转换陷阱
console.log('5' + 3); // '53'(字符串拼接,不是数字相加)
console.log('5' - 3); // 2(数字相减)

逻辑错误的特点

  • 不会抛出异常
  • 需要通过测试和调试发现
  • 通常需要理解业务逻辑才能修复
  • 单元测试和代码审查有助于发现

1.2 try / catch / finally

try...catch...finally 是 JavaScript 中处理异常的核心机制。

1.2.1 基本语法

1
2
3
4
5
6
7
8
9
10
try {
// 可能抛出错误的代码
riskyOperation();
} catch (error) {
// 捕获并处理错误
console.error('发生错误:', error);
} finally {
// 无论是否出错都会执行
cleanup();
}

1.2.2 try 块

try 块包含可能抛出错误的代码:

1
2
3
4
5
6
try {
const data = JSON.parse(invalidJson);
console.log(data.name);
} catch (error) {
// 如果 JSON.parse 失败,会跳到这里
}

1.2.3 catch 块

catch 块捕获 try 块中抛出的错误:

1
2
3
4
5
6
7
8
try {
throw new Error('测试错误');
} catch (error) {
// error 是捕获到的错误对象
console.log(error.message); // '测试错误'
console.log(error.name); // 'Error'
console.log(error.stack); // 堆栈跟踪信息
}

catch 的多种用法

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
30
31
32
33
// 方式1:捕获所有错误
try {
riskyCode();
} catch (error) {
// 处理所有类型的错误
console.error(error);
}

// 方式2:只捕获特定类型的错误(需要嵌套)
try {
try {
riskyCode();
} catch (error) {
if (error instanceof TypeError) {
// 只处理 TypeError
console.error('类型错误:', error);
} else {
// 重新抛出其他错误
throw error;
}
}
} catch (error) {
// 处理其他错误
console.error('其他错误:', error);
}

// 方式3:忽略错误对象(不推荐,但有时有用)
try {
riskyCode();
} catch {
// ES2019+,不需要错误对象时可以省略
console.error('发生错误,但不需要错误详情');
}

1.2.4 finally 块

finally 块中的代码无论是否发生错误都会执行

1
2
3
4
5
6
7
8
9
10
11
12
let connection;
try {
connection = openConnection();
connection.send(data);
} catch (error) {
console.error('发送失败:', error);
} finally {
// 无论成功还是失败,都要关闭连接
if (connection) {
connection.close();
}
}

finally 的执行时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test() {
try {
console.log('1. try 开始');
throw new Error('测试');
console.log('2. 这行不会执行');
} catch (error) {
console.log('3. catch 执行');
return 'catch 返回值'; // 注意:即使有 return
} finally {
console.log('4. finally 执行'); // finally 仍然会执行!
}
console.log('5. 这行不会执行');
}

const result = test();
// 输出:
// 1. try 开始
// 3. catch 执行
// 4. finally 执行
console.log(result); // 'catch 返回值'

重要finally 中的 return 会覆盖 trycatch 中的 return

1
2
3
4
5
6
7
8
9
10
11
function test() {
try {
return 'try 返回值';
} catch (error) {
return 'catch 返回值';
} finally {
return 'finally 返回值'; // 这个会覆盖上面的返回值
}
}

console.log(test()); // 'finally 返回值'

1.2.5 最佳实践

1. 只包裹可能出错的最小代码块

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
30
31
32
33
34
35
36
37
// ❌ 不好的做法:包裹太多代码
try {
const user = getUser();
const data = processData(user);
const result = saveData(data);
updateUI(result);
sendNotification(result);
} catch (error) {
console.error(error); // 不知道具体哪一步出错了
}

// ✅ 好的做法:分别处理
let user, data, result;
try {
user = getUser();
} catch (error) {
console.error('获取用户失败:', error);
return;
}

try {
data = processData(user);
} catch (error) {
console.error('处理数据失败:', error);
return;
}

try {
result = saveData(data);
} catch (error) {
console.error('保存数据失败:', error);
return;
}

// 这些操作可能不需要 try-catch
updateUI(result);
sendNotification(result);

2. 在 catch 中记录足够的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 不好的做法:信息不足
try {
saveUserData(user);
} catch (error) {
console.error('错误'); // 不知道是什么错误、在什么场景下
}

// ✅ 好的做法:记录详细上下文
try {
saveUserData(user);
} catch (error) {
console.error('保存用户数据失败', {
error: error.message,
stack: error.stack,
userId: user.id,
userData: user,
timestamp: new Date().toISOString()
});
// 或者发送到错误监控服务
errorTracker.log(error, {
context: 'saveUserData',
userId: user.id
});
}

3. 不要用空的 catch 块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 不好的做法:吞掉错误
try {
riskyOperation();
} catch (error) {
// 什么都不做,错误被隐藏了
}

// ✅ 好的做法:至少记录错误
try {
riskyOperation();
} catch (error) {
console.error('操作失败:', error);
// 或者根据错误类型决定是否继续
if (error instanceof NetworkError) {
// 网络错误可以重试
retry();
} else {
// 其他错误需要处理
handleError(error);
}
}

4. 使用 finally 进行清理

1
2
3
4
5
6
7
8
9
10
11
12
13
// ✅ 好的做法:确保资源被释放
let fileHandle;
try {
fileHandle = await openFile('data.txt');
await writeFile(fileHandle, data);
} catch (error) {
console.error('写入文件失败:', error);
} finally {
// 无论成功还是失败,都要关闭文件
if (fileHandle) {
await fileHandle.close();
}
}

1.3 throw 抛出异常

throw 语句用于主动抛出错误,中断当前执行流程。

1.3.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为 0');
}
return a / b;
}

try {
const result = divide(10, 0);
} catch (error) {
console.error(error.message); // '除数不能为 0'
}

1.3.2 可以抛出任何值

JavaScript 允许抛出任何类型的值,但强烈推荐抛出 Error 对象

1
2
3
4
5
6
7
8
9
// 可以抛出任何值,但不推荐
throw '字符串错误'; // ❌ 不推荐
throw 123; // ❌ 不推荐
throw { code: 500 }; // ❌ 不推荐

// ✅ 推荐:抛出 Error 对象
throw new Error('错误信息');
throw new TypeError('类型错误');
throw new RangeError('范围错误');

为什么推荐 Error 对象?

1
2
3
4
5
6
7
8
9
// Error 对象包含有用的信息
const error = new Error('操作失败');
console.log(error.message); // '操作失败'
console.log(error.name); // 'Error'
console.log(error.stack); // 堆栈跟踪(非常有用!)

// 字符串错误没有这些信息
const strError = '操作失败';
console.log(strError.stack); // undefined

1.3.3 抛出错误的常见场景

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 场景1:参数验证
function createUser(name, age) {
if (!name || typeof name !== 'string') {
throw new TypeError('name 必须是字符串');
}
if (age < 0 || age > 150) {
throw new RangeError('age 必须在 0-150 之间');
}
return { name, age };
}

// 场景2:状态检查
class BankAccount {
constructor(balance) {
this.balance = balance;
}

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

// 场景3:异步操作失败
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`获取用户数据失败: ${response.status}`);
}
return response.json();
}

// 场景4:业务逻辑错误
function processOrder(order) {
if (order.items.length === 0) {
throw new Error('订单不能为空');
}
if (order.total < 0) {
throw new Error('订单总额不能为负数');
}
// 处理订单...
}

1.3.4 错误传播

错误会沿着调用栈向上传播,直到被 catch 捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function level1() {
level2(); // 错误从这里抛出
}

function level2() {
level3(); // 错误继续向上传播
}

function level3() {
throw new Error('深层错误'); // 错误在这里抛出
}

try {
level1();
} catch (error) {
console.error('捕获到错误:', error);
// 错误从 level3 → level2 → level1 → catch
}

停止错误传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function riskyOperation() {
try {
throw new Error('内部错误');
} catch (error) {
// 在这里处理错误,不重新抛出
console.error('内部处理:', error);
// 错误不会继续向上传播
}
}

// 这里不会捕获到错误
try {
riskyOperation();
} catch (error) {
// 不会执行,因为错误已经在 riskyOperation 中被处理了
}

1.4 Error 对象与自定义错误类型

1.4.1 Error 对象的结构

1
2
3
4
5
const error = new Error('错误信息');

console.log(error.name); // 'Error'
console.log(error.message); // '错误信息'
console.log(error.stack); // 堆栈跟踪信息(字符串)

1.4.2 内建错误类型

JavaScript 提供了多种内建错误类型,每种都有特定的用途:

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
// 1. Error - 通用错误基类
throw new Error('通用错误');

// 2. TypeError - 类型错误
const obj = null;
obj.property; // 自动抛出 TypeError
// 或手动抛出
throw new TypeError('期望函数,但得到其他类型');

// 3. ReferenceError - 引用错误
console.log(undefinedVar); // 自动抛出 ReferenceError
// 或手动抛出
throw new ReferenceError('变量未定义');

// 4. RangeError - 范围错误
new Array(-1); // 自动抛出 RangeError
// 或手动抛出
throw new RangeError('数值超出有效范围');

// 5. SyntaxError - 语法错误(通常不能手动抛出,由解析器抛出)
// eval('无效代码'); // 会抛出 SyntaxError

// 6. URIError - URI 错误
decodeURIComponent('%'); // 自动抛出 URIError
// 或手动抛出
throw new URIError('URI 格式错误');

错误类型选择指南

错误类型 使用场景 示例
Error 通用错误 业务逻辑错误、未知错误
TypeError 类型不符合预期 null.property123()
ReferenceError 引用未定义 undefinedVar
RangeError 数值超出范围 new Array(-1)、栈溢出
URIError URI 编码/解码错误 decodeURIComponent('%')
SyntaxError 语法错误 通常由解析器抛出

1.4.3 自定义错误类型

创建自定义错误类型可以更好地组织和处理错误:

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
30
31
32
33
// 基础自定义错误
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}

// 带额外属性的自定义错误
class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'ValidationError';
this.field = field; // 哪个字段出错
this.value = value; // 错误的值
}
}

// 使用
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError('邮箱格式不正确', 'email', email);
}
}

try {
validateEmail('invalid');
} catch (error) {
if (error instanceof ValidationError) {
console.error(`字段 ${error.field} 验证失败: ${error.message}`);
console.error(`错误值: ${error.value}`);
}
}

完整的错误类型体系

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
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
// 基础错误类
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = this.constructor.name;
this.code = code;
Error.captureStackTrace(this, this.constructor); // 保存堆栈跟踪
}
}

// 验证错误
class ValidationError extends AppError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR');
this.field = field;
}
}

// 网络错误
class NetworkError extends AppError {
constructor(message, statusCode) {
super(message, 'NETWORK_ERROR');
this.statusCode = statusCode;
}
}

// 权限错误
class PermissionError extends AppError {
constructor(message) {
super(message, 'PERMISSION_ERROR');
}
}

// 使用
function login(username, password) {
if (!username) {
throw new ValidationError('用户名不能为空', 'username');
}
if (!password) {
throw new ValidationError('密码不能为空', 'password');
}

// 模拟网络请求
if (Math.random() > 0.5) {
throw new NetworkError('网络连接失败', 500);
}

// 模拟权限检查
if (username !== 'admin') {
throw new PermissionError('权限不足');
}

return { token: 'fake-token' };
}

// 错误处理
try {
login('', 'password');
} catch (error) {
if (error instanceof ValidationError) {
console.error(`验证失败 [${error.field}]:`, error.message);
} else if (error instanceof NetworkError) {
console.error(`网络错误 [${error.statusCode}]:`, error.message);
} else if (error instanceof PermissionError) {
console.error('权限错误:', error.message);
} else {
console.error('未知错误:', error);
}
}

1.4.4 错误对象的最佳实践

1. 提供有意义的错误信息

1
2
3
4
5
// ❌ 不好的做法:错误信息不明确
throw new Error('错误');

// ✅ 好的做法:提供上下文信息
throw new Error(`无法加载用户数据: 用户ID ${userId} 不存在`);

2. 附加必要的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DatabaseError extends Error {
constructor(message, query, params) {
super(message);
this.name = 'DatabaseError';
this.query = query; // 出错的 SQL 查询
this.params = params; // 查询参数
this.timestamp = new Date();
}
}

// 使用
try {
await db.query('SELECT * FROM users WHERE id = ?', [userId]);
} catch (error) {
throw new DatabaseError('查询失败', 'SELECT * FROM users WHERE id = ?', [userId]);
}

3. 使用错误代码

1
2
3
4
5
6
7
8
9
10
11
12
class ApiError extends Error {
constructor(message, code, statusCode) {
super(message);
this.name = 'ApiError';
this.code = code; // 业务错误代码
this.statusCode = statusCode; // HTTP 状态码
}
}

// 使用
throw new ApiError('用户不存在', 'USER_NOT_FOUND', 404);
throw new ApiError('参数错误', 'INVALID_PARAMS', 400);

1.5 异步中的错误处理

异步代码的错误处理比同步代码更复杂,需要特别注意。

1.5.1 Promise 链中的错误处理

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 方式1:使用 .catch() 捕获错误
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('请求失败:', error);
});

// 方式2:在 then 中处理错误
fetch('/api/data')
.then(
response => response.json(),
error => {
console.error('请求失败:', error);
return null; // 返回默认值
}
)
.then(data => {
if (data) {
console.log(data);
}
});

// 方式3:链式 catch(可以多次使用)
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('网络错误:', error);
return { data: [] }; // 返回默认数据
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('处理数据失败:', error);
});

Promise 错误传播规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Promise.resolve()
.then(() => {
throw new Error('错误1');
})
.then(() => {
console.log('这行不会执行');
})
.catch(error => {
console.error('捕获到错误1:', error);
throw new Error('错误2'); // 重新抛出错误
})
.catch(error => {
console.error('捕获到错误2:', error);
});

1.5.2 async/await 中的错误处理

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
30
31
32
33
34
// 方式1:使用 try-catch
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error; // 重新抛出,让调用者处理
}
}

// 方式2:在调用处处理
async function main() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('处理失败:', error);
}
}

// 方式3:使用 Promise.catch(不推荐,但有时有用)
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}

fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));

async 函数中的错误处理技巧

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
30
31
32
33
34
35
36
37
38
// 技巧1:并行请求的错误处理
async function fetchMultiple() {
try {
// 使用 Promise.allSettled 即使部分失败也继续
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);

results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`请求 ${index} 失败:`, result.reason);
}
});
} catch (error) {
console.error('整体失败:', error);
}
}

// 技巧2:带重试的请求
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === retries - 1) {
throw error; // 最后一次重试失败,抛出错误
}
console.log(`重试 ${i + 1}/${retries}`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}

1.5.3 全局未捕获错误处理

浏览器环境

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
30
// 1. 捕获同步错误和未处理的 Promise rejection
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
// 发送到错误监控服务
errorTracker.log(event.error, {
type: 'uncaught',
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});

// 2. 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise rejection:', event.reason);
// 阻止默认行为(在控制台显示错误)
event.preventDefault();
// 发送到错误监控服务
errorTracker.log(event.reason, {
type: 'unhandledrejection'
});
});

// 3. 捕获资源加载错误
window.addEventListener('error', (event) => {
if (event.target !== window) {
// 这是资源加载错误(图片、脚本等)
console.error('资源加载失败:', event.target.src || event.target.href);
}
}, true); // 使用捕获阶段

Node.js 环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 捕获未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 记录错误
errorLogger.log(error);
// 优雅退出
process.exit(1);
});

// 2. 捕获未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
// 记录错误
errorLogger.log(reason);
// 可以继续运行,但建议退出
// process.exit(1);
});

// 3. 警告(deprecation warnings 等)
process.on('warning', (warning) => {
console.warn('警告:', warning.name, warning.message);
});

完整的错误监控示例

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
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
class ErrorTracker {
constructor(apiEndpoint) {
this.apiEndpoint = apiEndpoint;
this.setupGlobalHandlers();
}

setupGlobalHandlers() {
// 浏览器环境
if (typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
this.log({
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
userAgent: navigator.userAgent,
url: window.location.href
});
});

window.addEventListener('unhandledrejection', (event) => {
this.log({
type: 'unhandledrejection',
reason: event.reason,
stack: event.reason?.stack
});
event.preventDefault();
});
}

// Node.js 环境
if (typeof process !== 'undefined') {
process.on('uncaughtException', (error) => {
this.log({
type: 'uncaughtException',
error: error.message,
stack: error.stack
});
});

process.on('unhandledRejection', (reason) => {
this.log({
type: 'unhandledRejection',
reason: reason
});
});
}
}

log(errorInfo) {
// 发送到服务器
fetch(this.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
}).catch(err => {
console.error('发送错误日志失败:', err);
});
}
}

// 使用
const errorTracker = new ErrorTracker('/api/errors');

2. 内存管理与垃圾回收

2.1 内存生命周期三阶段

理解内存生命周期有助于编写更高效、更少内存泄漏的代码。

2.1.1 分配(Allocation)

当创建变量、对象、数组、函数、闭包时,JavaScript 引擎会在内存中分配空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 栈内存分配(基本类型、引用地址)
let num = 42; // 分配栈内存
let str = 'hello'; // 分配栈内存
let bool = true; // 分配栈内存

// 堆内存分配(对象、数组、函数)
let obj = { name: 'test' }; // 分配堆内存
let arr = [1, 2, 3]; // 分配堆内存
let fn = function() {}; // 分配堆内存
let closure = (function() {
let data = 'large data'; // 闭包中的数据也在堆中
return function() {
return data;
};
})();

内存分配的位置

  • 栈(Stack):存储基本类型值、函数调用栈、局部变量
  • 堆(Heap):存储对象、数组、函数、闭包等复杂数据结构

2.1.2 使用(Use)

分配内存后,通过读写变量、传参、返回值、闭包引用等方式使用内存。

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
// 读取
const value = obj.name;

// 写入
obj.name = 'new name';

// 传参(传递引用)
function process(data) {
data.processed = true;
}
process(obj);

// 返回值
function createObject() {
return { value: 123 };
}
const newObj = createObject();

// 闭包引用
function createCounter() {
let count = 0; // 这个变量被闭包引用,不会立即释放
return function() {
return ++count;
};
}
const counter = createCounter();

2.1.3 释放(Release)

当内存不再被引用(不可达)时,垃圾回收器会在合适的时机自动回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例1:局部变量自动释放
function test() {
let obj = { data: 'test' };
// 函数执行完毕后,obj 不再可达,会被回收
}
test();

// 示例2:重新赋值释放旧对象
let obj = { data: 'old' };
obj = { data: 'new' }; // 旧对象不再被引用,会被回收

// 示例3:清除引用
let data = { large: 'data' };
data = null; // 显式清除引用,帮助 GC

关键概念:可达性(Reachability)

对象只有在”可达”时才会被保留,否则会被回收。从”根”对象出发,能通过引用链到达的对象都是可达的。

1
2
3
4
5
6
根对象(Root)
├── 全局变量
├── 当前执行栈中的变量
└── 闭包中的变量
└── 引用的对象
└── 引用的对象...

2.2 垃圾回收(GC)基本策略

现代 JavaScript 引擎(如 V8)使用复杂的垃圾回收算法。

2.2.1 标记-清除算法(Mark and Sweep)

这是最基础的垃圾回收算法:

  1. 标记阶段:从根对象开始,标记所有可达对象
  2. 清除阶段:清除所有未标记的对象
1
2
3
4
5
6
7
8
9
10
11
12
// 简化示例说明
let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };

obj1.ref = obj2;
obj2.ref = obj1; // 循环引用

obj1 = null;
obj2 = null;

// 即使有循环引用,标记-清除算法也能正确回收
// 因为从根对象无法到达 obj1 和 obj2

2.2.2 分代回收(Generational Collection)

V8 将堆内存分为两代:

新生代(Young Generation)

  • 存储新创建的对象
  • 使用 Scavenge 算法(复制算法)
  • 回收频繁但快速
  • 对象经过多次回收后仍存活,会晋升到老生代

老生代(Old Generation)

  • 存储长期存活的对象
  • 使用标记-清除和标记-整理算法
  • 回收较慢但频率低
1
2
3
4
5
6
7
8
9
10
// 新生代对象(短命)
function createTemp() {
return { temp: 'data' }; // 可能在新生代
}

// 老生代对象(长寿)
const globalCache = {}; // 长期存在,在老生代
function addToCache(key, value) {
globalCache[key] = value; // 对象会晋升到老生代
}

2.2.3 增量标记(Incremental Marking)

为了避免长时间阻塞主线程,V8 使用增量标记:

  • 将标记过程分成多个小步骤
  • 在每个步骤之间执行 JavaScript 代码
  • 减少卡顿,提高用户体验

2.2.4 避免无意中保留引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ❌ 问题:全局变量永远不会被回收
window.cache = {};
function processData(data) {
window.cache[Date.now()] = data; // 不断积累,永不释放
}

// ✅ 解决:使用 WeakMap 或定期清理
const cache = new Map();
function processData(data) {
cache.set(Date.now(), data);
// 定期清理
if (cache.size > 1000) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
}

// ✅ 更好的方案:使用 WeakMap(键是对象时)
const cache = new WeakMap();
function processData(obj, data) {
cache.set(obj, data); // obj 被回收时,对应的值也会被回收
}

2.3 常见内存泄漏场景

内存泄漏是指不再需要的对象仍然被引用,导致无法被垃圾回收。

2.3.1 全局变量泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 问题:全局变量永远不会被回收
var globalData = {
large: new Array(1000000).fill('data')
};

// 即使不再需要,globalData 也不会被回收
// 因为它是全局变量,始终可达

// ✅ 解决:使用局部变量或显式清除
function process() {
const localData = {
large: new Array(1000000).fill('data')
};
// 函数执行完毕后,localData 会被回收
}

// 或者
globalData = null; // 显式清除

2.3.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
30
// ❌ 问题:定时器持有对 DOM 和闭包的引用
function startTimer() {
const element = document.getElementById('timer');
const data = new Array(1000000).fill('data'); // 大对象

setInterval(() => {
element.textContent = new Date().toLocaleTimeString();
// data 被闭包引用,永远不会被回收
}, 1000);
}

// ✅ 解决:保存定时器 ID,在不需要时清除
function startTimer() {
const element = document.getElementById('timer');
const timerId = setInterval(() => {
element.textContent = new Date().toLocaleTimeString();
}, 1000);

// 在组件卸载或不需要时清除
return () => clearInterval(timerId);
}

// React 示例
useEffect(() => {
const timer = setInterval(() => {
// ...
}, 1000);

return () => clearInterval(timer); // 清理函数
}, []);

2.3.3 事件监听器未移除

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
30
31
32
33
34
35
36
37
38
39
40
// ❌ 问题:事件监听器持有对 DOM 和闭包的引用
function setupListener() {
const element = document.getElementById('button');
const largeData = new Array(1000000).fill('data');

element.addEventListener('click', function() {
console.log('clicked');
// largeData 被闭包引用,即使 element 被移除也不会被回收
});

// 如果 element 被移除,但监听器未移除,会导致内存泄漏
element.remove(); // DOM 元素被移除,但监听器仍在
}

// ✅ 解决:保存引用,在不需要时移除
function setupListener() {
const element = document.getElementById('button');
const handler = function() {
console.log('clicked');
};

element.addEventListener('click', handler);

// 在不需要时移除
return () => {
element.removeEventListener('click', handler);
};
}

// React 示例
useEffect(() => {
const handler = () => {
// ...
};
element.addEventListener('click', handler);

return () => {
element.removeEventListener('click', handler);
};
}, []);

2.3.4 闭包保留大对象引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 问题:闭包保留了不需要的大对象
function createHandler() {
const largeData = new Array(1000000).fill('data'); // 大对象
const config = { option: 'value' }; // 小对象,实际需要

return function(event) {
// 只使用了 config,但 largeData 也被闭包引用
console.log(config.option);
};
}

// ✅ 解决:只保留需要的数据
function createHandler() {
const largeData = new Array(1000000).fill('data');
const config = { option: 'value' };

// 只提取需要的数据
const option = config.option;

return function(event) {
console.log(option); // 只引用小数据
// largeData 不会被闭包引用,可以被回收
};
}

2.3.5 缓存不断积累

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ❌ 问题:缓存无限增长
const cache = new Map();
function getData(key) {
if (cache.has(key)) {
return cache.get(key);
}
const data = fetchData(key);
cache.set(key, data); // 不断添加,从不清理
return data;
}

// ✅ 解决1:限制缓存大小(LRU)
class LRUCache {
constructor(maxSize) {
this.cache = new Map();
this.maxSize = maxSize;
}

get(key) {
if (this.cache.has(key)) {
// 移到末尾(最近使用)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}

set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// 删除最久未使用的(第一个)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}

// ✅ 解决2:使用 WeakMap(键是对象时)
const cache = new WeakMap();
function getData(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const data = fetchData(obj);
cache.set(obj, data); // obj 被回收时,对应的值也会被回收
return data;
}

2.3.6 DOM 引用未清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ❌ 问题:保存 DOM 引用,即使 DOM 被移除
const elements = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // 保存引用
// 即使从 DOM 移除,elements 中的引用仍然存在
}

function removeElement() {
const div = elements.pop();
div.remove(); // 从 DOM 移除,但 elements 中仍有引用
// div 不会被回收,因为 elements 还在引用它
}

// ✅ 解决:清除数组中的引用
function removeElement() {
const div = elements.pop();
div.remove();
// 不需要显式清除,pop() 已经移除了引用
// 但如果有其他引用,需要清除
}

2.4 Chrome DevTools 内存调试(详细指南)

2.4.1 打开内存分析工具

  1. 打开 Chrome DevTools(F12)
  2. 切换到 Memory 标签页
  3. 或使用 Performance 标签页并勾选 Memory

2.4.2 Heap Snapshot(堆快照)

使用步骤

  1. 记录初始快照

    • 点击 “Take heap snapshot”
    • 等待快照完成
    • 命名为 “Snapshot 1 - Initial”
  2. 执行可疑操作

    • 执行可能导致内存泄漏的操作(如打开/关闭弹窗、切换路由)
    • 重复多次
  3. 记录操作后快照

    • 点击 “Take heap snapshot”
    • 命名为 “Snapshot 2 - After operations”
  4. 对比快照

    • 选择 “Snapshot 2”
    • 在顶部下拉框选择 “Comparison”
    • 选择 “Snapshot 1” 作为对比基准
    • 查看 “Size Delta” 列,找出增长最多的对象类型

关键指标

  • Size:对象占用的内存大小
  • Size Delta:与对比快照的差值(正数表示增长)
  • # New:新增的对象数量
  • # Deleted:删除的对象数量
  • # Delta:净变化量

重点关注

  1. Detached DOM trees

    • 表示从 DOM 移除但仍被 JavaScript 引用的元素
    • 这是常见的内存泄漏源
  2. Retainers

    • 查看对象的引用链
    • 找出是谁在持有对象的引用

示例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 有内存泄漏的代码
const detachedElements = [];
function createAndDetach() {
const div = document.createElement('div');
document.body.appendChild(div);
detachedElements.push(div); // 保存引用
div.remove(); // 从 DOM 移除,但仍在数组中
}

// 在 DevTools 中:
// 1. 记录快照 A
// 2. 调用 createAndDetach() 10 次
// 3. 记录快照 B
// 4. 对比:会发现 Detached DOM tree 增加了 10 个元素

2.4.3 Allocation Instrumentation(分配时间线)

使用步骤

  1. 选择 “Allocation instrumentation on timeline”
  2. 点击录制按钮(圆点)
  3. 执行操作
  4. 停止录制
  5. 查看时间线上的内存分配

优势

  • 可以看到内存分配的时间点
  • 可以定位哪些操作导致大量内存分配
  • 可以看到分配的对象类型和大小

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 频繁创建大对象
function processData() {
const data = new Array(1000000).fill('data');
// 处理数据...
// 函数结束后,data 应该被回收
}

// 在 DevTools 中:
// 1. 开始录制
// 2. 调用 processData() 多次
// 3. 停止录制
// 4. 查看时间线:应该看到内存先增长后下降
// 如果内存持续增长不下降,说明有泄漏

2.4.4 Allocation Sampling(分配采样)

使用步骤

  1. 选择 “Allocation sampling”
  2. 点击开始
  3. 执行操作
  4. 停止
  5. 查看统计信息

优势

  • 性能开销小
  • 可以看到内存分配的统计信息
  • 适合长时间运行的应用

2.4.5 实战调试流程

场景:单页应用路由切换导致内存泄漏

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
// 有问题的代码
class Router {
constructor() {
this.components = []; // 问题:组件引用从未清除
}

navigate(path) {
const component = this.createComponent(path);
this.components.push(component); // 不断积累
this.render(component);
}

createComponent(path) {
const div = document.createElement('div');
div.innerHTML = `<h1>${path}</h1>`;
return div;
}
}

// 调试步骤:
// 1. 打开页面,记录快照 A
// 2. 切换路由 10 次
// 3. 记录快照 B
// 4. 对比:发现 components 数组不断增长
// 5. 修复:在 navigate 前清除旧组件

修复后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Router {
constructor() {
this.currentComponent = null; // 只保存当前组件
}

navigate(path) {
// 清除旧组件
if (this.currentComponent) {
this.currentComponent.remove();
this.currentComponent = null;
}

const component = this.createComponent(path);
this.currentComponent = component; // 只保存当前组件
this.render(component);
}
}

3. 性能优化与调试

3.1 常见性能瓶颈类型

3.1.1 DOM 操作性能问题

问题:频繁或大规模的 DOM 操作会导致回流(reflow)和重绘(repaint),严重影响性能。

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
30
31
32
33
34
35
36
// ❌ 不好的做法:频繁操作 DOM
function updateList(items) {
const list = document.getElementById('list');
list.innerHTML = ''; // 清空(触发回流)

items.forEach(item => {
const li = document.createElement('li'); // 创建元素
li.textContent = item.name; // 设置内容(可能触发回流)
list.appendChild(li); // 添加(触发回流)
});
// 如果有 1000 个 items,会触发 1000+ 次回流
}

// ✅ 好的做法:批量操作 DOM
function updateList(items) {
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 文档片段

items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // 先添加到片段(不触发回流)
});

list.innerHTML = ''; // 清空(1 次回流)
list.appendChild(fragment); // 一次性添加(1 次回流)
// 总共只触发 2 次回流
}

// ✅ 更好的做法:使用 innerHTML(如果内容简单)
function updateList(items) {
const list = document.getElementById('list');
list.innerHTML = items.map(item =>
`<li>${item.name}</li>`
).join(''); // 只触发 1 次回流
}

优化技巧

  1. 使用文档片段(DocumentFragment)
  2. 批量修改样式(使用 classList 而不是多次设置 style
  3. 使用 requestAnimationFrame 优化动画
  4. 虚拟滚动(只渲染可见区域)

3.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ❌ 不好的做法:在主线程进行大量计算
function processLargeArray(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
// 复杂的计算
result.push(heavyComputation(data[i]));
}
return result;
// 如果 data 有 100 万条,会阻塞主线程几秒钟
}

// ✅ 好的做法:使用 Web Worker
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (event) => {
const result = event.data;
updateUI(result);
};

// worker.js
self.onmessage = (event) => {
const { data } = event.data;
const result = data.map(item => heavyComputation(item));
self.postMessage(result);
};

// ✅ 或者:分批处理,使用 requestIdleCallback
function processLargeArray(data, callback) {
const batchSize = 1000;
let index = 0;

function processBatch() {
const end = Math.min(index + batchSize, data.length);
for (let i = index; i < end; i++) {
callback(data[i]);
}
index = end;

if (index < data.length) {
requestIdleCallback(processBatch); // 在空闲时继续
}
}

processBatch();
}

3.1.3 网络请求优化

问题:不必要的请求、重复请求、过大的资源。

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
30
31
32
33
34
35
36
// ❌ 不好的做法:重复请求
function getUserData(userId) {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
// 每次调用都发起新请求
});
}

// ✅ 好的做法:添加缓存
const userCache = new Map();
function getUserData(userId) {
if (userCache.has(userId)) {
return Promise.resolve(userCache.get(userId));
}

return fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
userCache.set(userId, data);
return data;
});
}

// ✅ 更好的做法:使用 HTTP 缓存头
// 服务器设置:Cache-Control: max-age=3600
// 浏览器会自动缓存

// ✅ 请求合并
function getUsersData(userIds) {
// 而不是为每个 userId 发起一个请求
return fetch('/api/users/batch', {
method: 'POST',
body: JSON.stringify({ ids: userIds })
}).then(res => res.json());
}

3.1.4 内存泄漏导致性能下降

内存泄漏会导致页面越来越慢,最终可能崩溃。

1
2
// 见 2.3 节的内存泄漏场景
// 关键:及时清理定时器、事件监听器、缓存等

3.2 使用 Chrome DevTools 检查性能

3.2.1 Performance 面板

使用步骤

  1. 打开 DevTools → Performance 标签页
  2. 点击录制按钮(圆点)或按 Ctrl+E(Windows)/ Cmd+E(Mac)
  3. 执行要分析的操作(如滚动、点击、输入等)
  4. 停止录制
  5. 查看性能分析结果

关键指标

  • FPS:帧率(绿色表示 60fps,红色表示低于 60fps)
  • CPU:CPU 使用率
  • NET:网络活动
  • HEAP:堆内存使用

火焰图(Flame Chart)

  • 横轴:时间
  • 纵轴:调用栈
  • 颜色:不同颜色代表不同类型的活动
    • 黄色:JavaScript 执行
    • 紫色:渲染(Layout)
    • 绿色:绘制(Paint)

分析技巧

  1. 查找长任务(Long Tasks)

    • 长任务(>50ms)会导致页面卡顿
    • 点击长任务查看调用栈,找出耗时函数
  2. 查找强制同步布局(Forced Reflow)

    • 在 JavaScript 中读取布局属性(如 offsetWidth)会强制同步布局
    • 应该批量读取,或使用 requestAnimationFrame
  3. 查找频繁的绘制

    • 频繁的 Paint 操作表示可能有性能问题
    • 考虑使用 CSS transformopacity 优化动画

示例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 有性能问题的代码
function updateLayout() {
const elements = document.querySelectorAll('.item');
elements.forEach((el, index) => {
const width = el.offsetWidth; // 强制同步布局
el.style.width = (width + 10) + 'px'; // 修改样式
const height = el.offsetHeight; // 又一次强制同步布局
el.style.height = (height + 10) + 'px';
});
// 如果有 100 个元素,会触发 200 次强制同步布局
}

// 在 Performance 面板中:
// 1. 录制 updateLayout() 的执行
// 2. 查看火焰图:会发现大量的 Layout(紫色)操作
// 3. 优化:批量读取和写入

优化后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function updateLayout() {
const elements = document.querySelectorAll('.item');

// 批量读取
const dimensions = Array.from(elements).map(el => ({
el,
width: el.offsetWidth,
height: el.offsetHeight
}));

// 批量写入
dimensions.forEach(({ el, width, height }) => {
el.style.width = (width + 10) + 'px';
el.style.height = (height + 10) + 'px';
});
// 只触发 2 次布局(读取一次,写入一次)
}

3.2.2 Network 面板

使用步骤

  1. 打开 DevTools → Network 标签页
  2. 刷新页面或执行操作
  3. 查看网络请求列表

关键指标

  • Status:HTTP 状态码
  • Type:资源类型(JS、CSS、图片等)
  • Size:资源大小(实际大小/传输大小)
  • Time:请求耗时
  • Waterfall:请求时间线

分析技巧

  1. 查找阻塞请求

    • 红色表示失败的请求
    • 查看 Waterfall,找出阻塞其他请求的请求
  2. 查找重复请求

    • 相同的 URL 出现多次可能表示缓存未生效
  3. 查找大文件

    • 按 Size 排序,找出最大的资源
    • 考虑压缩、懒加载、代码分割
  4. 检查缓存

    • 查看响应头中的 Cache-Control
    • 确认资源是否被正确缓存

优化建议

  • 启用 Gzip/Brotli 压缩
  • 使用 CDN 加速
  • 实现资源懒加载
  • 使用 HTTP/2 多路复用
  • 合并小文件(HTTP/1.1)或保持分离(HTTP/2)

3.2.3 Lighthouse

使用步骤

  1. 打开 DevTools → Lighthouse 标签页
  2. 选择要测试的类别(Performance、Accessibility、Best Practices、SEO)
  3. 选择设备类型(Mobile 或 Desktop)
  4. 点击 “Generate report”
  5. 查看报告和建议

报告内容

  • Performance Score:性能分数(0-100)
  • 关键指标
    • First Contentful Paint (FCP)
    • Largest Contentful Paint (LCP)
    • Total Blocking Time (TBT)
    • Cumulative Layout Shift (CLS)
  • 优化建议:具体的优化建议和预期收益

使用场景

  • 定期检查网站性能
  • 部署前检查
  • 性能回归测试
  • 获取优化建议

4. 调试工具与技巧

4.1 console 调试方法

4.1.1 基础 console 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// console.log - 普通日志
console.log('普通信息', { data: 123 });

// console.error - 错误信息(红色)
console.error('错误信息', error);

// console.warn - 警告信息(黄色)
console.warn('警告信息');

// console.info - 信息(蓝色)
console.info('提示信息');

// console.debug - 调试信息(默认不显示,需在控制台设置中开启)
console.debug('调试信息');

4.1.2 高级 console 方法

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
30
31
32
33
34
35
36
37
// console.table - 表格形式显示对象数组
const users = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 }
];
console.table(users);

// console.group - 分组显示
console.group('用户信息');
console.log('姓名: 张三');
console.log('年龄: 25');
console.groupEnd();

// console.time - 计时
console.time('操作耗时');
// 执行操作...
console.timeEnd('操作耗时'); // 输出:操作耗时: 123.456ms

// console.trace - 堆栈跟踪
function a() {
b();
}
function b() {
c();
}
function c() {
console.trace('调用栈');
}
a(); // 显示从 a → b → c 的调用栈

// console.assert - 断言
console.assert(1 === 2, '1 不等于 2'); // 条件为 false 时输出

// console.count - 计数
console.count('标签'); // 标签: 1
console.count('标签'); // 标签: 2
console.countReset('标签'); // 重置计数

4.1.3 console 样式

1
2
3
4
5
6
7
8
9
10
11
// 使用 CSS 样式
console.log(
'%c这是红色文字',
'color: red; font-size: 20px; font-weight: bold;'
);

console.log(
'%c红色 %c蓝色',
'color: red;',
'color: blue;'
);

4.2 断点调试

4.2.1 代码断点

在代码中添加 debugger 语句:

1
2
3
4
5
function processData(data) {
debugger; // 执行到这里会暂停
const result = data.map(item => item * 2);
return result;
}

4.2.2 条件断点

在 Sources 面板中:

  1. 找到要设置断点的行
  2. 右键 → “Add conditional breakpoint”
  3. 输入条件,如 i > 100

4.2.3 日志点(Logpoint)

在 Sources 面板中:

  1. 右键 → “Add logpoint”
  2. 输入要输出的表达式,如 {i}, {data[i]}
  3. 执行时会输出日志,但不会暂停

4.3 Source Maps

Source Maps 允许在 DevTools 中查看和调试原始源代码(而不是编译后的代码)。

配置

1
2
3
4
5
// webpack.config.js
module.exports = {
devtool: 'source-map', // 生成 source map
// 其他配置...
};

使用

  • 在 Sources 面板中查看原始源代码
  • 设置断点
  • 查看变量值

5. 实战案例

5.1 错误监控系统

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
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
class ErrorMonitor {
constructor(config) {
this.apiEndpoint = config.apiEndpoint;
this.environment = config.environment;
this.setupGlobalHandlers();
}

setupGlobalHandlers() {
// 捕获同步错误
window.addEventListener('error', (event) => {
this.report({
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
});
});

// 捕获 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
this.report({
type: 'unhandledrejection',
reason: event.reason,
stack: event.reason?.stack,
timestamp: new Date().toISOString()
});
event.preventDefault();
});
}

report(errorInfo) {
// 发送到服务器
fetch(this.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...errorInfo,
environment: this.environment
})
}).catch(err => {
console.error('发送错误报告失败:', err);
});
}

// 手动报告错误
captureException(error, context = {}) {
this.report({
type: 'exception',
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString()
});
}
}

// 使用
const errorMonitor = new ErrorMonitor({
apiEndpoint: '/api/errors',
environment: 'production'
});

try {
riskyOperation();
} catch (error) {
errorMonitor.captureException(error, {
userId: currentUser.id,
action: 'processPayment'
});
}

5.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class PerformanceMonitor {
constructor() {
this.metrics = [];
this.observePerformance();
}

observePerformance() {
// 监控长任务
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.reportLongTask(entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
}

// 监控资源加载
window.addEventListener('load', () => {
const timing = performance.timing;
const metrics = {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
dom: timing.domContentLoadedEventEnd - timing.domLoading,
load: timing.loadEventEnd - timing.navigationStart
};
this.reportMetrics(metrics);
});
}

reportLongTask(entry) {
console.warn('长任务 detected:', {
duration: entry.duration,
startTime: entry.startTime
});
}

reportMetrics(metrics) {
console.log('性能指标:', metrics);
// 发送到服务器...
}
}

// 使用
const perfMonitor = new PerformanceMonitor();

6. 代码片段(直接可用)

6.1 错误处理工具函数

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
30
31
// 安全的 JSON 解析
function safeJsonParse(str, defaultValue = null) {
try {
return JSON.parse(str);
} catch (error) {
console.error('JSON 解析失败:', error);
return defaultValue;
}
}

// 安全的异步操作
async function safeAsync(fn, defaultValue = null) {
try {
return await fn();
} catch (error) {
console.error('异步操作失败:', error);
return defaultValue;
}
}

// 重试函数
async function retry(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}

6.2 内存泄漏检测工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检测全局变量泄漏
function detectGlobalLeaks() {
const initialGlobals = new Set(Object.keys(window));

return function check() {
const currentGlobals = new Set(Object.keys(window));
const newGlobals = [...currentGlobals].filter(x => !initialGlobals.has(x));
if (newGlobals.length > 0) {
console.warn('检测到新的全局变量:', newGlobals);
}
};
}

// 使用
const checkLeaks = detectGlobalLeaks();
// 执行一些操作后
checkLeaks();

6.3 性能测量工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 测量函数执行时间
function measureTime(fn, label = '操作') {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${label} 耗时: ${(end - start).toFixed(2)}ms`);
return result;
}

// 异步版本
async function measureTimeAsync(fn, label = '操作') {
const start = performance.now();
const result = await fn();
const end = performance.now();
console.log(`${label} 耗时: ${(end - start).toFixed(2)}ms`);
return result;
}

// 使用
measureTime(() => {
// 执行操作
}, '数据处理');

7. 速记与总结

7.1 核心概念速记

  • “try 小块 catch 记录 finally 收尾,不要用大 try 包一切。”

    • 只包裹可能出错的最小代码块,在 catch 中记录详细信息,用 finally 进行清理。
  • “只抛 Error 及子类,给错误类型与上下文,方便排查。”

    • 抛出 Error 对象而不是字符串,提供错误类型和上下文信息。
  • “内存三阶段:分配→使用→释放;泄漏多半是引用没断干净。”

    • 理解内存生命周期,内存泄漏通常是因为对象仍被引用。
  • “定时器/监听/缓存要记得清理,否则闭包和 DOM 会一直活着。”

    • 及时清理定时器、事件监听器、缓存,避免内存泄漏。
  • “性能调试先看 Performance 火焰图,再看 Network 与 Memory 快照。”

    • 使用 Performance 面板查找性能瓶颈,使用 Network 面板优化请求,使用 Memory 面板查找内存泄漏。

7.2 错误处理检查清单

  • 是否只包裹了可能出错的最小代码块?
  • catch 块中是否记录了足够的错误信息?
  • 是否使用了 finally 进行资源清理?
  • 是否抛出了 Error 对象而不是字符串?
  • 是否创建了自定义错误类型?
  • 异步代码是否正确处理了错误?
  • 是否设置了全局错误处理器?

7.3 内存管理检查清单

  • 是否清除了所有定时器?
  • 是否移除了所有事件监听器?
  • 缓存是否有大小限制?
  • 是否避免了在全局对象上存储大对象?
  • 闭包是否只保留了必要的数据?
  • DOM 引用是否在不需要时清除?

7.4 性能优化检查清单

  • 是否批量操作 DOM?
  • 是否避免了强制同步布局?
  • 大量计算是否使用了 Web Worker?
  • 网络请求是否使用了缓存?
  • 资源是否进行了压缩和优化?
  • 是否使用了代码分割和懒加载?

7.5 调试技巧总结

  1. 使用 console 方法console.tableconsole.timeconsole.trace
  2. 设置断点:代码断点、条件断点、日志点
  3. 使用 Performance 面板:查找性能瓶颈
  4. 使用 Memory 面板:查找内存泄漏
  5. 使用 Network 面板:优化网络请求
  6. 使用 Lighthouse:获取优化建议

结语:错误处理和性能优化是前端开发的重要技能。掌握这些工具和技巧,能够帮助你快速定位和解决问题,编写更健壮、更高效的代码。记住,预防胜于治疗,在编写代码时就要考虑错误处理和性能问题。