Skip to content
On this page

JavaScript 调试技巧

JavaScript调试是开发过程中的重要技能。掌握有效的调试技巧可以显著提高开发效率,快速定位和解决问题。

调试工具概览

浏览器开发者工具

现代浏览器都提供了强大的开发者工具,是JavaScript调试的主要工具。

主要面板功能:

  • Elements: 查看和编辑DOM结构
  • Console: 查看日志、执行代码、查看错误
  • Sources: 设置断点、单步调试、查看调用栈
  • Network: 监控网络请求
  • Performance: 性能分析
  • Memory: 内存分析
  • Application: 存储、缓存等应用数据

Node.js 调试工具

bash
node --inspect app.js          # 启动调试模式
node --inspect-brk app.js      # 在第一行暂停
node --inspect=0.0.0.0:9229    # 允许远程调试

Console API 详解

基本输出方法

javascript
// 1. 基本日志输出
console.log('普通日志');
console.info('信息日志');
console.warn('警告信息');
console.error('错误信息');

// 2. 格式化输出
const user = { name: 'John', age: 30 };
console.log('用户信息: %o', user);
console.log('用户姓名: %s, 年龄: %d', user.name, user.age);

// 3. 对象和数组的特殊输出
console.log('用户对象:', user);
console.table([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }]); // 以表格形式显示
console.dir(document.body); // 显示对象的属性

// 4. 条件日志
const DEBUG = true;
if (DEBUG) {
  console.log('调试信息:', { step: 1, data: 'initial' });
}

// 5. 分组日志
console.group('用户操作流程');
console.log('步骤 1: 用户登录');
console.log('步骤 2: 数据加载');
console.log('步骤 3: 界面更新');
console.groupEnd();

// 带标签的分组
console.group('API调用');
console.log('请求: GET /api/users');
console.log('响应: 200 OK');
console.groupEnd();

高级 Console 技巧

javascript
// 1. 计数器
function processItem(item) {
  console.count('处理项目'); // 自动计数
  return item * 2;
}

processItem(1); // "处理项目: 1"
processItem(2); // "处理项目: 2"

// 2. 时间测量
console.time('数据处理');
// 执行一些操作
for (let i = 0; i < 1000000; i++) {
  Math.random();
}
console.timeEnd('数据处理'); // 显示耗时

// 3. 条件断言
const value = 5;
console.assert(value > 10, '值应该大于10,但实际是:', value);

// 4. 追踪调用栈
function a() {
  b();
}

function b() {
  c();
}

function c() {
  console.trace('调用追踪'); // 显示完整的调用栈
}

a();

// 5. 清空控制台
console.clear(); // 清空控制台

断点调试技术

debugger 语句

javascript
function calculateTotal(items) {
  let total = 0;
  
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    debugger; // 程序会在这里暂停
    
    if (item.price > 0) {
      total += item.price;
    }
  }
  
  return total;
}

// 使用示例
const items = [
  { name: 'Item 1', price: 10 },
  { name: 'Item 2', price: 20 },
  { name: 'Item 3', price: 30 }
];

const total = calculateTotal(items);

条件断点

javascript
// 在循环中设置条件断点
for (let i = 0; i < 1000; i++) {
  const value = processData(i);
  
  // 只在特定条件下暂停
  if (i === 500) {
    debugger; // 只在i=500时暂停
  }
  
  // 或者使用条件表达式
  if (value < 0) {
    debugger; // 当value为负数时暂停
  }
}

// 复杂条件断点
function processArray(arr) {
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    
    // 在开发者工具中设置断点,条件为: item.id === 123
    // 这样可以避免在代码中硬编码debugger
    const result = transformItem(item);
  }
}

异常断点

javascript
// 使用开发者工具的"Pause on exceptions"功能
function riskyOperation() {
  try {
    const data = fetchData();
    const processed = JSON.parse(data); // 可能抛出异常
    return processed;
  } catch (error) {
    console.error('处理失败:', error);
    throw error; // 在这里设置断点可以捕获异常
  }
}

