MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript错误处理与调试技巧总结

2024-05-092.8k 阅读

JavaScript 错误类型

  1. 语法错误(SyntaxError)
    • 定义:当 JavaScript 代码不符合语法规则时,就会抛出 SyntaxError。这种错误通常在代码解析阶段就被发现,意味着代码根本无法正常执行。
    • 常见原因及示例
      • 括号不匹配
// 缺少右括号
let x = (1 + 2; 

上述代码中,(1 + 2 缺少右括号,会抛出 SyntaxError: Unexpected end of input。 - 关键字错误

// 使用错误的关键字
var let = 10; 

在 JavaScript 中,let 是关键字,不能用作变量名,会抛出 SyntaxError: Unexpected token let。 2. 引用错误(ReferenceError)

  • 定义:当尝试引用一个不存在的变量时,会抛出 ReferenceError。这表明在作用域中找不到该变量的声明。
  • 常见原因及示例
    • 使用未声明变量
console.log(undefinedVariable); 

这里 undefinedVariable 未声明,会抛出 ReferenceError: undefinedVariable is not defined。 - 作用域问题导致引用错误

function test() {
    console.log(x); 
    var x = 10; 
}
test(); 

在函数 test 中,console.log(x) 执行时,x 虽然声明了,但还未初始化。根据 JavaScript 的变量提升机制,变量声明被提升到函数顶部,但初始化仍在原来位置。所以这里会抛出 ReferenceError: Cannot access 'x' before initialization。 3. 类型错误(TypeError)

  • 定义:当操作或函数尝试以不适当的数据类型执行时,会抛出 TypeError。这意味着数据类型与预期不符。
  • 常见原因及示例
    • 调用非函数对象
let num = 10; 
num(); 

这里 num 是数字类型,不是函数,不能被调用,会抛出 TypeError: num is not a function。 - nullundefined 进行属性访问

let obj = null; 
console.log(obj.property); 

null 没有任何属性,会抛出 TypeError: Cannot read property 'property' of null。类似地,对 undefined 进行属性访问也会抛出同样类型的错误。 4. 范围错误(RangeError)

  • 定义:当一个值超出了合法的范围时,会抛出 RangeError。通常与数组、数值等操作有关。
  • 常见原因及示例
    • 数组长度设置非法
let arr = []; 
arr.length = -1; 

数组长度必须是非负整数,这里设置为 -1,会抛出 RangeError: Invalid array length。 - 数值超出范围

// 创建一个过大的数组
let hugeArray = new Array(Number.MAX_SAFE_INTEGER + 1); 

Number.MAX_SAFE_INTEGER 是 JavaScript 中能安全表示的最大整数,创建一个比它还大长度的数组会抛出 RangeError: Invalid array length。 5. URI 错误(URIError)

  • 定义:当使用全局 URI 处理函数(如 encodeURI()decodeURI() 等)时,如果传入的参数格式不正确,就会抛出 URIError
  • 常见原因及示例
    • 非法的 URI 编码
decodeURI('%'); 

% 是不完整的 URI 编码,会抛出 URIError: URI malformed。 - 不支持的编码格式

encodeURIComponent('\uD800'); 

\uD800 是代理对的高代理,单独编码会抛出 URIError: malformed URI sequence

错误处理机制

  1. try - catch - finally 语句
    • 语法
try {
    // 可能抛出错误的代码块
    let result = 10 / 0; 
    console.log(result); 
} catch (error) {
    // 捕获错误并处理
    console.log('捕获到错误:', error.message); 
} finally {
    // 无论是否有错误,都会执行的代码块
    console.log('这是 finally 块'); 
}
  • 工作原理try 块中的代码会被执行。如果在执行过程中抛出错误,程序会立即跳转到 catch 块,catch 块中的参数 error 就是捕获到的错误对象,它包含了错误的相关信息,如 message(错误信息描述)、name(错误类型名称)等。finally 块无论 try 块中是否抛出错误,都会执行。
  • 注意事项
    • 捕获特定类型错误:虽然 catch 块可以捕获任何类型的错误,但在实际应用中,有时需要根据不同的错误类型进行不同的处理。可以通过检查 error.name 来区分错误类型。
try {
    let num = 'abc'; 
    let result = num * 2; 
} catch (error) {
    if (error.name === 'TypeError') {
        console.log('类型错误:', error.message); 
    } else if (error.name === 'ReferenceError') {
        console.log('引用错误:', error.message); 
    } else {
        console.log('其他错误:', error.message); 
    }
}
 - **嵌套 `try - catch - finally`**:可以在 `try`、`catch` 或 `finally` 块中再嵌套 `try - catch - finally` 语句,以处理更复杂的错误情况。但要注意,过多的嵌套可能会使代码变得难以阅读和维护。
try {
    try {
        let result = 10 / 0; 
    } catch (innerError) {
        console.log('内部捕获到错误:', innerError.message); 
    } finally {
        console.log('内部 finally 块'); 
    }
} catch (outerError) {
    console.log('外部捕获到错误:', outerError.message); 
} finally {
    console.log('外部 finally 块'); 
}
  1. throw 语句
    • 语法throw expression;,其中 expression 可以是一个错误对象、字符串、数字等。但通常建议抛出错误对象,以便更好地处理错误。
    • 手动抛出错误示例
function divide(a, b) {
    if (b === 0) {
        throw new Error('除数不能为零'); 
    }
    return a / b; 
}
try {
    let result = divide(10, 0); 
    console.log(result); 
} catch (error) {
    console.log('捕获到错误:', error.message); 
}
  • 自定义错误类型:可以通过继承 Error 类来创建自定义错误类型。
class MyCustomError extends Error {
    constructor(message) {
        super(message); 
        this.name = 'MyCustomError'; 
    }
}
function customFunction() {
    throw new MyCustomError('这是一个自定义错误'); 
}
try {
    customFunction(); 
} catch (error) {
    if (error instanceof MyCustomError) {
        console.log('捕获到自定义错误:', error.message); 
    } else {
        console.log('捕获到其他错误:', error.message); 
    }
}
  1. 全局错误处理
    • window.onerror:在浏览器环境中,可以使用 window.onerror 来捕获全局未处理的错误。
window.onerror = function (message, source, lineno, colno, error) {
    console.log('全局捕获到错误:'); 
    console.log('错误信息:', message); 
    console.log('错误源:', source); 
    console.log('行号:', lineno); 
    console.log('列号:', colno); 
    console.log('错误对象:', error); 
    return true; 
};
// 触发一个全局未处理的错误
let nonExistentFunction = function () {}; 
nonExistentFunction(); 

window.onerror 的回调函数接收错误信息、错误源文件、行号、列号以及错误对象作为参数。返回 true 表示错误已被处理,不会触发浏览器默认的错误提示。

  • Node.js 中的全局错误处理
    • process.on('uncaughtException'):在 Node.js 中,process.on('uncaughtException') 用于捕获未被 try - catch 块处理的异常。
process.on('uncaughtException', function (error) {
    console.log('捕获到未处理的异常:', error.message); 
    console.log('堆栈跟踪:', error.stack); 
});
// 触发一个未处理的异常
let result = 10 / 0; 
 - **process.on('unhandledRejection')**:用于捕获未处理的 Promise 拒绝。
process.on('unhandledRejection', function (reason, promise) {
    console.log('未处理的 Promise 拒绝:'); 
    console.log('拒绝原因:', reason); 
    console.log('Promise 对象:', promise); 
});
let promise = new Promise((resolve, reject) => {
    reject('这是一个拒绝原因'); 
});

调试技巧

  1. console 调试
    • console.log():这是最基本的调试方法,用于在控制台输出信息。可以输出变量的值、执行结果等。
let num1 = 10; 
let num2 = 20; 
let sum = num1 + num2; 
console.log('两数之和为:', sum); 
  • console.error():用于输出错误信息,通常会以红色字体在控制台显示,方便区分正常输出和错误信息。
try {
    let result = 10 / 0; 
} catch (error) {
    console.error('发生错误:', error.message); 
}
  • console.warn():输出警告信息,一般以黄色字体显示。用于提示可能存在的问题,但不会像错误那样导致程序终止。
let age = -5; 
if (age < 0) {
    console.warn('年龄不能为负数'); 
}
  • console.table():当需要输出的数据是数组或对象时,console.table() 可以以表格形式展示,使数据结构更清晰。
let students = [
    { name: 'Alice', age: 20 },
    { name: 'Bob', age: 22 },
    { name: 'Charlie', age: 21 }
];
console.table(students); 
  1. 断点调试
    • 浏览器开发者工具中的断点
      • 设置断点:在 Chrome、Firefox 等浏览器的开发者工具中,打开对应的 JavaScript 文件,在代码行号旁边点击即可设置断点。例如,在以下代码的 let sum = num1 + num2; 行设置断点:
function addNumbers() {
    let num1 = 10; 
    let num2 = 20; 
    let sum = num1 + num2; 
    return sum; 
}
addNumbers(); 
 - **调试过程**:当代码执行到断点处时,程序会暂停,开发者可以查看当前作用域中的变量值,单步执行代码(通过点击开发者工具中的单步执行按钮,如“Step over”、“Step into”、“Step out”),观察代码的执行流程,从而找出问题所在。“Step over” 会执行当前函数的下一行代码,但不会进入函数内部;“Step into” 会进入函数内部执行;“Step out” 会从当前函数跳出,执行函数调用后的下一行代码。
  • Node.js 中的断点调试
    • 使用 debugger 语句:在 Node.js 代码中插入 debugger 语句。
function multiplyNumbers(a, b) {
    let result = a * b; 
    debugger; 
    return result; 
}
let product = multiplyNumbers(5, 3); 
console.log(product); 

然后使用 node inspect 命令来启动调试会话。当执行到 debugger 语句时,Node.js 会暂停执行,进入调试模式。可以在调试控制台中使用类似浏览器开发者工具的命令,如 cont(继续执行)、next(单步执行)、step(进入函数内部)、out(跳出函数)等进行调试。 3. 使用调试工具

  • Chrome DevTools:除了断点调试功能外,Chrome DevTools 还有许多强大的调试功能。
    • 性能分析:通过“Performance”标签页,可以记录代码的性能数据,如函数执行时间、页面渲染时间等。这对于优化代码性能非常有帮助。例如,可以录制一段页面交互操作,然后在性能分析结果中查看哪些函数执行时间过长,从而进行针对性优化。
    • 内存分析:“Memory”标签页可以帮助开发者分析内存使用情况。可以进行堆快照,查看对象的内存占用,检测内存泄漏等问题。比如,在一个单页应用中,不断创建新的 DOM 元素但没有及时释放,通过内存分析工具可以发现内存占用持续上升,进而定位到可能存在内存泄漏的代码区域。
  • WebStorm:这是一款强大的 JavaScript 集成开发环境(IDE)。它具有智能代码补全、语法检查、调试等功能。在调试方面,它提供了直观的调试界面,与浏览器和 Node.js 都有良好的集成。可以在 WebStorm 中直接设置断点,调试前端 JavaScript 代码和 Node.js 应用程序,并且能方便地查看变量值、调用堆栈等信息。例如,在开发一个 Node.js 的 Express 应用时,在 WebStorm 中设置断点,可以很清晰地跟踪请求的处理流程,查看中间件和路由函数的执行情况。
  1. 日志记录
    • 简单日志记录:在代码中适当位置使用 console.log 等输出信息作为日志。但要注意,在生产环境中,过多的 console.log 可能会影响性能,并且可能会暴露敏感信息,所以在上线前需要清理或配置为仅在开发环境中输出。
function login(username, password) {
    console.log('开始登录,用户名:', username); 
    // 模拟登录验证
    if (username === 'admin' && password === '123456') {
        console.log('登录成功'); 
        return true; 
    } else {
        console.log('登录失败'); 
        return false; 
    }
}
login('admin', '123456'); 
  • 使用日志库:在大型项目中,推荐使用专业的日志库,如 winston(Node.js)或 log4js(Node.js)。以 winston 为例:
const winston = require('winston'); 

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transport.Console(),
        new winston.transport.File({ filename: 'app.log' })
    ]
});

