10大DOM/BOM核心考点:从入门到精通,让面试官眼前一亮

722 阅读20分钟

前言

上周面试了一位3年经验的前端,简历上写着"精通DOM操作"。我问他事件委托的原理,他说"就是给父元素绑定事件"。我追问"为什么用事件委托?性能提升多少?",他支支吾吾答不上来。

这就是典型的'表面理解':知道概念名词,不懂底层原理和性能影响。

今天这些DOM与BOM操作题目,每道题我都会告诉你:面试官为什么问这个、标准答案怎么说、什么回答会让你直接出局。每题都配"速记公式",面试前一晚看这篇就够了。

1. 什么是DOM?DOM操作有哪些方法?

速记公式:DOM文档树,查询增删改,性能要关注

  • DOM本质:文档对象模型,HTML的树形结构表示
  • 核心操作:查询、创建、插入、删除、修改、样式操作
  • 性能关键:减少重排重绘,批量操作

标准答案

DOM(Document Object Model) 是浏览器将HTML文档解析成的树形数据结构,每个HTML标签都是树中的一个节点。通过DOM API,JavaScript可以访问和操作页面内容、结构和样式。

DOM节点类型:

  • 元素节点(如div、p)
  • 文本节点(元素内的文本内容)
  • 属性节点(元素的属性)
  • 注释节点

常用DOM操作方法:

查询操作:

// 基本查询
document.getElementById('id')           // 按id查询
document.getElementsByClassName('class') // 按class查询(返回HTMLCollection)
document.getElementsByTagName('div')    // 按标签名查询
document.querySelector('.class')        // CSS选择器查询单个
document.querySelectorAll('div.class')  // CSS选择器查询所有(返回NodeList)

// 关系查询
parentNode.children                    // 子元素
element.parentNode                     // 父元素
element.previousElementSibling         // 前一个兄弟元素
element.nextElementSibling             // 后一个兄弟元素

创建和修改:

// 创建节点
const newDiv = document.createElement('div')
const newText = document.createTextNode('Hello')

// 修改内容
element.innerHTML = '<span>内容</span>'  // 设置HTML
element.textContent = '文本内容'         // 设置文本
element.setAttribute('data-id', '123')  // 设置属性
element.classList.add('active')         // 添加类名

// 样式操作
element.style.color = 'red'             // 设置样式
element.style.display = 'none'          // 隐藏元素

插入和删除:

// 插入节点
parent.appendChild(newNode)             // 末尾插入
parent.insertBefore(newNode, refNode)   // 指定位置插入

// 删除节点
parent.removeChild(childNode)           // 删除子节点
element.remove()                        // 删除自身

// 替换节点
parent.replaceChild(newNode, oldNode)   // 替换节点

面试官真正想听什么

这题考察你对DOM操作的系统性理解和性能意识。

很多人只会用querySelector,不知道其他方法,更不知道不同查询方法的性能差异。面试官想看你是否关注操作效率。

加分回答

"在实际项目中,我特别注意DOM操作的性能。比如:

查询优化:

  • 优先使用getElementById,速度最快
  • 复杂查询用querySelector,但避免在循环中频繁使用
  • 缓存查询结果,避免重复查询

批量操作减少重排:

// ❌ 不好的做法 - 多次重排
for (let i = 0; i < 100; i++) {
  const div = document.createElement('div')
  document.body.appendChild(div) // 每次append都会重排
}

// ✅ 好的做法 - 一次重排
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
  const div = document.createElement('div')
  fragment.appendChild(div)
}
document.body.appendChild(fragment) // 只重排一次

class操作优于style操作:

// ❌ 直接修改多个样式
element.style.width = '100px'
element.style.height = '100px'
element.style.backgroundColor = 'red'

// ✅ 使用class批量修改
element.classList.add('active-box')

理解DOM性能让我在开发大型列表、动画等场景时避免了很多卡顿问题。"

减分回答

❌ "DOM就是HTML标签"(理解太表面)

❌ "只用querySelector就行"(不知道性能差异)

❌ 不知道重排重绘的概念(缺乏性能意识)

2. JavaScript中的事件流是怎样的?事件对象有哪些属性?

速记公式:事件三阶段,捕获目标冒泡,对象含信息

  • 事件流三阶段:捕获 → 目标 → 冒泡
  • 事件对象属性:target、currentTarget、type、preventDefault()、stopPropagation()
  • 事件类型:鼠标、键盘、表单、触摸事件

标准答案

事件流(Event Flow) 描述的是事件在DOM结构中传播的顺序,分为三个阶段:

1. 捕获阶段(Capturing Phase) 事件从window对象向下传播到目标元素的父元素。在这个阶段,使用addEventListener的第三个参数为true可以监听捕获阶段。

2. 目标阶段(Target Phase) 事件到达目标元素本身。

3. 冒泡阶段(Bubbling Phase) 事件从目标元素向上冒泡回window对象。大多数事件默认在冒泡阶段处理。

<div id="parent">
  <button id="child">点击我</button>
</div>

<script>
document.getElementById('parent').addEventListener('click', function() {
  console.log('父元素捕获阶段') // 先执行
}, true)

document.getElementById('parent').addEventListener('click', function() {
  console.log('父元素冒泡阶段') // 后执行
}, false)

