JavaScript WebAPI(进阶)高频面试题
1. DOM 是什么?
DOM(Document Object Model,文档对象模型) 是浏览器将 HTML/XML 文档解析为树形结构的编程接口,允许 JavaScript 动态访问和操作网页的内容、结构和样式。
javascript
// ========== DOM 树结构示意 ==========
// document
// └── html
// ├── head
// │ ├── title
// │ └── meta
// └── body
// ├── div#app
// │ ├── h1
// │ └── p
// └── script
// ========== DOM 节点类型 ==========
// 常见的节点类型(nodeType 值):
// 1 ------ 元素节点(Element) <div>, <p>, <span>
// 2 ------ 属性节点(Attr) class="box"
// 3 ------ 文本节点(Text) "Hello World"
// 8 ------ 注释节点(Comment) <!-- 注释 -->
// 9 ------ 文档节点(Document) document
// 11 --- 文档片段(DocumentFragment)
const el = document.getElementById('app')
console.log(el.nodeType) // 1(元素节点)
console.log(el.nodeName) // 'DIV'
console.log(el.nodeValue) // null(元素节点的 nodeValue 为 null)
// ========== 获取 DOM 元素的方法 ==========
document.getElementById('app') // 通过 ID(返回单个元素)
document.getElementsByClassName('box') // 通过类名(返回 HTMLCollection)
document.getElementsByTagName('div') // 通过标签名(返回 HTMLCollection)
document.querySelector('.box') // CSS 选择器(返回第一个匹配)
document.querySelectorAll('.box') // CSS 选择器(返回 NodeList)
// ⚠️ 注意区别:
// HTMLCollection ------ 实时更新(live),无 forEach
// NodeList ------ 静态快照(querySelectorAll),有 forEach
💡 面试加分点:
getElementById是最快的 DOM 查询方法(直接通过 ID 哈希查找)。querySelector更灵活但稍慢。现代框架(React/Vue)通过虚拟 DOM 减少直接 DOM 操作,因为 DOM 操作是性能瓶颈。
2. Attribute 和 Property 的区别?
Attribute 是 HTML 标签上的属性(定义在 HTML 中),Property 是 DOM 对象上的属性(JavaScript 对象的属性)。
javascript
// ========== 基本区别 ==========
// <input id="username" type="text" value="initial" custom-data="hello">
const input = document.getElementById('username')
// ✅ Attribute(HTML 属性)------ 通过 getAttribute/setAttribute 操作
input.getAttribute('value') // 'initial'(始终返回 HTML 中的初始值)
input.getAttribute('custom-data') // 'hello'(自定义属性只能用 getAttribute)
input.setAttribute('value', 'new') // 修改 HTML 属性
input.hasAttribute('type') // true
input.removeAttribute('custom-data')
// ✅ Property(DOM 属性)------ 通过点号或方括号操作
input.value // 用户输入后的实际值(动态变化)
input.type // 'text'
input.id // 'username'
input.className // 对应 HTML 的 class
// ========== 关键区别 ==========
// 1. Attribute 反映的是 HTML 源码中的初始值
// 2. Property 反映的是 DOM 对象的当前状态
// 示例:用户在输入框中输入 "hello" 后
input.getAttribute('value') // 'initial'(仍然是 HTML 初始值)
input.value // 'hello'(当前实际值)
// ========== 常见对应关系 ==========
// HTML Attribute → DOM Property
// class → className / classList
// for → htmlFor
// style → style(对象)
// checked → checked(boolean)
// disabled → disabled(boolean)
// ✅ classList 操作(推荐)
const el = document.querySelector('.box')
el.classList.add('active') // 添加类
el.classList.remove('active') // 移除类
el.classList.toggle('active') // 切换类
el.classList.contains('active') // 是否包含类
el.classList.replace('old', 'new') // 替换类
// ✅ data-* 自定义属性的推荐方式
// <div data-user-id="123" data-role="admin">
const div = document.querySelector('[data-user-id]')
div.dataset.userId // '123'(自动转为 camelCase)
div.dataset.role // 'admin'
div.dataset.newProp = 'value' // 动态添加 data-new-prop
💡 面试加分点: 对于布尔类型的属性(如
checked、disabled),Attribute 只关心是否存在(<input checked>即为 true),而 Property 是 true/false 的布尔值。建议使用datasetAPI 操作自定义数据属性,而非getAttribute('data-xxx')。
3. DOM 节点操作有哪些?(创建/添加/移除/查找)
javascript
// ========== 1. 创建节点 ==========
const div = document.createElement('div') // 创建元素
const text = document.createTextNode('Hello') // 创建文本节点
const frag = document.createDocumentFragment() // 创建文档片段(不触发重排)
// ========== 2. 添加/插入节点 ==========
const parent = document.getElementById('container')
const child = document.createElement('p')
child.textContent = '新段落'
parent.appendChild(child) // 末尾追加
parent.prepend(child) // 开头插入(新API)
parent.append('文本', child) // 可以同时插入文本和元素
parent.insertBefore(newNode, refNode) // 在参考节点前插入
refNode.after(newNode) // 在参考节点后插入(新API)
refNode.before(newNode) // 在参考节点前插入(新API)
// ✅ 使用 DocumentFragment 批量操作(减少重排)
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
const li = document.createElement('li')
li.textContent = `Item ${i}`
fragment.appendChild(li)
}
document.querySelector('ul').appendChild(fragment) // 只触发一次重排
// ========== 3. 移除/替换节点 ==========
parent.removeChild(child) // 移除子节点
child.remove() // 自我移除(新API,更简洁)
parent.replaceChild(newChild, oldChild) // 替换子节点
oldChild.replaceWith(newChild) // 自我替换(新API)
// ========== 4. 克隆节点 ==========
const clone = node.cloneNode(false) // 浅克隆(只克隆自身)
const deepClone = node.cloneNode(true) // 深克隆(包含所有后代)
// ⚠️ 注意:克隆的节点不包含事件监听器(通过 addEventListener 添加的)
// ========== 5. 查找节点 ==========
el.parentNode // 父节点
el.parentElement // 父元素(忽略非元素节点)
el.children // 子元素集合(HTMLCollection)
el.childNodes // 子节点集合(包含文本节点等)
el.firstElementChild // 第一个子元素
el.lastElementChild // 最后一个子元素
el.nextElementSibling // 下一个兄弟元素
el.previousElementSibling // 上一个兄弟元素
el.closest('.ancestor') // 最近的祖先元素(含自身)
// ========== 6. 修改内容 ==========
el.textContent = '纯文本' // 设置文本(安全,不解析 HTML)
el.innerHTML = '<b>HTML</b>' // 设置 HTML(⚠️ 有 XSS 风险)
el.outerHTML // 包含自身的 HTML
el.innerText // 考虑 CSS 可见性的文本(触发重排)
// ========== 7. 修改样式 ==========
el.style.color = 'red' // 内联样式
el.style.cssText = 'color: red; font-size: 16px' // 批量设置
getComputedStyle(el).color // 获取计算后的样式(只读)
💡 面试加分点:
textContentvsinnerHTMLvsinnerText的区别------textContent性能最好且安全(不解析 HTML),innerHTML可以设置 HTML 但有 XSS 风险,innerText会触发重排(因为要考虑 CSS 计算)。批量 DOM 操作时使用DocumentFragment可以避免多次重排。
4. 事件监听三要素是什么?
事件监听三要素:
- 事件源 ------ 谁触发了事件(DOM 元素)
- 事件类型 ------ 什么事件(click、input、scroll 等)
- 事件处理函数 ------ 做什么(回调函数)
javascript
// ========== 三种绑定事件的方式 ==========
// 1. HTML 内联(不推荐,耦合度高)
// <button onclick="handleClick()">点击</button>
// 2. DOM0 级:通过属性赋值(同类型只能绑定一个)
const btn = document.querySelector('#btn')
btn.onclick = function(e) {
console.log('点击了', e.target)
}
btn.onclick = null // 移除事件
// 3. DOM2 级:addEventListener(推荐,可绑定多个)
function handleClick(e) {
console.log('点击了', e.target)
}
btn.addEventListener('click', handleClick)
btn.removeEventListener('click', handleClick) // 移除(必须传同一个函数引用)
// ========== addEventListener 的第三个参数 ==========
btn.addEventListener('click', handler, {
capture: false, // 是否在捕获阶段触发(默认 false)
once: true, // 是否只触发一次(触发后自动移除)
passive: true, // 不会调用 preventDefault(提升滚动性能)
signal: controller.signal // 通过 AbortController 统一管理
})
// ✅ 使用 AbortController 统一管理事件(现代最佳实践)
const controller = new AbortController()
btn.addEventListener('click', handler1, { signal: controller.signal })
btn.addEventListener('mouseover', handler2, { signal: controller.signal })
window.addEventListener('resize', handler3, { signal: controller.signal })
// 一次性移除所有绑定的事件
controller.abort()
// ========== 常见事件类型 ==========
// 鼠标事件:click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave
// 键盘事件:keydown, keyup, keypress(已废弃)
// 表单事件:input, change, focus, blur, submit, reset
// 触摸事件:touchstart, touchmove, touchend
// 滚动事件:scroll, wheel
// 其他:load, DOMContentLoaded, resize, transitionend, animationend
5. 事件流是什么?什么是事件捕获和事件冒泡?
事件流描述了页面接收事件的顺序,分为三个阶段:
- 捕获阶段(Capture): 从
window→document→html→body→ ... → 目标元素的父级 - 目标阶段(Target): 到达目标元素
- 冒泡阶段(Bubble): 从目标元素 → ... →
body→html→document→window
arduino
// 事件流示意图:
// ① 捕获阶段(从外到内) ③ 冒泡阶段(从内到外)
// window ──────────────▶ ◀────────────── window
// document ────────────▶ ◀──────────── document
// html ────────────────▶ ◀────────────── html
// body ────────────────▶ ◀────────────── body
// div.parent ──────────▶ ◀──────── div.parent
// ② 目标阶段
// button(目标元素)
javascript
// ========== 捕获 vs 冒泡 ==========
const parent = document.querySelector('.parent')
const child = document.querySelector('.child')
// 冒泡阶段触发(默认)
parent.addEventListener('click', () => console.log('parent 冒泡'))
child.addEventListener('click', () => console.log('child 冒泡'))
// 点击 child → 输出:child 冒泡 → parent 冒泡
// 捕获阶段触发(第三个参数设为 true)
parent.addEventListener('click', () => console.log('parent 捕获'), true)
child.addEventListener('click', () => console.log('child 捕获'), true)
// 点击 child → 输出:parent 捕获 → child 捕获
// 完整事件流(同时监听捕获和冒泡)
// 点击 child → parent捕获 → child捕获 → child冒泡 → parent冒泡
// ========== 阻止冒泡 ==========
child.addEventListener('click', (e) => {
e.stopPropagation() // 阻止事件继续传播(冒泡或捕获)
console.log('只有 child 触发')
})
// e.stopImmediatePropagation() 不仅阻止冒泡,还阻止同元素的其他事件监听器
child.addEventListener('click', (e) => {
e.stopImmediatePropagation()
console.log('handler1')
})
child.addEventListener('click', () => {
console.log('handler2') // ❌ 不会执行
})
// ========== 阻止默认行为 ==========
// 阻止链接跳转
document.querySelector('a').addEventListener('click', (e) => {
e.preventDefault()
console.log('链接不会跳转')
})
// 阻止表单提交
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault()
console.log('表单不会提交,可以用 AJAX 代替')
})
// 阻止右键菜单
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
})
💡 面试加分点: 并非所有事件都会冒泡------
focus、blur、mouseenter、mouseleave、scroll(在某些情况下)不冒泡。替代方案:focusin/focusout和mouseover/mouseout会冒泡。React 的合成事件系统将所有事件代理到 root 容器上(React 17+),利用的就是事件冒泡机制。
6. 什么是事件委托?
事件委托(Event Delegation) 利用事件冒泡 机制,将子元素的事件监听器绑定到父元素上,通过 e.target 判断实际触发的元素。
优点:
- 减少事件监听器数量(内存优化)
- 动态添加的子元素也能响应事件
- 简化代码管理
javascript
// ❌ 不使用事件委托(每个 li 都绑定事件)
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', handleClick)
})
// 问题:新增的 li 没有事件;100 个 li 就有 100 个监听器
// ✅ 使用事件委托(只绑定一个事件到 ul)
const list = document.querySelector('ul')
list.addEventListener('click', (e) => {
const li = e.target.closest('li') // 使用 closest 更健壮
if (!li) return // 没有点击到 li 就忽略
if (!list.contains(li)) return // 确保 li 在 list 内部
const { id, action } = li.dataset
console.log(`点击了 id=${id} 的 li,action=${action}`)
// 根据 action 做不同操作
switch (action) {
case 'edit': editItem(id); break
case 'delete': deleteItem(id); break
}
})
// HTML:
// <ul>
// <li data-id="1" data-action="edit"><span>📝</span> 编辑</li>
// <li data-id="2" data-action="delete"><span>🗑️</span> 删除</li>
// </ul>
// ✅ 动态添加元素也能自动响应
const newLi = document.createElement('li')
newLi.dataset.id = '3'
newLi.dataset.action = 'view'
newLi.textContent = '查看'
list.appendChild(newLi) // 无需重新绑定事件
💡 面试加分点: 使用
e.target.closest('li')比e.target === li更健壮------即使点击的是 li 内部的子元素(如 span、icon),也能正确匹配到 li。事件委托是 React 的底层实现思路之一(React 17+ 将事件代理到 root 容器而非 document)。
7. JS 动画与 CSS 动画的区别?
| 对比 | CSS 动画 | JS 动画 |
|---|---|---|
| 实现方式 | transition / @keyframes + animation |
requestAnimationFrame / setInterval |
| 性能 | ✅ GPU 加速(transform/opacity) | 一般,在主线程执行 |
| 控制力 | 有限(不好暂停/反向/动态计算) | ✅ 完全可控(暂停/反向/条件判断) |
| 复杂度 | 适合简单动画 | 适合复杂交互动画 |
| 兼容性 | ✅ 好(现代浏览器) | ✅ 好 |
javascript
// ========== CSS 动画(推荐简单场景) ==========
// CSS:
// .box {
// transition: transform 0.3s ease, opacity 0.3s ease;
// }
// .box.active {
// transform: translateX(100px);
// opacity: 0.5;
// }
// JS 只需切换类名
document.querySelector('.box').classList.add('active')
// CSS @keyframes 动画
// @keyframes fadeIn {
// from { opacity: 0; transform: translateY(20px); }
// to { opacity: 1; transform: translateY(0); }
// }
// .element { animation: fadeIn 0.5s ease forwards; }
// ========== JS 动画(复杂场景用 requestAnimationFrame) ==========
function animate(element, from, to, duration) {
const start = performance.now()
function frame(currentTime) {
const elapsed = currentTime - start
const progress = Math.min(elapsed / duration, 1)
// 缓动函数(easeOutCubic)
const eased = 1 - Math.pow(1 - progress, 3)
const current = from + (to - from) * eased
element.style.transform = `translateX(${current}px)`
if (progress < 1) {
requestAnimationFrame(frame)
}
}
requestAnimationFrame(frame)
}
animate(document.querySelector('.box'), 0, 300, 1000)
// ========== Web Animations API(结合两者优点 ✅) ==========
const el = document.querySelector('.box')
const animation = el.animate([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(300px)', opacity: 0.5 }
], {
duration: 1000,
easing: 'ease-out',
fill: 'forwards'
})
// 完全可控
animation.pause() // 暂停
animation.play() // 播放
animation.reverse() // 反向
animation.cancel() // 取消
animation.finished.then(() => console.log('动画结束'))
💡 面试加分点: 性能最优的做法------能用 CSS 就用 CSS(GPU 加速的
transform和opacity),复杂交互用Web Animations API,最后才考虑requestAnimationFrame。避免动画使用left/top/width/height(会触发重排)。
8. DOM 和 BOM 的区别?
| 对比 | DOM | BOM |
|---|---|---|
| 全称 | Document Object Model | Browser Object Model |
| 作用 | 操作网页内容(HTML/CSS) | 操作浏览器(窗口/导航/历史等) |
| 顶级对象 | document |
window |
| 标准化 | W3C 标准 | 无统一标准(各浏览器不一致) |
javascript
// ========== DOM 相关(操作页面内容) ==========
document.getElementById('app') // 获取元素
document.createElement('div') // 创建元素
document.querySelector('.box') // CSS 选择器查询
document.title = '新标题' // 修改页面标题
document.cookie // 操作 Cookie
// ========== BOM 相关(操作浏览器) ==========
// 1. window 对象(全局对象)
window.innerWidth // 视口宽度
window.innerHeight // 视口高度
window.scrollTo(0, 0) // 滚动到顶部
window.open('https://example.com') // 打开新窗口
window.close() // 关闭当前窗口
// 2. location 对象(URL 信息)
location.href // 完整 URL
location.protocol // 'https:'
location.hostname // 'www.example.com'
location.pathname // '/path/to/page'
location.search // '?key=value'
location.hash // '#section'
location.reload() // 刷新页面
location.assign('https://new-url.com') // 跳转(保留历史)
location.replace('https://new-url.com') // 跳转(不保留历史)
// 3. navigator 对象(浏览器信息)
navigator.userAgent // 浏览器标识字符串
navigator.language // 浏览器语言 'zh-CN'
navigator.onLine // 是否在线
navigator.clipboard // 剪贴板 API
navigator.geolocation // 地理位置 API
navigator.mediaDevices // 媒体设备 API
// 4. history 对象(浏览历史)
history.back() // 后退
history.forward() // 前进
history.go(-2) // 后退两页
history.pushState({ page: 1 }, '', '/page1') // 添加历史记录(不刷新页面)
history.replaceState({}, '', '/page2') // 替换当前历史记录
// 5. screen 对象(屏幕信息)
screen.width // 屏幕宽度
screen.height // 屏幕高度
screen.availWidth // 可用宽度(排除任务栏)
screen.availHeight // 可用高度
💡 面试加分点:
history.pushState是单页应用(SPA)路由的核心------Vue Router 和 React Router 的history模式就是基于此实现的。popstate事件可以监听浏览器前进/后退操作。
9. 定时器有哪些?setTimeout 和 setInterval 的区别?
| 对比 | setTimeout | setInterval |
|---|---|---|
| 执行次数 | 一次 | 重复执行 |
| 清除方法 | clearTimeout |
clearInterval |
| 延迟保证 | 至少 delay 毫秒 | 间隔可能不精确 |
javascript
// ========== setTimeout(延迟执行一次) ==========
const timerId = setTimeout(() => {
console.log('2 秒后执行')
}, 2000)
clearTimeout(timerId) // 取消
// ========== setInterval(重复执行) ==========
let count = 0
const intervalId = setInterval(() => {
count++
console.log(`第 ${count} 次执行`)
if (count >= 5) clearInterval(intervalId) // 达到条件后停止
}, 1000)
// ⚠️ setInterval 的问题:如果回调执行时间超过间隔,会导致堆积
// 回调需要 300ms,间隔 200ms → 可能出现回调排队
// ✅ 推荐用 setTimeout 模拟 setInterval(更可控)
function safeInterval(fn, delay) {
let timer = null
function loop() {
fn()
timer = setTimeout(loop, delay) // 执行完再安排下一次
}
timer = setTimeout(loop, delay)
return () => clearTimeout(timer) // 返回取消函数
}
const cancel = safeInterval(() => {
console.log('安全的定时执行')
}, 1000)
// cancel() // 需要停止时调用
// ========== setTimeout(fn, 0) 的用途 ==========
// 不是立即执行!而是把任务放入宏任务队列
console.log('1')
setTimeout(() => console.log('2'), 0)
console.log('3')
// 输出:1 → 3 → 2(setTimeout 的回调要等同步代码执行完)
// ========== 定时器的最小延迟 ==========
// 嵌套超过 5 层的 setTimeout,最小延迟会被强制设为 4ms
// 页面在后台标签页时,定时器可能被降频到至少 1000ms
// ========== 获取高精度时间 ==========
const start = performance.now() // 微秒级精度
// ... 执行代码 ...
const end = performance.now()
console.log(`耗时: ${end - start}ms`)
💡 面试加分点:
setTimeout的延迟时间不是保证精确的------它只保证回调至少在 delay 毫秒后执行(取决于事件循环和宏任务队列)。如果需要精确的定时动画,应使用requestAnimationFrame。
10. document.write() 和 innerHTML 的区别?
javascript
// ========== document.write() ==========
// 直接向文档流写入内容
// ⚠️ 如果在页面加载完成后调用,会覆盖整个页面!
// 页面加载中调用(追加内容)
document.write('<h1>Hello</h1>') // 追加到当前位置
// 页面加载完成后调用(覆盖整个页面!)
window.onload = function() {
document.write('<h1>覆盖了整个页面!</h1>') // ⚠️ 危险
}
// ========== innerHTML ==========
// 设置/获取元素的 HTML 内容,不会影响页面其他部分
const container = document.getElementById('app')
// 获取
console.log(container.innerHTML)
// 设置(替换该元素的内部内容)
container.innerHTML = '<p>新的内容</p>'
// 追加
container.innerHTML += '<p>追加的内容</p>'
// ⚠️ 注意:+= 会导致已有内容重新解析,已绑定的事件会丢失!
// ✅ 推荐用 insertAdjacentHTML 追加
container.insertAdjacentHTML('beforeend', '<p>安全追加</p>')
// 位置参数:'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'
// ========== 对比 ==========
// | 特性 | document.write() | innerHTML |
// |------------|--------------------|-----------------------|
// | 作用范围 | 整个文档流 | 指定元素内部 |
// | 加载后调用 | ⚠️ 覆盖整个页面 | ✅ 只影响目标元素 |
// | XSS 风险 | ⚠️ 有 | ⚠️ 有 |
// | 推荐程度 | ❌ 几乎不用 | ⚠️ 简单场景可用 |
// ⚠️ XSS 风险示例
const userInput = '<script>alert("XSS")</script>'
container.innerHTML = userInput // 有安全风险
// ✅ 安全替代:使用 textContent(不解析 HTML)
container.textContent = userInput // 安全,显示纯文本
11. 浏览器渲染过程是怎样的?
css
浏览器渲染流水线:
HTML → DOM 树 ──────────┐
├→ 渲染树(Render Tree)→ 布局(Layout)→ 绘制(Paint)→ 合成(Composite)
CSS → CSSOM 树 ─────────┘
详细步骤:
- 解析 HTML → 构建 DOM 树(Document Object Model)
- 解析 CSS → 构建 CSSOM 树(CSS Object Model)
- 合并 DOM 和 CSSOM → 生成 渲染树(Render Tree)
- 布局(Layout/Reflow) → 计算每个节点的位置和大小
- 绘制(Paint) → 将节点绘制到屏幕上
- 合成(Composite) → 将各层合并显示(GPU 加速)
javascript
// ========== 影响渲染的注意事项 ==========
// 1. CSS 放在 <head> 中(避免页面闪烁 FOUC)
// <head>
// <link rel="stylesheet" href="style.css">
// </head>
// 2. JS 放在 </body> 前(或使用 defer/async)
// <script src="app.js" defer></script>
// <script src="analytics.js" async></script>
// defer vs async:
// defer:按顺序执行,在 DOMContentLoaded 之前
// async:下载完立即执行,不保证顺序
// 3. CSS 不阻塞 DOM 解析,但阻塞渲染和 JS 执行
// 4. JS 阻塞 DOM 解析(除非 async/defer)
// ========== DOMContentLoaded vs load ==========
// DOMContentLoaded:DOM 解析完成(不等待图片/样式表)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 解析完成')
})
// load:所有资源加载完成(包括图片、样式表、iframe)
window.addEventListener('load', () => {
console.log('所有资源加载完成')
})
// ========== 关键渲染路径优化 ==========
// 1. 减少关键资源数量(合并文件、内联关键 CSS)
// 2. 压缩资源大小(gzip、minify)
// 3. 使用 preload/prefetch 预加载关键资源
// <link rel="preload" href="critical.css" as="style">
// <link rel="prefetch" href="next-page.js"> // 低优先级预加载
💡 面试加分点:
DOMContentLoaded事件在 DOM 解析完成时触发(不等待图片等资源),load事件在所有资源加载完成后触发。SPA 应用中,首屏渲染优化的关键是减少关键渲染路径(Critical Rendering Path)的长度。
12. DOM 树和渲染树有什么区别?
javascript
// ========== DOM 树(Document Object Model Tree) ==========
// 由 HTML 解析器生成,包含页面中所有的 HTML 节点
// 包括:元素节点、文本节点、注释节点等
// DOM 树包含的(渲染树不包含的):
// 1. <head> 及其子元素(meta, title, link 等)
// 2. display: none 的元素
// 3. <script> 标签
// ========== 渲染树(Render Tree) ==========
// 由 DOM 树 + CSSOM 树合并生成,只包含需要显示的节点
// 每个节点包含可视信息(颜色、尺寸等)
// 渲染树不包含的:
// 1. display: none 的元素(不占空间,不参与渲染)
// 2. <head>, <script>, <meta> 等不可见元素
// 3. 注释节点
// 渲染树包含但 DOM 树没有的:
// 伪元素(::before, ::after)------ 不在 DOM 中,但需要渲染
// ========== visibility: hidden vs display: none ==========
// | 属性 | 是否在渲染树中 | 是否占据空间 | 是否触发重排 |
// |-------------------|-------------|-------------|------------|
// | display: none | ❌ 不在 | ❌ 不占 | ✅ 触发 |
// | visibility: hidden| ✅ 在 | ✅ 占据 | ❌ 不触发 |
// | opacity: 0 | ✅ 在 | ✅ 占据 | ❌ 不触发 |
const el = document.querySelector('.box')
// 从渲染树移除(触发重排)
el.style.display = 'none'
// 仍在渲染树中(只是不可见)
el.style.visibility = 'hidden'
el.style.opacity = '0'
13. 什么是重绘和重排(回流)?如何优化?
重排(Reflow / Layout): 当元素的尺寸、位置、布局 发生变化时,浏览器重新计算元素的几何信息。
重绘(Repaint): 当元素的外观(如颜色、背景、边框)变化但不影响布局时,浏览器重新绘制。
性能影响: 重排 > 重绘(重排必然触发重绘,重绘不一定触发重排)
javascript
// ========== 触发重排的操作 ==========
// 1. 添加/删除 DOM 元素
// 2. 改变元素尺寸(width, height, padding, margin, border)
// 3. 改变元素位置(top, left, position)
// 4. 改变字体大小
// 5. 改变窗口大小(resize)
// 6. 读取某些属性(强制同步布局)
// ⚠️ 读取以下属性会强制触发重排(浏览器必须立即计算最新值):
// offsetTop/Left/Width/Height
// clientTop/Left/Width/Height
// scrollTop/Left/Width/Height
// getComputedStyle()
// getBoundingClientRect()
// ========== 触发重绘(但不重排)的操作 ==========
// 修改 color, background, visibility, box-shadow, outline 等
// ========== 优化策略 ==========
// ❌ 错误:逐个修改样式(每次都可能触发重排)
const el = document.querySelector('.box')
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
// ✅ 优化1:批量修改样式(使用 cssText 或 class)
el.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
// 或者
el.classList.add('new-style')
// ❌ 错误:在循环中读取布局属性(强制同步布局)
for (let i = 0; i < 100; i++) {
items[i].style.width = container.offsetWidth + 'px' // 每次读取都触发重排!
}
// ✅ 优化2:缓存布局属性
const width = container.offsetWidth // 只读一次
for (let i = 0; i < 100; i++) {
items[i].style.width = width + 'px'
}
// ✅ 优化3:使用 DocumentFragment 批量操作 DOM
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
const li = document.createElement('li')
li.textContent = `Item ${i}`
fragment.appendChild(li)
}
document.querySelector('ul').appendChild(fragment) // 只触发一次重排
// ✅ 优化4:脱离文档流后操作
el.style.display = 'none' // 脱离渲染树
// ... 执行多次 DOM 操作 ...
el.style.display = 'block' // 重新加入渲染树(只触发一次重排)
// ✅ 优化5:使用 transform 代替几何属性(GPU 加速,不触发重排)
// ❌ el.style.left = '100px' // 触发重排
// ✅ el.style.transform = 'translateX(100px)' // 不触发重排
// ✅ 优化6:使用 will-change 提示浏览器
// .animated { will-change: transform, opacity; }
// 告诉浏览器该元素会变化,提前做优化准备
💡 面试加分点: 浏览器有重排队列优化 机制------会把多次重排操作合并为一次,但读取布局属性(如
offsetWidth)会强制清空队列并立即执行重排(称为"强制同步布局")。最佳实践:先批量读取,再批量写入。
14. 什么是 requestAnimationFrame?
requestAnimationFrame(rAF) 是浏览器提供的动画 API,会在下一次重绘之前执行回调,频率通常与屏幕刷新率一致(60fps → 约 16.7ms 一次)。
javascript
// ❌ 使用 setTimeout 做动画(不精确,可能掉帧)
function animateWithTimeout(el) {
let start = null
function step() {
// 动画逻辑...
setTimeout(step, 16) // 不一定是 16ms,受其他任务影响
}
step()
}
// ✅ 使用 requestAnimationFrame(与浏览器刷新同步,更流畅)
function animate(el) {
let start = null
function step(timestamp) {
if (!start) start = timestamp
const progress = (timestamp - start) / 1000 // 秒
el.style.transform = `translateX(${progress * 100}px)`
if (progress < 2) {
requestAnimationFrame(step) // 继续下一帧
}
}
requestAnimationFrame(step)
}
// ✅ 实用:节流滚动事件(每帧最多执行一次)
let ticking = false
window.addEventListener('scroll', () => {
if (ticking) return
ticking = true
requestAnimationFrame(() => {
// 更新 UI 逻辑
console.log('scroll:', window.scrollY)
ticking = false
})
})
// ✅ 取消动画
const id = requestAnimationFrame(callback)
cancelAnimationFrame(id)
💡 面试加分点:
requestAnimationFrame在页面不可见(切换标签页)时会自动暂停,节省资源。它的回调在微任务之后、重绘之前执行,是介于微任务和宏任务之间的一种机制。
15. 什么是 IntersectionObserver?
IntersectionObserver 用于异步监测元素与视口(或祖先元素)的交叉状态,常用于懒加载、无限滚动、曝光统计等。
javascript
// ✅ 图片懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src // 将 data-src 赋值给 src
img.classList.add('loaded')
observer.unobserve(img) // 加载后停止观察
}
})
}, {
root: null, // 默认为视口
rootMargin: '200px', // 提前 200px 开始加载
threshold: 0 // 只要出现一点就触发
})
// 观察所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img)
})
// ✅ 无限滚动
const sentinel = document.querySelector('.load-more-trigger')
const scrollObserver = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
await loadMoreData()
}
})
scrollObserver.observe(sentinel)
// ✅ 曝光统计(广告/内容是否被用户看到)
const trackObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
// 元素 50% 以上可见时,记录曝光
trackExposure(entry.target.dataset.id)
trackObserver.unobserve(entry.target)
}
})
}, { threshold: 0.5 })
// ✅ 滚动动画(元素进入视口时触发动画)
const animateObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('animate-in', entry.isIntersecting)
})
}, { threshold: 0.1 })
💡 面试加分点:
IntersectionObserver是替代传统scroll事件 +getBoundingClientRect()的现代方案,性能更好(异步、不阻塞主线程)。配合 CSS 的content-visibility: auto可以实现原生虚拟列表效果。
16. 什么是正则表达式?常用的正则有哪些?
正则表达式(RegExp) 是用于匹配字符串模式的强大工具。
javascript
// ========== 创建正则的两种方式 ==========
const regex1 = /pattern/flags // 字面量(推荐)
const regex2 = new RegExp('pattern', 'flags') // 构造函数(动态模式时用)
// ========== 常用标志(flags) ==========
// g ------ 全局匹配(不只匹配第一个)
// i ------ 不区分大小写
// m ------ 多行匹配
// s ------ 让 . 匹配换行符
// u ------ Unicode 模式
// ========== 常用元字符 ==========
// . 匹配任意字符(除换行)
// \d 匹配数字 [0-9]
// \D 匹配非数字
// \w 匹配字母数字下划线 [a-zA-Z0-9_]
// \W 匹配非字母数字下划线
// \s 匹配空白字符
// \S 匹配非空白字符
// ^ 匹配开头
// $ 匹配结尾
// * 0次或多次
// + 1次或多次
// ? 0次或1次
// {n} 恰好n次
// {n,} 至少n次
// {n,m} n到m次
// ========== 常用方法 ==========
const str = 'Hello World 123'
// test() ------ 测试是否匹配
/\d+/.test(str) // true
// match() ------ 返回匹配结果
str.match(/\d+/) // ['123']
str.match(/\d+/g) // ['123'](全局匹配)
// replace() ------ 替换
str.replace(/world/i, 'JS') // 'Hello JS 123'
// 使用捕获组
'2026-04-16'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1')
// '04/16/2026'
// exec() ------ 逐次匹配(配合 g 标志)
const regex = /\d+/g
let result
while ((result = regex.exec('a1b2c3')) !== null) {
console.log(result[0], result.index)
// '1' 1 → '2' 3 → '3' 5
}
// matchAll() ------ 返回所有匹配(ES2020,推荐替代 exec 循环)
for (const match of 'a1b2c3'.matchAll(/\d+/g)) {
console.log(match[0], match.index)
}
// ========== 常用正则模式 ==========
// 手机号(中国大陆)
/^1[3-9]\d{9}$/.test('13800138000') // true
// 邮箱
/^[\w.-]+@[\w-]+\.[\w.]+$/.test('test@example.com') // true
// URL
/^https?:\/\/.+/.test('https://example.com') // true
// 中文
/[\u4e00-\u9fa5]/.test('你好') // true
// 去除首尾空格
' hello '.replace(/^\s+|\s+$/g, '') // 'hello'(等同于 trim())
// 千分位格式化
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',') // '1,234,567'
// 驼峰转换
'get-user-name'.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) // 'getUserName'
💡 面试加分点: 正则中的贪婪匹配(
*、+)会尽可能多匹配,在后面加?变成非贪婪(*?、+?)。命名捕获组(?<name>...)可以给匹配结果命名,更具可读性。/u标志在处理 emoji 等 Unicode 字符时很重要。
17. Fetch API 和 XMLHttpRequest 的区别?如何使用 Fetch?
| 对比 | XMLHttpRequest | Fetch API |
|---|---|---|
| 语法 | 回调方式,代码冗长 | Promise,简洁优雅 |
| 错误处理 | HTTP 错误不会 reject | ❌ HTTP 错误(404/500)不会 reject |
| 请求取消 | xhr.abort() |
AbortController |
| 进度监听 | ✅ onprogress |
❌ 不支持上传进度 |
| Cookie | 默认携带 | 默认不携带(需设置 credentials) |
| 流式读取 | ❌ | ✅ ReadableStream |
javascript
// ========== XMLHttpRequest(传统方式)==========
function xhrRequest(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`HTTP ${xhr.status}`))
}
}
}
xhr.onerror = () => reject(new Error('Network Error'))
xhr.send()
})
}
// ========== Fetch API(现代方式 ✅)==========
// GET 请求
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) { // ⚠️ Fetch 不会因 HTTP 错误 reject
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
// POST 请求
async function createUser(data) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify(data),
credentials: 'include', // 携带 Cookie
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
// ========== 请求取消(AbortController)==========
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') console.log('请求被取消')
else throw err
})
// 取消请求
controller.abort()
// ✅ 超时控制
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
})
clearTimeout(timeoutId)
return response
} catch (err) {
clearTimeout(timeoutId)
if (err.name === 'AbortError') throw new Error('请求超时')
throw err
}
}
// ========== 封装通用请求函数 ==========
async function request(url, options = {}) {
const { timeout = 10000, ...fetchOptions } = options
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...fetchOptions.headers,
},
})
clearTimeout(timer)
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`)
error.status = response.status
error.response = await response.json().catch(() => null)
throw error
}
return response.json()
} catch (err) {
clearTimeout(timer)
throw err
}
}
💡 面试加分点: Fetch 的一个重要特点是 HTTP 错误状态码(如 404、500)不会 导致 Promise reject------只有网络错误才会 reject。因此必须手动检查
response.ok或response.status。Axios 等库封装了这个问题,自动将非 2xx 状态码视为错误。
18. AJAX 的原理是什么?如何实现?
AJAX(Asynchronous JavaScript And XML) 是一种在不刷新整个页面 的情况下,与服务器交换数据并更新部分网页的技术。核心对象是 XMLHttpRequest(现代用 Fetch API)。
javascript
// ========== AJAX 的工作原理 ==========
// 1. 创建 XMLHttpRequest 对象
// 2. 配置请求(方法、URL、是否异步)
// 3. 发送请求
// 4. 监听状态变化,处理响应数据
// ========== readyState 的 5 种状态 ==========
// 0 - UNSENT:未初始化,尚未调用 open()
// 1 - OPENED:已调用 open(),未调用 send()
// 2 - HEADERS_RECEIVED:已调用 send(),已收到响应头
// 3 - LOADING:正在接收响应体
// 4 - DONE:响应完成
// ========== 手写 AJAX ==========
function ajax({ method = 'GET', url, data, headers = {}, timeout = 10000 }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 如果是 GET 请求,将参数拼接到 URL
if (method.toUpperCase() === 'GET' && data) {
const params = new URLSearchParams(data).toString()
url += (url.includes('?') ? '&' : '?') + params
data = null
}
xhr.open(method, url, true)
xhr.timeout = timeout
// 设置请求头
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value)
}
if (data && typeof data === 'object') {
xhr.setRequestHeader('Content-Type', 'application/json')
data = JSON.stringify(data)
}
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText))
} catch {
resolve(xhr.responseText)
}
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`))
}
}
xhr.onerror = () => reject(new Error('Network Error'))
xhr.ontimeout = () => reject(new Error('Request Timeout'))
xhr.send(data)
})
}
// 使用
const user = await ajax({
method: 'POST',
url: '/api/users',
data: { name: '张三', age: 25 },
headers: { 'Authorization': 'Bearer token123' }
})
// ========== Fetch 版本(现代推荐 ✅)==========
// GET
const users = await fetch('/api/users?page=1').then(r => r.json())
// POST
const newUser = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '张三' }),
}).then(r => r.json())
💡 面试加分点: AJAX 不是一门新技术,而是多种技术的组合(HTML、CSS、JavaScript、XMLHttpRequest、DOM)。现代项目通常使用 Axios 或 Fetch API,而非直接操作 XMLHttpRequest。同源策略是 AJAX 最大的限制,跨域请求需要 CORS 或代理。
19. 什么是 FormData?如何实现文件上传?
FormData 是一个用来构造表单键值对数据的 API,特别适合文件上传和模拟表单提交。
javascript
// ========== 基本用法 ==========
const formData = new FormData()
formData.append('name', '张三')
formData.append('age', '25')
formData.append('avatar', fileInput.files[0]) // 文件
// 从 form 元素创建
const form = document.querySelector('form')
const formDataFromForm = new FormData(form)
// FormData 常用方法
formData.get('name') // '张三'
formData.getAll('tags') // 获取同名多个值
formData.has('name') // true
formData.delete('name')
formData.set('name', '李四') // set 会覆盖,append 会追加
// ========== 文件上传(单文件)==========
async function uploadFile(file) {
const formData = new FormData()
formData.append('file', file)
formData.append('type', 'avatar')
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// ⚠️ 不要手动设置 Content-Type!浏览器会自动加上 multipart/form-data 和 boundary
})
return response.json()
}
// HTML: <input type="file" id="fileInput" accept="image/*">
const fileInput = document.getElementById('fileInput')
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0]
if (file) uploadFile(file)
})
// ========== 文件上传(多文件 + 进度)==========
function uploadWithProgress(files, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData()
for (const file of files) {
formData.append('files', file)
}
const xhr = new XMLHttpRequest()
xhr.open('POST', '/api/upload')
// 上传进度
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100)
onProgress?.(percent)
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`Upload failed: ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('Upload failed'))
xhr.send(formData)
})
}
// ========== 大文件分片上传 ==========
async function uploadLargeFile(file, chunkSize = 2 * 1024 * 1024) {
const totalChunks = Math.ceil(file.size / chunkSize)
const fileId = `${file.name}-${file.size}-${Date.now()}`
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('fileId', fileId)
formData.append('chunkIndex', i.toString())
formData.append('totalChunks', totalChunks.toString())
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData,
})
}
// 通知服务端合并分片
await fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId, fileName: file.name, totalChunks }),
})
}
// ========== 拖拽上传 ==========
const dropZone = document.getElementById('dropZone')
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
dropZone.classList.add('dragover')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover')
})
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
dropZone.classList.remove('dragover')
const files = e.dataTransfer.files
uploadWithProgress(files, (percent) => console.log(`${percent}%`))
})
💡 面试加分点: 使用 FormData 时不要手动设置 Content-Type ,浏览器会自动添加
multipart/form-data及正确的boundary。大文件上传应使用分片上传 + 断点续传方案。Fetch API 不支持上传进度监听,需要用 XMLHttpRequest。
20. 什么是拖拽 API(Drag and Drop)?
javascript
// ========== 拖拽事件流程 ==========
// 被拖拽元素的事件:
// dragstart → drag(持续触发)→ dragend
//
// 目标区域的事件:
// dragenter → dragover(持续触发)→ dragleave / drop
// ========== 基本拖拽实现 ==========
// HTML:
// <div class="item" draggable="true" data-id="1">拖拽我</div>
// <div class="drop-zone">放这里</div>
const draggable = document.querySelector('.item')
const dropZone = document.querySelector('.drop-zone')
// 被拖拽元素
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.dataset.id)
e.dataTransfer.effectAllowed = 'move'
e.target.classList.add('dragging')
})
draggable.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging')
})
// 目标区域
dropZone.addEventListener('dragover', (e) => {
e.preventDefault() // ⚠️ 必须阻止默认行为才能触发 drop
e.dataTransfer.dropEffect = 'move'
dropZone.classList.add('drag-over')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over')
})
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
dropZone.classList.remove('drag-over')
const id = e.dataTransfer.getData('text/plain')
console.log(`放下了 ID 为 ${id} 的元素`)
})
// ========== 列表排序拖拽 ==========
const list = document.querySelector('.sortable-list')
list.addEventListener('dragstart', (e) => {
const item = e.target.closest('.sortable-item')
if (!item) return
e.dataTransfer.effectAllowed = 'move'
item.classList.add('dragging')
})
list.addEventListener('dragover', (e) => {
e.preventDefault()
const dragging = document.querySelector('.dragging')
const afterElement = getDragAfterElement(list, e.clientY)
if (afterElement) {
list.insertBefore(dragging, afterElement)
} else {
list.appendChild(dragging)
}
})
list.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging')
})
function getDragAfterElement(container, y) {
const elements = [...container.querySelectorAll('.sortable-item:not(.dragging)')]
return elements.reduce((closest, child) => {
const box = child.getBoundingClientRect()
const offset = y - box.top - box.height / 2
if (offset < 0 && offset > closest.offset) {
return { offset, element: child }
}
return closest
}, { offset: Number.NEGATIVE_INFINITY }).element
}
💡 面试加分点:
dragover事件必须调用e.preventDefault()才能使元素成为有效的放置目标。dataTransfer对象可以传递数据和设置拖拽效果。实际项目中推荐使用dnd-kit(React)或SortableJS等库。
21. 什么是剪贴板 API?如何实现复制粘贴功能?
javascript
// ========== 现代 Clipboard API(推荐 ✅)==========
// 需要 HTTPS 环境(或 localhost)
// 复制文本
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
console.log('复制成功!')
} catch (err) {
console.error('复制失败:', err)
// 降级方案
fallbackCopy(text)
}
}
// 读取剪贴板文本
async function pasteText() {
try {
const text = await navigator.clipboard.readText()
console.log('粘贴内容:', text)
return text
} catch (err) {
console.error('读取失败:', err)
}
}
// 复制图片
async function copyImage(imgElement) {
const canvas = document.createElement('canvas')
canvas.width = imgElement.naturalWidth
canvas.height = imgElement.naturalHeight
canvas.getContext('2d').drawImage(imgElement, 0, 0)
const blob = await new Promise(resolve => canvas.toBlob(resolve))
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
])
}
// ========== 降级方案(兼容旧浏览器)==========
function fallbackCopy(text) {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy') // 已废弃但仍广泛支持
document.body.removeChild(textarea)
}
// ========== 监听剪贴板事件 ==========
document.addEventListener('copy', (e) => {
e.preventDefault()
const selection = document.getSelection().toString()
// 自定义复制内容(如添加来源信息)
e.clipboardData.setData('text/plain', `${selection}\n\n来源: 我的网站`)
})
document.addEventListener('paste', (e) => {
const text = e.clipboardData.getData('text/plain')
const html = e.clipboardData.getData('text/html')
const files = e.clipboardData.files // 粘贴的文件(如截图)
if (files.length > 0) {
// 处理粘贴的图片
const file = files[0]
if (file.type.startsWith('image/')) {
const url = URL.createObjectURL(file)
console.log('粘贴了图片:', url)
}
}
})
// ========== 实用封装:一键复制按钮 ==========
function createCopyButton(targetSelector) {
const btn = document.createElement('button')
btn.textContent = '复制'
btn.addEventListener('click', async () => {
const text = document.querySelector(targetSelector).textContent
await copyText(text)
btn.textContent = '已复制 ✓'
setTimeout(() => { btn.textContent = '复制' }, 2000)
})
return btn
}
💡 面试加分点:
navigator.clipboardAPI 是异步的,需要用户授权(首次使用时浏览器会弹出权限请求)。document.execCommand('copy')已被废弃但兼容性最好。粘贴事件可以获取图片文件,这是富文本编辑器支持截图粘贴的原理。
22. ResizeObserver 和 MutationObserver 是什么?
javascript
// ========== ResizeObserver:监听元素尺寸变化 ==========
// 比 window.resize 更精确------监听的是具体元素,而非整个窗口
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
console.log(`元素尺寸变化: ${width} x ${height}`)
// 实用:根据容器宽度切换布局
const el = entry.target
if (width < 600) {
el.classList.add('compact')
} else {
el.classList.remove('compact')
}
}
})
const container = document.querySelector('.container')
resizeObserver.observe(container)
// resizeObserver.unobserve(container) // 停止观察
// resizeObserver.disconnect() // 停止所有观察
// ========== MutationObserver:监听 DOM 变化 ==========
// 监听元素的属性变化、子节点变化、文本内容变化
const mutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case 'childList':
console.log('子节点变化')
mutation.addedNodes.forEach(node => console.log('新增:', node))
mutation.removedNodes.forEach(node => console.log('移除:', node))
break
case 'attributes':
console.log(`属性 ${mutation.attributeName} 变化`)
break
case 'characterData':
console.log('文本内容变化')
break
}
}
})
const target = document.querySelector('#app')
mutationObserver.observe(target, {
childList: true, // 监听子节点增删
attributes: true, // 监听属性变化
characterData: true, // 监听文本变化
subtree: true, // 监听所有后代节点
attributeFilter: ['class', 'style'], // 只监听特定属性
attributeOldValue: true, // 记录属性旧值
})
// mutationObserver.disconnect() // 停止观察
// ========== 实用场景 ==========
// 1. 图片懒加载 + ResizeObserver(容器变大时可能需要加载更多)
// 2. 虚拟列表的容器尺寸变化监听
// 3. 富文本编辑器监听内容变化
// 4. 广告插入检测(MutationObserver 监听 DOM 变化)
// 5. 动态表单校验(监听 input 值变化)
// ✅ 对比三种 Observer
// | Observer | 监听对象 | 应用场景 |
// |----------------------|----------------|----------------------------|
// | IntersectionObserver | 元素可见性 | 懒加载、无限滚动、曝光统计 |
// | ResizeObserver | 元素尺寸变化 | 响应式布局、图表自适应 |
// | MutationObserver | DOM 树变化 | 内容监听、插件注入检测 |
💡 面试加分点: 三种 Observer 都是异步 的,不会阻塞主线程。MutationObserver 是微任务级别的回调------多次 DOM 变化会被合并成一批 通知,性能远优于已废弃的
Mutation Events。Vue2 内部使用 MutationObserver 作为微任务降级方案(在Promise不可用时)。
23. WebSocket 是什么?如何使用?
WebSocket 是一种在客户端和服务器之间建立全双工(双向)持久连接的协议,适用于实时通信场景。
javascript
// ========== 基本使用 ==========
const ws = new WebSocket('wss://echo.websocket.org')
ws.onopen = () => {
console.log('连接已建立')
ws.send('Hello Server!')
ws.send(JSON.stringify({ type: 'chat', message: '你好' }))
}
ws.onmessage = (event) => {
console.log('收到消息:', event.data)
const data = JSON.parse(event.data)
handleMessage(data)
}
ws.onclose = (event) => {
console.log(`连接关闭: code=${event.code}, reason=${event.reason}`)
}
ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
// 关闭连接
ws.close(1000, '正常关闭')
// ========== 带自动重连的 WebSocket 封装 ==========
class ReconnectWebSocket {
constructor(url, options = {}) {
this.url = url
this.maxRetries = options.maxRetries ?? 5
this.retryDelay = options.retryDelay ?? 3000
this.retries = 0
this.handlers = { message: [], open: [], close: [] }
this.connect()
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.retries = 0
this.handlers.open.forEach(fn => fn())
}
this.ws.onmessage = (e) => {
this.handlers.message.forEach(fn => fn(JSON.parse(e.data)))
}
this.ws.onclose = () => {
this.handlers.close.forEach(fn => fn())
if (this.retries < this.maxRetries) {
this.retries++
console.log(`${this.retryDelay}ms 后第 ${this.retries} 次重连...`)
setTimeout(() => this.connect(), this.retryDelay)
}
}
this.ws.onerror = () => this.ws.close()
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data))
}
}
on(event, handler) {
this.handlers[event]?.push(handler)
}
close() {
this.maxRetries = 0 // 阻止重连
this.ws.close()
}
}
// 使用
const socket = new ReconnectWebSocket('wss://api.example.com/ws')
socket.on('message', (data) => console.log('收到:', data))
socket.on('open', () => socket.send({ type: 'subscribe', channel: 'chat' }))
// ========== 心跳检测 ==========
// 防止长时间无数据交互导致连接被防火墙/代理关闭
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30000)
// ========== WebSocket vs HTTP 对比 ==========
// | 特性 | HTTP | WebSocket |
// |------------|-------------------|------------------------|
// | 通信方向 | 请求-响应(半双工) | 全双工 |
// | 连接性质 | 短连接/长连接 | 持久连接 |
// | 协议开销 | 每次请求都有头部 | 首次握手后开销极小 |
// | 适用场景 | 普通数据请求 | 实时通信/推送/游戏 |
💡 面试加分点: WebSocket 的 URL 协议是
ws://或wss://(加密)。它通过 HTTP 握手升级协议建立连接。常见应用:聊天室、实时通知、股票行情、协同编辑、在线游戏。如果不需要全双工,Server-Sent Events(SSE) 是更轻量的单向推送方案。
24. 什么是 Fullscreen API 和 Page Visibility API?
javascript
// ========== Fullscreen API(全屏) ==========
// 进入全屏
async function enterFullscreen(element = document.documentElement) {
try {
if (element.requestFullscreen) {
await element.requestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen() // Safari
}
} catch (err) {
console.error('全屏失败:', err)
}
}
// 退出全屏
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
}
}
// 判断是否全屏
function isFullscreen() {
return !!document.fullscreenElement
}
// 切换全屏
function toggleFullscreen(el) {
isFullscreen() ? exitFullscreen() : enterFullscreen(el)
}
// 监听全屏变化
document.addEventListener('fullscreenchange', () => {
console.log('全屏状态:', isFullscreen())
})
// 让视频全屏
const video = document.querySelector('video')
video.addEventListener('dblclick', () => enterFullscreen(video))
// ========== Page Visibility API(页面可见性)==========
// 检测页面是否被用户看到(如切换了标签页、最小化等)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面不可见(用户切走了)')
// 暂停视频/动画/轮询,节省资源
pauseVideo()
clearInterval(pollingTimer)
} else {
console.log('页面可见(用户回来了)')
// 恢复视频/动画/轮询
resumeVideo()
startPolling()
}
})
// 当前可见状态
console.log(document.visibilityState) // 'visible' | 'hidden' | 'prerender'
// ========== 实用场景 ==========
// 1. 页面不可见时暂停视频播放
// 2. 页面不可见时停止轮询请求(节省带宽)
// 3. 页面不可见时暂停动画(节省 CPU)
// 4. 页面不可见时暂停倒计时(防止作弊)
// 5. 用户回来时刷新数据(保证数据最新)
// ✅ 标签页切换时的标题提示
const originalTitle = document.title
document.addEventListener('visibilitychange', () => {
document.title = document.hidden ? '👀 别走呀~' : originalTitle
})
💡 面试加分点: Fullscreen API 需要在用户交互事件(如 click)中调用才会生效,不能在页面加载时自动全屏(浏览器安全策略)。
visibilitychange是比blur/focus更可靠的页面可见性检测方式。
25. 什么是 Geolocation API?
javascript
// ========== 获取当前位置 ==========
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy, altitude, speed, heading } = position.coords
console.log(`纬度: ${latitude}, 经度: ${longitude}`)
console.log(`精度: ${accuracy}m`)
},
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.log('用户拒绝了定位请求')
break
case error.POSITION_UNAVAILABLE:
console.log('位置信息不可用')
break
case error.TIMEOUT:
console.log('获取位置超时')
break
}
},
{
enableHighAccuracy: true, // 高精度模式(GPS,更耗电)
timeout: 10000, // 超时时间(毫秒)
maximumAge: 60000, // 缓存有效期(毫秒)
}
)
// ========== 持续监听位置变化 ==========
const watchId = navigator.geolocation.watchPosition(
(position) => {
updateMap(position.coords.latitude, position.coords.longitude)
},
(error) => console.error(error),
{ enableHighAccuracy: true }
)
// 停止监听
navigator.geolocation.clearWatch(watchId)
// ========== 计算两点距离(Haversine 公式)==========
function getDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3 // 地球半径(米)
const toRad = (deg) => deg * Math.PI / 180
const dLat = toRad(lat2 - lat1)
const dLon = toRad(lon2 - lon1)
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) ** 2
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}
// 使用
const dist = getDistance(39.9042, 116.4074, 31.2304, 121.4737)
console.log(`北京到上海: ${(dist / 1000).toFixed(0)} km`) // 约 1068 km
💡 面试加分点: Geolocation API 需要 HTTPS 环境(或 localhost)和用户授权。移动端可以获取更精确的 GPS 位置,桌面端通常使用 IP 定位(精度较低)。在用户拒绝权限后,不能再次弹出请求------需要引导用户手动到浏览器设置中开启。
26. 什么是 Web Worker?如何使用?
Web Worker 允许在后台线程中运行 JavaScript 代码,避免耗时计算阻塞 UI 线程。
javascript
// ========== 基本用法 ==========
// 主线程(main.js)
const worker = new Worker('worker.js')
// 发送消息给 Worker
worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] })
// 接收 Worker 的消息
worker.onmessage = (event) => {
console.log('Worker 返回:', event.data)
}
// 错误处理
worker.onerror = (error) => {
console.error('Worker 错误:', error.message)
}
// 终止 Worker
worker.terminate()
// ========== Worker 文件(worker.js)==========
// self.onmessage = (event) => {
// const { type, data } = event.data
// if (type === 'calculate') {
// // 执行耗时计算(不会阻塞主线程)
// const result = data.reduce((sum, n) => sum + n, 0)
// self.postMessage({ type: 'result', value: result })
// }
// }
// ========== 内联 Worker(无需单独文件)==========
function createInlineWorker(fn) {
const blob = new Blob(
[`self.onmessage = function(e) { (${fn.toString()})(e) }`],
{ type: 'text/javascript' }
)
return new Worker(URL.createObjectURL(blob))
}
const inlineWorker = createInlineWorker((e) => {
const { numbers } = e.data
// 模拟耗时计算
const result = numbers.reduce((sum, n) => sum + n, 0)
self.postMessage(result)
})
inlineWorker.postMessage({ numbers: [1, 2, 3, 4, 5] })
inlineWorker.onmessage = (e) => console.log('结果:', e.data) // 15
// ========== Shared Worker(多标签页共享)==========
// 多个标签页可以共享同一个 Worker 实例
const sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.start()
sharedWorker.port.postMessage('hello')
sharedWorker.port.onmessage = (e) => console.log(e.data)
// ========== Worker 的限制 ==========
// 1. ❌ 不能访问 DOM(document、window)
// 2. ❌ 不能使用 alert、confirm 等 UI API
// 3. ✅ 可以使用 fetch、XMLHttpRequest
// 4. ✅ 可以使用 setTimeout、setInterval
// 5. ✅ 可以使用 IndexedDB
// 6. ✅ 可以 importScripts() 加载外部脚本
// 7. ✅ 可以创建子 Worker
// ========== 使用 Transferable Objects 提升性能 ==========
// 大型数据(如 ArrayBuffer)可以"转移"而非"复制"
const buffer = new ArrayBuffer(1024 * 1024 * 100) // 100MB
// ❌ 普通发送(复制数据,慢)
// worker.postMessage({ buffer })
// ✅ 转移所有权(零拷贝,快)
worker.postMessage({ buffer }, [buffer])
// 注意:转移后主线程无法再使用 buffer
💡 面试加分点: Worker 通信使用结构化克隆算法 (类似深拷贝),大数据传输可用
Transferable Objects实现零拷贝。常见使用场景:大量数据处理、图片/视频编码、加密计算、Markdown 渲染。Service Worker 是一种特殊的 Worker,用于网络代理和 PWA 离线功能。