高级前端面试题整理

272 阅读16分钟

一、技术选型与标准化(团队领导力)

1. 在推行ESLint和Commitlint时,遇到团队成员抵制或历史项目迁移困难,您如何推动落地?

  • 深入解答
    这是一个典型的“技术”与“人”结合的问题。我的策略是降低阻力、展示价值、逐步渗透

    1. 降低阻力(技术层面)

      • 渐进式配置:不会一次性开启所有规则(特别是error级别的规则)。我会从extends一个基础规范(如@vue/eslint-config-airbnb)开始,然后在一个独立的.eslintrc.patch.js文件中,先只将那些最可能避免生产Bug的规则(如no-unused-varsno-debugger)设为error,其他规则设为warnoff。这样既不会阻塞开发,又能在IDE和构建输出中看到提示,逐步培养习惯。
      • 自动化修复:充分利用eslint --fixprettier的能力,将风格相关的规则(如缩进、分号)的修复自动化,并通过Git Hooks(Husky)在pre-commit阶段自动执行,开发者无感修复,只关注逻辑错误。
      • 差异化处理历史项目:对于庞大的历史项目,我会使用/* eslint-disable */注释先局部禁用现有文件的检查,只对新增加或修改的文件进行严格检查(可以通过lint-staged实现)。随着迭代,代码库会自然过渡。
    2. 展示价值(沟通层面)

      • 数据说话:在推行前,我会抽样分析一段时间的Git记录,统计因低级错误(变量未定义、拼写错误)导致的Bug和Hotfix。推行一段时间后,用数据对比证明规范的有效性。
      • 与Code Review结合:在CR中,将ESLint错误作为评论的首要依据,让规范成为客观标准,减少主观争论,提升CR效率。
    3. 工具与流程保障

      • 集成到CI/CD:在GitLab CI或Jenkins流水线中加入lint阶段,如果存在error级别的报错,则构建失败,无法合并代码。这是最后的硬性防线。
      • Commitlint的好处标准化Commit Message的核心价值在于自动化生成CHANGELOG和基于语义的版本控制(semantic-release) 。我会向团队演示,如何通过feat:fix:等类型,自动决定版本号(次版本/修订版)并生成变更日志,这极大地减少了发布前的心智负担和手动操作错误。

2. 如何设计前端技术选型的评估矩阵?

  • 深入解答
    技术选型是一个多维度的决策过程,不能仅凭个人喜好。我会建立一个加权评分卡模型。

    以构建工具选型(Webpack vs Vite)为例:

    评估维度权重 (示例)WebpackVite说明
    开发体验30%710Vite的冷启动和HMR速度是革命性的,极大提升开发幸福感。
    生态成熟度25%107Webpack插件生态极其丰富,几乎所有需求都有现成方案。Vite插件兼容Rollup,生态在快速追赶。
    生产性能20%99Webpack经过极致优化后Bundle更小。Vite(Rollup)打包同样出色,但需注意依赖预构建。
    学习成本15%58Webpack配置复杂,概念多(Loader, Plugin, Bundle)。Vite配置更简单,开箱即用。
    社区趋势10%79Vite是未来趋势,社区活跃度高,但Webpack仍是当前最稳定的基石。
    综合得分100%7.658.55(7*0.3 + 10*0.25 + ...)

    结论与决策

    • 新项目、追求极致开发体验:选择Vite。特别是Vue/React生态的项目,享受其带来的速度优势。
    • 复杂、高度定制化的企业级项目:选择Webpack。其无与伦比的生态和可定制性可以应对各种复杂场景(如微前端、自定义非标资源处理)。
    • 中间路线“开发用Vite,生产用Webpack”  也是一种探索性方案,但需维护两套配置,成本较高,一般不推荐。

3. 如何制定前端代码规范确保可维护性?

  • 深入解答
    代码规范分为风格质量两层。

    1. 风格层 (Formattable) :使用Prettier主导。它几乎没有配置选项(有意的),通过强制统一的代码风格,彻底终结“缩进是2空格还是4空格”之类的争论。它与ESLint集成(eslint-config-prettier确保规则不冲突)。

    2. 质量层 (Lintable) :使用ESLint主导。它负责发现可能错误或低效的代码模式。

      • 自定义规则实践:我曾为项目写过一条自定义规则,禁止直接使用localStorage.getItem(‘token’)。原因是我们的Token需要自动续期,逻辑封装在一个统一的auth模块里。直接使用原生API会绕过期制。规则会提示开发者:“请从@/utils/auth模块的getToken方法获取”。
      • 基于AST:这条规则就是通过分析AST,查找MemberExpression节点,如果对象名是localStorage,属性名是getItem,且参数包含’token’,就报错。这体现了对编码约束的深入思考。

