JavaScript WebAPI(进阶)高频面试题

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

💡 面试加分点: 对于布尔类型的属性(如 checkeddisabled),Attribute 只关心是否存在(<input checked> 即为 true),而 Property 是 true/false 的布尔值。建议使用 dataset API 操作自定义数据属性,而非 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              // 获取计算后的样式(只读)

💡 面试加分点: textContent vs innerHTML vs innerText 的区别------textContent 性能最好且安全(不解析 HTML),innerHTML 可以设置 HTML 但有 XSS 风险,innerText 会触发重排(因为要考虑 CSS 计算)。批量 DOM 操作时使用 DocumentFragment 可以避免多次重排。


4. 事件监听三要素是什么?

事件监听三要素:

  1. 事件源 ------ 谁触发了事件(DOM 元素)
  2. 事件类型 ------ 什么事件(click、input、scroll 等)
  3. 事件处理函数 ------ 做什么(回调函数)
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. 事件流是什么?什么是事件捕获和事件冒泡?

事件流描述了页面接收事件的顺序,分为三个阶段:

  1. 捕获阶段(Capture):windowdocumenthtmlbody → ... → 目标元素的父级
  2. 目标阶段(Target): 到达目标元素
  3. 冒泡阶段(Bubble): 从目标元素 → ... → bodyhtmldocumentwindow
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()
})

💡 面试加分点: 并非所有事件都会冒泡------focusblurmouseentermouseleavescroll(在某些情况下)不冒泡。替代方案:focusin/focusoutmouseover/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 加速的 transformopacity),复杂交互用 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 树 ─────────┘

详细步骤:

  1. 解析 HTML → 构建 DOM 树(Document Object Model)
  2. 解析 CSS → 构建 CSSOM 树(CSS Object Model)
  3. 合并 DOM 和 CSSOM → 生成 渲染树(Render Tree)
  4. 布局(Layout/Reflow) → 计算每个节点的位置和大小
  5. 绘制(Paint) → 将节点绘制到屏幕上
  6. 合成(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.okresponse.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.clipboard API 是异步的,需要用户授权(首次使用时浏览器会弹出权限请求)。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 离线功能。

相关推荐
禁止摆烂_才浅1 小时前
ES6+ 高频面试题
前端·javascript·面试
J船长1 小时前
Vscode/Cursor开了太多项目怕搞混?上Peacock啊
前端
禁止摆烂_才浅1 小时前
React Hooks 高频面试题
前端·react.js·面试
禁止摆烂_才浅1 小时前
React 高频面试题
前端·react.js·面试
禁止摆烂_才浅1 小时前
JavaScript 高级面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
JavaScript 基础 高频面试题
前端·javascript·面试
何时梦醒1 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强1 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
禁止摆烂_才浅1 小时前
HTML 高频面试题
前端·面试·html