目标:掌握 JavaScript 错误处理机制、内存管理原理、性能调试技巧,以及使用 Chrome DevTools 进行问题排查。
目录
错误与异常基础
内存管理与垃圾回收
性能优化与调试
调试工具与技巧
实战案例
代码片段(直接可用)
速记与总结
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 function test ( ) { console .log ('hello' ; } 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 const obj = null ;console .log (obj.name ); const num = 123 ;num (); console .log (undefinedVar); const arr = new Array (-1 ); function recurse ( ) { recurse (); } decodeURIComponent ('%' );
常见运行时错误类型 :
错误类型
触发场景
示例
TypeError
类型不符合预期
null.name、123()
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 function isAdult (age ) { if (age >= 18 ) { return true ; } return false ; } function calculateTotal (items ) { let total = 0 ; for (let i = 0 ; i < items.length ; i++) { let item = items[i]; total += item.price ; } return total; } function fetchData ( ) { let result; fetch ('/api/data' ) .then (res => res.json ()) .then (data => { result = data; }); return result; } console .log ('5' + 3 ); console .log ('5' - 3 );
逻辑错误的特点 :
不会抛出异常
需要通过测试和调试发现
通常需要理解业务逻辑才能修复
单元测试和代码审查有助于发现
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) { }
1.2.3 catch 块 catch 块捕获 try 块中抛出的错误:
1 2 3 4 5 6 7 8 try { throw new Error ('测试错误' ); } catch (error) { console .log (error.message ); console .log (error.name ); 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 try { riskyCode (); } catch (error) { console .error (error); } try { try { riskyCode (); } catch (error) { if (error instanceof TypeError ) { console .error ('类型错误:' , error); } else { throw error; } } } catch (error) { console .error ('其他错误:' , error); } try { riskyCode (); } catch { 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 返回值' ; } finally { console .log ('4. finally 执行' ); } console .log ('5. 这行不会执行' ); } const result = test ();console .log (result);
重要 :finally 中的 return 会覆盖 try 或 catch 中的 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 ());
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 ; } 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 ); }
1.3.2 可以抛出任何值 JavaScript 允许抛出任何类型的值,但强烈推荐抛出 Error 对象 :
1 2 3 4 5 6 7 8 9 throw '字符串错误' ; throw 123 ; throw { code : 500 }; throw new Error ('错误信息' );throw new TypeError ('类型错误' );throw new RangeError ('范围错误' );
为什么推荐 Error 对象?
1 2 3 4 5 6 7 8 9 const error = new Error ('操作失败' );console .log (error.message ); console .log (error.name ); console .log (error.stack ); const strError = '操作失败' ;console .log (strError.stack );
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 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 }; } class BankAccount { constructor (balance ) { this .balance = balance; } withdraw (amount ) { if (amount > this .balance ) { throw new Error ('余额不足' ); } this .balance -= amount; return this .balance ; } } async function fetchUserData (userId ) { const response = await fetch (`/api/users/${userId} ` ); if (!response.ok ) { throw new Error (`获取用户数据失败: ${response.status} ` ); } return response.json (); } 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); }
停止错误传播 :
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) { }
1.4 Error 对象与自定义错误类型 1.4.1 Error 对象的结构 1 2 3 4 5 const error = new Error ('错误信息' );console .log (error.name ); 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 throw new Error ('通用错误' );const obj = null ;obj.property ; throw new TypeError ('期望函数,但得到其他类型' );console .log (undefinedVar); throw new ReferenceError ('变量未定义' );new Array (-1 ); throw new RangeError ('数值超出有效范围' );decodeURIComponent ('%' ); throw new URIError ('URI 格式错误' );
错误类型选择指南 :
错误类型
使用场景
示例
Error
通用错误
业务逻辑错误、未知错误
TypeError
类型不符合预期
null.property、123()
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; 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; } } 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 fetch ('/api/data' ) .then (response => response.json ()) .then (data => { console .log (data); }) .catch (error => { console .error ('请求失败:' , error); }); fetch ('/api/data' ) .then ( response => response.json (), error => { console .error ('请求失败:' , error); return null ; } ) .then (data => { if (data) { console .log (data); } }); 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 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; } } async function main ( ) { try { const data = await fetchData (); console .log (data); } catch (error) { console .error ('处理失败:' , error); } } 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 async function fetchMultiple ( ) { try { 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); } } 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 window .addEventListener ('error' , (event ) => { console .error ('全局错误:' , event.error ); errorTracker.log (event.error , { type : 'uncaught' , filename : event.filename , lineno : event.lineno , colno : event.colno }); }); window .addEventListener ('unhandledrejection' , (event ) => { console .error ('未处理的 Promise rejection:' , event.reason ); event.preventDefault (); errorTracker.log (event.reason , { type : 'unhandledrejection' }); }); 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 process.on ('uncaughtException' , (error ) => { console .error ('未捕获的异常:' , error); errorLogger.log (error); process.exit (1 ); }); process.on ('unhandledRejection' , (reason, promise ) => { console .error ('未处理的 Promise rejection:' , reason); errorLogger.log (reason); }); 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 (); }); } 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 function test ( ) { let obj = { data : 'test' }; } test ();let obj = { data : 'old' };obj = { data : 'new' }; let data = { large : 'data' };data = null ;
关键概念:可达性(Reachability)
对象只有在”可达”时才会被保留,否则会被回收。从”根”对象出发,能通过引用链到达的对象都是可达的。
1 2 3 4 5 6 根对象(Root) ├── 全局变量 ├── 当前执行栈中的变量 └── 闭包中的变量 └── 引用的对象 └── 引用的对象...
2.2 垃圾回收(GC)基本策略 现代 JavaScript 引擎(如 V8)使用复杂的垃圾回收算法。
2.2.1 标记-清除算法(Mark and Sweep) 这是最基础的垃圾回收算法:
标记阶段 :从根对象开始,标记所有可达对象
清除阶段 :清除所有未标记的对象
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 ;
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; } 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); } } const cache = new WeakMap ();function processData (obj, data ) { cache.set (obj, data); }
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' ) }; function process ( ) { const localData = { large : new Array (1000000 ).fill ('data' ) }; } 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 function startTimer ( ) { const element = document .getElementById ('timer' ); const data = new Array (1000000 ).fill ('data' ); setInterval (() => { element.textContent = new Date ().toLocaleTimeString (); }, 1000 ); } function startTimer ( ) { const element = document .getElementById ('timer' ); const timerId = setInterval (() => { element.textContent = new Date ().toLocaleTimeString (); }, 1000 ); return () => clearInterval (timerId); } 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 function setupListener ( ) { const element = document .getElementById ('button' ); const largeData = new Array (1000000 ).fill ('data' ); element.addEventListener ('click' , function ( ) { console .log ('clicked' ); }); element.remove (); } function setupListener ( ) { const element = document .getElementById ('button' ); const handler = function ( ) { console .log ('clicked' ); }; element.addEventListener ('click' , handler); return () => { element.removeEventListener ('click' , handler); }; } 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 ) { 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); }; }
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; } 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); } } const cache = new WeakMap ();function getData (obj ) { if (cache.has (obj)) { return cache.get (obj); } const data = fetchData (obj); cache.set (obj, data); 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 const elements = [];function addElement ( ) { const div = document .createElement ('div' ); document .body .appendChild (div); elements.push (div); } function removeElement ( ) { const div = elements.pop (); div.remove (); } function removeElement ( ) { const div = elements.pop (); div.remove (); }
2.4.1 打开内存分析工具
打开 Chrome DevTools(F12)
切换到 Memory 标签页
或使用 Performance 标签页并勾选 Memory
2.4.2 Heap Snapshot(堆快照) 使用步骤 :
记录初始快照
点击 “Take heap snapshot”
等待快照完成
命名为 “Snapshot 1 - Initial”
执行可疑操作
执行可能导致内存泄漏的操作(如打开/关闭弹窗、切换路由)
重复多次
记录操作后快照
点击 “Take heap snapshot”
命名为 “Snapshot 2 - After operations”
对比快照
选择 “Snapshot 2”
在顶部下拉框选择 “Comparison”
选择 “Snapshot 1” 作为对比基准
查看 “Size Delta” 列,找出增长最多的对象类型
关键指标 :
Size :对象占用的内存大小
Size Delta :与对比快照的差值(正数表示增长)
# New :新增的对象数量
# Deleted :删除的对象数量
# Delta :净变化量
重点关注 :
Detached DOM trees
表示从 DOM 移除但仍被 JavaScript 引用的元素
这是常见的内存泄漏源
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 (); }
2.4.3 Allocation Instrumentation(分配时间线) 使用步骤 :
选择 “Allocation instrumentation on timeline”
点击录制按钮(圆点)
执行操作
停止录制
查看时间线上的内存分配
优势 :
可以看到内存分配的时间点
可以定位哪些操作导致大量内存分配
可以看到分配的对象类型和大小
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 function processData ( ) { const data = new Array (1000000 ).fill ('data' ); }
2.4.4 Allocation Sampling(分配采样) 使用步骤 :
选择 “Allocation sampling”
点击开始
执行操作
停止
查看统计信息
优势 :
性能开销小
可以看到内存分配的统计信息
适合长时间运行的应用
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 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 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); }); } 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 = '' ; list.appendChild (fragment); } function updateList (items ) { const list = document .getElementById ('list' ); list.innerHTML = items.map (item => `<li>${item.name} </li>` ).join ('' ); }
优化技巧 :
使用文档片段(DocumentFragment)
批量修改样式 (使用 classList 而不是多次设置 style)
使用 requestAnimationFrame 优化动画
虚拟滚动 (只渲染可见区域)
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; } const worker = new Worker ('worker.js' );worker.postMessage ({ data : largeArray }); worker.onmessage = (event ) => { const result = event.data ; updateUI (result); }; self.onmessage = (event ) => { const { data } = event.data ; const result = data.map (item => heavyComputation (item)); self.postMessage (result); }; 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; }); } function getUsersData (userIds ) { return fetch ('/api/users/batch' , { method : 'POST' , body : JSON .stringify ({ ids : userIds }) }).then (res => res.json ()); }
3.1.4 内存泄漏导致性能下降 内存泄漏会导致页面越来越慢,最终可能崩溃。
使用步骤 :
打开 DevTools → Performance 标签页
点击录制按钮(圆点)或按 Ctrl+E(Windows)/ Cmd+E(Mac)
执行要分析的操作(如滚动、点击、输入等)
停止录制
查看性能分析结果
关键指标 :
FPS :帧率(绿色表示 60fps,红色表示低于 60fps)
CPU :CPU 使用率
NET :网络活动
HEAP :堆内存使用
火焰图(Flame Chart) :
横轴:时间
纵轴:调用栈
颜色:不同颜色代表不同类型的活动
黄色:JavaScript 执行
紫色:渲染(Layout)
绿色:绘制(Paint)
分析技巧 :
查找长任务(Long Tasks)
长任务(>50ms)会导致页面卡顿
点击长任务查看调用栈,找出耗时函数
查找强制同步布局(Forced Reflow)
在 JavaScript 中读取布局属性(如 offsetWidth)会强制同步布局
应该批量读取,或使用 requestAnimationFrame
查找频繁的绘制
频繁的 Paint 操作表示可能有性能问题
考虑使用 CSS transform 和 opacity 优化动画
示例分析 :
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' ; }); }
优化后的代码 :
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' ; }); }
3.2.2 Network 面板 使用步骤 :
打开 DevTools → Network 标签页
刷新页面或执行操作
查看网络请求列表
关键指标 :
Status :HTTP 状态码
Type :资源类型(JS、CSS、图片等)
Size :资源大小(实际大小/传输大小)
Time :请求耗时
Waterfall :请求时间线
分析技巧 :
查找阻塞请求
红色表示失败的请求
查看 Waterfall,找出阻塞其他请求的请求
查找重复请求
查找大文件
按 Size 排序,找出最大的资源
考虑压缩、懒加载、代码分割
检查缓存
查看响应头中的 Cache-Control
确认资源是否被正确缓存
优化建议 :
启用 Gzip/Brotli 压缩
使用 CDN 加速
实现资源懒加载
使用 HTTP/2 多路复用
合并小文件(HTTP/1.1)或保持分离(HTTP/2)
3.2.3 Lighthouse 使用步骤 :
打开 DevTools → Lighthouse 标签页
选择要测试的类别(Performance、Accessibility、Best Practices、SEO)
选择设备类型(Mobile 或 Desktop)
点击 “Generate report”
查看报告和建议
报告内容 :
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 ('普通信息' , { data : 123 });console .error ('错误信息' , error);console .warn ('警告信息' );console .info ('提示信息' );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 const users = [ { name : '张三' , age : 25 }, { name : '李四' , age : 30 } ]; console .table (users);console .group ('用户信息' );console .log ('姓名: 张三' );console .log ('年龄: 25' );console .groupEnd ();console .time ('操作耗时' );console .timeEnd ('操作耗时' ); function a ( ) { b (); } function b ( ) { c (); } function c ( ) { console .trace ('调用栈' ); } a (); console .assert (1 === 2 , '1 不等于 2' ); console .count ('标签' ); console .count ('标签' ); console .countReset ('标签' );
4.1.3 console 样式 1 2 3 4 5 6 7 8 9 10 11 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 面板中:
找到要设置断点的行
右键 → “Add conditional breakpoint”
输入条件,如 i > 100
4.2.3 日志点(Logpoint) 在 Sources 面板中:
右键 → “Add logpoint”
输入要输出的表达式,如 {i}, {data[i]}
执行时会输出日志,但不会暂停
4.3 Source Maps Source Maps 允许在 DevTools 中查看和调试原始源代码(而不是编译后的代码)。
配置 :
1 2 3 4 5 module .exports = { devtool : '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 () }); }); 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 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 错误处理检查清单
7.3 内存管理检查清单
7.4 性能优化检查清单
7.5 调试技巧总结
使用 console 方法 :console.table、console.time、console.trace 等
设置断点 :代码断点、条件断点、日志点
使用 Performance 面板 :查找性能瓶颈
使用 Memory 面板 :查找内存泄漏
使用 Network 面板 :优化网络请求
使用 Lighthouse :获取优化建议
结语 :错误处理和性能优化是前端开发的重要技能。掌握这些工具和技巧,能够帮助你快速定位和解决问题,编写更健壮、更高效的代码。记住,预防胜于治疗,在编写代码时就要考虑错误处理和性能问题。