二、Vue & React 原理深度

1. Vue3响应式原理:

  • 深入解答

    • Proxy的优势Object.defineProperty递归地劫持对象的已有属性get/set。对于新增属性,需要手动Vue.set,对于数组,需要重写7个变更方法。Proxy是代理整个对象,监听的是整个对象的13种基本操作(如getsetdeletePropertyhas)。因此,无论对象属性的增删、数组的push/pop,甚至in操作符,都能被天然监听,这是架构上的降维打击。

    • 依赖收集的细节

      • track函数中,会用一个全局的WeakMap<target, Map<key, Set<Effect>>>数据结构来存储依赖关系。WeakMap的键是原始对象,值是一个Map;这个Map的键是对象的属性名,值是一个Set,里面存储了所有依赖这个属性的effect函数。Set结构天然实现了依赖的去重
      • 嵌套effect:Vue3用一个effectStack数组来模拟栈结构。在执行effect前,将当前effect推入栈顶,执行完毕后弹出。这样就能保证当前激活的effect永远是栈顶的那个,从而在依赖收集时建立正确的联系。

2. React渲染机制:

  • 深入解答

    • 批量更新(Batching) :React基于事务机制进行批量更新。在合成事件(如onClick)和生命周期函数中,所有的setState都会被推入一个队列,在函数执行结束时统一进行一次更新,从而避免多次渲染。在setTimeout原生事件等异步代码中,React的“事务”已经结束,所以setState会立即触发更新。React 18中,使用createRoot后,即使在setTimeout中也会自动批量更新,这是一个重要的行为变更。
    • 并发特性(Concurrency) :这本质上是一种可中断的、基于优先级的渲染调度机制。Fiber架构将渲染工作拆分成多个小的Fiber节点单元。React在遍历和处理这些单元时,可以检查浏览器是否有更高优先级的任务(如用户输入)。如果有,就中断当前渲染过程,先去响应输入,之后再回来继续或重新开始渲染。useTransition就是将某个更新标记为低优先级,使得高优先级的更新(如输入)可以打断它,从而保持页面响应。

3. 跨框架设计:

  • 深入解答
    真正的“一次编写,随处运行”非常困难,更务实的方案是适配器模式Web Components

    • 适配器模式:编写一套框架无关的核心逻辑(纯JS/TS)。然后为不同框架编写薄薄的适配层组件

      • Vue适配器:使用setup()函数,将核心逻辑的实例作为reactive数据,并将方法暴露给模板。
      • React适配器:使用Hooks(useEffectuseState)来连接核心逻辑的状态和生命周期。
      • 优点:核心逻辑复用,性能最佳。缺点:需要维护多套组件外壳。
    • Web Components:使用Custom Elements和Shadow DOM封装组件。然后在各框架中使用:Vue中直接当普通元素使用;React中,需使用ref来处理自定义事件(因为React的合成事件系统无法冒泡通过Shadow DOM边界)。

      • 优点:浏览器原生标准,真正跨框架。缺点:生态较弱,CSS穿透、复杂数据传递(如传递函数)比较别扭。

三、JavaScript & 浏览器底层

1. V8垃圾回收:

  • 深入解答

    • 新生代 (New Space) :使用Scavenge算法(Cheney算法)。空间一分为二(From和To)。分配对象先到From空间,满了之后,将存活对象复制到To空间,然后清空From空间,最后交换两者角色。复制操作很快,但空间利用率只有一半。适用于生命周期短的对象。

    • 老生代 (Old Space) :存活次数多的对象会晋升至此。使用Mark-Sweep(标记清除)和Mark-Compact(标记整理)

      • Mark-Sweep:标记阶段遍历GC Root(全局变量、活动函数调用栈上的变量等)所有可达对象。清除阶段释放未标记对象的内存。速度快,但会产生内存碎片
      • Mark-Compact:在标记完成后,将存活对象移动到内存的一端,然后清理掉边界外的内存。解决了碎片,但速度最慢。V8会根据碎片化程度交替使用这两种策略。
    • 写屏障 (Write Barrier) :当新生代对象被老生代对象引用时,这个引用被称为“跨代引用”。为了避免在Minor GC时扫描整个老生代,V8使用写屏障机制:每当老生代对象指向新生代对象时,会把这个引用记录在一个单独的列表中。Minor GC时,只扫描这个列表,而不是整个老生代,大大提升了效率。

