最常问面试题

0 阅读14分钟

1. Vue 的双向数据绑定原理(v-model 的实现原理)?

答案:

Vue 2.x

  • 使用 Object.defineProperty 劫持对象属性
  • 通过 getter 收集依赖(Watcher)
  • 通过 setter 触发更新
  • v-model 本质是 :value + @input 的语法糖
// v-model 等价于
<input :value="message" @input="message = $event.target.value" />

Vue 3

  • 使用 Proxy 代理整个对象
  • 支持数组和对象深层响应式
  • 性能更好,无需递归遍历属性

2. Vue 组件间通信的方式有哪些?

答案:

  1. 父子组件通信

    • props:父传子
    • $emit:子传父
    • $parent / $children:访问父/子实例
  2. 跨级组件通信

    • provide / inject:祖先提供,后代注入
    • $attrs / $listeners:透传属性和事件
  3. 兄弟组件通信

    • 通过共同的父组件
    • 事件总线(EventBus)
    • Vuex / Pinia
  4. 全局状态管理

    • Vuex(Vue 2)
    • Pinia(Vue 3 推荐)

3. Vue 的响应式原理(Object.defineProperty vs Proxy)?

答案:

Vue 2.x(Object.defineProperty)

  • 只能劫持对象属性,需要递归遍历
  • 无法监听数组索引和长度变化
  • 需要 Vue.set / this.$set 添加新属性

Vue 3(Proxy)

  • 代理整个对象,无需递归
  • 可监听数组变化
  • 支持动态添加属性
  • 性能更好
// Vue 2
Object.defineProperty(obj, 'key', {
  get() { return value },
  set(newVal) { 
    value = newVal
    // 触发更新
  }
})

// Vue 3
new Proxy(obj, {
  get(target, key) { return target[key] },
  set(target, key, value) {
    target[key] = value
    // 触发更新
    return true
  }
})

4. v-if 和 v-show 的区别?

答案:

特性v-ifv-show
渲染方式条件为 false 时不渲染 DOM始终渲染,切换 display: none
切换开销高(创建/销毁组件)低(仅切换 CSS)
初始渲染条件为 false 时开销低初始渲染开销高
使用场景条件很少改变频繁切换显示/隐藏

选择建议

  • 需要频繁切换用 v-show
  • 条件很少改变用 v-if

5. Vue 中 key 的作用?为什么不能用 index?

答案:

key 的作用

  • 唯一标识元素,帮助 Vue 识别节点
  • 优化列表渲染性能
  • 避免状态错乱

为什么不能用 index

// 错误示例
<div v-for="(item, index) in list" :key="index">
  <input v-model="item.name" />
</div>

// 问题:删除中间项时,index 会变化,导致:
// - 输入框内容错乱
// - 组件状态错乱
// - 性能问题

// 正确做法
<div v-for="item in list" :key="item.id">
  <input v-model="item.name" />
</div>

使用唯一 ID 可确保:

  • 元素正确复用
  • 状态不混乱
  • 性能更好

6. Webpack 的打包原理是什么?工作流程是怎样的?

答案:

核心原理

  • 将项目视为依赖图(Dependency Graph)
  • 从入口文件开始,递归解析依赖
  • 将模块转换为浏览器可执行的代码

工作流程

1. 初始化阶段
   - 读取配置文件和命令行参数
   - 创建 Compiler 实例

2. 编译阶段
   - 从入口(entry)开始
   - 使用 Loader 转换模块(如 .js, .css, .vue)
   - 解析模块依赖(AST 分析)
   - 构建依赖图(Dependency Graph)

3. 优化阶段
   - 执行插件(Plugins)
   - 代码分割(Code Splitting)
   - Tree Shaking
   - 压缩优化

4. 输出阶段
   - 生成 Chunk
   - 输出到 output 目录

关键概念

  • Entry:入口文件
  • Output:输出配置
  • Loader:文件转换器(如 babel-loader)
  • Plugin:插件,在打包过程中执行任务
  • Chunk:代码块,一个或多个模块的集合

7. Webpack 的 Loader 和 Plugin 的区别?

答案:

