携程暑期面经1

看的第一篇完整面经。

一、 JavaScript & ES 基础

1. 讲讲原型链

  • 核心机制: 在 JavaScript 中,对象是基于原型继承的。每个实例对象都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),指向它的构造函数的 prototype 对象。
  • 查找链路: 当我们访问对象的一个属性或方法时,引擎会先在对象自身查找;如果找不到,就会沿着 __proto__ 指针去它的原型对象上找;如果还找不到,就继续沿着原型对象的 __proto__ 向上查找,直到终点 Object.prototype.__proto__(即 null)。这根链条就是原型链。
  • 性能与陷阱: 原型链查找是耗时的,过深的原型链会影响性能。同时,修改原型上的引用类型属性(如数组),会导致所有实例共享这个修改,这也是为什么在 Vue 组件的 data 必须是一个函数的原因。
  • 判断方法: 常用 instanceof 操作符来判断原型链,它的原理就是顺着实例对象的 __proto__ 一直往上找,看是否能找到对应构造函数的 prototype

2. ES6新特性

  • Promise & 异步控制: 解决了回调地狱,是现在 fetch 和各类异步请求的基础。
  • 解构赋值 & 扩展运算符 (...): 在处理复杂的数据流(如合并配置项、深浅拷贝一层数据)时非常高频。
  • MapSet 在需要频繁增删键值对,或者对大数组去重时,性能远超普通 ObjectArray。比如在处理前端大量埋点数据或图表节点关系时,Map 的键可以是对象,这点非常有用。
  • ProxyReflect 这是 Vue3 响应式的基石,用于拦截对对象的各种操作。

3. ES7 新特性

Array.prototype.includes()includes()indexOf() 更好用、更语义化、还能识别 NaN,而 indexOf() 做不到。

指数运算符 (): 替代 Math.pow(),例如在一些复杂的物理引擎或动画计算中会用到。

二、 框架与技术栈 (Vue & TS)

4. 为什么要用 TypeScript,怎么用的?

  • 根本原因: 解决 JS 动态弱类型在大型工程中的维护灾难。它把运行时错误提前到了编译时发现。
  • 工程价值:
    • 重构利器: 修改一个基础接口,IDE 会提示所有不兼容的地方。
    • 代码即文档: 配合现代 IDE,写代码时有极佳的属性提示,不需要频繁翻阅后端 API 文档。
  • 高级用法: 在实际开发中,除了基础的 interfacetype,还会大量使用泛型 (Generics) 来封装通用的网络请求方法,或者使用内置工具类型如 Partial<T>(将属性变为可选)、Pick<T, K>(提取部分属性)来处理复杂的组件 Props 传递。

5. Vue 2 和 Vue 3 的区别与技术演进

  • 响应式系统重构: Vue 2 的 Object.defineProperty 只能劫持对象的已有属性,对于新增属性(需 $set)、删除属性以及数组下标修改无能为力。Vue 3 改用 Proxy,直接代理整个对象,支持更丰富的拦截操作,性能也大幅提升。
  • 逻辑复用(Options API -> Composition API): Vue 2 按 datamethodscomputed 组织代码,导致同一个功能的逻辑被物理打散(碎片化),后期维护非常痛苦。Vue 3 的 setup 允许按“功能”聚集代码,并且可以通过组合式函数(Hooks)轻松地跨组件复用逻辑。
  • 编译优化: Vue 3 引入了静态提升(Static Hoisting)和 PatchFlag。它在编译模板时会标记动态节点,在 Diff 算法对比时,直接跳过静态节点,只对比带有标记的动态节点,极大提升了渲染性能。

6. Components、Utils、Hooks 的区别与选择