document.getElementById('child').addEventListener('click', function(e) {
  console.log('目标元素') // 中间执行
})
// 点击按钮输出顺序:父元素捕获阶段 → 目标元素 → 父元素冒泡阶段
</script>

事件对象常用属性:

element.addEventListener('click', function(event) {
  // 事件目标
  console.log(event.target)        // 实际触发事件的元素
  console.log(event.currentTarget) // 绑定事件处理程序的元素(this指向这个)
  
  // 事件信息
  console.log(event.type)          // 事件类型 'click'
  console.log(event.timeStamp)     // 事件发生时间戳
  
  // 鼠标事件
  console.log(event.clientX, event.clientY) // 相对视口的坐标
  console.log(event.pageX, event.pageY)     // 相对文档的坐标
  console.log(event.button)                 // 鼠标按钮
  
  // 键盘事件
  console.log(event.key)           // 按键值 'a'
  console.log(event.keyCode)       // 按键码 65
  console.log(event.ctrlKey)       // 是否按下Ctrl
  
  // 方法
  event.preventDefault()           // 阻止默认行为
  event.stopPropagation()          // 阻止事件传播
})

面试官真正想听什么

这题考察你对事件机制的理解深度和实际调试能力。

事件流是前端开发中的重要概念,能清晰解释说明你理解事件传播机制,这在复杂组件开发中很重要。

加分回答

在开发弹窗组件时,事件流机制帮了大忙。点击弹窗外部关闭弹窗的功能:

// 点击弹窗外部关闭
document.addEventListener('click', function(event) {
  const modal = document.getElementById('modal')
  if (!modal.contains(event.target)) {
    closeModal() // 点击弹窗外部,关闭弹窗
  }
})

// 阻止弹窗内部点击事件冒泡到document
modal.addEventListener('click', function(event) {
  event.stopPropagation() // 防止点击弹窗内部触发外部点击事件
})

事件委托就是利用冒泡机制:

// 给父元素绑定事件,处理动态添加的子元素
list.addEventListener('click', function(event) {
  if (event.target.classList.contains('delete-btn')) {
    deleteItem(event.target.dataset.id)
  }
})

理解事件对象让我能处理复杂交互。比如拖拽功能要用到clientX/clientY,键盘快捷键要用keyCode,表单验证要用preventDefault()阻止提交。

减分回答

❌ "事件就是点击触发函数"(理解太简单)

❌ "target和currentTarget一样"(概念混淆)

❌ 不知道事件流三个阶段(基础不扎实)

3. 事件委托是什么?如何实现?有什么优势?

速记公式:委托父元素,利用冒泡,动态高效

  • 原理:利用事件冒泡,在父元素处理子元素事件
  • 实现:event.target判断实际触发元素
  • 优势:减少内存、动态元素、代码简洁

标准答案

事件委托(Event Delegation) 是一种利用事件冒泡机制的技术,将子元素的事件处理程序绑定到父元素上,通过判断event.target来识别实际触发事件的子元素。

基本实现:

// ❌ 传统做法 - 给每个子元素绑定事件
const items = document.querySelectorAll('.item')
items.forEach(item => {
  item.addEventListener('click', function() {
    console.log('点击了项目:', this.textContent)
  })
})

// ✅ 事件委托 - 只需一个事件监听
const list = document.getElementById('list')
list.addEventListener('click', function(event) {
  // 检查点击的是否是.item元素
  if (event.target.classList.contains('item')) {
    console.log('点击了项目:', event.target.textContent)
  }
})

处理动态添加的元素:

// 动态添加按钮
function addNewItem(text) {
  const item = document.createElement('div')
  item.className = 'item'
  item.textContent = text
  document.getElementById('list').appendChild(item)
  // 无需为新元素单独绑定事件
}

// 事件委托自动处理所有现有和未来的.item元素
document.getElementById('list').addEventListener('click', function(event) {
  if (event.target.classList.contains('item')) {
    console.log('点击:', event.target.textContent)
  }
})

// 添加新项目,自动具有点击事件
addNewItem('新项目1')
addNewItem('新项目2')

事件委托的优势:

  1. 减少内存占用:只需一个事件处理程序,而不是N个
  2. 动态元素支持:新添加的元素自动具有事件处理
  3. 代码简洁:避免循环绑定和内存泄漏风险
  4. 性能更好:事件处理程序更少,初始化更快

面试官真正想听什么

这题考察你对性能优化和代码设计的理解。

事件委托是前端性能优化的重要技巧,能熟练使用说明你有良好的编程习惯和性能意识。

加分回答

在大型数据表格中,事件委托的优势特别明显。我之前做项目管理后台,表格有几百行数据,每行都有编辑、删除按钮:

传统做法的问题:

// 初始化时绑定几百个事件
const rows = document.querySelectorAll('tr')
rows.forEach(row => {
  row.querySelector('.edit-btn').addEventListener('click', handleEdit)
  row.querySelector('.delete-btn').addEventListener('click', handleDelete)
})
// 内存占用大,初始化慢

改用事件委托:

// 只需一个事件监听
document.getElementById('table').addEventListener('click', function(event) {
  const target = event.target
  
  if (target.classList.contains('edit-btn')) {
    const row = target.closest('tr')
    handleEdit(row.dataset.id)
  }
  
  if (target.classList.contains('delete-btn')) {
    const row = target.closest('tr')
    handleDelete(row.dataset.id)
  }
})