特性LoaderPlugin
作用时机模块加载时执行整个打包过程
功能转换文件(如 .ts → .js)执行更广泛的任务
输入接收源文件内容访问整个编译过程
输出返回转换后的内容可修改输出文件
配置module.rulesplugins 数组

Loader 示例

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader' // 转换 ES6+ 代码
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'] // 处理 CSS
      }
    ]
  }
}

Plugin 示例

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    }),
    new webpack.optimize.UglifyJsPlugin() // 压缩代码
  ]
}

8. Webpack 的代码分割(Code Splitting)原理和实现方式?

答案:

原理

  • 将代码拆分为多个 chunk
  • 按需加载,减少初始加载体积
  • 利用浏览器缓存

实现方式

1. 入口分割

module.exports = {
  entry: {
    main: './src/main.js',
    vendor: './src/vendor.js'
  }
}

2. 动态导入(推荐)

// 使用 import()
const loadModule = () => {
  return import('./heavy-module.js').then(module => {
    // 使用模块
  })
}

// React 懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'))

3. SplitChunksPlugin 配置

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

优化效果

  • 减少初始包体积
  • 提高加载速度
  • 更好的缓存策略

9. Tree Shaking 的原理是什么?如何实现?

答案:

原理

  • 静态分析代码,移除未使用的代码
  • 基于 ES6 模块的静态结构(import/export)
  • 在打包时删除 dead code

实现条件

  1. 使用 ES6 模块(import/export
  2. 启用生产模式(mode: 'production'
  3. 配置 sideEffects: false

配置示例

// package.json
{
  "sideEffects": false, // 标记无副作用
  // 或指定有副作用的文件
  "sideEffects": ["./src/polyfill.js", "*.css"]
}

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true, // 标记未使用的导出
    minimize: true     // 压缩时删除
  }
}

注意事项

  • CommonJS 不支持(动态导入)
  • 有副作用的代码不会被删除
  • CSS 的 Tree Shaking 需要额外配置

10. Webpack 打包优化策略有哪些?

答案:

1. 减少打包体积

// 使用 Tree Shaking
optimization: {
  usedExports: true,
  minimize: true
}

// 排除不需要的依赖
externals: {
  'react': 'React',
  'react-dom': 'ReactDOM'
}

2. 加快构建速度

// 使用缓存
module.exports = {
  cache: {
    type: 'filesystem'
  },
  // 或
  cache: true
}

// 多线程打包
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true // 启用多进程
      })
    ]
  }
}

3. 代码分割

// 路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('./Home.vue')
  }
]

4. 资源优化

// 图片压缩
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: [
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: { quality: 80 }
      }
    }
  ]
}

5. 使用 CDN

externals: {
  'vue': 'Vue',
  'vue-router': 'VueRouter'
}

11. Vite 的打包原理是什么?为什么比 Webpack 快?

答案:

核心原理

开发环境

  • 使用原生 ES 模块(Native ESM)
  • 不打包,直接启动开发服务器
  • 按需编译,只编译请求的模块
  • 使用 esbuild 预构建依赖(比 Webpack 快 10-100 倍)

生产环境

  • 使用 Rollup 打包(更小的包体积)
  • 代码分割和优化

为什么快

  1. 开发服务器启动快

    Webpack: 打包所有模块 → 启动服务器(慢)
    Vite:   直接启动服务器 → 按需编译(快)
    
  2. HMR(热更新)快

    • Webpack:重新打包相关模块
    • Vite:只更新变化的模块,利用 ESM 的浏览器缓存
  3. 预构建优化

    • 使用 esbuild(Go 编写)预构建 node_modules
    • 比 Babel/TypeScript 快 10-100 倍

工作流程

开发环境:
浏览器请求 → Vite 服务器 → 按需编译 → 返回 ESM → 浏览器执行

生产环境:
源码 → Rollup 打包 → 优化 → 输出文件

12. Vite 和 Webpack 的主要区别?

答案:

特性WebpackVite
开发启动慢(需要打包)快(无需打包)
HMR 速度较慢(重新打包)快(只更新变化模块)
打包工具WebpackRollup(生产环境)
配置复杂度低(开箱即用)
生态成熟快速发展
适用场景大型项目,复杂配置现代项目,快速开发

