面试用

49 阅读11分钟

封装的业务组件

滑动验证码校验

身份证正反面拍照框

统一报错组件

统一弹窗组件

script 标签中 defer 和 async 的区别?

  • script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
  • async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
  • defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

下图清晰地展示了三种 script 的过程: image.png

总结

  • 使用 defer

    • 当脚本之间有依赖关系时。
    • 需要脚本在DOM完全加载后执行时。
    • 提高页面加载速度,同时保证脚本按顺序执行。
  • 使用 async

    • 当脚本之间没有依赖关系时。

    • 脚本不需要操作DOM元素时。

    • 加载第三方脚本时,避免阻塞页面渲染。

      const HtmlWebpackPlugin = require('html-webpack-plugin');
      ​
      module.exports = {
        // 其他配置项...
        plugins: [
          new HtmlWebpackPlugin({
            template: './src/index.html', // 你的HTML模板文件路径
            scriptLoading: 'defer' // 设置script标签的defer属性
          })
        ]
      };
      

webpack文件压缩

使用 compression-webpack-plugin 进行 Gzip 压缩

const CompressionPlugin = require('compression-webpack-plugin');
​
module.exports = {
  // 其他配置项...
  plugins: [
    new CompressionPlugin({
      filename: '[path][base].gz',
      algorithm: 'gzip',
      test: /.js$|.css$|.html$/,
      threshold: 10240,
      minRatio: 0.8,
      deleteOriginalAssets: false
    })
  ]
};

使用 compression-webpack-plugin 进行 Brotli 压缩

const CompressionPlugin = require('compression-webpack-plugin');
​
module.exports = {
  // 其他配置项...
  plugins: [
    new CompressionPlugin({
      filename: '[path][base].br', // 压缩后的文件名
      algorithm: 'brotliCompress', // 使用Brotli压缩算法
      test: /.js$|.css$|.html$/, // 匹配需要压缩的文件
      threshold: 10240, // 只有文件大于10KB才会被压缩
      minRatio: 0.8, // 压缩后的文件大小与原始文件大小的比率,小于该值才会被压缩
      deleteOriginalAssets: false // 是否删除原始文件
    })
  ]
};

nginx配置

server {
  listen 80;
  server_name example.com;
​
  location / {
    root /path/to/dist;
    try_files $uri $uri/ /index.html;
​
    # 启用Gzip压缩
    gzip on;
    gzip_types text/plain text/css application/javascript application/json application/xml text/xml application/xml+rss text/javascript;
    gzip_vary on;
​
    # 提供Gzip文件
    location ~* .(js|css|html)$ {
      add_header Content-Encoding gzip;
      add_header Content-Type $mime_type;
      try_files $uri.gz $uri =404;
    }
​
    # 启用Brotli压缩
    brotli on;
    brotli_types text/plain text/css application/javascript application/json application/xml text/xml application/xml+rss text/javascript;
    brotli_static on;
​
    # 提供Brotli文件
    location ~* .(js|css|html)$ {
      add_header Content-Encoding br;
      add_header Content-Type $mime_type;
      try_files $uri.br $uri =404;
    }
  }
}

对比总结

特性GzipBrotli
压缩效率相对较低相对较高
性能压缩和解压缩速度快压缩和解压缩速度较慢
CPU 负担较小较大
文件扩展名.gz.br
支持情况几乎所有现代浏览器大多数现代浏览器
适用场景资源有限、广泛支持高压缩率、资源充足

从用户在浏览器地址栏输入 URL 到请求返回并显示页面

1. 输入 URL

用户在浏览器地址栏输入 URL 并按下回车键。

2. DNS 解析

浏览器需要将输入的域名(如 www.example.com)转换为对应的 IP 地址。

  1. 检查浏览器缓存

    • 浏览器首先检查其缓存中是否已经存在该域名的 IP 地址。
    • 如果存在且未过期,则直接使用缓存的 IP 地址。
  2. 检查操作系统缓存

    • 如果浏览器缓存中没有,浏览器会检查操作系统的 DNS 缓存。
  3. 检查路由器缓存

    • 如果操作系统缓存中也没有,请求会发送到路由器,路由器会检查其缓存。
  4. 联系 DNS 服务器

    • 如果所有缓存中都没有,浏览器会向本地 DNS 服务器(如 ISP 提供的 DNS 服务器)发送 DNS 查询请求。
  5. 递归查询

    • 本地 DNS 服务器会递归地查询根 DNS 服务器、顶级域 DNS 服务器(如 .com 服务器)和权威 DNS 服务器,直到找到目标域名的 IP 地址。
  6. 返回 IP 地址

    • DNS 服务器将 IP 地址返回给浏览器,并缓存该结果以备后续使用。

