最佳实践

JavaScript 开发的最佳实践涵盖代码组织、性能优化、安全性等多个方面。

代码组织

模块化开发

// utils/math.js
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}

export class Calculator {
  static add(a, b) {
    return a + b
  }
}

// utils/validation.js
export function isValidEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return regex.test(email)
}

export function isValidPassword(password) {
  return password.length >= 8
}

// main.js
import { add, multiply, Calculator } from './utils/math.js'
import { isValidEmail, isValidPassword } from './utils/validation.js'

console.log(add(2, 3))                    // 5
console.log(Calculator.add(4, 5))         // 9
console.log(isValidEmail('test@example.com')) // true

单一职责原则

// 不好的做法:一个函数做太多事情
function processUserData(data) {
  // 验证数据
  if (!data.name || !data.email) {
    throw new Error('Invalid data')
  }

  // 保存到数据库
  saveToDatabase(data)

  // 发送通知邮件
  sendNotificationEmail(data.email)

  // 更新 UI
  updateUI(data)
}

// 好的做法:拆分为多个专门的函数
function validateUserData(data) {
  if (!data.name || !data.email) {
    throw new Error('Invalid data')
  }
}

function saveUserData(data) {
  return saveToDatabase(data)
}

function notifyUser(data) {
  return sendNotificationEmail(data.email)
}

function updateUserInterface(data) {
  updateUI(data)
}

function processUserData(data) {
  validateUserData(data)
  const savedData = saveUserData(data)
  notifyUser(savedData)
  updateUserInterface(savedData)
  return savedData
}

性能优化

防抖和节流

// 防抖:延迟执行,直到停止触发
function debounce(func, delay) {
  let timeoutId
  return function(...args) {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func.apply(this, args), delay)
  }
}

// 节流:限制执行频率
function throttle(func, delay) {
  let lastCall = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      func.apply(this, args)
    }
  }
}

// 使用示例
const debouncedSearch = debounce(searchFunction, 300)
const throttledScroll = throttle(scrollHandler, 16)

input.addEventListener('input', debouncedSearch)
window.addEventListener('scroll', throttledScroll)

内存管理

// 避免内存泄漏
class DataManager {
  constructor() {
    this.data = new Map()
    this.listeners = new Set()
  }

  addData(key, value) {
    this.data.set(key, value)
    this.notifyListeners('add', { key, value })
  }

  removeData(key) {
    if (this.data.has(key)) {
      this.data.delete(key)
      this.notifyListeners('remove', { key })
    }
  }

  addListener(callback) {
    this.listeners.add(callback)
  }

  removeListener(callback) {
    this.listeners.delete(callback)
  }

  notifyListeners(event, data) {
    for (let listener of this.listeners) {
      try {
        listener(event, data)
      } catch (error) {
        console.error('Listener error:', error)
      }
    }
  }

  destroy() {
    // 清理所有引用
    this.data.clear()
    this.listeners.clear()
  }
}

// 正确使用
const manager = new DataManager()

// 添加监听器
const listener = (event, data) => {
  console.log(event, data)
}
manager.addListener(listener)

// 使用完毕后清理
manager.removeListener(listener)
manager.destroy()

懒加载

// 图片懒加载
class LazyImageLoader {
  constructor(images) {
    this.images = images
    this.observer = null
    this.init()
  }

  init() {
    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadImage(entry.target)
            this.observer.unobserve(entry.target)
          }
        })
      })

      this.images.forEach(img => {
        this.observer.observe(img)
      })
    } else {
      // 降级方案:滚动监听
      this.fallbackLoad()
    }
  }

  loadImage(img) {
    const src = img.dataset.src
    if (src) {
      img.src = src
      img.classList.remove('lazy')
    }
  }

  fallbackLoad() {
    const loadImages = () => {
      this.images.forEach(img => {
        if (this.isInViewport(img)) {
          this.loadImage(img)
        }
      })
    }

    window.addEventListener('scroll', loadImages)
    window.addEventListener('resize', loadImages)
    loadImages() // 初始加载
  }

  isInViewport(element) {
    const rect = element.getBoundingClientRect()
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
      rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    )
  }
}

// 使用示例
document.addEventListener('DOMContentLoaded', () => {
  const lazyImages = document.querySelectorAll('img[data-src]')
  new LazyImageLoader(lazyImages)
})

安全性

输入验证和清理

// HTML 转义
function escapeHtml(text) {
  const div = document.createElement('div')
  div.textContent = text
  return div.innerHTML
}

// URL 验证
function isValidUrl(url) {
  try {
    new URL(url)
    return true
  } catch {
    return false
  }
}