选择建议

  • Webpack:需要复杂配置、大量插件、兼容性要求高
  • Vite:现代项目、快速开发、Vue 3/React 新项目

13. Rollup 的打包原理和特点?

答案:

核心原理

  • 基于 ES6 模块的静态分析
  • Tree Shaking 友好
  • 生成更小的包体积

特点

  1. Tree Shaking 优秀

    • 静态分析,精确删除未使用代码
    • 比 Webpack 的 Tree Shaking 更彻底
  2. 输出格式多样

    // rollup.config.js
    export default {
      output: {
        format: 'es',      // ES 模块
        // format: 'cjs',  // CommonJS
        // format: 'umd',  // UMD
        // format: 'iife'  // 立即执行函数
      }
    }
    
  3. 适合库开发

    • 生成更小的包
    • 更好的 Tree Shaking
    • 适合发布 npm 包

配置示例

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'es',
    sourcemap: true
  },
  plugins: [
    resolve(),
    commonjs(),
    terser() // 压缩
  ],
  external: ['react', 'react-dom'] // 外部依赖
}

使用场景

  • 库/组件库开发
  • 需要小体积的包
  • Vite 生产环境打包

13. 如何优化打包后的代码体积?

答案:

1. 代码分割

// Webpack
optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

// Vite
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['vue', 'vue-router']
      }
    }
  }
}

2. Tree Shaking

// 使用 ES 模块
import { debounce } from 'lodash-es' // ✅
// 而不是
import _ from 'lodash' // ❌

3. 压缩代码

// Webpack
optimization: {
  minimize: true,
  minimizer: [new TerserPlugin()]
}

// Vite(自动启用)
build: {
  minify: 'terser'
}

4. 图片优化

// 使用 WebP 格式
// 图片压缩
// 懒加载图片

5. Gzip/Brotli 压缩

// 服务器配置
// Nginx
gzip on;
gzip_types text/javascript application/javascript;

6. 分析打包体积

// webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

7. 按需引入

// ❌ 全量引入
import _ from 'lodash'

// ✅ 按需引入
import debounce from 'lodash/debounce'

14. 前端性能优化有哪些方法?

答案:

1. 资源加载优化

// 图片懒加载
<img src="placeholder.jpg" data-src="real.jpg" loading="lazy" />

// 预加载关键资源
<link rel="preload" href="critical.css" as="style" />
<link rel="prefetch" href="next-page.html" />

// CDN 加速
// 使用 CDN 加载静态资源

2. 代码优化

// 代码分割
const LazyComponent = React.lazy(() => import('./LazyComponent'))

// Tree Shaking
import { debounce } from 'lodash-es' // ✅
// import _ from 'lodash' // ❌

// 防抖节流
const debouncedSearch = debounce(search, 300)

3. 渲染优化

// 虚拟滚动
// 只渲染可见区域的列表项

// 使用 requestAnimationFrame
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate)
}

// 避免强制同步布局
// ❌ 强制同步
const width = element.offsetWidth
element.style.width = width + 1 + 'px'

// ✅ 批量读取,批量写入
const width = element.offsetWidth
requestAnimationFrame(() => {
  element.style.width = width + 1 + 'px'
})

4. 缓存策略

// 浏览器缓存
// Cache-Control: max-age=31536000

// Service Worker 缓存
// 离线缓存策略

// HTTP 缓存
// 强缓存 + 协商缓存

5. 网络优化

// HTTP/2 多路复用
// Gzip/Brotli 压缩
// 减少 HTTP 请求
// 使用 WebSocket 替代轮询

15. 如何优化首屏加载时间?

答案:

1. 减少资源体积

// 代码分割
const routes = [
  {
    path: '/home',
    component: () => import('./Home.vue') // 懒加载
  }
]

// 压缩代码
// webpack: optimization.minimize = true

// Tree Shaking
// 移除未使用的代码

2. 关键资源优化

<!-- 内联关键 CSS -->
<style>
  /* 首屏关键样式 */
</style>