性能提升明显,而且新增行自动具有事件处理。

注意点: 要用closest()向上查找,因为可能点击的是按钮内的图标或文本。

减分回答

❌ "事件委托就是给父元素加事件"(理解不准确)

❌ "所有事件都用委托"(不区分场景)

❌ 不知道event.target和closest的用法(实践经验不足)

4. 如何阻止事件冒泡和默认行为?

速记公式:stop冒泡,prevent默认,return全阻

  • 阻止冒泡:event.stopPropagation()
  • 阻止默认:event.preventDefault()
  • 同时阻止:return false(jQuery中)

标准答案

阻止事件冒泡:

事件冒泡是指事件从目标元素向上级元素传播的过程。使用event.stopPropagation()可以阻止事件继续传播。

document.getElementById('child').addEventListener('click', function(event) {
  console.log('子元素点击')
  event.stopPropagation() // 阻止事件冒泡到父元素
})

document.getElementById('parent').addEventListener('click', function() {
  console.log('父元素点击') // 不会执行
})

阻止默认行为:

默认行为是浏览器对特定事件的预设反应,如点击链接跳转、提交表单刷新页面等。使用event.preventDefault()可以阻止这些默认行为。

// 阻止链接跳转
document.getElementById('link').addEventListener('click', function(event) {
  event.preventDefault() // 阻止跳转
  console.log('点击了链接但不跳转')
})

// 阻止表单提交
document.getElementById('form').addEventListener('submit', function(event) {
  event.preventDefault() // 阻止表单提交刷新
  // 执行自定义提交逻辑
  submitForm()
})

return false的行为:

在jQuery事件处理程序中,return false会同时调用event.preventDefault()event.stopPropagation()。但在原生JavaScript中,return false只阻止默认行为,不阻止冒泡。

// jQuery中
$('#link').click(function() {
  return false // 等同于 event.preventDefault() + event.stopPropagation()
})

// 原生JavaScript中  
document.getElementById('link').addEventListener('click', function(event) {
  return false // 只阻止默认行为,不阻止冒泡
})

面试官真正想听什么

这题考察你对事件控制的理解和实际应用经验。

阻止冒泡和默认行为是事件处理中的常见需求,能准确使用说明你对事件机制有扎实理解。

加分回答

在开发下拉菜单时,阻止冒泡很关键:

// 点击按钮显示菜单
toggleBtn.addEventListener('click', function(event) {
  event.stopPropagation() // 阻止冒泡,避免触发document点击事件
  menu.style.display = 'block'
})

// 点击页面其他地方隐藏菜单
document.addEventListener('click', function() {
  menu.style.display = 'none'
})

阻止默认行为的实用场景:

  1. 自定义表单验证
form.addEventListener('submit', function(event) {
  if (!validateForm()) {
    event.preventDefault() // 验证不通过,阻止提交
    showError('请填写完整信息')
  }
})
  1. 右键菜单自定义
document.addEventListener('contextmenu', function(event) {
  event.preventDefault() // 阻止浏览器默认右键菜单
  showCustomMenu(event.clientX, event.clientY) // 显示自定义菜单
})
  1. 拖拽文件上传
dropZone.addEventListener('dragover', function(event) {
  event.preventDefault() // 必须阻止默认行为才能触发drop
})

dropZone.addEventListener('drop', function(event) {
  event.preventDefault() // 阻止浏览器打开文件
  const files = event.dataTransfer.files
  handleFiles(files)
})

理解这些方法让我能精确控制事件行为。

减分回答

❌ "stopPropagation和preventDefault一样"(概念混淆)

❌ "return false什么都能阻止"(不了解jQuery和原生的区别)

❌ 说不出具体使用场景(缺乏实践经验)

5. 浏览器的存储方式有哪些?localStorage和sessionStorage的区别?

速记公式:存储四方式,local永久,session会话,cookie小量

  • 存储方式:Cookie、localStorage、sessionStorage、IndexedDB
  • 核心区别:生命周期、容量、与服务端通信
  • 选择标准:根据数据大小、持久性需求选择

标准答案

浏览器主要存储方式:

存储方式容量生命周期通信/特点主要用途
Cookie约4KB可设置过期时间每次请求自动携带到服务端用户认证、会话管理
localStorage约5MB永久存储,除非手动删除不自动发送到服务端本地缓存、用户偏好设置
sessionStorage约5MB会话期间有效,关闭即清除不自动发送到服务端表单数据暂存、单次会话状态
IndexedDB理论无限(几百MB)永久存储支持事务、索引的NoSQL数据库大量结构化数据、离线应用

基本使用方法:

// localStorage
localStorage.setItem('key', 'value')           // 存储
const value = localStorage.getItem('key')      // 读取
localStorage.removeItem('key')                 // 删除单个
localStorage.clear()                           // 清空所有

// sessionStorage  
sessionStorage.setItem('sessionKey', 'data')
const sessionData = sessionStorage.getItem('sessionKey')

// 存储对象需要JSON序列化
const user = {name: 'Tom', age: 25}
localStorage.setItem('user', JSON.stringify(user))
const savedUser = JSON.parse(localStorage.getItem('user'))

面试官真正想听什么

这题考察你对客户端存储的理解和合理选择能力。

不同存储方式有不同适用场景,能根据需求合理选择说明你有架构思维。

加分回答

