前言
上周面试了一位3年经验的前端,简历上写着"精通DOM操作"。我问他事件委托的原理,他说"就是给父元素绑定事件"。我追问"为什么用事件委托?性能提升多少?",他支支吾吾答不上来。
这就是典型的'表面理解':知道概念名词,不懂底层原理和性能影响。
今天这5道DOM与BOM操作题目,每道题我都会告诉你:面试官为什么问这个、标准答案怎么说、什么回答会让你直接出局。每题都配"速记公式",面试前一晚看这篇就够了。
1. 什么是DOM?DOM操作有哪些方法?
速记公式:DOM文档树,查询增删改,性能要关注
- DOM本质:文档对象模型,HTML的树形结构表示
- 核心操作:查询、创建、插入、删除、修改、样式操作
- 性能关键:减少重排重绘,批量操作
标准答案
DOM(Document Object Model) 是浏览器将HTML文档解析成的树形数据结构,每个HTML标签都是树中的一个节点。通过DOM API,JavaScript可以访问和操作页面内容、结构和样式。
DOM节点类型:
- 元素节点(如div、p)
- 文本节点(元素内的文本内容)
- 属性节点(元素的属性)
- 注释节点
常用DOM操作方法:
查询操作:
javascript
// 基本查询
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 // 后一个兄弟元素
创建和修改:
javascript
// 创建节点
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' // 隐藏元素
插入和删除:
javascript
// 插入节点
parent.appendChild(newNode) // 末尾插入
parent.insertBefore(newNode, refNode) // 指定位置插入
// 删除节点
parent.removeChild(childNode) // 删除子节点
element.remove() // 删除自身
// 替换节点
parent.replaceChild(newNode, oldNode) // 替换节点
面试官真正想听什么
这题考察你对DOM操作的系统性理解和性能意识。
很多人只会用querySelector,不知道其他方法,更不知道不同查询方法的性能差异。面试官想看你是否关注操作效率。
加分回答
"在实际项目中,我特别注意DOM操作的性能。比如:
查询优化:
- 优先使用getElementById,速度最快
- 复杂查询用querySelector,但避免在循环中频繁使用
- 缓存查询结果,避免重复查询
批量操作减少重排:
javascript
// ❌ 不好的做法 - 多次重排
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操作:
javascript
// ❌ 直接修改多个样式
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对象。大多数事件默认在冒泡阶段处理。
html
<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>
事件对象常用属性:
javascript
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() // 阻止事件传播
})
面试官真正想听什么
这题考察你对事件机制的理解深度和实际调试能力。
事件流是前端开发中的重要概念,能清晰解释说明你理解事件传播机制,这在复杂组件开发中很重要。
加分回答
在开发弹窗组件时,事件流机制帮了大忙。点击弹窗外部关闭弹窗的功能:
javascript
// 点击弹窗外部关闭
document.addEventListener('click', function(event) {
const modal = document.getElementById('modal')
if (!modal.contains(event.target)) {
closeModal() // 点击弹窗外部,关闭弹窗
}
})
// 阻止弹窗内部点击事件冒泡到document
modal.addEventListener('click', function(event) {
event.stopPropagation() // 防止点击弹窗内部触发外部点击事件
})
事件委托就是利用冒泡机制:
javascript
// 给父元素绑定事件,处理动态添加的子元素
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来识别实际触发事件的子元素。
基本实现:
javascript
// ❌ 传统做法 - 给每个子元素绑定事件
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)
}
})
处理动态添加的元素:
javascript
// 动态添加按钮
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个
- 动态元素支持:新添加的元素自动具有事件处理
- 代码简洁:避免循环绑定和内存泄漏风险
- 性能更好:事件处理程序更少,初始化更快
面试官真正想听什么
这题考察你对性能优化和代码设计的理解。
事件委托是前端性能优化的重要技巧,能熟练使用说明你有良好的编程习惯和性能意识。
加分回答
在大型数据表格中,事件委托的优势特别明显。我之前做项目管理后台,表格有几百行数据,每行都有编辑、删除按钮:
传统做法的问题:
javascript
// 初始化时绑定几百个事件
const rows = document.querySelectorAll('tr')
rows.forEach(row => {
row.querySelector('.edit-btn').addEventListener('click', handleEdit)
row.querySelector('.delete-btn').addEventListener('click', handleDelete)
})
// 内存占用大,初始化慢
改用事件委托:
javascript
// 只需一个事件监听
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()
可以阻止事件继续传播。
javascript
document.getElementById('child').addEventListener('click', function(event) {
console.log('子元素点击')
event.stopPropagation() // 阻止事件冒泡到父元素
})
document.getElementById('parent').addEventListener('click', function() {
console.log('父元素点击') // 不会执行
})
阻止默认行为:
默认行为是浏览器对特定事件的预设反应,如点击链接跳转、提交表单刷新页面等。使用event.preventDefault()
可以阻止这些默认行为。
javascript
// 阻止链接跳转
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
只阻止默认行为,不阻止冒泡。
javascript
// jQuery中
$('#link').click(function() {
return false // 等同于 event.preventDefault() + event.stopPropagation()
})
// 原生JavaScript中
document.getElementById('link').addEventListener('click', function(event) {
return false // 只阻止默认行为,不阻止冒泡
})
面试官真正想听什么
这题考察你对事件控制的理解和实际应用经验。
阻止冒泡和默认行为是事件处理中的常见需求,能准确使用说明你对事件机制有扎实理解。
加分回答
在开发下拉菜单时,阻止冒泡很关键:
javascript
// 点击按钮显示菜单
toggleBtn.addEventListener('click', function(event) {
event.stopPropagation() // 阻止冒泡,避免触发document点击事件
menu.style.display = 'block'
})
// 点击页面其他地方隐藏菜单
document.addEventListener('click', function() {
menu.style.display = 'none'
})
阻止默认行为的实用场景:
- 自定义表单验证:
javascript
form.addEventListener('submit', function(event) {
if (!validateForm()) {
event.preventDefault() // 验证不通过,阻止提交
showError('请填写完整信息')
}
})
- 右键菜单自定义:
javascript
document.addEventListener('contextmenu', function(event) {
event.preventDefault() // 阻止浏览器默认右键菜单
showCustomMenu(event.clientX, event.clientY) // 显示自定义菜单
})
- 拖拽文件上传:
javascript
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数据库 | 大量结构化数据、离线应用 |
基本使用方法:
javascript
// 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:
javascript
// 保存主题设置
function saveTheme(theme) {
localStorage.setItem('theme', theme)
}
// 应用保存的主题
function applySavedTheme() {
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
document.body.className = savedTheme
}
}
表单草稿用sessionStorage:
javascript
// 自动保存表单草稿
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"(不考虑安全性和场景)
❌ 不知道存储容量限制(可能造成存储失败)
总结
这5道DOM与BOM操作题目,是前端面试的基础关。能清晰回答这些问题,说明你的基础扎实;答不上来,再花哨的框架经验也难让人信服。
每道题的核心不是死记硬背,而是理解:
- 浏览器为什么这样设计(设计理念)
- 你在项目中怎么应用的(实战经验)
- 性能影响和安全性考虑(工程思维)
学习建议:
- 理解机制原理:搞懂事件流、存储生命周期等核心概念
- 动手实践验证:在控制台测试不确定的特性
- 关注性能安全:养成性能优化和安全防护的意识
留言区互动: 这5题里,你在实际项目中用得最多的是哪个技术?或者遇到过什么有意思的DOM操作问题?
在评论区告诉我,点赞最高的问题,我会单独写一篇深度解析!