实操面试题

27 阅读15分钟

一、 webpack能做什么

  1. webpack 解决什么问题?

    • 模块打包工具:把多文件、多类型资源(js、css、图片)打成浏览器可识别的 bundle。
  2. 核心概念:

    • entry:入口
    • output:打包输出
    • loader:让 webpack 能处理 JS 以外的文件,比如 css-loader、url-loader、ts-loader
    • plugin:在打包流程的各个阶段做额外的事,比如 HtmlWebpackPlugin、DefinePlugin
  3. 常见的性能优化点可以背几条:

    • 代码分割(SplitChunks、路由懒加载)
    • 开启持久化缓存(filename 带 hash)
    • Tree Shaking(移除未使用代码)
    • 使用 CDN / externals 把大库抽出去

二、为什么选择webpack而不是vite

  1. 项目复杂度:我们预期这是一个中大型后台系统,会有复杂的构建需求
  • 微前端集成
  • 自定义构建配置:
    • 自定义loader:

      • 自动移除console.log(生产环境)
      • 自定义文件格式处理)
    • 自定义Plugin

      • 构建时安全检查
      • 资源优化(图片压缩、代码混淆)
  • 多入口(没做)
  1. 生态依赖:需要大量第三方插件(如安全扫描、代码质量检测、多环境配置)
  2. 团队因素:团队熟悉webpack,能更快落地,降低初期风险”
  3. webpack配置虽然复杂,但文档丰富,新人更容易理解,出现问题有大量社区解决方案

三、websockt里面怎么设计的心跳机制

1. 为什么要设计心跳:

  • 连接是否还活着
  • 尽早发现“假在线”
  • 为重连 / 补偿提供触发点

在浏览器里尤其重要,因为:

  • 切后台
  • 断网
  • 系统回收
    都可能导致 TCP 已断,但 JS 不知道

2. 如何设计心跳

采用 客户端主动心跳 + 服务端被动响应 的模式:

  • 客户端定时发 PING
  • 服务端返回 PONG

原因:

  • 浏览器更容易感知异常
  • 心跳节奏可控
  • 前端可以结合页面状态做优化

心跳检测判断断连接后采用斐波那契的重连方式

四、描述一下宏任务和微任务

1. 事件循环到底怎么跑(关键顺序)

浏览器里 JS 的执行节奏大致是:

  1. 宏任务队列 取出一个宏任务执行(比如执行一段脚本、一个定时器回调、一次点击事件回调)
  2. 这个宏任务执行过程中,可能会产生一些 微任务(比如 Promise.then)
  3. 当前宏任务执行完以后,立刻把微任务队列清空(注意:是清空到没有为止)
  4. 微任务清空后,浏览器才有机会去做 渲染(render)
  5. 然后再回到第 1 步,取下一个宏任务

一句话:
每执行完一个宏任务,都会把所有微任务做完,再考虑渲染,然后才进入下一个宏任务。

2. 宏任务有哪些?微任务有哪些?

常见宏任务(大任务)

  • setTimeout / setInterval 的回调
  • DOM 事件回调:clickinputscroll
  • I/O 回调、网络事件回调(在浏览器里可理解为某些异步事件)
  • requestAnimationFrame(严格来说它在渲染前回调,跟宏任务队列不完全一回事,但面试中常放在“宏任务/渲染相关”一起讨论)
  • 执行整段 <script>(入口代码本身就是一个宏任务)

常见微任务(插队任务)

  • Promise.then / catch / finally 的回调
  • queueMicrotask
  • MutationObserver 回调

五、聊聊原型与原型链

JavaScript 常被描述为一种基于原型的语言——每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

javascript 查找机制

查找.jpg

① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性,找到即返回,不再向上查找。

② 如果没有就查找它的原型(也就是__proto__ 指向的 prototype 原型对象)。

③ 如果还没有就查找原型对象的原型( Object 的原型对象)。

④ 依此类推一直找到 Object 为止( null ),返回 undefined。

proto对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

原型对象中 this 指向

  • 在构造函数中,里面的 this 指向的是实例对象
  • 原型对象函数里面的 this,是谁调用这个函数,this 就指向谁,因此也是指向实例对象

如何判断一个对象是否在原型链上(instanceof)

在JavaScript中,可以通过几种方式来判断一个对象是否存在于另一个对象的原型链上。以下是一些常用的方法:

  1. instanceof运算符

  2. Object.prototype.isPrototypeOf方法

  3. __proto__属性(非标准,但在很多环境中可用)

  4. Object.getPrototypeOf方法

在实际应用中,推荐使用instanceof运算符或Object.prototype.isPrototypeOf方法,因为这两种方法更加标准化,且不需要直接操作对象的原型属性。

六、为什么需要用到qiankun