3. 建立 TCP 连接

浏览器使用 TCP 协议与目标服务器建立连接。

  1. 三次握手

    • 第一次握手:浏览器向服务器发送一个 SYN(同步)数据包。
    • 第二次握手:服务器收到 SYN 数据包后,发送一个 SYN-ACK(同步确认)数据包给浏览器。
    • 第三次握手:浏览器收到 SYN-ACK 数据包后,发送一个 ACK(确认)数据包给服务器。
    • 三次握手完成后,TCP 连接建立成功。
  2. 保持连接

    • 建立连接后,浏览器和服务器可以开始传输数据。

4. 发送 HTTP 请求

浏览器通过建立的 TCP 连接向服务器发送 HTTP 请求。

  1. 构建请求

    • 浏览器构建一个 HTTP 请求,通常包括请求行(如 GET /index.html HTTP/1.1)、请求头(如 Host: www.example.com)和请求体(对于 POST 请求)。
  2. 发送请求

    • 浏览器将构建好的 HTTP 请求通过 TCP 连接发送给服务器。

5. 服务器处理请求

服务器接收到请求后,处理并生成响应。

  1. 解析请求

    • 服务器解析 HTTP 请求,确定请求的资源路径和其他信息。
  2. 查找资源

    • 服务器根据请求路径查找相应的文件或执行相应的处理逻辑(如动态生成内容)。
  3. 生成响应

    • 服务器生成 HTTP 响应,通常包括响应行(如 HTTP/1.1 200 OK)、响应头(如 Content-Type: text/html)和响应体(如 HTML 内容)。
  4. 发送响应

    • 服务器将生成的 HTTP 响应通过 TCP 连接发送给浏览器。

6. 浏览器接收响应

浏览器接收到服务器的响应后,开始解析和渲染页面。

  1. 解析响应

    • 浏览器解析 HTTP 响应,提取响应头和响应体。
  2. 构建 DOM 树

    • 浏览器使用 HTML 解析器将响应体中的 HTML 代码解析成 DOM 树。
  3. 构建 CSSOM 树

    • 浏览器使用 CSS 解析器将 CSS 代码解析成 CSSOM 树。
  4. 生成渲染树

    • 浏览器将 DOM 树和 CSSOM 树合并成渲染树,确定每个元素的样式和布局。
  5. 布局(Reflow)

    • 浏览器根据渲染树计算每个元素的精确位置和大小。
  6. 绘制(Repaint)

    • 浏览器将渲染树中的每个节点绘制到屏幕上,形成最终的页面。

7. 加载和渲染资源

页面加载过程中,浏览器会继续请求和加载其他资源(如 CSS、JavaScript、图像等)。

  1. 并行请求

    • 浏览器会并行请求页面中引用的所有资源,以加快加载速度。
  2. 缓存

    • 浏览器会缓存静态资源(如 CSS、JavaScript、图像),以减少后续请求。
  3. 执行 JavaScript

    • 浏览器会执行页面中的 JavaScript 代码,动态修改页面内容或添加交互功能。

8. 页面显示完成

所有资源加载和渲染完成后,页面最终显示在用户面前。

总结

从用户在浏览器地址栏输入 URL 到页面显示完成,整个过程涉及以下几个关键步骤:

  1. DNS 解析:将域名转换为 IP 地址。
  2. TCP 连接:建立与服务器的 TCP 连接。
  3. HTTP 请求:浏览器向服务器发送 HTTP 请求。
  4. 服务器处理:服务器处理请求并生成响应。
  5. HTTP 响应:服务器将响应发送给浏览器。
  6. 页面解析和渲染:浏览器解析响应并渲染页面。
  7. 资源加载:浏览器加载和渲染其他资源。
  8. 页面显示:页面最终显示在用户面前。

通过这些步骤,浏览器能够高效地从服务器获取资源并呈现给用户。

