Skip to content

第 07 章 · 面试题集(DOM 与 BOM)


Q1:事件流的三个阶段是什么?

考察点:事件机制基础

标准答案: JS 事件流分三个阶段:

  1. 捕获阶段:从 window 向目标元素传递
  2. 目标阶段:到达事件目标
  3. 冒泡阶段:从目标向 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:getElementsByXxxquerySelectorAll 的区别?

考察点:DOM API

标准答案

维度getElementsByXxxquerySelectorAll
返回HTMLCollectionNodeList
实时性实时(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 操作性能优化有哪些方法?

考察点:性能优化

标准答案

  1. 批量操作用 DocumentFragment

    js
    const frag = document.createDocumentFragment();
    for (let i = 0; i < 1000; i++) {
      const li = document.createElement('li');
      li.textContent = i;
      frag.appendChild(li);
    }
    ul.appendChild(frag); // 只触发一次回流
  2. 缓存查询结果:避免反复 querySelector

  3. 样式合并:用 className 切换,不要逐条改 style

  4. 离线再上线:先 display:none,改完再显示

  5. 事件委托:减少监听器数量

  6. 节流防抖:scroll、resize、input 高频事件

  7. 虚拟滚动:只渲染可视区域

  8. 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性能好,原生 APIIE 不支持

追问

  • IntersectionObserver 的 threshold 是什么?(触发回调的可见比例)

Q10:什么是 XSS?innerHTML 怎么用才安全?

考察点:安全

标准答案XSS(Cross-Site Scripting)跨站脚本攻击:恶意脚本被注入到页面执行。

innerHTML 会解析 HTML 并执行 <script>onerror 等,是 XSS 高危点。

安全做法

  1. 用户输入用 textContent 不用 innerHTML
  2. 必须用 HTML 时先转义:<&lt;>&gt;"&quot;
  3. 用成熟库(DOMPurify)净化
  4. 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监听什么应用场景
MutationObserverDOM 树变化监听 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 事件)
  • 怎么实现两个列表互相拖动排序?