function performTask() {
    logger.info('开始执行任务'); 
    try {
        // 任务逻辑
        let result = 10 / 2; 
        logger.info('任务执行成功,结果:', result); 
    } catch (error) {
        logger.error('任务执行失败:', error.message); 
    }
}
performTask(); 

winston 可以灵活配置日志级别(如 infowarnerror 等),将日志输出到控制台和文件,方便调试和排查问题。

  1. 代码审查
    • 团队代码审查:在团队开发中,代码审查是一种重要的发现错误和提高代码质量的方式。通过团队成员之间互相审查代码,可以发现逻辑错误、潜在的性能问题、代码风格不一致等。例如,在审查一个 JavaScript 函数时,发现其中对数组的操作使用了多次循环,而实际上可以使用更高效的数组方法,如 mapfilter 等,从而进行优化。
    • 自我审查:开发者自己在完成代码编写后,也应该进行自我审查。可以从代码逻辑、变量命名、错误处理等方面进行检查。比如,检查函数的参数是否有合理的验证,是否对可能出现的错误进行了适当的处理,变量命名是否清晰易懂等。在审查以下代码时:
function calculate(a, b) {
    if (typeof a!== 'number' || typeof b!== 'number') {
        // 这里没有返回错误处理结果,直接进行计算会导致潜在错误
        let result = a + b; 
        return result; 
    }
}