前端性能优化是一个多方面的任务,涉及代码分割、资源压缩、缓存策略等多个方面。Webpack 是一个强大的构建工具,可以帮助你实现这些优化。以下是一些常见的 Webpack 配置优化策略:

1. 代码分割(Code Splitting)

代码分割可以将代码拆分成多个小块,按需加载,减少初始加载时间。

  • 使用 SplitChunksPlugin

    • Webpack 内置的 SplitChunksPlugin 可以帮助你自动分割代码。

    • 示例:

      javascriptmodule.exports = {
        optimization: {
          splitChunks: {
            chunks: 'all', // 对所有模块都进行代码分割
            cacheGroups: {
              vendors: {
                test: /[\/]node_modules[\/]/,
                name: 'vendors',
                chunks: 'all',
              },
            },
          },
        },
      };
      

2. 压缩资源

压缩 JavaScript 和 CSS 文件可以显著减少文件大小。

  • 使用 TerserPlugin 压缩 JavaScript

    • Webpack 默认使用 TerserPlugin 压缩 JavaScript。

    • 示例:

      javascriptconst TerserPlugin = require('terser-webpack-plugin');module.exports = {
        optimization: {
          minimize: true,
          minimizer: [new TerserPlugin()],
        },
      };
      
  • 使用 CssMinimizerPlugin 压缩 CSS

    • 使用 css-minimizer-webpack-plugin 压缩 CSS。

    • 示例:

      javascriptconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin');module.exports = {
        optimization: {
          minimize: true,
          minimizer: [
            new TerserPlugin(),
            new CssMinimizerPlugin(),
          ],
        },
      };
      

3. 使用 Tree Shaking

Tree Shaking 可以移除未使用的代码,减少最终打包文件的大小。

  • 确保使用 ES6 模块语法

    • Tree Shaking 依赖于 ES6 模块语法(importexport)。

    • 示例:

      javascriptimport { someFunction } from './module';
      someFunction();
      

4. 使用缓存

通过配置缓存,可以避免重复加载未更改的资源。

  • 设置文件名哈希

    • 使用文件内容的哈希值作为文件名的一部分,确保文件内容变化时文件名也变化。

    • 示例:

      javascriptmodule.exports = {
        output: {
          filename: '[name].[contenthash].js',
          chunkFilename: '[name].[contenthash].js',
        },
      };
      

5. 使用 CDN

将常用的库(如 React、Vue 等)通过 CDN 加载,减少打包文件的大小。

  • 配置 externals

    • 使用 externals 配置项将某些模块排除在打包文件之外。

    • 示例:

      javascriptmodule.exports = {
        externals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      };
      

6. 图片优化

优化图片可以显著减少文件大小,提高加载速度。

  • 使用 image-webpack-loader

    • 使用 image-webpack-loader 压缩图片。

    • 示例:

      javascriptmodule.exports = {
        module: {
          rules: [
            {
              test: /.(png|jpe?g|gif|svg)$/i,
              use: [
                {
                  loader: 'file-loader',
                  options: {
                    name: '[path][name].[ext]',
                  },
                },
                {
                  loader: 'image-webpack-loader',
                  options: {
                    mozjpeg: {
                      progressive: true,
                      quality: 65,
                    },
                    optipng: {
                      enabled: false,
                    },
                    pngquant: {
                      quality: [0.65, 0.90],
                      speed: 4,
                    },
                    gifsicle: {
                      interlaced: false,
                    },
                    webp: {
                      quality: 75,
                    },
                  },
                },
              ],
            },
          ],
        },
      };
      

7. 使用 MiniCssExtractPlugin

将 CSS 提取到单独的文件中,减少初始加载时间。

  • 配置 MiniCssExtractPlugin

    • 使用 mini-css-extract-plugin 提取 CSS。

    • 示例:

      javascriptconst MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = {
        module: {
          rules: [
            {
              test: /.css$/,
              use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
          ],
        },
        plugins: [
          new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css',
            chunkFilename: '[id].[contenthash].css',
          }),
        ],
      };
      

8. 使用 BundleAnalyzerPlugin

分析打包后的文件,找出体积较大的模块。

  • 配置 BundleAnalyzerPlugin

    • 使用 webpack-bundle-analyzer 分析打包文件。

    • 示例:

      javascriptconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;module.exports = {
        plugins: [
          new BundleAnalyzerPlugin(),
        ],
      };
      