<!-- 延迟非关键 CSS -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- 预加载关键资源 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

3. 图片优化

<!-- 使用 WebP 格式 -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="description">
</picture>

<!-- 响应式图片 -->
<img srcset="small.jpg 480w, large.jpg 800w" sizes="(max-width: 600px) 480px, 800px">

<!-- 懒加载 -->
<img src="placeholder.jpg" data-src="real.jpg" loading="lazy">

4. 服务端优化

// SSR(服务端渲染)
// Next.js, Nuxt.js

// 预渲染
// 静态站点生成(SSG)

// 启用 Gzip/Brotli
// 服务器配置压缩

5. 监控指标

// FCP (First Contentful Paint) - 首次内容绘制
// LCP (Largest Contentful Paint) - 最大内容绘制
// FID (First Input Delay) - 首次输入延迟
// CLS (Cumulative Layout Shift) - 累积布局偏移

// 使用 Performance API
performance.getEntriesByType('navigation')

16. 如何优化长列表性能?

答案:

1. 虚拟滚动

// Vue 3 示例
import { VirtualList } from 'vue-virtual-scroll-list'

<VirtualList
  :data-key="'id'"
  :data-sources="items"
  :data-component="Item"
  :keeps="20"
/>

// React 示例
import { FixedSizeList } from 'react-window'

<FixedSizeList
  height={600}
  itemCount={1000}
  itemSize={50}
  width={300}
>
  {Row}
</FixedSizeList>

2. 分页加载

// 无限滚动
const loadMore = async () => {
  const newItems = await fetchItems(page + 1)
  items.value.push(...newItems)
  page++
}

// 使用 Intersection Observer
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMore()
  }
})
observer.observe(loadMoreTrigger.value)

3. 防抖节流

// 滚动事件节流
const handleScroll = throttle(() => {
  // 处理滚动
}, 100)

// 搜索防抖
const handleSearch = debounce((keyword) => {
  // 搜索逻辑
}, 300)

4. 使用 key 优化

<!-- ✅ 使用唯一 key -->
<div v-for="item in list" :key="item.id">
  {{ item.name }}
</div>

<!-- ❌ 不要用 index -->
<div v-for="(item, index) in list" :key="index">

17. 浏览器兼容性问题如何解决?

答案:

1. CSS 兼容性

/* 使用 Autoprefixer */
/* 自动添加浏览器前缀 */
display: flex;
/* 自动转换为 */
display: -webkit-box;
display: -ms-flexbox;
display: flex;

/* 使用 PostCSS */
/* 自动处理兼容性 */

/* 使用 normalize.css 或 reset.css */
/* 统一浏览器默认样式 */

2. JavaScript 兼容性

// 使用 Babel 转译
// ES6+ → ES5

// Polyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'

// 条件判断
if (typeof Promise === 'undefined') {
  // 加载 Promise polyfill
}

// 特性检测
if ('IntersectionObserver' in window) {
  // 使用 IntersectionObserver
} else {
  // 降级方案
}

3. 使用工具