// 或者在可能出错的地方设置断点
function parseUserData(userData) {
  // 在开发者工具中设置"Pause on exceptions"
  // 这样任何异常都会暂停执行
  return {
    name: userData.name,
    email: userData.profile.email, // 如果profile不存在会出错
    age: parseInt(userData.age)
  };
}

源码映射和压缩代码调试

处理压缩代码

javascript
// 开发时使用source map
// webpack.config.js 示例
module.exports = {
  devtool: 'source-map', // 生产环境使用
  // devtool: 'eval-source-map', // 开发环境使用
};

// 在压缩代码中设置断点的技巧
function minifiedCode() {
  // 即使代码被压缩,source map也能映射到原始代码
  var a = 1;
  var b = 2;
  var c = a + b; // 在这里设置断点
  return c;
}

网络请求调试

Fetch API 调试

javascript
// 1. 详细的请求日志
async function debugFetch(url, options = {}) {
  console.log('发起请求:', {
    url,
    method: options.method || 'GET',
    headers: options.headers,
    body: options.body
  });
  
  try {
    const response = await fetch(url, options);
    const data = await response.json();
    
    console.log('响应结果:', {
      status: response.status,
      statusText: response.statusText,
      headers: [...response.headers.entries()],
      data
    });
    
    return data;
  } catch (error) {
    console.error('请求失败:', error);
    throw error;
  }
}

// 2. 请求拦截和日志
class ApiDebugger {
  constructor() {
    this.originalFetch = window.fetch;
    this.requestLog = [];
  }
  
  enable() {
    const self = this;
    window.fetch = function(...args) {
      const [url, options = {}] = args;
      const logEntry = {
        timestamp: Date.now(),
        url,
        method: options.method || 'GET',
        startTime: performance.now(),
        requestHeaders: options.headers || {}
      };
      
      self.requestLog.push(logEntry);
      
      return self.originalFetch.apply(this, args)
        .then(response => {
          logEntry.endTime = performance.now();
          logEntry.duration = logEntry.endTime - logEntry.startTime;
          logEntry.status = response.status;
          
          console.log(`API请求: ${url}`, {
            status: response.status,
            duration: `${logEntry.duration.toFixed(2)}ms`,
            size: response.headers.get('content-length')
          });
          
          return response;
        })
        .catch(error => {
          logEntry.error = error.message;
          console.error(`API请求失败: ${url}`, error);
          throw error;
        });
    };
  }
  
  getLog() {
    return this.requestLog;
  }
  
  clearLog() {
    this.requestLog = [];
  }
}

// 使用API调试器
const apiDebugger = new ApiDebugger();
apiDebugger.enable();

内存泄漏调试

检测内存泄漏

javascript
// 1. 监控内存使用
function monitorMemory() {
  if (performance.memory) {
    const memory = performance.memory;
    console.log('内存使用情况:', {
      used: Math.round(memory.usedJSHeapSize / 1048576) + ' MB',
      total: Math.round(memory.totalJSHeapSize / 1048576) + ' MB',
      limit: Math.round(memory.jsHeapSizeLimit / 1048576) + ' MB'
    });
  }
}

// 定期监控
setInterval(monitorMemory, 5000);

// 2. 事件监听器泄漏检测
class EventLeakDetector {
  constructor() {
    this.eventCounts = new Map();
  }
  
  addEventListener(element, event, handler) {
    const key = `${element.tagName}-${event}`;
    const count = this.eventCounts.get(key) || 0;
    this.eventCounts.set(key, count + 1);
    
    console.debug(`添加事件监听器: ${key}, 当前数量: ${count + 1}`);
    
    element.addEventListener(event, handler);
  }
  
  removeEventListener(element, event, handler) {
    const key = `${element.tagName}-${event}`;
    const count = this.eventCounts.get(key) || 0;
    this.eventCounts.set(key, Math.max(0, count - 1));
    
    console.debug(`移除事件监听器: ${key}, 剩余数量: ${Math.max(0, count - 1)}`);
    
    element.removeEventListener(event, handler);
  }
  
  getStats() {
    return Object.fromEntries(this.eventCounts);
  }
}

// 3. 定时器泄漏检测
class TimerLeakDetector {
  constructor() {
    this.timers = new Map();
    this.timerId = 1;
  }
  