qiankun可以让子应用独立开发、独立部署、独立发布或者回滚,很适合大型的项目;qiankun 用“微前端 + 隔离 + 独立发布”把复杂度从“互相影响”变成“各自边界清晰”

1. qiankun 运行机制

从加载到渲染的流程:

  1. 主应用按路由匹配到某个子应用
  2. qiankun 拉取子应用资源(html/js/css)
  3. 创建隔离环境(沙箱)并执行子应用脚本
  4. 触发子应用生命周期:bootstrap → mount
  5. 路由切换时触发:unmount,清理 DOM 与副作用(理想情况)

**一句话总结:**主应用负责“装载/卸载”,子应用负责“在容器里把自己渲染好并清理干净”。

2. 主应用 vs 子应用边界怎么划分

主应用(壳)通常负责:

  • 登录态/鉴权信息(token、用户信息、权限)
  • 全局布局(Header/SideMenu/Tab)
  • 路由分发(匹配哪个子应用)
  • 统一能力:埋点、监控、国际化、主题、灰度配置

子应用负责:

  • 自己的业务页面与内部路由
  • 自己的状态管理与接口调用
  • 遵守主应用协议(接收 props、按规范上报)

3. 主子应用通信怎么设计(避免强耦合)

常用方式与适用场景:

  • props:主 → 子传入用户、语言、主题、能力函数(最推荐,最清晰)
  • globalState:需要多应用共享、且频率不高的状态(如语言、主题)
  • URL 参数:跨应用跳转带条件(简单透明)
  • 自定义事件总线:谨慎使用,容易失控(必须协议化)

