前端面试复习与准备指南

🧠 一、 JavaScript 深度 (重中之重,必问原理)

  1. 闭包 (Closure)

    • 解析: 函数与其声明时所在词法作用域的组合。即使函数在其词法作用域外执行,也能访问该作用域的变量。

    • 核心要点:

      • 形成条件: 函数嵌套 + 内部函数引用外部函数变量 + 内部函数在外部被调用。

      • 作用: 创建私有变量、模块化封装(IIFE)、函数工厂、柯里化、事件处理回调(注意循环陷阱)。

      • 内存泄漏风险: 闭包引用的变量不会被 GC 回收。解决方法: 不再需要时,将持有闭包引用的变量显式置为 null (例如移除事件监听器后)。

    • 面试回答示例: "闭包让我可以在 Vue 的组件方法或工具函数中,封装一些私有状态。比如实现一个带缓存的计算函数,或者一个防抖函数。但要注意,如果闭包引用了 DOM 元素或大型对象,且该闭包生命周期很长(如全局事件监听),需要适时解除引用防止内存泄漏。我在项目 [X] 中用闭包实现了 [Y] 功能,解决了 [Z] 问题。"

    • 常考代码:

    复制代码
    for (var i = 0; i < 5; i++) {
        setTimeout(function() { console.log(i); }, 1000); // 输出 5 个 5
    }
    // 解决1:使用 let (块级作用域)
    for (let i = 0; i < 5; i++) { ... } // 输出 0,1,2,3,4
    // 解决2:使用闭包 (IIFE)
    for (var i = 0; i < 5; i++) {
        (function(j) {
            setTimeout(function() { console.log(j); }, 1000);
        })(i);
    } // 输出 0,1,2,3,4
  2. 原型链 (Prototype Chain) & 继承

    • 解析: JS 实现继承的机制。每个对象 (obj) 都有一个内部属性 [[Prototype]] (可通过 __proto__ 访问,非标准) 指向其原型对象。查找属性时,若自身没有,则沿着 __proto__ 链向上查找,直到 Object.prototype (其 __proto__null)。

    • 核心要点:

      • 构造函数 (Constructor): 使用 new 调用的函数。其 prototype 属性指向一个对象,该对象将作为新创建实例的 __proto__

      • new 操作符步骤: 1. 创建一个空对象 {}; 2. 将该对象的 __proto__ 指向构造函数的 prototype; 3. 将构造函数的 this 绑定到这个新对象并执行; 4. 如果构造函数返回非对象,则返回这个新对象;否则返回构造函数的返回值。

      • instanceof 原理: 检查右侧构造函数的 prototype 属性是否出现在左侧对象的原型链上。

      • 继承方式优劣:

        • 原型链继承: Child.prototype = new Parent(); 缺点:共享引用属性、无法向父类传参。

        • 构造函数继承: function Child() { Parent.call(this); } 缺点:方法不能复用(每个实例都创建方法副本)、无法继承父类原型上的方法。

        • 组合继承 (最常用): function Child() { Parent.call(this); } + Child.prototype = new Parent(); 缺点:调用了两次父类构造函数(一次在 call,一次在 new),子类原型上有多余的父类实例属性。

        • 寄生组合式继承 (最优): function Child() { Parent.call(this); } + Child.prototype = Object.create(Parent.prototype); + Child.prototype.constructor = Child; (修正 constructor)。重点掌握!

        • ES6 class: 语法糖,底层基于寄生组合式继承。使用 extendssuper

    • 面试回答示例: "JS 的继承主要通过原型链实现。ES6 的 class 让写法更清晰,其本质是寄生组合式继承的语法糖。理解原型链对于调试(查看对象属性来源)、理解库的实现(如 Vue 的插件机制)都很重要。在项目 [X] 中,我们 [描述一个继承的应用场景,如自定义组件基类]。"

    • 必会手写:

      • new

      • instanceof

      • Object.create (简易版)

      • 寄生组合式继承

  3. Event Loop (事件循环)

    • 解析: JS 单线程处理异步的机制。管理调用栈 (Call Stack)、宏任务队列 (MacroTask Queue / Task Queue)、微任务队列 (MicroTask Queue / Job Queue)。

    • 核心要点:

      • 执行顺序:

        1. 执行当前调用栈中的所有同步任务。

        2. 栈空后,检查微任务队列,依次执行所有微任务直到队列清空。

        3. 执行一个宏任务(通常是队列中最早的那个)。

        4. 重复步骤 1-3 (下一个 Tick)。

      • 宏任务 (MacroTask): setTimeout, setInterval, setImmediate (Node), requestAnimationFrame (通常归类于此,但标准未明确定义), I/O (如 AJAX 回调), UI rendering (浏览器)。

      • 微任务 (MicroTask): Promise.then/catch/finally, MutationObserver, process.nextTick (Node)。

      • async/awaitasync 函数返回 Promise。await 后面的表达式会立即执行,但 await 下面的代码相当于放在 Promise.then 中(即微任务)。

    • 面试回答示例: "事件循环是 JS 异步编程的核心。比如在 Vue 中,nextTick 就是利用微任务(或降级到宏任务)来确保 DOM 更新后执行回调。我理解其顺序是:同步代码 -> 清空所有微任务 -> 执行一个宏任务 -> 再次清空微任务 -> 如此循环。这解释了为什么 Promise 回调比 setTimeout 快。在优化项目 [X] 时,我利用 nextTick 解决了 [Y] 问题。"

    • 经典题:

    复制代码
    console.log('script start');
    setTimeout(function() { console.log('setTimeout'); }, 0);
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });
    console.log('script end');
    // 输出顺序: script start -> script end -> promise1 -> promise2 -> setTimeout
  4. Promise

    • 解析: 异步编程的解决方案,代表一个最终可能可用(或失败)的值。状态:pending, fulfilled, rejected (一旦改变不可逆)。

    • 核心要点:

      • 链式调用: .then(onFulfilled, onRejected) / .catch(onRejected) / .finally(onFinally) 都返回新的 Promise。前一个 Promise 的状态和值决定下一个 then 的执行。

      • 静态方法:

        • Promise.resolve(value):创建已 fulfilled 的 Promise。

        • Promise.reject(reason):创建已 rejected 的 Promise。

        • Promise.all(iterable):所有成功才成功(值数组),任一失败立即失败(失败原因)。

        • Promise.race(iterable):第一个 settled(无论成功失败)的状态和值决定结果。

        • Promise.allSettled(iterable):所有 Promise 都 settled 后返回结果数组(包含状态和值/原因)。

        • Promise.any(iterable):任一成功即成功(值),全部失败才失败(AggregateError)。

      • 错误处理: .catch 捕获链中前面的任何错误。建议在链尾加 .catchtry/catch 不能 捕获异步错误(如 setTimeout 内部),但能捕获 async/await 中的错误。

    • 面试回答示例: "Promise 解决了回调地狱。在 Vue 中,异步操作(如 API 请求)大量使用 Promise。我常用 Promise.all 并行请求多个接口,用 async/await 让异步逻辑更同步化。nextTick 也返回 Promise。在项目 [X] 的登录模块,我使用 Promise 链式调用处理了 token 刷新逻辑。"

    • 必会手写:

      • Promise (简易版,实现状态、then、异步执行)

      • Promise.all

      • Promise.race