判断逻辑(重要):

  1. 这个功能包含 UI 渲染吗? -> 是,用 Components。例如:一个带有时序播放控制的进度条。
  2. 这个功能纯逻辑,且不涉及任何组件的状态(响应式数据/生命周期)吗? -> 是,用 Utils。例如:一个将时间戳转换为 YYYY-MM-DD 的纯函数。
  3. 这个功能是纯逻辑,但它需要维护状态,或者需要挂载到组件的生命周期上吗? -> 是,用 Hooks。例如:useWebSocket,它需要在内部维护 isConnected 状态,并且在组件 onUnmounted 时自动断开连接。

三、 前端工程化

7. Webpack vs Vite

  • Webpack 的痛点(Bundle 机制): Webpack 在冷启动时,需要从入口文件开始,静态分析所有的依赖,编译、打包成一个或多个巨大的 Bundle(束,捆) 后,才启动开发服务器。项目一旦变大,哪怕只改一行代码,HMR(热更新)也可能需要好几秒。
  • Vite 的破局(No-Bundle 机制): Vite 巧妙利用了现代浏览器原生支持 ESM 的特性。冷启动时,它只是启动一个极快的静态文件服务器。当浏览器请求某个文件时,Vite 才在服务端拦截请求,即时编译(如把 Vue/TS 转成 JS)并返回。更新速度属于 O(1) 级别,完全不受项目规模影响。

8. Tree-shaking & 代码分割原理

  • Tree-shaking,我理解它就像摇树一样把“死代码”摇掉。它的实现依赖于 ES Modules 的静态结构。因为 import 和 export 在编译时就能确定依赖关系,所以打包工具可以分析出哪些代码是没被用到的,然后在压缩阶段把这部分代码删掉,以此来减小包的体积。
  • 代码分割 (Code Splitting) 的核心是按需加载。它主要是通过 import() 动态导入语法实现的。打包工具遇到动态导入时,会把这部分代码单独打包成一个文件(chunk)一大块。这个文件只有在用户触发特定操作,比如访问某个路由时,才会被浏览器下载和执行。这样做能极大地提升应用的首屏加载速度。

9. 按需加载

通过动态导入(Dynamic Import)实现。在路由层(Vue Router)或大型组件层使用 () => import('./MyComp.vue'),只有当用户访问该路径或触发该动作时,浏览器才会去下载对应的 JS chunk。

10. ESM 和 CJS 的区别

面试官您好,ESM 和 CJS 是 JavaScript 生态中最主流的两种模块系统。CJS(CommonJS)是 Node.js 诞生初期为解决服务端模块化问题制定的社区标准,而 ESM(ECMAScript Modules)是 ES6 推出的官方语言级模块标准,现在已经成为浏览器和 Node.js 通用的跨平台标准

它们的核心区别主要体现在以下 5 个方面:

  1. 语法与使用方式不同 这是最直观的区别:
  • CJS 使用 require() 函数导入模块,使用 module.exportsexports 对象导出内容
  • ESM 使用 import 关键字导入模块,使用 export 关键字导出内容
  1. 加载时机与原理不同(最本质区别)
  • CJS 是运行时同步加载:代码执行到 require() 语句时才会去加载并执行模块文件,加载完成后才继续执行后续代码。它本质上是一个函数调用,无法在编译阶段进行静态分析。
  • ESM 是编译时静态加载:JS 引擎在代码运行前的编译阶段,就会扫描所有 import 语句,提前加载所有依赖模块。这使得 ESM 支持 Tree Shaking(摇树优化),可以剔除未使用的代码,显著减小打包体积。
  1. 导出值的特性不同
  • CJS 导出的是值的拷贝:模块导出后,导出值与模块内部变量就断开了联系。如果模块内部后续修改了这个变量,外部导入的值不会同步更新。
  • ESM 导出的是值的只读引用:外部导入的只是模块内部变量的一个 "指针",当模块内部修改了变量值时,外部导入的值会同步更新。同时,ESM 不允许在导入方修改导出的值。
  1. 动态性支持不同
  • CJS 天然支持动态加载require() 可以写在任何地方,包括 if 条件语句、for 循环内部,可以根据运行时条件动态决定加载哪个模块。
  • ESM 原生是静态的import 语句只能写在文件的最顶层,不能出现在任何代码块内部。不过 ESM 提供了 import() 函数来实现动态导入,返回一个 Promise。
  1. 运行环境与其他细节差异
  • 运行环境:CJS 主要用于 Node.js 环境;ESM 同时支持浏览器和 Node.js 14+ 版本。
  • 文件扩展名:CJS 默认使用 .js;ESM 需要将文件改为 .mjs,或在 package.json 中添加 "type": "module" 字段。
  • 顶层作用域:CJS 中顶层 this 指向当前模块;ESM 中顶层 thisundefined
  • 内置变量:CJS 中有 __dirname__filename 等模块级变量;ESM 中没有这些变量,需要通过 import.meta.url 来实现类似功能。
  • 总结与实际应用