在实际项目中,我根据数据特性选择存储方式:

用户登录状态用Cookie:因为需要每次请求自动发送到服务端验证。

用户主题偏好用localStorage

// 保存主题设置
function saveTheme(theme) {
  localStorage.setItem('theme', theme)
}

// 应用保存的主题
function applySavedTheme() {
  const savedTheme = localStorage.getItem('theme')
  if (savedTheme) {
    document.body.className = savedTheme
  }
}

表单草稿用sessionStorage

// 自动保存表单草稿
form.addEventListener('input', function() {
  const formData = new FormData(form)
  sessionStorage.setItem('formDraft', JSON.stringify(Object.fromEntries(formData)))
})

// 页面加载时恢复草稿
window.addEventListener('load', function() {
  const draft = sessionStorage.getItem('formDraft')
  if (draft) {
    // 填充表单数据
  }
})

大量数据用IndexedDB:比如离线笔记应用、图片缓存等。

安全注意: 敏感信息不要存在localStorage,因为容易通过XSS攻击窃取。

减分回答

❌ "localStorage和sessionStorage差不多"(不理解核心区别)

❌ "所有数据都存localStorage"(不考虑安全性和场景)

❌ 不知道存储容量限制(可能造成存储失败)

6. AJAX的原理?fetch和XMLHttpRequest的区别?

速记公式:XHR老将功能全,fetch新秀语法简,async/await最优雅

  • AJAX核心:异步JavaScript和XML,不刷新页面更新数据
  • XHR特点:功能完整、回调复杂、支持进度监控
  • fetch特点:Promise风格、语法简洁、默认不带cookie

标准答案

AJAX(Asynchronous JavaScript and XML) 是一种创建快速动态网页的技术,通过在后台与服务器进行少量数据交换,实现异步更新页面内容,而不需要重新加载整个页面。

XMLHttpRequest 基本使用:

// 创建XHR对象
const xhr = new XMLHttpRequest()

// 配置请求
xhr.open('GET', '/api/data', true) // 异步请求

// 设置回调
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) { // 请求完成
    if (xhr.status === 200) { // 请求成功
      const data = JSON.parse(xhr.responseText)
      console.log('获取数据:', data)
    } else {
      console.error('请求失败:', xhr.status)
    }
  }
}

// 设置超时和错误处理
xhr.timeout = 5000
xhr.ontimeout = function() {
  console.error('请求超时')
}
xhr.onerror = function() {
  console.error('网络错误')
}

// 发送请求
xhr.send()

fetch 基本使用:

// 基本GET请求
fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('网络响应异常')
    }
    return response.json()
  })
  .then(data => {
    console.log('获取数据:', data)
  })
  .catch(error => {
    console.error('请求失败:', error)
  })

// 使用async/await
async function fetchData() {
  try {
    const response = await fetch('/api/data')
    if (!response.ok) throw new Error('网络响应异常')
    const data = await response.json()
    return data
  } catch (error) {
    console.error('请求失败:', error)
  }
}

XHR vs fetch 主要区别:

特性XMLHttpRequestfetch
语法回调函数Promise
请求取消xhr.abort()AbortController
超时设置xhr.timeout需手动实现
进度监控支持不支持
Cookie默认携带默认不携带
错误处理onerror捕获网络错误只在网络故障时reject
响应类型需手动解析内置json()、text()等方法

面试官真正想听什么

这题考察你对网络请求演进的理解和实际应用经验。

能说清区别说明你关注技术发展,有现代化编程意识。

加分回答

在实际项目中,我根据需求选择请求方式:

需要进度监控时用XHR

// 文件上传进度
xhr.upload.onprogress = function(event) {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100
    updateProgress(percent)
  }
}

大多数场景用fetch,配合async/await更优雅:

async function apiCall(url, options = {}) {
  const config = {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    },
    credentials: 'include', // 携带cookie
    ...options
  }
  
  try {
    const response = await fetch(url, config)
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    return await response.json()
  } catch (error) {
    console.error('API调用失败:', error)
    throw error
  }
}

请求取消用AbortController

const controller = new AbortController()

fetch('/api/data', {
  signal: controller.signal
})

// 需要时取消请求
controller.abort()

理解这些区别让我能根据场景选择最合适的方案。

减分回答

❌ "fetch是XHR的替代品"(不准确,各有适用场景)

❌ "只用过axios,不知道原生方法"(缺乏底层理解)

❌ 不知道fetch默认不携带cookie(可能造成认证问题)

7. 什么是跨域?有哪些解决跨域的方法?

速记公式:同源策略保安全,CORS代理JSONP,开发生产各不同

  • 跨域原因:浏览器同源策略限制
  • 解决方案:CORS、代理、JSONP、WebSocket等
  • 选择标准:根据场景和环境选择

标准答案

同源策略(Same-Origin Policy) 是浏览器的安全机制,限制一个源的文档或脚本与另一个源的资源进行交互。"同源"指协议、域名、端口完全相同。

跨域场景示例:

常见跨域解决方案:

1. CORS(跨域资源共享) 服务端设置响应头允许跨域访问:

// Node.js Express示例
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000')
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  res.header('Access-Control-Allow-Credentials', 'true')
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200) // 预检请求直接响应
  }
  next()
})

2. 开发环境代理 使用webpack-dev-server或Vite代理:

// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
}