通过自我审查,可以发现对参数类型的检查没有正确处理,应返回错误提示,而不是继续执行可能导致错误的计算。

  1. 单元测试与调试
    • 单元测试框架:在 JavaScript 中,常用的单元测试框架有 JestMocha 等。以 Jest 为例:
// add.js
function add(a, b) {
    return a + b; 
}
module.exports = add; 

// add.test.js
const add = require('./add'); 

test('两数相加', () => {
    expect(add(2, 3)).toBe(5); 
});
  • 结合调试:当单元测试失败时,可以在测试代码中添加断点或 console.log 语句来调试。例如,在上述 add 函数的测试中,如果测试失败,可以在 add 函数内部添加 console.log 输出参数值,查看是否是参数传递或计算逻辑的问题。同时,在 Jest 中,可以使用 --watchAll=false 命令来一次性运行所有测试,并且结合 IDE 的调试功能,在测试执行到断点处时,查看变量值和调用堆栈,帮助定位错误。

通过深入了解 JavaScript 的错误类型、掌握有效的错误处理机制以及熟练运用各种调试技巧,开发者能够更高效地开发出健壮、稳定的 JavaScript 应用程序。无论是前端 Web 开发还是后端 Node.js 开发,这些知识和技能都是必不可少的。在实际开发过程中,要根据具体的场景和需求,灵活选择和运用这些方法,以提升代码质量和开发效率。