⚛️ 二、 Vue 2 & Vue 3 深度 (核心战场)

核心概念通用 (Vue2 & Vue3):

  1. 响应式原理 (Reactivity) - 必问!

    • Vue2 (基于 Object.defineProperty):

      • 解析: 遍历 data 对象属性,用 Object.defineProperty 设置 getter/setter。

      • 流程:

        1. 依赖收集 (Dep + Watcher): getter 中,将当前正在计算的 Watcher (如组件的 render watcher) 添加到该属性的 Dep 中 (dep.depend())。

        2. 派发更新: setter 中,通知 Dep 中所有 Watcher 更新 (dep.notify())。

        3. Watcher 更新: 执行更新函数(如 vm._update(vm._render(), ...)),触发重新渲染。

      • 缺点:

        • 无法检测对象属性的添加或删除 (需用 Vue.set/this.$setVue.delete/this.$delete)。

        • 无法检测数组索引的直接设置 (arr[index] = newValue) 和 length 修改 (需用变异方法 push/pop/shift/unshift/splice/sort/reverseVue.set)。Vue 重写了这些数组方法。

        • 初始化时递归遍历所有属性,对大对象性能开销大。

    • Vue3 (基于 Proxy):

      • 解析: 用 Proxy 包裹整个对象,拦截其所有操作(包括增删属性、数组索引修改)。

      • 流程:

        1. 创建响应式对象: reactive(obj) 返回一个 Proxy 代理对象。

        2. 拦截操作: 拦截 get (依赖收集)、set (派发更新)、deleteProperty 等操作。

        3. 依赖收集 (effect + track): get 操作中,用全局 activeEffect (当前运行的副作用函数,如组件的 setupRenderEffect) 和 track(target, key) 建立依赖关系。

        4. 派发更新 (trigger): set/deleteProperty 操作中,trigger(target, key, type) 找到依赖的副作用函数并执行。

      • 优势:

        • 直接检测对象属性的增删和数组索引/长度的变化。

        • 惰性访问:只有真正访问到的属性才会被代理和收集依赖,性能更好。

        • 支持 Map, Set, WeakMap, WeakSet 等集合类型。

      • API: reactive, ref (处理基本类型或对象引用,通过 .value 访问), computed, watch, watchEffect

    • 面试回答示例: "Vue2 的响应式通过 Object.defineProperty 拦截 get/set,需要递归遍历对象初始化,且对新增属性或数组索引修改不敏感,必须用 $set。Vue3 用 Proxy 解决了这些问题,性能更好且功能更强大。我在项目 [X] 中升级到 Vue3 后,明显感觉开发更顺畅,特别是处理动态表单字段时不再需要频繁 $setref 在组合式 API 中非常常用,用于包装基本类型或需要保持引用的对象。"

  2. 虚拟 DOM (Virtual DOM) & Diff 算法

    • 解析: 用 JS 对象模拟真实 DOM 树。状态变化时,生成新的 VNode 树,与旧的 VNode 树进行对比 (Diff),找出最小差异,然后批量更新到真实 DOM。

    • 为什么需要? 直接操作 DOM 昂贵(重排重绘)。JS 操作对象快,通过 Diff 计算最小变更,减少 DOM 操作次数,提升性能。

    • Vue 的 Diff 策略 (基于 Snabbdom):

      • 同层比较: 只比较同一层级的节点,不跨层级移动(性能考虑)。

      • Key 的重要性: Diff 算法通过 key 识别节点身份。强烈建议使用唯一且稳定的 key (如 id),绝对避免用 index! 使用 index 可能导致:错误的节点复用、不必要的 DOM 操作、状态错乱(如列表项有内部状态或组件)。

      • 双端比较 (Vue2): 同时从新旧子节点的两端开始对比。

      • 最长递增子序列 (Vue3): 在移动节点时,利用最长递增子序列算法 (LIS) 找出最少的移动操作。Vue3 的 Diff 效率更高。

    • 面试回答示例: "Vue 使用 VNode 描述视图,通过 Diff 算法找出新旧 VNode 的最小差异来更新真实 DOM,避免了昂贵的全量 DOM 操作。key 是 Diff 高效工作的关键,它帮助算法识别节点的唯一性。在项目 [X] 的可拖拽列表组件中,我使用唯一 ID 作为 key,确保了拖拽后节点状态正确且更新高效。Vue3 的 Diff 在移动节点方面比 Vue2 更智能,使用了最长递增子序列算法。"

  3. 组件通信 (Component Communication)

    • 常用方式:

      • Props Down: 父 -> 子。父组件通过属性传递数据,子组件用 props 接收。单向数据流,子组件不应直接修改 prop (Vue 会警告)。如需修改,可在子组件中用 data 或 computed 接收。

      • Events Up: 子 -> 父。子组件 $emit('event-name', payload),父组件 @event-name="handler"

      • v-model (语法糖): 父 v-model="data" ≈ 子 :value="data" + @input="data = $event"。Vue3 支持多个 v-model (v-model:title) 和自定义修饰符。

      • ref & $parent / $children (慎用): 直接访问组件实例。破坏封装性,耦合度高,不推荐在复杂应用中使用。

      • Provide / Inject: 祖先组件 provide(key, value),后代组件 inject(key[, defaultValue])。适合跨多级组件传递配置、主题、Locale 等。不是响应式的 (Vue2),除非传递响应式对象。Vue3 默认是响应式的。

      • Event Bus (Vue2 常用): 创建一个空的 Vue 实例 (const bus = new Vue()) 作为中央事件总线。组件 bus.$on, bus.$emit。缺点: 事件名冲突、难以调试、不适合大型应用。Vue3 官方推荐使用 mitt 等第三方库。

      • Vuex (Vue2) / Pinia (Vue3 推荐): 状态管理库,解决跨组件、跨层级状态共享和复杂状态逻辑。核心概念:

        • Vuex: State (状态), Getters (计算属性), Mutations (同步修改, commit), Actions (异步操作, dispatch), Modules (模块化)。

        • Pinia: 更简洁!Stores (定义 defineStore), State (ref), Getters (computed), Actions (function)。无 mutations,支持组合式 API,DevTools 支持好,TypeScript 友好。

    • 面试回答示例: "根据通信关系和场景选择方式。父子常用 props/events;深层嵌套用 provide/inject (如主题切换);跨组件或复杂状态用 Pinia (Vue3) / Vuex (Vue2)。我在项目 [X] 的用户模块,用户信息全局共享,就用了 Pinia 管理。对于简单的兄弟组件通信,有时用父组件做中转 (props+events),有时用 Pinia,避免引入事件总线。Vue3 的 v-model 支持多个非常方便,比如在弹窗组件上同时绑定 visibletitle。"

  4. 生命周期钩子 (Lifecycle Hooks)

    • Vue2 (Options API): beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed

    • Vue3 (Composition API): 对应 setup() 函数内的钩子:onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmountedbeforeCreate/createdsetup() 替代。

    • 核心用途:

      • created / setup():访问响应式 data/props/computed,发起异步请求 (注意:此时 DOM 未生成)。

      • mounted / onMounted:访问/操作 DOM,集成第三方 DOM 库 (如 D3、地图)。

      • beforeUpdate / onBeforeUpdate:数据变化后,DOM 更新前。

      • updated / onUpdated:数据变化导致 DOM 更新后。避免在此修改状态,可能导致无限循环!

      • beforeUnmount / onBeforeUnmount:实例销毁前,清理定时器、事件监听器、取消未完成的请求。

      • unmounted / onUnmounted:实例销毁后。

    • 面试回答示例: "生命周期钩子让我们在特定时机执行代码。比如在 created (Vue2) 或 setup 里发请求获取初始数据;在 mounted 里初始化需要 DOM 的图表库;在 beforeUnmount 里清除定时器或事件监听防止内存泄漏。Vue3 的组合式 API 将生命周期钩子变成了函数 (onMounted 等),可以在 setup 里按需引入,逻辑组织更灵活。在项目 [X] 的图表组件中,我在 onMounted 里初始化 ECharts 实例,在 onBeforeUnmount 里调用 dispose 销毁实例。"

  5. Vue3 Composition API vs Vue2 Options API

    • Options API 问题:

      • 逻辑关注点分散:相同功能的代码(如数据、方法、计算属性、生命周期)被拆分到不同的选项中。

      • 组件复杂时难以理解和维护。

      • 逻辑复用困难(Mixins 有命名冲突、来源不清晰等问题)。

    • Composition API 优势:

      • 逻辑复用: 通过自定义 Hook (函数) 封装可复用的逻辑,清晰且无命名冲突。如 useFetch, useMousePosition

      • 代码组织: 将同一功能的代码(数据、计算属性、方法、生命周期)聚合在一个函数 (setup 内部或自定义 Hook 中),提高可读性和可维护性。

      • 更好的 TypeScript 支持: 基于函数和变量,类型推导更自然。

      • 更灵活的代码组织: 不再受限于 data, methods 等选项的约束。

    • 面试回答示例: "Options API 在简单组件中很直观,但当组件变大变复杂时,相关逻辑被拆散到不同选项里,跳来跳去看很麻烦。Composition API 解决了这个问题,允许我把一个功能的所有相关代码(数据、计算、方法、生命周期)组织在一个地方,甚至可以提取成独立的可复用 Hook。在项目 [X] 中,我们将用户认证逻辑抽成 useAuth Hook,在多个组件和页面中使用,代码复用性和可维护性大大提高。TypeScript 支持也更好。"