3. JSONP 利用script标签不受同源策略限制:

function jsonp(url, callback) {
  const callbackName = 'jsonp_callback_' + Date.now()
  window[callbackName] = function(data) {
    delete window[callbackName]
    document.body.removeChild(script)
    callback(data)
  }
  
  const script = document.createElement('script')
  script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
  document.body.appendChild(script)
}

// 使用
jsonp('http://api.example.com/data?callback=?', function(data) {
  console.log('获取数据:', data)
})

4. 其他方案

  • Nginx反向代理
  • WebSocket(不受同源策略限制)
  • postMessage跨文档通信
  • 浏览器禁用安全策略(仅开发环境)

面试官真正想听什么

这题考察你对浏览器安全机制的理解和实际问题解决能力。

跨域是前端开发中的常见问题,能提供多种解决方案说明你有丰富的实战经验。

加分回答

"在实际项目中,我根据环境选择跨域方案:

开发环境用代理,配置简单,避免服务端修改:

// 开发时调用 /api/users → 代理到 http://localhost:8080/users
fetch('/api/users')

生产环境用CORS,服务端精确控制访问权限:

// Nginx配置
location /api {
  add_header Access-Control-Allow-Origin $http_origin;
  add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
  add_header Access-Control-Allow-Headers 'Authorization, Content-Type';
  
  if ($request_method = 'OPTIONS') {
    return 204;
  }
  
  proxy_pass http://backend;
}

简单跨域用JSONP,但只支持GET请求,安全性较差。

理解预检请求很重要:复杂请求(如Content-Type为application/json)会先发OPTIONS预检请求,服务端必须正确处理。"

减分回答

❌ "跨域就是后端没配置CORS"(理解片面)

❌ "JSONP可以解决所有跨域问题"(不了解局限性)

❌ 不知道预检请求机制(可能遇到莫名奇妙的错误)

8. WebSocket的使用场景?与HTTP的区别?

速记公式:WebSocket全双工,实时低延迟,HTTP请求响应,简单无状态

  • WebSocket特点:双向通信、低延迟、持久连接
  • HTTP特点:请求-响应模式、无状态、简单易用
  • 适用场景:实时应用用WebSocket,普通API用HTTP

标准答案

WebSocket 是一种在单个TCP连接上进行全双工通信的协议,适用于需要实时双向数据交换的场景。

WebSocket基本使用:

// 创建WebSocket连接
const socket = new WebSocket('ws://localhost:8080')

// 连接打开
socket.onopen = function(event) {
  console.log('WebSocket连接已建立')
  socket.send('Hello Server!') // 发送消息
}

// 接收消息
socket.onmessage = function(event) {
  console.log('收到消息:', event.data)
  const data = JSON.parse(event.data)
  handleMessage(data)
}

// 连接关闭
socket.onclose = function(event) {
  console.log('WebSocket连接已关闭', event.code, event.reason)
}

// 错误处理
socket.onerror = function(error) {
  console.error('WebSocket错误:', error)
}

// 发送消息
function sendMessage(message) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(message))
  }
}

// 关闭连接
function closeConnection() {
  socket.close(1000, '正常关闭')
}

WebSocket vs HTTP 主要区别:

特性WebSocketHTTP
通信模式全双工双向通信半双工请求-响应
连接方式持久连接短连接(HTTP/1.1可保持)
头部开销首次握手后头部很小每次请求都有完整头部
实时性服务器可主动推送必须客户端主动请求
适用场景实时聊天、游戏、监控网页浏览、API调用

WebSocket适用场景:

  1. 实时聊天应用:消息即时收发
  2. 在线游戏:玩家状态实时同步
  3. 实时数据监控:股票行情、系统监控
  4. 协同编辑:多用户实时协作
  5. 在线通知:消息推送、状态更新

面试官真正想听什么

这题考察你对不同通信协议的理解和架构设计能力。

能说清适用场景说明你有技术选型能力,知道什么时候该用什么技术。

加分回答

"我在实时协作项目中用过WebSocket:

聊天室功能

class ChatRoom {
  constructor() {
    this.socket = new WebSocket('wss://chat.example.com')
    this.setupEventHandlers()
  }
  
  setupEventHandlers() {
    this.socket.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.displayMessage(message)
    }
    
    this.socket.onclose = () => {
      this.showReconnectButton()
    }
  }
  
  sendMessage(text) {
    const message = {
      type: 'chat',
      text: text,
      timestamp: Date.now()
    }
    this.socket.send(JSON.stringify(message))
  }
  
  // 心跳检测保持连接
  startHeartbeat() {
    setInterval(() => {
      if (this.socket.readyState === WebSocket.OPEN) {
        this.socket.send(JSON.stringify({type: 'ping'}))
      }
    }, 30000)
  }
}

与HTTP混合使用:普通数据请求用HTTP REST API,实时功能用WebSocket。

注意重连机制:网络异常时自动重连,避免连接中断。"

减分回答

❌ "WebSocket比HTTP快"(不准确,适用场景不同)

❌ "所有实时功能都用WebSocket"(不考虑复杂度)

❌ 不知道心跳检测和重连机制(连接可靠性差)

9. 什么是JWT?前端如何处理用户认证?

速记公式:JWT三部分,头载荷签名,前端存token,请求带Authorization

  • JWT结构:Header.Payload.Signature
  • 前端存储:localStorage、sessionStorage、httpOnly Cookie
  • 请求携带:Authorization头、自动拦截器