  setTimeout(callback, delay, ...args) {
    const id = this.timerId++;
    const timerInfo = {
      id,
      delay,
      created: Date.now(),
      callback: callback.toString().substring(0, 50) + '...'
    };
    
    this.timers.set(id, timerInfo);
    
    const timeoutId = window.setTimeout(() => {
      this.timers.delete(id);
      callback.apply(this, args);
    }, delay);
    
    return timeoutId;
  }
  
  clearTimeout(timeoutId) {
    // 找到对应的timer id并删除
    for (const [id, info] of this.timers) {
      if (info.nativeId === timeoutId) {
        this.timers.delete(id);
        break;
      }
    }
    return window.clearTimeout(timeoutId);
  }
  
  getActiveTimers() {
    return Array.from(this.timers.values());
  }
}

异步代码调试

Promise 调试

javascript
// 1. Promise 链调试
function debugPromiseChain() {
  return Promise.resolve('start')
    .then(value => {
      console.log('Step 1:', value);
      return value + ' -> step1';
    })
    .then(value => {
      console.log('Step 2:', value);
      return value + ' -> step2';
    })
    .then(value => {
      console.log('Step 3:', value);
      return value + ' -> step3';
    })
    .catch(error => {
      console.error('Promise链错误:', error);
      throw error;
    });
}

// 2. Promise.all 调试
async function debugPromiseAll() {
  const promises = [
    fetch('/api/users').then(r => r.json()).then(data => {
      console.log('Users loaded:', data.length);
      return data;
    }),
    fetch('/api/posts').then(r => r.json()).then(data => {
      console.log('Posts loaded:', data.length);
      return data;
    }),
    fetch('/api/comments').then(r => r.json()).then(data => {
      console.log('Comments loaded:', data.length);
      return data;
    })
  ];
  
  try {
    const results = await Promise.all(promises);
    console.log('所有数据加载完成');
    return results;
  } catch (error) {
    console.error('Promise.all失败:', error);
    throw error;
  }
}

// 3. 未处理的Promise拒绝
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的Promise拒绝:', {
    reason: event.reason,
    promise: event.promise,
    timestamp: event.timeStamp
  });
  
  // 阻止默认的错误处理
  event.preventDefault();
});

// 4. Promise 状态监控
class PromiseMonitor {
  constructor() {
    this.pendingPromises = new Map();
    this.promiseId = 0;
  }
  
  wrap(promise, label = 'anonymous') {
    const id = ++this.promiseId;
    this.pendingPromises.set(id, { label, startTime: Date.now() });
    
    const monitoredPromise = promise
      .then(result => {
        const duration = Date.now() - this.pendingPromises.get(id).startTime;
        console.log(`Promise完成 [${label}]: ${duration}ms`);
        this.pendingPromises.delete(id);
        return result;
      })
      .catch(error => {
        const duration = Date.now() - this.pendingPromises.get(id).startTime;
        console.error(`Promise失败 [${label}]: ${duration}ms`, error);
        this.pendingPromises.delete(id);
        throw error;
      });
    
    return monitoredPromise;
  }
  
  getStats() {
    const now = Date.now();
    const stats = Array.from(this.pendingPromises.entries()).map(([id, info]) => ({
      id,
      label: info.label,
      duration: now - info.startTime
    }));
    
    return {
      totalPending: stats.length,
      details: stats
    };
  }
}

// 使用Promise监控器
const promiseMonitor = new PromiseMonitor();

const monitoredPromise = promiseMonitor.wrap(
  fetch('/api/data').then(r => r.json()),
  'API数据加载'
);

调用栈分析

错误堆栈解析

javascript
// 1. 自定义错误堆栈
class CustomError extends Error {
  constructor(message, context = {}) {
    super(message);
    this.name = this.constructor.name;
    this.context = context;
    
    // 保持堆栈跟踪
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
  
  toString() {
    return `${this.name}: ${this.message}\nContext: ${JSON.stringify(this.context, null, 2)}\n${this.stack}`;
  }
}

// 2. 堆栈追踪工具
function getStackTrace() {
  try {
    throw new Error();
  } catch (error) {
    return error.stack;
  }
}

function logCallStack() {
  const stack = getStackTrace();
  const lines = stack.split('\n').slice(2); // 跳过前两行
  
  console.group('调用栈追踪');
  lines.forEach((line, index) => {
    if (line.trim()) {
      console.log(`${index + 1}: ${line.trim()}`);
    }
  });
  console.groupEnd();
}

// 3. 函数调用追踪
class FunctionTracer {
  constructor() {
    this.callHistory = [];
  }
  