9. 使用 HMR(Hot Module Replacement)

在开发环境中使用 HMR 可以提高开发效率,减少重新加载时间。

  • 配置 HMR

    • 在开发配置中启用 HMR。

    • 示例:

      javascriptconst webpack = require('webpack');module.exports = {
        devServer: {
          hot: true,
        },
        plugins: [
          new webpack.HotModuleReplacementPlugin(),
        ],
      };
      

10. 使用 PurgeCSSPlugin

移除未使用的 CSS,减少 CSS 文件大小。

  • 配置 PurgeCSSPlugin

    • 使用 purgecss-webpack-plugin 移除未使用的 CSS。

    • 示例:

      javascriptconst PurgeCSSPlugin = require('purgecss-webpack-plugin');
      const glob = require('glob');
      const path = require('path');
      ​
      const PATHS = {
        src: path.join(__dirname, 'src'),
      };module.exports = {
        plugins: [
          new PurgeCSSPlugin({
            paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
          }),
        ],
      };
      

Vue 2 和 Vue 3 在多个方面有显著区别,包括响应式系统、组件结构、性能优化、API 设计等方面。以下是 Vue 2 与 Vue 3 的核心差异总结


🧩 一、响应式系统的实现

特性Vue 2Vue 3
响应式机制Object.definePropertyProxy + ref / reactive
数组响应性需要特殊处理(如 this.$set完全自动响应
对象新增属性响应性不响应,需使用 $set自动响应
性能相对较低(每个属性都需要定义 getter/setter)更高效(基于 Proxy)

🧱 二、API 风格

特性Vue 2 (Options API)Vue 3 (Composition API)
组件逻辑组织方式按选项分块(data, methods, computed 等)使用 setup()<script setup> 组织逻辑
可复用逻辑mixins(易命名冲突)custom hooks(函数封装,高内聚低耦合)
默认语法Options API支持 Options API 和 Composition API
推荐新项目使用❌ 已逐步淘汰✅ 推荐使用 Composition API

⚙️ 三、生命周期钩子

Vue 2 钩子名Vue 3 钩子名(setup() 中)
beforeCreate❌ 移除(用 setup() 替代)
created❌ 移除(用 setup() 替代)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured

Vue 3 还支持在 <script setup> 中直接导入并调用这些钩子。


📦 四、组件通信和 props

特性Vue 2Vue 3
props 类型校验使用 props: {}同样支持,但可配合 TypeScript 更强类型检查
emit 事件this.$emit('event')defineEmits()(在 <script setup> 中)
插槽使用 this.$slots支持更灵活的插槽 API
v-model单向绑定,默认为 .sync支持多 v-model,可自定义修饰符

📁 五、模块化与工具链

特性Vue 2Vue 3
构建工具Webpack / BabelVite 原生支持(更快的开发服务器)
TypeScript 支持有限完全支持(TypeScript First)
包体积较大更小(Tree-shaking 更彻底)
模板编译器runtime-only 和 runtime-with-compiler支持编译时优化(block tree)
虚拟 DOM 实现传统 Diff 算法Block Tree 架构,提升 diff 性能

🧠 六、全局状态管理

Vue 2Vue 3
Vuex 3Vuex 4(兼容 Composition API)
可使用 provide/inject✅ 同样支持
推荐使用Pinia(Vue 3 推荐状态管理库)

🧪 七、模板语法增强

特性Vue 2Vue 3
Fragment(多根节点)❌ 不支持✅ 支持
Teleport(传送组件)❌ 不支持✅ 支持(类似 React Portals)
Suspense(异步依赖加载)❌ 不支持✅ 支持
自定义渲染器✅ 支持✅ 更加模块化

🧩 八、组合式 API(Composition API)

特性Vue 2Vue 3
是否支持 Composition API❌ 不支持✅ 完全支持
是否支持 <script setup>❌ 不支持✅ 完全支持(推荐写法)
是否支持 ref / reactive❌ 不支持✅ 完全支持
是否支持 watchEffect / watch❌ 不支持✅ 完全支持

总结对比

对比项watchEffectwatch
是否自动追踪依赖✅ 是❌ 否
是否能获取新/旧值❌ 否✅ 是
是否需要手动指定监听对象❌ 否✅ 是
是否适合监听对象/数组✅ 可以(但不推荐深层监听)✅ 推荐配合 { deep: true }
是否适合执行清理逻辑✅ 支持 onInvalidate✅ 支持
使用场景快速监听多个依赖精确监听某个值或多个值

🔥 九、性能优化

特性Vue 2Vue 3
编译优化✅ Block Tree 编译优化
渲染速度一般✅ 更快(diff 算法优化)
包体积较大✅ 更小(Tree-shaking 更彻底)
SSR 支持✅ 支持✅ 更高效(Hydration 支持)

📦 十、模块导出方式

特性Vue 2Vue 3
默认构建方式IIFE / UMDESM(默认使用 ES Module)
是否支持按需导入❌ 不支持✅ 支持按需引入(如 ref, computed

💡 十一、其他重要改进

功能Vue 2Vue 3
开发体验使用 Vue Devtools 支持✅ 支持更好
生态更新社区活跃,但已进入维护模式✅ 主流框架,持续更新
TypeScript 支持有限✅ 完全支持
JSX 支持✅ 支持✅ 支持
多版本共存✅ 支持✅ 支持

✅ 十二、示例对比:同一个组件的不同写法

Vue 2(Options API)

jsexport default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

Vue 3(Composition API + <script setup>

vue<script setup>
import { ref } from 'vue'const count = ref(0)
function increment() {
  count.value++
}
</script>

📌 十三、总结对比表

对比项Vue 2Vue 3
响应式系统Object.definePropertyProxy + ref/reactive
API 风格Options API支持 Options + Composition API
生命周期钩子beforeCreate, created, mountedonBeforeMount, onMounted
支持 TypeScript✅ 支持✅ 更友好
支持 <script setup>❌ 不支持✅ 强烈推荐
支持多 v-model❌ 不支持✅ 支持
支持 Fragment、Teleport、Suspense❌ 不支持✅ 支持
构建工具Webpack / Babel✅ 支持 Vite
包大小较大✅ 更小
社区生态成熟✅ 新一代生态(Pinia、Vite、Vue Router 4)

✅ 最终建议

场景推荐版本
新项目✅ Vue 3(推荐搭配 <script setup>
老项目维护✅ Vue 2(仍可继续使用)
需要高性能✅ Vue 3
需要 TypeScript 支持✅ Vue 3
需要使用 Vite✅ Vue 3
需要使用 Pinia✅ Vue 3
需要使用 Composition API✅ Vue 3

闭包(Closure)定义

闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 闭包 = 函数 + 函数能够访问的自由变量。

闭包是指能够访问并记住其词法作用域(lexical scope),即使该函数在其作用域外执行。

闭包的本质是:

一个函数在定义时所处的环境(作用域)与它自身结合,形成闭包。


自由变量(Free Variable)

自由变量是指:

  • 函数中使用的变量;
  • 它既不是函数的参数,也不是函数内部定义的局部变量;
  • 它来自函数外部的作用域。

例如:

javascriptfunction outer() {
  const x = 10;
  return function inner(y) {
    return x + y; // `x` 是 inner 函数的自由变量
  };
}
​
const closureFunc = outer();
console.log(closureFunc(5)); // 输出 15

在这个例子中:

  • inner 是一个闭包;
  • 它捕获了外部函数 outer 中定义的变量 x,即自由变量;
  • 即使 outer 已经执行完毕,x 依然被保留在内存中,因为 inner 引用了它。

闭包的组成

闭包 = 函数 + 该函数能够访问的自由变量环境

上面的例子中:

  • 函数:inner
  • 自由变量环境:包含 x: 10

闭包的常见用途

使用场景示例说明
数据封装利用闭包实现私有变量和方法
柯里化 / 偏函数应用创建带有部分参数绑定的新函数
回调函数在异步编程中保留上下文状态
模块模式实现模块化的结构,避免全局污染

注意事项

  • 内存泄漏风险:如果闭包引用了外部变量,这些变量不会被垃圾回收器回收,可能导致内存占用过高。
  • 性能考虑:频繁创建闭包可能会带来额外的性能开销。 Promise 是 JavaScript 中处理异步操作的一种方式,它提供了比传统的回调函数和事件更强大和灵活的异步编程模型。以下是 Promise 的原理及其在面试中常被问到的关键点:

1. Promise 状态

Promise 对象有三种状态:

  • pending(进行中) :初始状态,既不是成功也不是失败。
  • fulfilled(已成功) :表示操作成功完成。
  • rejected(已失败) :表示操作失败。

一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,这个状态就不可逆,并且会触发相应的回调函数。

2. Promise 构造函数

Promise 是通过 new Promise() 创建的,构造函数接受一个函数作为参数,这个函数又接受两个参数 resolve 和 reject,分别用于将 Promise 的状态变为 fulfilled 或 rejected

javascript
const promise = new Promise((resolve, reject) => {
    // 异步操作
    if (/* 成功条件 */) {
        resolve(value); // 成功时调用 resolve
    } else {
        reject(error); // 失败时调用 reject
    }
});

3. then 和 catch 方法

  • then(onFulfilled, onRejected) :用于注册当 Promise 被解决(无论是 fulfilled 还是 rejected)时的回调函数。
  • catch(onRejected) :用于捕获 Promise 链中的错误,相当于 then(null, onRejected)

Promise 支持链式调用,因为 then 和 catch 方法都会返回一个新的 Promise 对象。

4. Promise 链式调用

Promise 的核心优势之一是支持链式调用。每个 then 返回一个新的 Promise,使得可以继续在其后添加新的 then 或 catch

javascript
promise
    .then(result => {
        return result * 2; // 返回值会传递给下一个 then
    })
    .then(doubleResult => {
        console.log(doubleResult);
    })
    .catch(error => {
        console.error(error); // 捕获前面的错误
    });

5. 错误传播

如果在 Promise 链中的任何一个环节发生错误,并且没有被当前的 catch 捕获,错误会一直向后传播,直到被某个 catch 捕获或者导致未处理的 Promise rejection。

6. Promise.all、Promise.race、Promise.resolve、Promise.reject

  • Promise.all(iterable) :等待所有 Promise 完成,如果其中任意一个 Promise 被拒绝,则立即拒绝。
  • Promise.race(iterable) :只要有一个 Promise 解决或拒绝,就立即以相同的结果结束。
  • Promise.resolve(value) :返回一个以给定值解析的 Promise。
  • Promise.reject(reason) :返回一个以给定原因拒绝的 Promise。

7. 微任务队列

Promise 的回调函数(如 then 和 catch)会被放入微任务队列中,确保它们在当前脚本执行完成后尽快执行,但会在任何宏任务之前执行。这保证了 Promise 回调具有更高的优先级。

8. 实现原理简述

内部实现上,Promise 包含以下几个部分:

  • 状态管理:跟踪 Promise 的状态(pending、fulfilled、rejected)。
  • 值存储:保存 Promise 解决后的结果或拒绝的原因。
  • 回调队列:当 Promise 的状态发生变化时,执行相应的回调函数。
  • 链式调用支持:通过返回新的 Promise 来支持链式调用。

9. 常见问题与陷阱

  • 忘记 catch 错误:未处理的 Promise rejection 可能会导致程序崩溃。
  • 嵌套 Promise:过度嵌套可能导致代码难以理解和维护。
  • Promise 链中断:如果没有正确返回值或抛出错误,可能会导致后续的 then 不执行。

10. 手动实现简易 Promise

虽然完整的 Promise/A+ 规范较为复杂,但在面试中通常只需要实现一个简化版的 Promise,支持基本的 then 和链式调用即可。在 JavaScript 中,事件循环(Event Loop)是理解异步编程的核心机制。以下是面试中常见的关于事件循环的知识点总结:


1. JavaScript 是单线程语言

JavaScript 最初设计为单线程语言,意味着它只有一个主线程来执行代码。为了避免阻塞,JS 引入了 事件循环机制 来处理异步操作。


2. 调用栈(Call Stack)

  • JS 引擎用来管理函数调用的数据结构。
  • 每当一个函数被调用,它会被压入调用栈;执行完成后弹出。
javascript
function foo() {
    console.log("foo");
}
function bar() {
    foo();
}
bar(); // call stack: bar -> foo

3. Web APIs(宿主环境提供的功能)

浏览器提供的一些异步功能,例如:

  • setTimeout
  • setInterval
  • fetch
  • DOM 事件

这些任务由浏览器执行,完成后将回调放入对应的队列中。


4. 回调队列(Callback Queue)

  • 当 Web API 完成任务后,其回调函数会被放入相应的队列中等待执行。

  • 包括:

    • 宏任务队列(Macro Task Queue)

      • 如:setTimeoutsetIntervalI/OUI 渲染script 标签加载等。
    • 微任务队列(Micro Task Queue)

      • 如:Promise.then/catch/finallyqueueMicrotaskMutationObserver

5. 事件循环流程图解

简化版流程如下:

+----------------------+
|      Call Stack      |
+----------+-----------+
           |
           v
+----------------------+
|     Web APIs         |
+----------+-----------+
           |
           v
+----------------------+     +------------------------+
|   Callback Queues    | --> | Event Loop (检查队列)  |
+----------+-----------+     +------------------------+
           |
    +------v-------+
    | Micro Task Q |   <---- 优先级更高
    +--------------+
    | Macro Task Q |
    +--------------+

6. 微任务 vs 宏任务

类型示例特点
微任务Promise.thenqueueMicrotask优先级高,在当前宏任务结束后立即清空微任务队列
宏任务setTimeoutsetInterval正常排队,每次事件循环处理一个宏任务

示例说明:

javascript
console.log("Start");

setTimeout(() => {
    console.log("setTimeout");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise then");
});

console.log("End");

// 输出顺序:
// Start
// End
// Promise then
// setTimeout

7. 事件循环的完整流程

  1. 执行全局同步代码(宏任务)。
  2. 清空所有微任务队列中的任务。
  3. 渲染页面(如果需要)。
  4. 等待下一个宏任务到来并重复上述流程。

8. 常见面试题解析

✅ 题目1:

javascript
console.log('A');
setTimeout(() => {
    console.log('B');
}, 0);
Promise.resolve().then(() => {
    console.log('C');
});
console.log('D');

// 输出:A D C B

✅ 题目2:

javascript
console.log('start');

setTimeout(() => {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
    console.log('then');
}).then(() => {
    console.log('then again');
});

console.log('end');

// 输出:start end then then again setTimeout

9. 实际应用与优化

  • 使用 Promise.then() 替代嵌套的 setTimeout 提高性能。
  • 避免在微任务中执行耗时操作,否则会阻塞后续渲染或用户交互。
  • 可以使用 queueMicrotask(fn) 或 MutationObserver 进行更细粒度的异步控制。

10. 扩展知识点(进阶面试)

  • Node.js 的事件循环和浏览器的区别
  • process.nextTick() 在 Node 中优先于 Promise.then
  • requestIdleCallback 和 requestAnimationFrame 的使用场景 在 JavaScript 面试中,深拷贝(Deep Copy)与浅拷贝(Shallow Copy)  是常被问到的基础知识点。它们涉及对象、数组等引用类型的数据复制行为。

🧠 一、基本概念

1. 浅拷贝(Shallow Copy)

  • 仅复制对象的第一层属性
  • 如果属性是引用类型(如对象或数组),则复制的是其 引用地址
  • 修改嵌套对象的值会影响原对象。

常见实现方式:

javascript
// Object.assign
const obj2 = Object.assign({}, obj1);

// 扩展运算符
const obj2 = { ...obj1 };

// 数组 slice
const arr2 = arr1.slice();

示例:

javascript
const user = {
  name: 'Tom',
  info: { age: 20 }
};

const copy = Object.assign({}, user);
copy.info.age = 30;

console.log(user.info.age); // 输出 30,说明引用共享了

2. 深拷贝(Deep Copy)

  • 递归复制对象的所有层级属性
  • 原始对象和拷贝对象完全独立,互不影响。

常见实现方式:

✅ 简单但有限的方法:
javascript
JSON.parse(JSON.stringify(obj))

⚠️ 缺点:不能处理函数、undefined、Symbol、循环引用等。

✅ 递归实现(基础版):
javascript
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  const copy = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key]);
    }
  }
  return copy;
}
✅ 使用第三方库(推荐):
  • lodash 的 cloneDeep
  • structuredClone()(现代浏览器支持)