标准答案

JWT(JSON Web Token) 是一种开放标准,用于在各方之间安全地传输信息作为JSON对象。

JWT结构:

  • Header:令牌类型和签名算法
  • Payload:包含声明(用户ID、过期时间等)
  • Signature:验证令牌完整性和来源
// JWT示例
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

// 解码后得到:
// Header: {"alg":"HS256","typ":"JWT"}
// Payload: {"sub":"1234567890","name":"John Doe","iat":1516239022}

前端认证流程:

  1. 用户登录获取JWT
  2. 安全存储JWT
  3. 请求携带JWT
  4. 处理过期和刷新
// 登录获取token
async function login(username, password) {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  })
  
  if (response.ok) {
    const { token, user } = await response.json()
    // 存储token和用户信息
    localStorage.setItem('token', token)
    localStorage.setItem('user', JSON.stringify(user))
    return user
  } else {
    throw new Error('登录失败')
  }
}

// 请求自动携带token
async function apiCall(url, options = {}) {
  const token = localStorage.getItem('token')
  
  const config = {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      ...options.headers
    },
    ...options
  }
  
  const response = await fetch(url, config
**速记公式:JWT三部分,头载荷签名,无状态易扩展,需防泄露篡改**

- **JWT本质**:JSON Web Token,开放标准的认证令牌
- **核心结构**:Header(头)、Payload(载荷)、Signature(签名)
- **主要特点**:无状态、自包含、易于跨域使用

### 标准答案

**JWTJSON Web Token)** 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。由于这些信息是经过数字签名的,因此可以被验证和信任。

**JWT的三大组成部分:**

```javascript
// 一个完整的JWT示例(已解码)
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

// 对应三个部分:
// 1. Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
// 2. Payload:   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
// 3. Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header(头部) 包含令牌类型和签名算法:

{
  "alg": "HS256",  // 签名算法:HMAC SHA256
  "typ": "JWT"     // 令牌类型
}

2. Payload(载荷) 包含声明(用户数据和其他元数据):

{
  "sub": "1234567890",      // 主题(用户ID)
  "name": "John Doe",       // 自定义声明
  "iat": 1516239022,        // 签发时间
  "exp": 1516242622,        // 过期时间
  "role": "admin"           // 用户角色
}

3. Signature(签名) 用于验证消息在传递过程中没有被篡改:

// 签名生成公式
HMACSHA256(
  base64UrlEncode(header) + "." + 
  base64UrlEncode(payload),
  secret
)

JWT的工作流程

graph LR
A[用户登录] --> B[服务端生成JWT]
B --> C[返回JWT给前端]
C --> D[前端存储JWT]
D --> E[请求时携带JWT]
E --> F[服务端验证JWT]
F --> G[返回受保护资源]

面试官真正想听什么

这题考察你对现代认证机制的理解和安全意识。

JWT不仅仅是"一个token",而是包含完整安全机制的标准。面试官想看你是否理解签名的作用、如何防止篡改。

加分回答

JWT最巧妙的设计在于签名机制。服务端用密钥对头部和载荷签名,任何对JWT的修改都会导致签名验证失败。

为什么JWT比Session好?

  • 无状态:服务端不需要存储会话信息
  • 易于扩展:适合微服务架构,任何服务都可以验证JWT
  • 跨域友好:适合前后端分离和移动端

但也要注意JWT的缺点

  • 令牌一旦签发,在过期前无法撤销
  • 载荷数据是base64编码,不是加密,敏感信息不能放在里面
  • 令牌长度可能比Session ID大

减分回答

❌ "JWT就是加密的用户信息"(不理解base64编码和加密的区别)

❌ "JWT绝对安全"(不知道JWT也有安全风险)

❌ "JWT可以替代所有认证方案"(不了解适用场景)

前端如何处理用户认证?

速记公式:登录获令牌,存储要安全,请求自动带,过期需刷新

  • 认证流程:登录 → 获取JWT → 存储 → 携带请求 → 处理过期
  • 存储方案:localStorage、sessionStorage、httpOnly Cookie
  • 安全要点:防XSS、防CSRF、定期刷新

标准答案

完整的前端认证流程:

1. 用户登录获取JWT

class AuthService {
  async login(credentials) {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(credentials)
      });

      if (!response.ok) {
        throw new Error('登录失败');
      }

      const { token, user, expiresIn } = await response.json();
      
      // 存储认证信息
      this.setAuthToken(token);
      this.setUserInfo(user);
      this.scheduleTokenRefresh(expiresIn);
      
      return user;
    } catch (error) {
      this.clearAuth();
      throw error;
    }
  }
}

2. 安全存储JWT

class AuthService {
  // 方案1:localStorage(简单但需防XSS)
  setAuthToken(token) {
    localStorage.setItem('access_token', token);
  }

  getAuthToken() {
    return localStorage.getItem('access_token');
  }

  // 方案2:内存存储(刷新页面丢失)
  setAuthTokenInMemory(token) {
    this.memoryToken = token;
  }

  // 方案3:httpOnly Cookie(最安全,但需后端配合)
  // 后端设置httpOnly Cookie,前端无法直接读取
  
  setUserInfo(user) {
    // 用户信息可以安全存储在localStorage
    localStorage.setItem('user_info', JSON.stringify(user));
  }

  clearAuth() {
    localStorage.removeItem('access_token');
    localStorage.removeItem('user_info');
    localStorage.removeItem('refresh_token');
    this.memoryToken = null;
  }
}

3. 请求自动携带JWT

// 请求拦截器
class ApiClient {
  constructor() {
    this.baseURL = '/api';
    this.setupInterceptors();
  }

  setupInterceptors() {
    // 保存原始fetch
    const originalFetch = window.fetch;
    
    // 重写fetch
    window.fetch = async (url, options = {}) => {
      const authToken = authService.getAuthToken();
      
      // 设置认证头
      const headers = {
        'Content-Type': 'application/json',
        ...options.headers,
      };

      if (authToken) {
        headers['Authorization'] = `Bearer ${authToken}`;
      }

      const config = {
        ...options,
        headers,
      };

      let response = await originalFetch(url, config);
      
      // 处理401未授权
      if (response.status === 401) {
        const refreshed = await authService.refreshToken();
        if (refreshed) {
          // 重试原始请求
          config.headers['Authorization'] = `Bearer ${authService.getAuthToken()}`;
          response = await originalFetch(url, config);
        } else {
          authService.clearAuth();
          window.location.href = '/login';
        }
      }

      return response;
    };
  }
}

4. Token刷新机制

class AuthService {
  async refreshToken() {
    try {
      const refreshToken = localStorage.getItem('refresh_token');
      
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${refreshToken}`
        }
      });

      if (response.ok) {
        const { token } = await response.json();
        this.setAuthToken(token);
        return true;
      }
    } catch (error) {
      console.error('Token刷新失败:', error);
    }
    
    return false;
  }

  scheduleTokenRefresh(expiresIn) {
    // 在token过期前5分钟刷新
    const refreshTime = (expiresIn - 300) * 1000;
    
    setTimeout(async () => {
      const success = await this.refreshToken();
      if (!success) {
        this.clearAuth();
        window.location.href = '/login';
      }
    }, refreshTime);
  }
}