  trace(fn, name) {
    const self = this;
    return function(...args) {
      const callInfo = {
        name: name || fn.name,
        args: args,
        timestamp: Date.now(),
        stack: getStackTrace()
      };
      
      self.callHistory.push(callInfo);
      console.log(`调用: ${callInfo.name}`, args);
      
      try {
        const result = fn.apply(this, args);
        console.log(`返回: ${callInfo.name}`, result);
        return result;
      } catch (error) {
        console.error(`错误: ${callInfo.name}`, error);
        throw error;
      }
    };
  }
  
  getHistory() {
    return this.callHistory;
  }
  
  clearHistory() {
    this.callHistory = [];
  }
}

// 使用示例
const tracer = new FunctionTracer();

const tracedFunction = tracer.trace(function add(a, b) {
  return a + b;
}, 'addFunction');

tracedFunction(2, 3); // 会记录调用信息

性能调试

性能分析工具

javascript
// 1. 函数性能监控
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map();
  }
  
  measure(fn, name) {
    const self = this;
    return function(...args) {
      const start = performance.now();
      const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
      
      const result = fn.apply(this, args);
      
      const end = performance.now();
      const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
      
      const metric = {
        name: name || fn.name,
        duration: end - start,
        memoryChange: endMemory - startMemory,
        timestamp: Date.now()
      };
      
      if (!self.metrics.has(metric.name)) {
        self.metrics.set(metric.name, []);
      }
      self.metrics.get(metric.name).push(metric);
      
      console.log(`${metric.name} 执行时间: ${metric.duration.toFixed(2)}ms`);
      
      return result;
    };
  }
  
  getStats(name) {
    if (name) {
      const metrics = this.metrics.get(name);
      if (!metrics) return null;
      
      const durations = metrics.map(m => m.duration);
      return {
        count: metrics.length,
        avg: durations.reduce((a, b) => a + b, 0) / durations.length,
        min: Math.min(...durations),
        max: Math.max(...durations)
      };
    }
    
    const allStats = {};
    for (const [name, metrics] of this.metrics) {
      allStats[name] = this.getStats(name);
    }
    return allStats;
  }
}

// 2. 渲染性能监控
class RenderPerformanceMonitor {
  constructor() {
    this.observer = null;
    this.metrics = [];
  }
  
  startMonitoring() {
    if ('PerformanceObserver' in window) {
      this.observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.entryType === 'measure' || entry.entryType === 'mark') {
            this.metrics.push(entry);
          }
        }
      });
      
      this.observer.observe({ entryTypes: ['measure', 'mark', 'paint', 'navigation'] });
    }
  }
  
  mark(name) {
    if ('performance' in window) {
      performance.mark(name);
    }
  }
  
  measure(name, startMark, endMark) {
    if ('performance' in window) {
      performance.measure(name, startMark, endMark);
    }
  }
  
  getMetrics() {
    return this.metrics;
  }
}

调试工具和库

自定义调试工具

javascript
// 1. 状态调试器
class StateDebugger {
  constructor(initialState = {}) {
    this.state = { ...initialState };
    this.history = [];
    this.watchers = [];
  }
  
  setState(newState) {
    const prevState = { ...this.state };
    this.state = { ...this.state, ...newState };
    
    // 记录状态变化
    const change = {
      timestamp: Date.now(),
      prevState,
      newState,
      changes: this.getChanges(prevState, this.state)
    };
    
    this.history.push(change);
    
    // 通知观察者
    this.watchers.forEach(watcher => watcher(change));
    
    return this.state;
  }
  