// 防止 XSS
function sanitizeInput(input) {
  // 移除危险的 HTML 标签和属性
  return input
    .replace(/<script[^>]*>.*?<\/script>/gi, '')
    .replace(/<[^>]*>/g, '')
    .trim()
}

// SQL 注入防护(客户端)
function sanitizeSqlInput(input) {
  // 移除或转义危险字符
  return input.replace(/['";\\]/g, '\\$&')
}

// CSRF 防护
function addCsrfToken(headers = {}) {
  const token = document.querySelector('meta[name="csrf-token"]')?.content
  if (token) {
    headers['X-CSRF-Token'] = token
  }
  return headers
}

// 使用示例
fetch('/api/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    ...addCsrfToken()
  },
  body: JSON.stringify({
    name: sanitizeInput(userName),
    email: sanitizeInput(userEmail)
  })
})

内容安全策略

<!-- CSP 头部 -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-src 'none';
">
// 动态设置 CSP(有限支持)
if ('csp' in document) {
  document.csp = "default-src 'self'"
}

错误处理

全局错误处理

// 同步错误
window.onerror = function(message, source, lineno, colno, error) {
  console.error('Global error:', {
    message,
    source,
    lineno,
    colno,
    stack: error?.stack
  })

  // 发送错误报告
  reportError({
    message,
    source,
    lineno,
    colno,
    stack: error?.stack,
    userAgent: navigator.userAgent,
    url: window.location.href,
    timestamp: Date.now()
  })

  // 阻止默认错误处理
  return true
}

// Promise 拒绝
window.onunhandledrejection = function(event) {
  console.error('Unhandled promise rejection:', event.reason)

  reportError({
    type: 'unhandledrejection',
    reason: event.reason,
    timestamp: Date.now()
  })

  // 阻止默认处理
  event.preventDefault()
}

// 资源加载错误
window.addEventListener('error', function(event) {
  if (event.target !== window) {
    console.error('Resource load error:', {
      src: event.target.src || event.target.href,
      tagName: event.target.tagName
    })
  }
}, true)

优雅降级

// 特性检测和降级
function setupFeature() {
  if ('geolocation' in navigator) {
    // 使用原生地理位置 API
    navigator.geolocation.getCurrentPosition(success, error)
  } else {
    // 降级方案
    console.log('Geolocation not supported, using fallback')
    useFallbackGeolocation()
  }
}

// API 兼容性处理
function createCompatibleXHR() {
  if (typeof XMLHttpRequest !== 'undefined') {
    return new XMLHttpRequest()
  } else if (typeof ActiveXObject !== 'undefined') {
    return new ActiveXObject('Microsoft.XMLHTTP')
  } else {
    throw new Error('XMLHttpRequest not supported')
  }
}

// 渐进增强
function enhanceUI() {
  const button = document.getElementById('submit-btn')

  // 基础功能
  button.addEventListener('click', basicSubmit)

  // 增强功能
  if ('FormData' in window) {
    button.addEventListener('click', enhancedSubmit)
  }

  if ('serviceWorker' in navigator) {
    setupOfflineSupport()
  }
}

测试

单元测试

// 简单的测试框架
class TestSuite {
  constructor(name) {
    this.name = name
    this.tests = []
    this.passed = 0
    this.failed = 0
  }

  test(name, fn) {
    this.tests.push({ name, fn })
  }

  assert(condition, message = 'Assertion failed') {
    if (!condition) {
      throw new Error(message)
    }
  }

  async run() {
    console.group(`Running test suite: ${this.name}`)

    for (let test of this.tests) {
      try {
        await test.fn.call(this)
        console.log(`${test.name}`)
        this.passed++
      } catch (error) {
        console.error(`${test.name}: ${error.message}`)
        this.failed++
      }
    }

    console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`)
    console.groupEnd()

    return { passed: this.passed, failed: this.failed }
  }
}

// 使用示例
const mathTests = new TestSuite('Math functions')

mathTests.test('Addition', function() {
  this.assert(add(2, 3) === 5, '2 + 3 should equal 5')
  this.assert(add(-1, 1) === 0, '-1 + 1 should equal 0')
})

mathTests.test('Multiplication', function() {
  this.assert(multiply(3, 4) === 12, '3 * 4 should equal 12')
  this.assert(multiply(0, 5) === 0, '0 * 5 should equal 0')
})

mathTests.run()

总结

JavaScript 最佳实践涵盖了开发的全过程:

  1. 代码组织:模块化、单一职责原则
  2. 性能优化:防抖节流、内存管理、懒加载
  3. 安全性:输入验证、XSS 防护、CSP
  4. 错误处理:全局错误处理、优雅降级
  5. 测试:单元测试、断言

遵循这些最佳实践可以提高代码质量、可维护性和安全性。