5. 路由守卫保护

// Vue Router示例
router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const isAuthenticated = authService.isAuthenticated();

  if (requiresAuth && !isAuthenticated) {
    next('/login');
  } else if (to.path === '/login' && isAuthenticated) {
    next('/dashboard');
  } else {
    next();
  }
});

// React Router示例
const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={props =>
      authService.isAuthenticated() ? (
        <Component {...props} />
      ) : (
        <Redirect to="/login" />
      )
    }
  />
);

面试官真正想听什么

这题考察你的工程实践能力和安全意识。

前端认证不仅仅是"存个token",而是完整的流程设计。面试官想看你是否考虑过各种边界情况和安全风险。

加分回答

在实际项目中,我采用分层防护策略:

1. 存储层安全:

  • 生产环境推荐httpOnly Cookie + CSRF Token双重防护
  • 开发环境用localStorage方便调试
  • 敏感信息绝不存储在客户端

2. 传输层安全:

// 所有请求强制HTTPS
if (location.protocol !== 'https:' && !location.hostname.includes('localhost')) {
  location.href = 'https:' + location.href.substring(5);
}

// 为敏感操作添加额外验证
async function sensitiveOperation(data) {
  // 要求重新输入密码或验证码
  const verified = await verifyIdentity();
  if (!verified) throw new Error('身份验证失败');
  
  return apiCall('/api/sensitive', data);
}

3. 用户体验优化:

  • Token过期前自动刷新,用户无感知
  • 请求失败友好提示,引导重新登录
  • 重要操作记录日志,便于审计

4. 安全监控:

  • 记录异常登录行为
  • 监控Token使用频率
  • 定期强制重新认证

减分回答

❌ "JWT就存localStorage,没什么风险"(安全意识薄弱)

❌ "认证就是登录时调个接口"(不理解完整流程)

❌ "不用考虑Token刷新,让用户重新登录就行"(用户体验差)

实战:完整的认证系统

// 完整的认证服务类
class AuthenticationService {
  constructor() {
    this.isRefreshing = false;
    this.requestsQueue = [];
  }
  
  // 检查认证状态
  isAuthenticated() {
    const token = this.getAuthToken();
    if (!token) return false;
    
    // 检查token是否过期
    const payload = this.decodeToken(token);
    return payload.exp > Date.now() / 1000;
  }
  
  // 解码JWT(不验证签名)
  decodeToken(token) {
    try {
      const payload = token.split('.')[1];
      return JSON.parse(atob(payload));
    } catch (error) {
      return null;
    }
  }
  
  // 处理并发刷新
  async refreshToken() {
    if (this.isRefreshing) {
      return new Promise((resolve) => {
        this.requestsQueue.push(resolve);
      });
    }
    
    this.isRefreshing = true;
    
    try {
      const newToken = await this.performTokenRefresh();
      this.setAuthToken(newToken);
      
      // 处理等待中的请求
      this.requestsQueue.forEach(resolve => resolve(true));
      this.requestsQueue = [];
      
      return true;
    } catch (error) {
      this.requestsQueue.forEach(resolve => resolve(false));
      this.requestsQueue = [];
      return false;
    } finally {
      this.isRefreshing = false;
    }
  }
  