  getChanges(prevState, newState) {
    const changes = {};
    const allKeys = new Set([...Object.keys(prevState), ...Object.keys(newState)]);
    
    for (const key of allKeys) {
      if (prevState[key] !== newState[key]) {
        changes[key] = {
          from: prevState[key],
          to: newState[key]
        };
      }
    }
    
    return changes;
  }
  
  subscribe(watcher) {
    this.watchers.push(watcher);
    return () => {
      this.watchers = this.watchers.filter(w => w !== watcher);
    };
  }
  
  getHistory() {
    return this.history;
  }
  
  replay(toIndex) {
    if (toIndex < 0 || toIndex >= this.history.length) {
      throw new Error('Invalid index');
    }
    
    // 恢复到指定状态
    this.state = { ...this.history[toIndex].prevState };
    return this.state;
  }
}

// 2. 函数调用记录器
class FunctionCallLogger {
  constructor() {
    this.calls = [];
  }
  
  log(fn, name) {
    const self = this;
    return function(...args) {
      const callInfo = {
        name: name || fn.name,
        args: JSON.parse(JSON.stringify(args)), // 深拷贝参数
        timestamp: Date.now(),
        result: undefined,
        error: null
      };
      
      try {
        const result = fn.apply(this, args);
        callInfo.result = result;
        self.calls.push(callInfo);
        return result;
      } catch (error) {
        callInfo.error = error.message;
        self.calls.push(callInfo);
        throw error;
      }
    };
  }
  
  getLogs() {
    return this.calls;
  }
  
  filterByName(name) {
    return this.calls.filter(call => call.name === name);
  }
  
  getStats() {
    const stats = {};
    for (const call of this.calls) {
      if (!stats[call.name]) {
        stats[call.name] = {
          count: 0,
          errors: 0,
          avgTime: 0
        };
      }
      
      stats[call.name].count++;
      if (call.error) {
        stats[call.name].errors++;
      }
    }
    return stats;
  }
}

// 使用示例
const callLogger = new FunctionCallLogger();
const loggedAdd = callLogger.log((a, b) => a + b, 'addFunction');
loggedAdd(2, 3);
loggedAdd(5, 7);
console.log(callLogger.getStats());

调试最佳实践

调试策略

javascript
// 1. 渐进式调试方法
class ProgressiveDebugger {
  static debug(fn, options = {}) {
    const { 
      logArgs = true, 
      logResult = true, 
      logTime = true,
      logStack = false 
    } = options;
    
    return function(...args) {
      if (logArgs) {
        console.log(`[DEBUG] ${fn.name} called with:`, args);
      }
      
      if (logStack) {
        console.trace(`[DEBUG] ${fn.name} call stack:`);
      }
      
      const startTime = logTime ? performance.now() : null;
      
      try {
        const result = fn.apply(this, args);
        
        if (logTime) {
          const duration = performance.now() - startTime;
          console.log(`[DEBUG] ${fn.name} executed in ${duration.toFixed(2)}ms`);
        }
        
        if (logResult) {
          console.log(`[DEBUG] ${fn.name} returned:`, result);
        }
        
        return result;
      } catch (error) {
        console.error(`[DEBUG] ${fn.name} threw error:`, error);
        throw error;
      }
    };
  }
}

// 2. 条件调试
function conditionalDebug(condition, message, data) {
  if (condition) {
    console.log(`[CONDITIONAL DEBUG] ${message}:`, data);
  }
}

// 使用示例
const debugCondition = location.hostname === 'localhost';
conditionalDebug(debugCondition, 'API Response', { status: 200, data: [...] });

// 3. 调试配置管理
class DebugConfig {
  constructor() {
    this.config = {
      enabled: false,
      modules: {},
      logLevel: 'info' // 'debug', 'info', 'warn', 'error'
    };
    
    // 从URL参数或localStorage加载配置
    this.loadConfig();
  }
  
  loadConfig() {
    // 从URL参数加载
    const urlParams = new URLSearchParams(window.location.search);
    this.config.enabled = urlParams.get('debug') === 'true';
    
    // 从localStorage加载
    const savedConfig = localStorage.getItem('debugConfig');
    if (savedConfig) {
      try {
        const parsed = JSON.parse(savedConfig);
        this.config = { ...this.config, ...parsed };
      } catch (e) {
        console.warn('Invalid debug config in localStorage');
      }
    }
  }
  