目前 ESM 已经成为 JavaScript 模块化的未来趋势,所有现代前端框架(React、Vue、Angular)和工具链(Vite、Rollup)都默认使用 ESM。但由于历史原因,Node.js 生态中仍然存在大量使用 CJS 的存量项目和第三方库。

在实际开发中,Node.js 支持 ESM 和 CJS 互相导入,但有一些注意事项:比如 ESM 可以导入 CJS 模块,但只能导入其默认导出;CJS 不能直接使用 import 导入 ESM 模块,需要使用 import() 函数。

11. Sass 和 Less 区别,为什么用 Sass

  • 客观对比: 两者都是优秀的 CSS 预处理器,支持变量、嵌套、混合(Mixin)。但 Sass(特别是基于 Dart 编译的最新版)在控制指令(@if, @each)和数学函数库方面更为强大。
  • 为什么用: Sass 的 @use 模块化系统比传统 @import 更先进,解决了全局变量污染问题。在构建大型组件库或复杂的主题定制系统时,Sass 的生态和工具链更为成熟。

四、 Node.js 与后端思维

12. Node 用过吗?怎么用的?中间件机制?

使用场景: 作为全栈开发方向,Node 常用来做中间层(BFF,Backend For Frontend),用于接口聚合、数据清洗过滤,或者用来写自动化脚本(CLI 工具)。

中间件是 Node.js 后端框架(Express、Koa 等)最核心的设计思想之一。中间件的本质就是一个函数,它可以访问请求对象(Request)、响应对象(Response),以及应用的请求 - 响应循环中的下一个中间件函数(通常命名为 next)。中间件的作用是在请求到达最终的路由处理器之前,或者响应返回给客户端之前,对请求和响应进行统一的加工和拦截。

中间件的典型应用场景:

  • 鉴权中间件:统一验证用户的登录状态和权限,拒绝未授权的请求
  • 日志中间件:记录所有请求的 URL、方法、状态码、耗时等信息
  • 跨域处理中间件:统一设置 CORS 响应头,解决跨域问题
  • 请求体解析中间件:解析 POST 请求的 JSON、FormData 等格式的请求体
  • 错误处理中间件:统一捕获和处理应用中的所有错误,返回标准化的错误响应

中间件的执行流程: 当一个请求到达服务器时,会按照中间件的注册顺序依次执行。每个中间件可以选择:

  1. 执行自己的逻辑,然后调用 next() 函数将控制权传递给下一个中间件
  2. 直接返回响应,结束请求 - 响应循环
  3. 调用 next(err) 将错误传递给错误处理中间件
text
面试官您好,BFF 全称是 Backend For Frontend,也就是 "服务于前端的后端",是一种架构设计模式。
它是随着微服务架构普及出现的,主要解决前端需要调用多个微服务接口导致的请求多、数据处理复杂、前后端耦合的问题。
BFF 层位于前端和后端微服务之间,核心职责是接口聚合、数据转换、统一鉴权和错误处理,为前端提供定制化的接口服务。
技术上通常用 Node.js 实现,因为前后端语言统一,前端工程师可以无缝开发。在组织架构上,BFF 层一般由前端团队维护,这样前端可以根据需求快速迭代接口,不需要依赖后端团队。