  // 退出登录
  async logout() {
    try {
      // 通知服务端注销
      await fetch('/api/auth/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.getAuthToken()}`
        }
      });
    } catch (error) {
      console.error('退出登录失败:', error);
    } finally {
      this.clearAuth();
      window.location.href = '/login';
    }
  }
}

// 使用示例
const authService = new AuthenticationService();

// 在应用初始化时检查认证状态
function initApp() {
  if (!authService.isAuthenticated()) {
    if (window.location.pathname !== '/login') {
      window.location.href = '/login';
    }
  }
}

10. JavaScript中的安全问题有哪些?如何防范XSS和CSRF?

速记公式:XSS注脚本,CSRF冒请求,转义验输入,Token防伪造

  • XSS:跨站脚本攻击,注入恶意代码
  • CSRF:跨站请求伪造,冒用用户身份
  • 防护措施:输入输出转义、CSP、验证码、Token校验

标准答案

XSS(跨站脚本攻击) 指攻击者在网站中注入恶意脚本,当其他用户访问时执行这些脚本。

XSS类型:

  • 存储型XSS:恶意脚本存储在服务器,所有访问者都会受影响
  • 反射型XSS:恶意脚本通过URL参数传递,需要用户点击
  • DOM型XSS:前端JavaScript不安全地操作DOM导致

XSS防护方案:

// 1. 输入验证和过滤
function sanitizeInput(input) {
  return input
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;')
}

// 2. 输出转义(现代框架自动处理)
// Vue、React等框架默认转义插值表达式

// 3. 设置CSP(Content Security Policy)
// HTTP头或meta标签设置可信来源
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' https://trusted.cdn.com">

// 4. 使用DOMPurify库处理HTML
const cleanHTML = DOMPurify.sanitize(dirtyHTML)
element.innerHTML = cleanHTML

CSRF(跨站请求伪造) 指攻击者诱导用户访问恶意网站,利用用户已登录的身份发起非预期请求。

CSRF防护方案:

// 1. 使用CSRF Token
// 服务端生成Token,前端在请求中携带
async function apiCall(url, data) {
  const token = getCSRFToken() // 从cookie或meta标签获取
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    body: JSON.stringify(data)
  })
  return response
}

// 2. 验证Referer头
// 服务端检查请求来源是否合法

// 3. 使用SameSite Cookie
// 设置Cookie的SameSite属性
document.cookie = "session=abc123; SameSite=Strict"

// 4. 关键操作添加验证码
function transferMoney(amount, toAccount) {
  if (!verifyCaptcha()) {
    throw new Error('请完成验证码验证')
  }
  // 执行转账逻辑
}

其他安全措施:

// HTTPS强制使用
if (location.protocol === 'http:') {
  location.href = 'https:' + location.href.substring(5)
}

// 敏感信息不存储在前端
// ❌ 错误做法
localStorage.setItem('password', '123456')

// ✅ 正确做法 - 敏感信息立即清理
function handlePassword(password) {
  // 使用后立即清理引用
  processPassword(password)
  password = null
}

面试官真正想听什么

这题考察你的安全意识和防护能力。

前端安全是生产环境必须考虑的问题,能系统阐述防护方案说明你有工程化思维。

加分回答

在实际项目中,我采用多层防护策略:

XSS防护组合拳:

  1. 输入层:对用户输入进行白名单验证
  2. 输出层:根据上下文选择转义方式
    • HTML上下文:转义 < > & " '
    • URL上下文:编码特殊字符
    • JavaScript上下文:JSON序列化
  3. 框架层:利用Vue/React的自动转义
  4. 传输层:设置CSP策略

CSRF防护实践:

// 自动为所有请求添加CSRF Token
const originalFetch = window.fetch
window.fetch = function(...args) {
  const [url, options = {}] = args
  const token = getCSRFToken()
  
  const newOptions = {
    ...options,
    headers: {
      ...options.headers,
      'X-CSRF-Token': token
    }
  }
  
  return originalFetch(url, newOptions)
}

// 双重Cookie验证
function setAuthCookies() {
  // 主Cookie HttpOnly
  document.cookie = "session=abc123; HttpOnly; Secure"
  // 辅助Cookie用于前端验证
  document.cookie = "csrf_token=xyz789; SameSite=Strict"
}

安全开发习惯:

  • 代码审查时重点关注安全漏洞
  • 使用安全扫描工具定期检查
  • 关注安全公告和漏洞信息

减分回答

❌ "用了框架就安全了"(过度依赖框架)

❌ "小项目不用考虑安全"(安全意识薄弱)

❌ 不知道常见的攻击类型和防护措施(安全知识缺乏)

总结

这些DOM与BOM操作题目,是前端面试的基础关。能清晰回答这些问题,说明你的基础扎实;答不上来,再花哨的框架经验也难让人信服。

每道题的核心不是死记硬背,而是理解:

  • 浏览器为什么这样设计(设计理念)
  • 你在项目中怎么应用的(实战经验)
  • 性能影响和安全性考虑(工程思维)

学习建议:

  1. 理解机制原理:搞懂事件流、存储生命周期等核心概念
  2. 动手实践验证:在控制台测试不确定的特性
  3. 关注性能安全:养成性能优化和安全防护的意识

留言区互动: 这10题里,你在实际项目中用得最多的是哪个技术?或者遇到过什么有意思的问题?

在评论区告诉我,点赞最高的问题,我会单独写一篇深度解析!