🛠️ 三、 Vue 工程化与性能优化 (体现深度)

  1. Vue CLI vs Vite

    • Vue CLI (Webpack-based): 成熟稳定,插件生态丰富,配置相对复杂 (vue.config.js)。

    • Vite: 下一代前端工具。核心优势:

      • 极速启动: 基于原生 ES Module (ESM),开发服务器启动时无需打包,按需编译。

      • 高效热更新 (HMR): 仅编译修改的文件,更新速度与模块数量无关。

      • 生产打包: 使用 Rollup (高效,Tree Shaking 好)。

      • 开箱即用: 对 Vue, JSX, CSS, TypeScript, 静态资源等有良好支持。

    • 面试回答示例: "Vite 的开发体验太棒了!启动几乎是瞬间完成,热更新也飞快。它利用了浏览器原生 ESM 和 ESBuild (Go 编写,极快) 做预构建依赖。Vue CLI 基于 Webpack,更成熟但启动和 HMR 在大型项目里会慢一些。我们新项目 [X] 直接用 Vite 创建,体验非常好。生产打包两者都很好,Vite 用 Rollup,Vue CLI 用 Webpack。"

  2. 性能优化 (结合 Vue 特性)

    • 代码层面:

      • v-for 优化: 始终使用唯一且稳定的 key!避免和 v-if 用在同一元素上(v-if 优先级更高,会导致 v-for 列表每次重新渲染都先走 v-if 判断)。优先用计算属性过滤列表。

      • 合理使用 v-show vs v-ifv-show (切换 CSS display) 适合频繁切换;v-if (销毁/重建 DOM) 适合条件很少改变或初始渲染开销大。

      • 避免不必要的组件渲染:

        • Vue2: 使用 v-once (静态内容),对纯展示组件使用 functional (函数式组件)。

        • Vue3: 使用 v-memo (记忆子树,依赖项不变跳过更新),组合式 API 中合理使用 shallowRef / shallowReactive (浅层响应式)。

      • 计算属性 (computed) vs 方法 (methods): 计算属性基于依赖缓存,依赖不变时直接返回缓存值。方法每次调用都执行。优先用 computed

      • 事件处理: 避免在模板中使用内联函数 (@click="() => doSomething(id)"),特别是 v-for 内部,因为每次渲染都会创建新函数。使用方法引用 (@click="doSomething") 或 事件参数 (@click="doSomething($event, id)")。

      • 大型列表虚拟滚动: 使用 vue-virtual-scroller 等库,只渲染可视区域 DOM。

      • 组件懒加载 / 路由懒加载: 使用 defineAsyncComponent (Vue3) / () => import('./MyComponent.vue') (Vue Router)。

    • 打包层面 (Webpack/Vite):

      • Tree Shaking: 确保使用 ES Module (import/export),配置 package.json"sideEffects" 属性。

      • 代码分割 (Code Splitting):

        • 路由级懒加载 (Vue Router)。

        • 组件级懒加载 (defineAsyncComponent)。

        • 利用 Webpack 的 import() 语法或 Vite 的动态导入。

      • 图片优化: 压缩、使用 WebP 格式、懒加载 (loading="lazy" 或 IntersectionObserver)。

      • Gzip/Brotli 压缩: 服务器配置。

    • 运行时:

      • 使用生产版本: Vue 有开发版 (带警告) 和生产版 (优化过,体积小)。

      • SSR (Server-Side Rendering) / SSG (Static Site Generation): 解决首屏加载慢和 SEO 问题。Nuxt.js (Vue) 是流行框架。

    • 面试回答示例: "优化 Vue 项目,我会从多个层面入手。代码层:确保 v-for 用正确的 key;避免 v-ifv-for 同元素;大量列表用虚拟滚动;复杂计算用 computed;事件避免内联函数。打包层:路由和组件懒加载;开启 Tree Shaking;压缩图片。运行层:用生产版本;对于 SEO 和首屏要求高的用 Nuxt.js SSR。在项目 [X] 中,我们通过路由懒加载和图片优化,将首屏加载时间减少了 40%。Vue3 的 v-memoshallowRef 在特定场景下对性能提升也很明显。"