📌 二、面试常见问题解析

Q1:为什么 JSON.parse(JSON.stringify(obj)) 不是真正的深拷贝?

  • 无法复制以下内容:

    • 函数(function)
    • Symbol 类型
    • undefined
    • 循环引用(如 obj.self = obj
    • Date 对象会被转为字符串

Q2:如何判断两个对象是否相等?

  • === 只比较引用。
  • 要判断内容是否相同,需手动遍历或使用工具库(如 lodash.isEqual)。

Q3:如何处理循环引用?

可以使用一个 WeakMap 来记录已克隆的对象:

javascript
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);
  
  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key], map);
    }
  }
  return copy;
}

📊 三、对比总结

特性浅拷贝深拷贝
复制层级第一层所有层级
引用共享
性能相对慢
实现复杂度简单复杂(需考虑循环引用等)
常用方法Object.assign, 扩展运算符JSON.parse/stringify, 递归, lodash

💡 四、实际应用场景

场景应该使用
数据展示浅拷贝即可
表单数据编辑(需还原)深拷贝
Vuex 中的状态快照备份深拷贝
配置项复制根据需求选择

在 JavaScript 中,原型(Prototype)与原型链(Prototype Chain)  是理解对象继承机制的核心概念。以下是面试中常被问到的原型和原型链知识点总结:


一、原型(Prototype)

1. 每个函数都有一个 prototype 属性

  • 函数的 prototype 是一个对象,用于构建实例对象的原型。
  • 实例对象会继承构造函数 prototype 上的方法和属性。
javascript
function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
};

const p1 = new Person('Tom');
p1.sayHello(); // Hello, Tom

2. 每个对象都有一个 __proto__ 属性

  • __proto__ 是对象的一个内部属性,指向其构造函数的 prototype
  • 它是实现继承的关键。
javascript
console.log(p1.__proto__ === Person.prototype); // true

二、原型链(Prototype Chain)

1. 原型链的本质

  • 当访问一个对象的属性或方法时,如果该对象自身没有这个属性,JavaScript 引擎会沿着 __proto__ 查找其原型对象。
  • 如果原型对象也没有,继续向上查找,直到 Object.prototype 或 null 为止。

2. 示例说明

javascript
function Animal() {}
Animal.prototype.eat = function() {
    console.log('Animal is eating.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype); // 继承 Animal
Dog.prototype.bark = function() {
    console.log('Dog is barking.');
};

const dog = new Dog();
dog.eat();  // Animal is eating.
dog.bark(); // Dog is barking.

三、构造函数、实例、原型之间的关系图

实例对象
   ↓ __proto__
构造函数.prototype
   ↓ __proto__
父级构造函数.prototype
   ↓ __proto__
Object.prototype
   ↓ __proto__
null

四、常见面试题解析

✅ 题目1:什么是原型链?为什么需要它?

答:  原型链是 JavaScript 实现继承的方式。每个对象都有一个 __proto__ 属性,指向其构造函数的 prototype,从而形成一条链式结构。通过原型链,子对象可以访问父对象上的属性和方法。


✅ 题目2:如何判断一个对象是否属于某个构造函数的实例?

答:  使用 instanceof 运算符。

javascript
function Person() {}
const p = new Person();

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true

✅ 题目3:如何修改原型对象?

答:  可以通过直接赋值给 构造函数.prototype 或使用 Object.setPrototypeOf() 修改原型。

javascript
Person.prototype.walk = function() {
    console.log(`${this.name} is walking.`);
};

⚠️ 注意:不要直接重写整个原型对象,否则会破坏原有的原型链。


✅ 题目4:constructor 属性的作用是什么?

答:  constructor 是原型对象上的默认属性,指向对应的构造函数。

javascript
console.log(Person.prototype.constructor === Person); // true

✅ 题目5:如何实现类的继承?

答:  使用 Object.create() 或 class extends(ES6+)

ES5 方式:

javascript
function Parent() {}
Parent.prototype.parentMethod = function() {};

function Child() {}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.childMethod = function() {};

ES6 方式:

javascript
class Parent {}
class Child extends Parent {}

五、原型链常见陷阱

问题描述
共享引用类型数据多个实例共享原型中的引用类型(如数组),修改一个会影响其他实例。
忘记绑定 this在原型方法中使用 this 时,如果作为回调传入其他上下文,可能会出错。
滥用原型链导致性能下降过长的原型链会增加属性查找时间。

六、扩展知识点(进阶面试)

知识点说明
Object.getPrototypeOf(obj)获取对象的原型
Object.setPrototypeOf(obj, prototype)设置对象的原型
isPrototypeOf()判断一个对象是否存在于另一个对象的原型链上
hasOwnProperty()判断属性是否为对象自身的属性