协议化原则(中级加分):

  • 统一事件命名(app:xxx
  • payload 定 schema(字段、类型、版本)
  • 约定向后兼容策略(新增字段不破坏旧版本)

4. 沙箱隔离与样式隔离

沙箱隔离(JS 侧)

  • 能隔离/减少:对 window 的全局变量污染、部分副作用
  • 不能完全隔离:localStorage/cookie、网络请求本身、浏览器级资源

**实践结论:**子应用仍然要自律:不要随意挂全局变量、不要滥用单例。

样式隔离(CSS 侧)

  • 最可靠:业务自带 CSS Modules/BEM/命名空间
  • qiankun 的样式隔离可作为补充,但不保证 100% (第三方库样式/全局标签选择器仍可能冲突)

路由怎么配合(hash/history & 刷新问题)

原则:主控子,层级清晰

  • 主应用决定“进入哪个子应用”(一级路由)
  • 子应用只维护“子应用内部路由”(二级路由)
  • 跨应用跳转应由主应用统一导航(保证前进后退正常)

history 模式必提:

  • 刷新 404 白屏要靠服务端 fallback(所有子路由重写到 index.html)

性能优化

  • 子应用按需注册(不要一次性全注册/全加载)
  • 预加载:只对“高概率访问”的子应用预加载
  • 依赖共享/外置:避免多份 React/Vue(但要控制版本一致性风险)
  • 加载失败兜底:错误页 + 重试 + 上报

你用 qiankun 遇到过哪些坑

  1. publicPath 导致资源 404 白屏:子应用嵌进主应用后路径变了,解决是动态 publicPath + 统一 CDN 路径策略。
  2. history 刷新 404:服务端没做 rewrite,解决是网关/静态服务器配置 fallback。
  3. 样式互相污染:尤其 UI 库全局样式,解决是命名空间/CSS Modules,隔离当补充。
  4. unmount 清理不干净导致越用越卡:定时器、事件监听、订阅、WebSocket 没清,解决是副作用清单 + 统一 dispose。
  5. 拦截器/埋点重复注册:切来切去重复上报,解决是保存注册句柄并在 unmount eject,或统一在主应用注入。
  6. 多份 React/Vue:导致 hooks/context 异常,解决是 external/共享依赖,版本强约束。
  7. 本地开发跨域:cookie/token 带不上,解决是统一 dev 域名 + 代理策略。
  8. eventBus 滥用:排查困难,解决是通信协议化、收敛到 props/globalState。

unmount 你会清理哪些副作用?怎么保证不会漏?

unmount 清理的核心是:计时器、全局监听、订阅、长连接、第三方实例和全局污染;保证不漏靠“统一注册/统一销毁 + 自动检测”,而不是靠人肉记。

七、Vue2 vs Vue3 的主要区别

  • 响应式原理

    • Vue2:Object.defineProperty(给每个属性加 getter/setter)
    • Vue3:Proxy(代理整个对象)
  • 性能与体积

    • Vue3 更快、tree-shaking 更好,按需引入更彻底
  • API 组织方式

    • Vue2 以 Options API 为主(data/methods/computed)
    • Vue3 增加 Composition API(逻辑更好复用、拆分更清晰)
  • 组件/渲染

    • Vue3 的虚拟 DOM 和编译优化更强,更新更精准
  • 生态与工程

    • Vue3 更友好 TypeScript;Teleport/Fragments/Suspense 等新能力

八、Vue3 为什么用 Proxy 改写数据劫持?

一句话总结:
Vue3 用 Proxy 是为了更完整地拦截对象操作(新增/删除/数组等),减少 Vue2 的各种补丁和限制,同时提升性能与一致性。

因为 defineProperty 有一些“先天限制”,Proxy 能更自然地解决:

  1. Vue2 不能监听“新增/删除属性”

    • Vue2 必须用 Vue.set / Vue.delete
    • Proxy 能直接拦截 set/deleteProperty,新增/删除也能自动响应
  2. Vue2 不能很好监听数组变化(很多要打补丁)

    • Vue2 需要重写数组方法(push/pop 等)来触发更新
    • Proxy 能拦截对数组的下标赋值、length 变化等,逻辑更统一
  3. Vue2 必须“递归遍历”把每个属性都变成响应式

    • 对大对象开销大,初始化慢
    • Proxy 可以按需代理/按需追踪(访问到才处理),整体更高效
  4. Proxy 能拦截的操作更全面

    • get/set/has/ownKeys/deleteProperty
    • 让响应式系统更完整,边界行为更一致(比如 in、遍历等场景)

九、聊聊Composition API和optionAPI的区别

Composition API 和 Options API 的区别,核心就一句:Options API 按“配置项”组织代码;Composition API 按“功能/逻辑块”组织代码。

1. 代码组织方式不同

Options API(Vue2/也支持Vue3)

  • 代码分散在 data / methods / computed / watch / mounted...
  • 同一个功能的代码会被拆开放在不同区块里(功能一复杂就难追)

适合:小组件、简单页面、团队习惯传统写法

Composition API(Vue3主推)

  • 把一个功能相关的:状态、计算属性、监听、副作用都放在一起
  • 可以抽成 useXxx() 组合函数复用

适合:业务复杂、逻辑复用强、多人协作、TS 要求高


2. 逻辑复用方式不同

  • Options API:主要靠 mixins(容易命名冲突、来源不清晰、组合困难)
  • Composition API:用 useXxx()(输入输出明确,组合灵活,冲突少)

3. TypeScript 体验不同

  • Options API:类型推导相对绕(this 上下文、装饰器/泛型处理麻烦)
  • Composition API:更像普通 TS 函数,类型自然、可读性更好

对“this”的依赖

  • Options API:大量依赖 this(新同学容易搞混 this 指向/类型)
  • Composition API:基本不依赖 this,变量/函数都是显式引用

可维护性差异(复杂组件时最明显)

  • Options API:功能逻辑分散,越写越“翻文件找代码”
  • Composition API:功能逻辑聚合,更容易把复杂业务拆成模块组合

什么时候选哪个(实用建议)

  • 组件简单、团队统一风格、快速交付:Options API
  • 组件复杂、需要强复用、要抽业务逻辑、TS 重:Composition API
  • 现实里常用:简单组件用 Options,复杂业务用 Composition(或逐步迁移)

十、说说http1.1和http2.0之间的异同

HTTP/1.1 和 HTTP/2 的共同点是:语义不变(还是同样的 URL、方法、状态码、Header 概念),主要差异在传输层怎么更高效地传

一句话总结

  • HTTP/1.1:为了并发常用“多连接 + 管线化有限”,仍容易队头阻塞、Header 冗余
  • HTTP/2:用“二进制分帧 + 多路复用 + Header 压缩(+可选 server push)”显著提升性能,但仍受 TCP 丢包导致的队头阻塞影响

相同点

  • 都是 请求-响应模型(Request/Response)
  • 都支持 持久连接(Keep-Alive,默认复用连接)
  • 都有 Header、状态码、缓存、Cookie 等机制(语义一致)
  • 应用层接口对业务基本一致(服务端路由、REST 这些不变)

不同点(HTTP/2 相比 1.1 的提升)

  1. 二进制分帧 vs 文本协议

    • 1.1:文本格式,解析/传输效率较低
    • 2:二进制帧(Frame),更紧凑、更易优化
  2. 多路复用(Multiplexing)

    • 1.1:同一连接上请求基本是串行/有限并行(容易“队头阻塞”)
    • 2:一个 TCP 连接上可以同时跑多个 stream,请求响应交错传输
      👉 解决 1.1 需要开多连接才能并发的问题
  3. 队头阻塞(Head-of-line blocking)差异

    • 1.1:应用层层面很明显(一个请求卡住,后面排队)
    • 2:应用层基本解决,但 TCP 层仍可能队头阻塞(丢包会影响所有 stream)
  4. Header 压缩(HPACK)

    • 1.1:Header 每次都带一堆重复字段(Cookie、UA 等)
    • 2:用 HPACK 压缩 + 索引表复用,显著减少头部开销
  5. 服务端推送(Server Push)

    • 2:服务端可主动推资源(现在实际使用变少/很多场景会关掉)
    • 1.1:没有原生 server push
  6. 连接使用方式

    • 1.1:浏览器通常对同域名开多个 TCP 连接提升并发(有上限)
    • 2:更倾向于 单连接高并发,减少握手、减少连接竞争

十一、promise(A).catch(f1).then(f2),f1执行后f2 会执行吗,为什么?

会不会执行 取决于 f1 执行后的“返回结果”

  • catch 处理了错误并正常返回 → 后面 then 继续
  • catch 处理时又抛错/拒绝 → 后面 then 不走,继续走后续 catch

1. 结论

promise(A).catch(f1).then(f2) 里:

  • 如果 A 成功catch 不会走,直接走 then(f2)

  • 如果 A 失败:会走 catch(f1),然后:

    • f1 正常结束(return 一个值 / 不写 return 等同 return undefined)
      → 链条被“救回来”变成 fulfilled,所以 f2 会执行
    • f1 抛异常 或 return Promise.reject(...)
      → 链条仍是 rejected,所以 f2 不会执行(除非你在 then 里写第二个参数或再接一个 catch)❌

2. 为什么

catch(f1) 本质等价于:then(undefined, f1)
它是一个“错误处理器”。只要错误处理器没有把错误再抛出去,Promise 链就会从“失败态”转成“成功态”,后面的 then(f2) 就会继续跑。

十二、 判断b是否是a的子集

function isSubset(a, b) {
  const setA = new Set(a);
  for (const x of b) {
    if (!setA.has(x)) return false;
  }
  return true;
}

如果要考虑重复次数(多重集合 / multiset) 比如 a=[1,1,2]b=[1,1] 才算子集,b=[1,1,1] 不算。

function isSubsetWithCount(a, b) {
  const cnt = new Map();
  for (const x of a) cnt.set(x, (cnt.get(x) || 0) + 1);

  for (const x of b) {
    const left = (cnt.get(x) || 0) - 1;
    if (left < 0) return false;
    cnt.set(x, left);
  }
  return true;
}

十三、js实现eventBus

class EventBus {
  constructor() {
    this.map = new Map();
  }
  on(event, fn) {
    if (!this.map.has(event)) this.map.set(event, []);
    this.map.get(event).push(fn);
  }
  emit(event, ...args) {
    (this.map.get(event) || []).forEach(fn => fn(...args));
  }
  off(event, fn) {
    if (!fn) return this.map.delete(event);
    const arr = this.map.get(event) || [];
    this.map.set(event, arr.filter(f => f !== fn));
  }
}

EventBus(事件总线)可以理解成一个全局的“广播站/中转站” ,用来让不同模块之间通过“事件”来通信。

  • 订阅(on) :某个模块说“我关心 X 事件,发生了就通知我”
  • 发布(emit) :某个模块说“X 事件发生了,通知所有关心的人”
  • 取消订阅(off) :不想再接收通知了

十四、Js实现有效括号匹配

function isValid(s) {
  const stack = [];
  const map = { ')': '(', ']': '[', '}': '{' };

  for (const ch of s) {
    if (ch === '(' || ch === '[' || ch === '{') {
      stack.push(ch);
    } else if (ch in map) {
      if (stack.pop() !== map[ch]) return false;
    } else {
      // 如果输入可能包含其他字符,可按需求处理;这里直接判无效
      return false;
    }
  }
  return stack.length === 0;
}

十五、js实现防抖节流

防抖(debounce)

特点:频繁触发只执行最后一次(比如输入框联想)

function debounce(fn, wait = 300) {
  let timer = null;
  function debounced(...args) {
    const ctx = this;
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(ctx, args), wait);
  }
  debounced.cancel = () => {
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
}

节流(throttle)

特点:固定时间内最多执行一次(比如滚动、拖拽)

function throttle(fn, wait = 300) {
  let last = 0;
  let timer = null;

  function throttled(...args) {
    const ctx = this;
    const now = Date.now();
    const remaining = wait - (now - last);

    if (remaining <= 0) {
      if (timer) clearTimeout(timer);
      timer = null;
      last = now;
      fn.apply(ctx, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        last = Date.now();
        timer = null;
        fn.apply(ctx, args);
      }, remaining);
    }
  }

  throttled.cancel = () => {
    clearTimeout(timer);
    timer = null;
    last = 0;
  };

  return throttled;
}

十六、概率题(智力题):两个人抛硬币,正面算赢,问第一个 开始抛赢到概率是多少

image.png

十七、时针和分针一天内重合多少次,分别是什么时间点

image.png