// package.json
{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

4. 渐进增强

// 先实现基础功能
// 再添加增强功能

// 检测支持情况
const supportsWebP = () => {
  const canvas = document.createElement('canvas')
  return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0
}

18. 如何处理移动端适配?

答案:

1. 视口设置

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

2. rem 方案

// 动态设置根字体大小
function setRem() {
  const width = document.documentElement.clientWidth
  const fontSize = width / 375 * 16 // 基于 375px 设计稿
  document.documentElement.style.fontSize = fontSize + 'px'
}
setRem()
window.addEventListener('resize', setRem)

// 使用 postcss-pxtorem
// 自动转换 px 为 rem

3. vw/vh 方案

/* 使用 vw 单位 */
.container {
  width: 100vw;
  font-size: 4vw; /* 基于视口宽度 */
}

/* 使用 postcss-px-to-viewport */
/* 自动转换 px 为 vw */

4. 媒体查询

/* 响应式设计 */
@media (max-width: 768px) {
  .container {
    width: 100%;
  }
}

/* 移动端优先 */
.container {
  width: 100%;
}
@media (min-width: 768px) {
  .container {
    width: 750px;
  }
}

19. 如何优化 JavaScript 执行性能?

答案:

1. 避免阻塞主线程

// 使用 Web Worker
const worker = new Worker('worker.js')
worker.postMessage(data)
worker.onmessage = (e) => {
  console.log(e.data)
}

// 使用 requestIdleCallback
requestIdleCallback(() => {
  // 低优先级任务
})

// 使用 setTimeout 分片
function processChunk(items, index = 0) {
  const chunk = items.slice(index, index + 100)
  chunk.forEach(processItem)
  if (index < items.length) {
    setTimeout(() => processChunk(items, index + 100), 0)
  }
}

2. 优化循环

// ❌ 在循环中查询 DOM
for (let i = 0; i < items.length; i++) {
  document.getElementById('item-' + i).style.color = 'red'
}

// ✅ 批量操作
const fragment = document.createDocumentFragment()
items.forEach(item => {
  const el = document.createElement('div')
  fragment.appendChild(el)
})
container.appendChild(fragment)

3. 防抖节流

// 防抖:延迟执行
function debounce(func, wait) {
  let timeout
  return function(...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func.apply(this, args), wait)
  }
}

// 节流:限制频率
function throttle(func, limit) {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

4. 使用事件委托

// ❌ 每个元素绑定事件
items.forEach(item => {
  item.addEventListener('click', handleClick)
})

// ✅ 事件委托
container.addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleClick(e)
  }
})


20. 如何处理内存泄漏?

答案:

1. 清理事件监听器

// Vue
onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

// React
useEffect(() => {
  window.addEventListener('resize', handleResize)
  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, [])

2. 清理定时器

const timer = setInterval(() => {
  // 逻辑
}, 1000)

// 清理
clearInterval(timer)

3. 避免闭包引用

// ❌ 可能导致内存泄漏
function createHandler() {
  const largeData = new Array(1000000).fill(0)
  return function() {
    // largeData 一直被引用
  }
}

// ✅ 使用后清理
function createHandler() {
  const largeData = new Array(1000000).fill(0)
  return function() {
    // 使用 largeData
    // 使用后设置为 null
    largeData = null
  }
}

4. 清理 DOM 引用

const element = document.getElementById('myElement')
// 使用后清理
element = null

5. 使用 WeakMap/WeakSet

// 弱引用,不会阻止垃圾回收
const weakMap = new WeakMap()
weakMap.set(element, data)

21. 什么是跨域?如何解决跨域问题?

答案:

跨域定义

  • 协议、域名、端口任一不同即为跨域
  • 浏览器同源策略限制

解决方案

1. CORS(跨域资源共享)

// 服务端设置响应头
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*') // 或指定域名
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  res.header('Access-Control-Allow-Credentials', 'true') // 允许携带 cookie
  next()
})

// 预检请求处理
if (req.method === 'OPTIONS') {
  res.sendStatus(200)
  return
}

2. 代理(Proxy)

// webpack devServer
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

// Vite
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

3. JSONP

// 只支持 GET 请求
function jsonp(url, callback) {
  const script = document.createElement('script')
  script.src = `${url}?callback=${callback}`
  document.body.appendChild(script)
  
  window[callback] = (data) => {
    // 处理数据
    document.body.removeChild(script)
  }
}

4. postMessage

// 窗口间通信
// 父窗口
iframe.contentWindow.postMessage('data', 'http://child-domain.com')

// 子窗口
window.addEventListener('message', (e) => {
  if (e.origin === 'http://parent-domain.com') {
    console.log(e.data)
  }
})

22. CORS 的预检请求(OPTIONS)是什么?

答案:

预检请求触发条件

  • 非简单请求(复杂请求)
  • 包含自定义头部
  • 使用 PUT、DELETE 等方法
  • Content-Type 为 application/json

简单请求

// GET、POST、HEAD
// Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data
// 无自定义头部

复杂请求示例

// 会触发预检请求
fetch('http://api.example.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  },
  body: JSON.stringify({ name: 'Vue' })
})

