主题
第 07 章 · 面试题集(DOM 与 BOM)
Q1:事件流的三个阶段是什么?
考察点:事件机制基础
标准答案: JS 事件流分三个阶段:
- 捕获阶段:从 window 向目标元素传递
- 目标阶段:到达事件目标
- 冒泡阶段:从目标向 window 传递
addEventListener(type, fn, true/false) 第三参数控制是哪个阶段触发,默认 false(冒泡)。
通俗理解: 你在卧室喊一声,捕获阶段就像物业从大门一路找过来确认;目标阶段就是到了卧室;冒泡阶段就是声音再从卧室传回客厅、家门外。
代码示例:
js
document.addEventListener('click', () => console.log('document 捕获'), true);
document.body.addEventListener('click', () => console.log('body 捕获'), true);
btn.addEventListener('click', () => console.log('btn 目标'));
document.body.addEventListener('click', () => console.log('body 冒泡'));
document.addEventListener('click', () => console.log('document 冒泡'));
// 点 btn 输出:
// document 捕获 → body 捕获 → btn 目标 → body 冒泡 → document 冒泡追问:
- 哪些事件不冒泡?(focus、blur、scroll 部分场景、mouseenter/leave)
Q2:什么是事件委托?为什么要用?
考察点:性能优化、事件冒泡应用
标准答案: 事件委托(Event Delegation)是利用事件冒泡,把子元素的事件统一挂到父元素上处理的技术。
优点:
- 只挂一个监听器,性能更好
- 动态新增的子元素自动生效,不用反复绑定
- 减少内存占用
通俗理解: 不在每个房间装门铃,只在大门口装一个统一收。
代码示例:
html
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
</ul>js
document.getElementById('list').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) console.log('点了:', li.textContent);
});
// 之后再 append 新的 li,事件依然能触发加分回答:
- 用
closest()处理嵌套元素更健壮 - focus/blur 不冒泡,用
focusin/focusout替代 - React 的合成事件就是基于事件委托实现的(17 之前委托到 document,17+ 委托到根容器)
追问:
- e.target 和 e.currentTarget 的区别?
Q3:e.target 和 e.currentTarget 区别?
考察点:事件对象
标准答案:
e.target:真正触发事件的元素(最深层的那个)e.currentTarget:当前监听器所绑定的元素(== this)
js
ul.addEventListener('click', function (e) {
console.log('target:', e.target); // 实际点击的 li
console.log('currentTarget:', e.currentTarget); // ul
console.log('this:', this); // ul(箭头函数除外)
});追问:
- 箭头函数里 this 是什么?(外层作用域,不是 currentTarget)
Q4:阻止冒泡和阻止默认行为的区别?
考察点:事件控制
标准答案:
e.stopPropagation():阻止事件继续传播(冒泡或捕获)e.stopImmediatePropagation():阻止冒泡 + 同元素其他监听器e.preventDefault():阻止浏览器的默认行为(如 a 跳转、form 提交、checkbox 切换)return false(仅在 onclick 属性内):相当于preventDefault + stopPropagation
js
a.addEventListener('click', (e) => {
e.preventDefault(); // a 不跳转
e.stopPropagation(); // 不冒泡到外层
customNavigate();
});追问:
- 怎么阻止表单提交?(form 的 submit 事件 preventDefault)
Q5:getElementsByXxx 和 querySelectorAll 的区别?
考察点:DOM API
标准答案:
| 维度 | getElementsByXxx | querySelectorAll |
|---|---|---|
| 返回 | HTMLCollection | NodeList |
| 实时性 | 实时(DOM 改它也改) | 静态快照 |
| 选择器 | 单一(id/tag/class) | CSS 选择器 |
| 性能 | 略快 | 略慢 |
| forEach | ❌ | ✅ |
坑:
js
const lis = document.getElementsByTagName('li'); // length=3
ul.appendChild(document.createElement('li'));
console.log(lis.length); // 4 ← 实时更新
const lis2 = document.querySelectorAll('li'); // length=3
ul.appendChild(document.createElement('li'));
console.log(lis2.length); // 还是 3追问:
- 为什么
for(let i=0; i<getElementsByClassName().length; i++)删 DOM 时容易死循环?(实时变化导致索引失效)
Q6:DOM 操作性能优化有哪些方法?
考察点:性能优化
标准答案:
批量操作用 DocumentFragment
jsconst frag = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const li = document.createElement('li'); li.textContent = i; frag.appendChild(li); } ul.appendChild(frag); // 只触发一次回流缓存查询结果:避免反复 querySelector
样式合并:用 className 切换,不要逐条改 style
离线再上线:先
display:none,改完再显示事件委托:减少监听器数量
节流防抖:scroll、resize、input 高频事件
虚拟滚动:只渲染可视区域
requestAnimationFrame:动画在帧间执行
追问:
- 什么是回流(reflow)和重绘(repaint)?
- 哪些操作会触发回流?(修改尺寸、位置、字体、隐藏/显示等)
Q7:history.pushState 和 location.href 的区别?
考察点:BOM、SPA 路由
标准答案:
| 维度 | location.href = '...' | history.pushState |
|---|---|---|
| 是否刷新页面 | ✅ 刷新 | ❌ 不刷新 |
| URL 是否变 | ✅ | ✅ |
| 是否发请求 | ✅ | ❌ |
| 留下历史记录 | ✅ | ✅ |
| 适合场景 | 跳到另一个页面 | SPA 路由切换 |
js
// SPA 路由实现
function navigate(path) {
history.pushState({ path }, '', path);
render(path);
}
window.addEventListener('popstate', (e) => {
render(e.state?.path || location.pathname);
});追问:
- pushState 的限制?(同源、URL 必须能解析)
- hash 路由 vs history 路由的区别?(hash 不需要服务端配合,history 需要)
Q8:什么是 DocumentFragment?
考察点:性能、DOM API
标准答案: DocumentFragment 是一个轻量级的"虚拟 DOM 容器",本身不在文档中。把元素先 append 到 fragment,再一次性 append 到真实 DOM,可以只触发一次回流/重绘。
js
const frag = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
frag.appendChild(div);
}
container.appendChild(frag); // ← 一次性插入追问:
- 模板
<template>标签和 fragment 关系?(template.content 就是 DocumentFragment)
Q9:怎么实现一个无限滚动?
考察点:IntersectionObserver / scroll 事件
标准答案: 推荐用 IntersectionObserver 监听一个"哨兵"元素。
js
const sentinel = document.querySelector('#sentinel');
const io = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const items = await fetchMore();
appendItems(items);
}
});
io.observe(sentinel);对比 scroll 事件:
| 方案 | 优 | 劣 |
|---|---|---|
| scroll + 节流 | 兼容老浏览器 | 性能差,要算位置 |
| IntersectionObserver | 性能好,原生 API | IE 不支持 |
追问:
- IntersectionObserver 的 threshold 是什么?(触发回调的可见比例)
Q10:什么是 XSS?innerHTML 怎么用才安全?
考察点:安全
标准答案: XSS(Cross-Site Scripting)跨站脚本攻击:恶意脚本被注入到页面执行。
innerHTML 会解析 HTML 并执行 <script>、onerror 等,是 XSS 高危点。
安全做法:
- 用户输入用
textContent不用innerHTML - 必须用 HTML 时先转义:
<→<、>→>、"→" - 用成熟库(DOMPurify)净化
- CSP(Content-Security-Policy)头限制脚本源
js
// ❌ 危险
el.innerHTML = userInput;
// ✅ 安全
el.textContent = userInput;
// 或
el.innerHTML = DOMPurify.sanitize(userInput);追问:
- React 的
dangerouslySetInnerHTML为什么名字这么"吓人"? - XSS 和 CSRF 的区别?
Q11(加餐):MutationObserver / IntersectionObserver / ResizeObserver 各是什么?
考察点:现代 DOM API
标准答案:
| Observer | 监听什么 | 应用场景 |
|---|---|---|
| MutationObserver | DOM 树变化 | 监听 DOM 增删、属性变化(替代 DOM Mutation Events) |
| IntersectionObserver | 元素与视口/容器的交叉 | 懒加载、无限滚动、曝光埋点 |
| ResizeObserver | 元素尺寸变化 | 响应式组件、自适应布局 |
| PerformanceObserver | 性能指标 | 监控 FCP / LCP / CLS |
js
// 监听 DOM 变化
const mo = new MutationObserver(mutations => console.log(mutations));
mo.observe(container, { childList: true, subtree: true, attributes: true });Q12(加餐):怎么实现拖拽?
考察点:mouse 事件 / drag API
简化代码:
js
let startX, startY, originLeft, originTop;
el.addEventListener('mousedown', (e) => {
startX = e.clientX;
startY = e.clientY;
const rect = el.getBoundingClientRect();
originLeft = rect.left;
originTop = rect.top;
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
function onMove(e) {
el.style.left = originLeft + e.clientX - startX + 'px';
el.style.top = originTop + e.clientY - startY + 'px';
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}追问:
- HTML5 Drag and Drop API 用过吗?(draggable + dragstart/dragover/drop 事件)
- 怎么实现两个列表互相拖动排序?