2. 事件循环与渲染:

  • 深入解答
    一个事件循环(Tick)的顺序是:

    1. 执行一个宏任务(如script整体代码、setTimeout回调)。
    2. 执行所有微任务Promise.thenqueueMicrotaskMutationObserver)。如果在此过程中又产生了新的微任务,会继续执行直到清空微任务队列
    3. 执行渲染(Rendering) :这里才是浏览器决定是否更新视图的时刻。它会计算样式变化(CSSOM)、布局(Layout)、绘制(Paint)。requestAnimationFrame的回调正是在这个渲染阶段之前执行的,它是做动画的完美时机,因为它能保证在每次浏览器绘制前更新动画帧。
    4. 执行requestIdleCallback:如果此时还有剩余时间,则会执行空闲回调,用于执行一些不紧急的任务。

    性能启示将长时间运行的JS任务拆分成小块,在宏任务中执行一部分,然后通过setTimeoutqueueMicrotask将剩余部分让给渲染和用户交互,可以避免页面卡顿。

四、工程化与工具链(Webpack/Vite/Git/部署)

1. Webpack与Vite对比与生产优化:

  • 深入解答

    • Vite开发模式快的本质:它利用了浏览器原生支持ES模块的特性。服务器端按需编译并返回源码,浏览器直接通过import/export发起请求,构建了原生ESM的依赖图。这省去了Webpack在开发时打包整个bundle的巨大开销。HMR时,Vite只需要让浏览器重新请求修改过的单个模块,速度极快。

    • Vite生产构建:生产环境中,为了更好的缓存和网络性能(减少HTTP请求),仍需打包。Vite使用Rollup进行生产构建,因为它基于ESM,Tree-shaking更高效。

      • 优化手段

        • 分包策略(ManualChunks) :将node_modules中的依赖(如vuereact-vendor)打包成单独的chunk,利用浏览器缓存。
        • CSS代码分割:自动将异步组件中的CSS提取为独立文件。
        • 预构建(Pre-building) :Vite会使用Esbuild将CommonJS格式的依赖(如lodash)提前转换为ESM,并将多个小文件合并成大文件,减少网络请求。这是其性能优势的关键一环。
    • Webpack的优化:对于Webpack,优化是更显式的。

      • 缓存:开启cache(Webpack 5持久化缓存)或hard-source-webpack-plugin(Webpack 4)。
      • 多线程thread-loader用于耗时的Loader(如Babel)。
      • DLL:在超大项目中,仍可使用DllPlugin将极度稳定的库提前打包,极大提升后续构建速度。

2. Git协同规范与微前端:

  • 深入解答

    • Git分支模型:在微前端架构中,每个子应用都是一个独立的仓库。基座(主应用)也是一个独立仓库。这天然契合MonorepoMultirepo的争论。

      • Multirepo:子应用团队自治性强,但版本协同复杂。需制定严格的依赖版本协议(如子应用必须兼容基座指定的React版本)。
      • Monorepo:使用pnpm workspacenpm workspace管理,所有应用在一个仓库。依赖提升,代码共享方便,git commit历史清晰。但权限管理更复杂。
    • Git Hooks拦截commit-msg钩子中,运行npx --no -- commitlint --edit $1来解析本次提交的message文件($1是其路径),不符合规范则直接exit 1,提交失败。

3. 部署与灰度发布:

  • 深入解答

    • 灰度发布(金丝雀发布)方案

      1. 前端侧灰度代码中内置开关。页面加载后请求一个配置中心接口,根据返回的配置决定是否展示新功能。优点是灵活,无需运维介入;缺点是代码包会变大,包含了所有版本的功能。

      2. 网关侧灰度最常用和彻底的方案。在Nginx或API网关上,根据特定规则(如Cookie、URL参数、IP段、用户比例)将流量路由到不同版本的服务端。

        • Nginx配置示例:使用split_clients模块按比例分流,或通过map指令根据$cookie_canary变量映射到不同的上游(upstream)服务器。
      3. Serverless/边缘计算灰度:在现代平台(如Vercel, Netlify)上,可以通过配置,将特定比例的流量分配给不同的部署版本。

五、微前端与跨团队协同