13. 洋葱模型 (Onion Model)

  • 原理: 以 Koa 为例,通过 async/awaitnext() 实现。请求像穿透洋葱一样进入,依次执行中间件 next() 前的逻辑(通常用于处理请求数据);到达最核心的处理逻辑后,再由内向外反向执行 next() 之后的逻辑(通常用于处理响应数据、计算总耗时)。
  • 优势: 使得请求和响应的拦截逻辑可以写在同一个函数体内,代码高内聚,处理异步流非常优雅。

五、项目优化与 AI 时代的前端

14. 讲讲项目性能优化 (建立体系化回答)

  • 构建时优化: Tree-shaking、配置合理的 SplitChunks 进行代码分割、压缩图片资源、去除 console。
  • 网络时优化: 强缓存与协商缓存策略、开启 Gzip/Brotli 压缩、HTTP/2 多路复用。
  • 运行时优化(重点):
    • 长列表:虚拟列表(Virtual List)只渲染可视区域的 DOM。
    • 动画/可视化:使用 requestAnimationFrame 替代 setTimeout,复杂的大量数据图表使用 Canvas/WebGL 替代 DOM 渲染(例如数字孪生或大屏展示)。
    • 防抖(Debounce)与节流(Throttle)处理高频事件。

15. Chunk 是什么及其命名

  • 概念: Chunk 指的是 Webpack 打包过程中生成的代码块。
  • 命名策略: 典型如 [name].[contenthash].js
  • 为什么要用 contenthash: 这是为了长效缓存(Long-term Caching)contenthash 是根据文件本身的内容计算出来的哈希值。如果只修改了业务代码 A,那么 A 的 chunk 名会变(强迫浏览器下载新文件),而没有修改的第三方库包(Vendor Chunk)的哈希值不变,浏览器依然可以使用本地缓存,极大提升二次加载速度。

16. AI 怎么用的:

不要说用来写增删改查。可以说用来处理复杂正则表达式、编写繁杂的单元测试、快速生成某种特定的算法(如贝塞尔曲线计算),或者用于技术选型方案的对比调研。

17. Claude Code 用的什么套餐价格

Claude Code 是一个运行在终端的命令行工具(CLI),它是包含在 Anthropic 的 Claude 订阅套餐中的功能。

18. 页面空白报错,如何保证 AI 修复的正确性:

  • Prompt 技巧: 不要只扔报错截图。要提供:1. 报错堆栈;2. 发生错误前后的相关代码片段;3. 期望的输入和输出。
  • 验证机制(核心): AI 给的代码绝对不能直接 git commit。必须放入沙箱或本地环境跑通。(如果是核心逻辑,我会要求 AI 顺便把对应的单元测试写出来,通过跑通单测来反向验证它逻辑的正确性。)

19. 印象深的 bug:

(你需要结合你自己的项目来说,举个例子:)在开发一个涉及多设备状态同步的看板时,遇到了竞态条件导致的数据覆盖问题。AI 帮我分析了网络请求的时序,并建议我引入队列机制或使用具有取消功能的 Promise(AbortController)来丢弃过期的请求,这超出了我当时纯 UI 开发的思维局限。

20. 最近学了什么新技术

结合你目前的知识储备,建议可以说一些偏底层或前沿的。比如:

  • Rspack/Turbopack: 基于 Rust 的新一代构建工具,了解前端工具链向底层语言迁移的趋势。
  • WebRTC 或 WebSocket 进阶: 在做双向通信同步时的实践。
  • 大模型在前端的接入: 比如如何处理 Server-Sent Events (SSE) 来实现类似 ChatGPT 的流式输出打字机效果。
高频八股合集
场景题汇总1