  setModuleEnabled(moduleName, enabled) {
    this.config.modules[moduleName] = enabled;
    this.saveConfig();
  }
  
  isEnabled(moduleName = null) {
    if (!this.config.enabled) return false;
    if (moduleName) {
      return this.config.modules[moduleName] !== false;
    }
    return true;
  }
  
  log(moduleName, level, message, data) {
    if (!this.isEnabled(moduleName) || this.getLogLevelIndex(level) < this.getLogLevelIndex(this.config.logLevel)) {
      return;
    }
    
    const prefix = `[${moduleName}]`;
    switch (level) {
      case 'debug':
        console.debug(prefix, message, data);
        break;
      case 'info':
        console.info(prefix, message, data);
        break;
      case 'warn':
        console.warn(prefix, message, data);
        break;
      case 'error':
        console.error(prefix, message, data);
        break;
    }
  }
  
  getLogLevelIndex(level) {
    const levels = ['debug', 'info', 'warn', 'error'];
    return levels.indexOf(level);
  }
  
  saveConfig() {
    localStorage.setItem('debugConfig', JSON.stringify(this.config));
  }
}

// 全局调试配置实例
const debugConfig = new DebugConfig();

// 使用全局调试配置
function apiCall(url) {
  debugConfig.log('api', 'debug', 'Making API call', { url });
  
  return fetch(url)
    .then(response => {
      debugConfig.log('api', 'debug', 'API response received', { 
        url, 
        status: response.status 
      });
      return response;
    })
    .catch(error => {
      debugConfig.log('api', 'error', 'API call failed', { url, error });
      throw error;
    });
}

常见调试场景

异步调试场景

javascript
// 1. 调试复杂的异步流程
class AsyncFlowDebugger {
  constructor() {
    this.flowId = 0;
    this.flows = new Map();
  }
  
  startFlow(name, initialData = {}) {
    const id = ++this.flowId;
    this.flows.set(id, {
      id,
      name,
      startTime: Date.now(),
      steps: [],
      data: { ...initialData }
    });
    
    console.log(`[ASYNC FLOW] ${name} started (ID: ${id})`);
    return id;
  }
  
  addStep(flowId, stepName, data = {}) {
    const flow = this.flows.get(flowId);
    if (!flow) {
      console.warn(`Flow ${flowId} not found`);
      return;
    }
    
    const step = {
      name: stepName,
      timestamp: Date.now(),
      duration: Date.now() - flow.startTime,
      data: { ...data }
    };
    
    flow.steps.push(step);
    console.log(`[ASYNC FLOW] Step "${stepName}" in flow "${flow.name}" (ID: ${flowId})`);
  }
  
  completeFlow(flowId, result = null) {
    const flow = this.flows.get(flowId);
    if (!flow) {
      console.warn(`Flow ${flowId} not found`);
      return;
    }
    
    const totalTime = Date.now() - flow.startTime;
    console.log(`[ASYNC FLOW] ${flow.name} completed in ${totalTime}ms (ID: ${flowId})`);
    
    this.flows.delete(flowId);
    return { ...flow, result, totalTime };
  }
  
  getFlow(flowId) {
    return this.flows.get(flowId);
  }
}

// 使用异步流程调试器
const asyncDebugger = new AsyncFlowDebugger();

async function complexAsyncOperation(userId) {
  const flowId = asyncDebugger.startFlow('user-data-processing', { userId });
  
  try {
    asyncDebugger.addStep(flowId, 'fetch-user');
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    
    asyncDebugger.addStep(flowId, 'fetch-posts', { user });
    const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
    
    asyncDebugger.addStep(flowId, 'process-data', { postsCount: posts.length });
    const processedData = { user, posts, processedAt: new Date() };
    
    return asyncDebugger.completeFlow(flowId, processedData);
  } catch (error) {
    console.error('Async operation failed:', error);
    asyncDebugger.completeFlow(flowId, { error: error.message });
    throw error;
  }
}

调试是JavaScript开发中的重要技能,通过合理使用工具和技巧,可以快速定位问题并提高开发效率。