1. 微前端技术难点详解:

  • 深入解答

    • JS沙箱qiankun的沙箱主要通过Proxy代理window对象实现。

      • SnapshotSandbox:在子应用挂载前,深度拷贝window的快照。卸载时,用快照恢复window,并记录当前的修改。适用于不支持Proxy的浏览器,但性能较差。
      • ProxySandbox(主流):为每个子应用创建一个假的全局对象(fakeWindow),用Proxy代理所有getset操作。set的操作作用于fakeWindow上,get则优先从fakeWindow读,没有再 fallback 到真正的window。这样多个子应用间的全局变量修改完全隔离,互不影响。
    • 样式隔离

      • Scoped CSS:Vue的<style scoped>或CSS Modules,通过添加随机属性选择器实现组件级隔离。但对于全局样式无效。
      • Shadow DOM:最彻底的隔离方案,但第三方UI库的样式可能无法穿透,兼容性要求高。
      • 动态样式表最实用的方案。子应用挂载时,将其<style><link>标签插入到DOM;卸载时,直接移除此样式表。简单有效,但需要注意样式表移除和应用的卸载顺序,避免闪屏。
    • 依赖共享Module Federation是Webpack 5的革命性特性。它允许一个JavaScript应用动态地从另一个应用加载代码并共享依赖。

      • 基座应用(host):remotes配置声明它要消费哪些远程应用暴露的模块。
      • 子应用(remote):exposes配置声明它要暴露哪些模块给外部使用;shared配置声明它愿意共享哪些依赖(如reactreact-dom),并指定版本要求和单例模式。
      • 结果:无论谁先加载,共享的依赖(如React)只会被下载和执行一次,避免了重复和冲突。

2. 跨团队协作:

  • 深入解答

    • 统一技术栈的权衡不强求绝对统一。对于基座和核心库(如React, Vue, 状态管理)应制定标准。对于工具函数、UI库,可以允许团队在一定规范内自选,并通过构建工具(如Webpack alias@team-a/utils)避免冲突。
    • 生命周期规范:这不是一个可选项,而是必须的强制契约。必须明确定义子应用的bootstrapmountunmount三个生命周期函数,并由基座框架统一调用。参数必须包含容器DOM节点基座下发的props(如用户信息、全局状态等)

六、性能优化与安全

1. 性能优化:

  • 深入解答

    • 监控与优化LCP

      • 监控:使用PerformanceObserver监听largest-contentful-paint条目。

      • 优化手段

        • 优先级:使用<link rel=preload>预加载LCP资源(如首屏大图或Web字体)。
        • 服务器优化:对于SSR应用,优化TTFB(Time to First Byte)直接利好LCP。使用CDN、缓存、更快的后端语言均可优化。
        • 资源优化:对图片进行现代格式(WebP/AVIF)转换、响应式图片(srcset)、适当压缩。
        • 渲染优化:避免CSS的@import(会造成串行请求),将关键CSS内联到HTML的<head>中,移除渲染阻塞的JS(使用defer/async)。
    • 缓存策略

      • HTTP缓存:静态资源(如/assets/*.js)使用Cache-Control: max-age=31536000, immutable(强缓存一年,且内容变化后文件名会变,所以安全)。HTML文件使用Cache-Control: no-cache(协商缓存)。
      • Service Worker:使用workbox等库实现更复杂的策略,如Stale-While-Revalidate:优先返回缓存中的旧版本响应,同时在后台更新缓存,下次请求时使用新版本。极大提升重复访问的体验。

2. 前端安全:

  • 深入解答

    • CSRF防御深度

      • Token验证:Token不应明文放在Cookie中,而应放在另一个自定义HTTP头(如X-CSRF-TOKEN)中。因为浏览器同源策略规定,自定义头的请求无法通过简单的<form>提交发起(只能发起“简单请求”),从而从源头上杜绝了CSRF。
      • SameSite Cookie:设置为StrictLaxLax模式允许在顶级导航(如从外部链接跳转过来)时携带Cookie,但阻止在跨站POST提交或通过<img>发起的GET请求中携带,是安全性和可用性的良好平衡。
    • DOM型XSS与CSP

      • DOM型XSS:攻击载荷由客户端JS执行写入DOM引发(如innerHTML = userInput)。防御:永远警惕innerHTML,优先使用textContent。如果必须使用,必须对用户输入进行严格的过滤和转义。

      • CSP(内容安全策略) :这是终极防御手段,它通过HTTP头告诉浏览器只允许加载指定来源的脚本、样式等资源。

        • 示例策略Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com; object-src 'none';
        • 含义:默认只允许同源资源;脚本允许同源、内联(为兼容旧代码)和来自trusted.cdn.com的资源;禁止Flash等插件。这可以有效阻止即使成功注入的XSS脚本的执行,因为它来自非法源。