// 浏览器先发送 OPTIONS 请求
// OPTIONS /data HTTP/1.1
// Origin: http://example.com
// Access-Control-Request-Method: PUT
// Access-Control-Request-Headers: content-type,authorization

服务端处理

// 处理预检请求
app.options('/api/*', (req, res) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin)
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  res.header('Access-Control-Max-Age', '86400') // 缓存 24 小时
  res.sendStatus(200)
})

23. 如何实现跨域携带 Cookie?

答案:

前端设置

// 必须设置 withCredentials
fetch('http://api.example.com/data', {
  credentials: 'include', // 或 'same-origin'
  headers: {
    'Content-Type': 'application/json'
  }
})

// XMLHttpRequest
const xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.open('GET', 'http://api.example.com/data')
xhr.send()

服务端设置

// 必须设置
res.header('Access-Control-Allow-Credentials', 'true')
// 不能使用通配符 *
res.header('Access-Control-Allow-Origin', 'http://example.com') // 必须指定域名

注意事项

  • Access-Control-Allow-Origin 不能是 *
  • 必须明确指定允许的域名
  • Cookie 的 SameSite 属性影响跨域

24. 什么是同源策略?如何绕过?

答案:

同源策略定义

  • 协议、域名、端口必须完全相同
  • 限制:Cookie、LocalStorage、DOM、AJAX

绕过方法

1. CORS

// 服务端设置响应头
Access-Control-Allow-Origin: *

2. 代理服务器

// 同源请求代理服务器
// 代理服务器转发到目标服务器

3. JSONP

// 利用 script 标签不受同源限制
<script src="http://api.example.com/data?callback=handleData"></script>

4. postMessage

// 窗口间通信
window.postMessage(data, targetOrigin)

5. document.domain

// 仅适用于主域相同的情况
// a.example.com 和 b.example.com
document.domain = 'example.com'

25. HTTP 和 HTTPS 的区别?

答案:

特性HTTPHTTPS
协议超文本传输协议安全超文本传输协议
端口80443
加密明文传输SSL/TLS 加密
证书不需要需要 CA 证书
性能稍慢(加密开销)
SEO一般更好(Google 推荐)

HTTPS 工作原理

1. 客户端请求 HTTPS 连接
2. 服务器返回证书
3. 客户端验证证书
4. 双方协商加密算法
5. 建立加密连接
6. 开始安全传输

为什么需要 HTTPS

  • 防止中间人攻击
  • 保护数据隐私
  • 提升 SEO 排名
  • 浏览器标记不安全

26. HTTP 缓存机制是什么?

答案:

强缓存

# 响应头
Cache-Control: max-age=3600
Expires: Wed, 21 Oct 2025 07:28:00 GMT

# Cache-Control 优先级更高
# max-age: 缓存时间(秒)
# no-cache: 需要验证
# no-store: 不缓存
# private: 仅客户端缓存
# public: 可被任何缓存

协商缓存

# Last-Modified / If-Modified-Since
Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2020 07:28:00 GMT

# ETag / If-None-Match(优先级更高)
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

缓存策略

// 1. 强缓存优先
// 2. 强缓存失效,使用协商缓存
// 3. 协商缓存 304,使用本地缓存
// 4. 协商缓存 200,返回新资源

// 最佳实践
// HTML: no-cache(总是验证)
// CSS/JS: max-age=31536000(长期缓存,文件名带 hash)
// 图片: max-age=2592000(30天)

27. 什么是 HTTP/2?有什么优势?

答案:

HTTP/2 特性

1. 多路复用(Multiplexing)

// HTTP/1.1: 每个请求一个连接
// HTTP/2: 一个连接多个请求并行

// 解决了队头阻塞问题

2. 服务器推送(Server Push)

// 服务器主动推送资源
// 减少往返次数

3. 头部压缩(HPACK)

// 压缩 HTTP 头部
// 减少传输数据量

4. 二进制分帧

// 二进制协议
// 更高效解析

优势对比

HTTP/1.1:
- 每个请求一个连接
- 头部重复传输
- 队头阻塞

HTTP/2:
- 多路复用
- 头部压缩
- 服务器推送
- 性能提升 50%+

