前言
上周面试了一位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')
事件委托的优势:
- 减少内存占用:只需一个事件处理程序,而不是N个
- 动态元素支持:新添加的元素自动具有事件处理
- 代码简洁:避免循环绑定和内存泄漏风险
- 性能更好:事件处理程序更少,初始化更快
面试官真正想听什么
这题考察你对性能优化和代码设计的理解。
事件委托是前端性能优化的重要技巧,能熟练使用说明你有良好的编程习惯和性能意识。
加分回答
在大型数据表格中,事件委托的优势特别明显。我之前做项目管理后台,表格有几百行数据,每行都有编辑、删除按钮:
传统做法的问题:
// 初始化时绑定几百个事件
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'
})
阻止默认行为的实用场景:
- 自定义表单验证:
form.addEventListener('submit', function(event) {
if (!validateForm()) {
event.preventDefault() // 验证不通过,阻止提交
showError('请填写完整信息')
}
})
- 右键菜单自定义:
document.addEventListener('contextmenu', function(event) {
event.preventDefault() // 阻止浏览器默认右键菜单
showCustomMenu(event.clientX, event.clientY) // 显示自定义菜单
})
- 拖拽文件上传:
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 主要区别:
| 特性 | XMLHttpRequest | fetch |
|---|---|---|
| 语法 | 回调函数 | 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 主要区别:
| 特性 | WebSocket | HTTP |
|---|---|---|
| 通信模式 | 全双工双向通信 | 半双工请求-响应 |
| 连接方式 | 持久连接 | 短连接(HTTP/1.1可保持) |
| 头部开销 | 首次握手后头部很小 | 每次请求都有完整头部 |
| 实时性 | 服务器可主动推送 | 必须客户端主动请求 |
| 适用场景 | 实时聊天、游戏、监控 | 网页浏览、API调用 |
WebSocket适用场景:
- 实时聊天应用:消息即时收发
- 在线游戏:玩家状态实时同步
- 实时数据监控:股票行情、系统监控
- 协同编辑:多用户实时协作
- 在线通知:消息推送、状态更新
面试官真正想听什么
这题考察你对不同通信协议的理解和架构设计能力。
能说清适用场景说明你有技术选型能力,知道什么时候该用什么技术。
加分回答
"我在实时协作项目中用过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}
前端认证流程:
- 用户登录获取JWT
- 安全存储JWT
- 请求携带JWT
- 处理过期和刷新
// 登录获取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(签名)
- **主要特点**:无状态、自包含、易于跨域使用
### 标准答案
**JWT(JSON 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/')
}
// 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防护组合拳:
- 输入层:对用户输入进行白名单验证
- 输出层:根据上下文选择转义方式
- HTML上下文:转义
< > & " ' - URL上下文:编码特殊字符
- JavaScript上下文:JSON序列化
- HTML上下文:转义
- 框架层:利用Vue/React的自动转义
- 传输层:设置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操作题目,是前端面试的基础关。能清晰回答这些问题,说明你的基础扎实;答不上来,再花哨的框架经验也难让人信服。
每道题的核心不是死记硬背,而是理解:
- 浏览器为什么这样设计(设计理念)
- 你在项目中怎么应用的(实战经验)
- 性能影响和安全性考虑(工程思维)
学习建议:
- 理解机制原理:搞懂事件流、存储生命周期等核心概念
- 动手实践验证:在控制台测试不确定的特性
- 关注性能安全:养成性能优化和安全防护的意识
留言区互动: 这10题里,你在实际项目中用得最多的是哪个技术?或者遇到过什么有意思的问题?
在评论区告诉我,点赞最高的问题,我会单独写一篇深度解析!