🧩 四、 手写代码题 (Vue 相关重点)

  1. 实现一个简易 v-model (理解双向绑定原理)

    复制代码
    // Vue2 风格 (使用 value 和 input)
    Vue.component('my-input', {
      props: ['value'],
      template: `
        <input
          :value="value"
          @input="$emit('input', $event.target.value)"
        >
      `
    });
    // 父组件: <my-input v-model="message"></my-input>
    
    // Vue3 风格 (支持 modelValue 和 update:modelValue)
    const MyInput = {
      props: ['modelValue'],
      emits: ['update:modelValue'],
      template: `
        <input
          :value="modelValue"
          @input="$emit('update:modelValue', $event.target.value)"
        >
      `
    };
    // 父组件: <my-input v-model="message"></my-input>
    // 或多个 v-model: <my-input v-model:first="first" v-model:last="last"></my-input>
  2. 实现一个简易 Event Bus (发布-订阅模式)
    *

    复制代码
    // Vue3 风格 (推荐用 mitt, 这里简易实现)
    class EventBus {
      constructor() {
        this.events = {};
      }
      $on(event, callback) {
        if (!this.events[event]) this.events[event] = [];
        this.events[event].push(callback);
      }
      $off(event, callback) {
        if (!this.events[event]) return;
        if (!callback) {
          delete this.events[event]; // 移除该事件所有监听
        } else {
          this.events[event] = this.events[event].filter(cb => cb !== callback);
        }
      }
      $emit(event, ...args) {
        if (!this.events[event]) return;
        this.events[event].forEach(callback => callback(...args));
      }
    }
    const bus = new EventBus();
    export default bus;
  3. 手写 $nextTick (理解其原理)
    *

    复制代码
    // 简易版 (利用微任务队列)
    function nextTick(callback) {
      if (typeof Promise !== 'undefined') {
        Promise.resolve().then(callback);
      } else if (typeof MutationObserver !== 'undefined') {
        // 降级到 MutationObserver (微任务)
        const textNode = document.createTextNode('0');
        const observer = new MutationObserver(callback);
        observer.observe(textNode, { characterData: true });
        textNode.data = '1';
      } else {
        // 最终降级到宏任务 (setTimeout)
        setTimeout(callback, 0);
      }
    }
    // Vue 实际实现更复杂,会维护一个回调队列,并在一个 Tick 中执行所有回调。
  4. 手写一个自定义 Hook useFetch (Vue3 Composition API)
    *

    复制代码
    import { ref } from 'vue';
    export function useFetch(url) {
      const data = ref(null);
      const error = ref(null);
      const isLoading = ref(false);
      const fetchData = async () => {
        isLoading.value = true;
        error.value = null;
        try {
          const response = await fetch(url);
          if (!response.ok) throw new Error(response.statusText);
          data.value = await response.json();
        } catch (err) {
          error.value = err.message || 'An error occurred';
        } finally {
          isLoading.value = false;
        }
      };
      fetchData(); // 自动发起请求 (可选)
      return { data, error, isLoading, fetchData }; // 返回 refs 和方法
    }
    // 在组件中使用: const { data, error, isLoading } = useFetch('/api/data');

📖 五、 项目经验梳理 (STAR 法则 + Vue 技术细节)

  • 选 2-3 个最能体现你 Vue 技术深度和解决问题能力的项目。