28. JavaScript 事件循环(Event Loop)原理?

答案:

核心概念

  • JavaScript 是单线程语言
  • 事件循环负责调度异步任务
  • 分为调用栈(Call Stack)、消息队列(Message Queue)、微任务队列(Microtask Queue)

执行顺序

1. 执行同步代码(调用栈)
2. 执行所有微任务(Promise.then, queueMicrotask, MutationObserver)
3. 执行一个宏任务(setTimeout, setInterval, I/O)
4. 重复步骤 2-3

代码示例

console.log('1') // 同步

setTimeout(() => {
  console.log('2') // 宏任务
}, 0)

Promise.resolve().then(() => {
  console.log('3') // 微任务
})

console.log('4') // 同步

// 输出顺序:1, 4, 3, 2

执行流程

同步代码 → 微任务队列 → 宏任务队列 → 微任务队列 → ...

29. 宏任务(MacroTask)和微任务(MicroTask)的区别?

答案:

特性宏任务微任务
执行时机每次事件循环执行一个每次事件循环执行全部
优先级
常见 APIsetTimeout, setInterval, I/OPromise.then, queueMicrotask
执行顺序在微任务之后在宏任务之前

宏任务类型

// 宏任务
setTimeout(() => {}, 0)
setInterval(() => {}, 0)
setImmediate() // Node.js
I/O 操作
UI 渲染

微任务类型

// 微任务
Promise.then()
Promise.catch()
Promise.finally()
queueMicrotask()
MutationObserver
process.nextTick() // Node.js(优先级最高)

执行示例

console.log('start')

setTimeout(() => {
  console.log('timeout1') // 宏任务
  Promise.resolve().then(() => {
    console.log('promise1') // 微任务
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise2') // 微任务
  setTimeout(() => {
    console.log('timeout2') // 宏任务
  }, 0)
})

console.log('end')

// 输出:start, end, promise2, timeout1, promise1, timeout2

关键点

  • 微任务优先级高于宏任务
  • 每次宏任务执行后,会清空所有微任务
  • process.nextTick 优先级最高(Node.js)

宏任务和微任务列表

宏任务(MacroTask)

  1. setTimeout
  2. setInterval
  3. setImmediate(仅 Node.js)
  4. I/O 操作(文件读取、网络请求等)
  5. UI 渲染(浏览器)
  6. MessageChannel
  7. postMessage
  8. script 标签加载
  9. requestAnimationFrame(浏览器)
  10. Web Workers

微任务(MicroTask)

  1. Promise(包括 .then、.catch、.finally)
  2. queueMicrotask
  3. MutationObserver
  4. process.nextTick(仅 Node.js,优先级最高)
  5. async/await 的后续代码

30. Node.js 的事件循环和浏览器有什么区别?

答案:

浏览器事件循环

同步代码 → 微任务 → 宏任务 → 微任务 → ...

Node.js 事件循环(6 个阶段)

1. timers(定时器阶段)
   - 执行 setTimeout, setInterval

2. pending callbacks(待处理回调)
   - 执行延迟的 I/O 回调

3. idle, prepare(内部使用)

4. poll(轮询阶段)
   - 获取新的 I/O 事件
   - 执行 I/O 相关回调

5. check(检查阶段)
   - 执行 setImmediate 回调

6. close callbacks(关闭回调)
   - 执行 close 事件回调

关键区别

1. process.nextTick 优先级最高

console.log('1')

setTimeout(() => console.log('2'), 0)
setImmediate(() => console.log('3'))
process.nextTick(() => console.log('4'))

// Node.js 输出:1, 4, 2, 3
// process.nextTick 优先级最高

2. setImmediate vs setTimeout

// 在 I/O 回调中
fs.readFile('file.txt', () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
  // 输出:immediate, timeout
  // setImmediate 在 check 阶段,先于 timers 阶段
})

3. 微任务执行时机

// Node.js 中,微任务在每个阶段之间执行
// 浏览器中,微任务在宏任务之后执行

执行顺序(Node.js)

同步代码 → process.nextTick → 微任务 → 宏任务阶段 → process.nextTick → 微任务 → ...