首页
/ Adobe 前端面试题全解:从 front-end-interview-handbook 看 JS 算法、UI 组件与系统设计考点

Adobe 前端面试题全解:从 front-end-interview-handbook 看 JS 算法、UI 组件与系统设计考点

2026-09-05 23:57:05作者:昌雅子Ethen

本文基于 front-end-interview-handbook 仓库中的 Adobe 前端面试指南,系统梳理 Adobe 前端面试(FE loop)中反复出现的 JavaScript 编码题、UI 编码题与系统设计题,并结合仓库中配套的 React Hooks 指南、UI 组件题与系统设计 RADIO 框架章节,为每类题目给出可直接演练的代码实现与备考策略。读完后你可以掌握 Adobe 面试的轮次结构、高频考点的完整解法,以及社区内推者(insider)总结的避坑要点。

Adobe 前端面试的整体流程

根据仓库文档中收录的 2025 年真实面经,Adobe 的前端面试循环(FE loop)通常是 5 轮,结构大致为:

轮次 内容 说明
Pre-onsite 快节奏技术问答 + 调试 覆盖团队看重的技术栈,现场调试一个应用并讨论性能改进
Architecture round 架构 + 编码 曾出现最后 10 分钟追问递归 API 调用的"陷阱题"
Behavioral / tech discussion 行为面 不写代码
Coding + chat 算法 + 自定义 React hook 曾出现基于 setInterval 的自定义 hook 题
Manager chat 经理面

更早期(2023 年 10 月)的面经则描述为:先完成一道 HackerRank 题目并快速 Q&A,随后进入 5 轮循环,每轮约 30 分钟技术 + 10 分钟行为面 + 5 分钟 Q&A,技术题包括 LC medium 的"数岛屿"(nested 0/1 数组中的岛屿总数)、LC easy 的日期格式转换(Oct 1 2310-01-23)、用 JS 或 React 构建 Accordion,以及手写 reduce polyfill。

从面经看,Adobe 的整体难度被评价为"比沉迷 LeetCode 的公司更简单一些,但 React 细节和 REST API 是明确的扣分点"。因此备考重点应放在:React 机制(hooks 生命周期与陷阱)、异步代码的边界情况、以及 UI 组件的可访问性

JavaScript 编码题

1. 递归抓取所有子孙链接(getAllLinks 异步 BFS)

这是 Adobe"语言与 Web 基础"轮的高频题:给定一个 API getAllLinks(url),它返回某页面上所有链接;要求写一个函数返回 url所有子孙链接(子 URL 的子 URL,以此类推)。在此基础上面试官会追问以下边界情况:

  • 页面之间存在循环依赖(A 链接到 B,B 又链接回 A)时怎么办;
  • getAllLinks 返回的是 promise 时如何处理;
  • 异步代码如何写得更高效(并行 vs 串行);
  • 时间复杂度是多少。

一个能覆盖上述追问的标准解法是异步 BFS + visited 集合去重:

// getAllLinks(url): Promise<string[]> —— 返回该页面上的一层直接链接
async function getAllDescendantLinks(startUrl) {
  const visited = new Set([startUrl]); // 去重:既防止无限递归,也避免重复请求
  const descendants = [];
  const queue = [startUrl];

  while (queue.length > 0) {
    const batch = queue.splice(0, queue.length);
    // 同一层级的请求并行发出,而不是逐个 await,这是"异步效率"追问的得分点
    const results = await Promise.all(
      batch.map((url) => getAllLinks(url))
    );
    for (const links of results) {
      for (const link of links) {
        if (!visited.has(link)) {
          visited.add(link);       // 标记已访问,天然解决循环依赖
          descendants.push(link);
          queue.push(link);       // 加入下一层继续遍历
        }
      }
    }
  }
  return descendants;
}

讲解要点(面试中应主动覆盖):

  • 循环依赖visited 集合在入队时就标记,保证同一 URL 只会被请求一次,图上有环也不会死循环;
  • 异步效率:同一层的请求用 Promise.all 并行发起,把总耗时从 O(层数 × 单次请求) 的串行等待压缩到接近 O(层数 × 单次请求) 的并行等待;
  • 复杂度:时间 O(V + E),V 为去重后的 URL 数、E 为链接总数;空间 O(V)。

仓库中一位 2025 年 6 月的面经特别提醒了一个实操细节:该轮测试环境未预置测试用例,是面试官现场输入测试用例,你需要手工跑(handrun)代码来验证正确性——所以本地推演能力比依赖测试框架更重要。

2. 手写 Array.prototype.reduce

reduce polyfill 是前端面试的常青树题,Adobe 曾在 UI/算法轮直接考到。仓库的 JavaScript 章节 也将其列为第一周基础周的核心练习(与 mapfilter polyfill 并列)。

if (!Array.prototype.reduce) {
  Array.prototype.reduce = function (callback, initialValue) {
    const arr = Object(this); // 支持类数组
    const len = arr.length;
    let i = 0;
    let accumulator;

    if (arguments.length > 1) {
      // 显式提供了初始值
      accumulator = initialValue;
    } else {
      // 未提供初始值:用第一个元素作为初始累加器
      while (i < len && !(i in arr)) i++;
      if (i >= len) throw new TypeError('Reduce of empty array with no initial value');
      accumulator = arr[i++];
    }

    for (; i < len; i++) {
      if (i in arr) {
        accumulator = callback(accumulator, arr[i], i, arr);
      }
    }
    return accumulator;
  };
}

考察点与 算法章节中列出的预期一致:reduce 遍历是 O(n)。面试时应主动提到三个规范细节:callback(accumulator, currentItem, index, array) 的四参数签名、无初始值时空数组要抛 TypeError、以及稀疏数组(in 判断)的处理。

3. 找出数组中的"领导者"(Leaders in an array)

题目:找出数组中所有严格大于其右侧所有元素的元素。例如 [9, 32, 8, 7, 1, 12] 的领导者是 [32, 12, 9](或按出现顺序 [9, 32, 12],以题目要求为准)。

标准做法是从右向左一次遍历维护"当前最大值":

function findLeaders(arr) {
  const leaders = [];
  let maxFromRight = -Infinity;
  for (let i = arr.length - 1; i >= 0; i--) {
    if (arr[i] > maxFromRight) {
      leaders.unshift(arr[i]); // 或 push 后再 reverse
      maxFromRight = arr[i];
    }
  }
  return leaders;
}
  • 时间 O(n)、空间 O(n)(存结果),只需一次遍历——面试官常追问"能否 O(1) 额外空间",答案是倒序输出时即可;
  • 这是典型的"贪心 + 一次扫描"题,与仓库算法章节推荐的备考方向(树、递归、BFS/DFS 优先于困难 DP)一致:前端面试官很少出 hard 级动态规划,这类 medium 级数组题是主流。

4. 基于 setInterval 的自定义 React Hook(useRef vs useState 陷阱)

2025 年 9 月的面经记录了 Adobe 第 4 轮的真实场景:面试官要求实现一个"使用 setInterval 的自定义 hook"。候选人代码"功能上能用",但定时器无法正确清除,且在编辑器中随机触发了两次——根因是把回调存进了 useState,而正确做法是用 useRef

仓库的 React Interview Playbook hooks 章节 中恰好列出了对应的两条规则:当某个值不影响渲染时用 useRef 而不是 useState,以及 setInterval 必须返回清理函数,否则内存泄漏。下面把这个知识点落成可作答的代码:

import { useEffect, useRef } from 'react';

// 正确版本:回调存 useRef,避免每次渲染生成新函数触发 effect 重建定时器
function useInterval(callback, delay) {
  const savedCallback = useRef(callback);

  // 每次渲染都同步最新回调,但同步本身不会触发 effect 重跑
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return; // delay 为 null 时暂停
    const id = setInterval(() => {
      savedCallback.current(); // 始终调用最新闭包
    }, delay);
    return () => clearInterval(id); // 关键:清理,防止定时器残留/重复触发
  }, [delay]);
}

面试讲解的三个得分点:

  1. 为什么 useState 会出 bug:把回调存入 useState 后,任何 setState 都可能引发重渲染,导致 effect 依赖变化、定时器被重建,出现"随机跑两次"的现象;useRef.current 是可变引用,赋值不触发渲染,定时器生命周期稳定;
  2. cleanup 缺失的后果:仓库 hooks 章节给出的反例正是 setInterval 无 cleanup 的内存泄漏场景,Adobe 面试官实际就是在这里给了"interval wouldn't clear properly"的反馈;
  3. React 知识缺口提示:面经总结指出 Adobe 会基于这类问题判定候选人"React 上有 gaps",备考时建议系统过一遍 hooks 的生命周期(mount/update/unmount 时 effect 与 cleanup 的时机)。

UI 编码题

1. 用 Vanilla JS 或 React 实现 Accordion

Accordion 在 Adobe 的 UI 轮中出现过(2023 年 10 月面经原话:"build an accordion using JS or react")。仓库的 User Interface 章节 将 Accordion 列为 UI 编码题第一梯队,并给出进阶路线:先做基础渲染与展开/收起,再补 ARIA 角色/状态,最后实现符合 ARIA 规范的键盘支持(Home/End/Arrow Up/Down 在 header 间移动焦点)。"基础版可能足以通过面试,但把可访问性做对会拿到额外加分,展示资深水平。"

最小可运行版本(React):

import { useState } from 'react';

function Accordion({ items }) {
  const [openIndex, setOpenIndex] = useState(null);

  return (
    <div>
      {items.map((item, i) => (
        <div key={item.title}>
          <button
            aria-expanded={openIndex === i}
            aria-controls={`panel-${i}`}
            onClick={() => setOpenIndex(openIndex === i ? null : i)}
          >
            {item.title}
          </button>
          {/* 用 hidden 而非 display:none 的样式 hack,配合 aria-controls 建立关联 */}
          <div id={`panel-${i}`} hidden={openIndex !== i}>
            {item.content}
          </div>
        </div>
      ))}
    </div>
  );
}

vanilla JS 版本的核心是把状态从 useState 换成 classList.toggle('open') + CSS 过渡,逻辑同构。答完后主动追问自己:"如果要支持多项同时展开 / 键盘导航 / 屏幕阅读器,改哪里?"——这正是仓库文档指出的加分项。

2. 把顶部 Header 设计稿实现为 React 组件

面经中的原题:"Given a top-header design of a document, implement it as a React component"。这是一道设计稿还原 + 组件化题,考察点集中在:

  • 布局:flex 对齐(logo 左、导航中、操作区右),gapflex: 1 的使用;
  • 语义化与可访问性:<header><nav>aria-label
  • 组件拆分:HeaderNavNavList 的职责边界,props 设计(links: {label, href}[]);
  • 响应式:窄屏下导航收起为 menu button(可顺手引出 Accordion 思路,与上一题形成呼应)。

仓库 UI 章节把这类"从设计稿到组件"的任务归入 Components 分类(与 Accordion、Tabs、Star Rating、Image Carousel 同类),建议在面试中边写边说布局决策,展示 tradeoff 意识。

3. Flood Fill:带形状轮廓的 m × n 网格填充

这是 2025 年 6 月面经中 hiring manager 轮的原题(当时只要求伪代码,但值得准备完整实现):

有一个 m × n 网格,其中包含若干形状的轮廓线。若点击某个形状内部,只 flood fill 该形状;若点击形状外部,则填充整个网格中除形状内部以外的区域。

思路是把网格看成图,对点击点做 BFS/DFS,只允许在同值(未填充)格子之间扩散,轮廓线(例如标记为 1 的边框)是天然屏障:

// grid: m x n 二维数组,0 = 空白可填充,1 = 轮廓线/障碍
function floodFill(grid, startX, startY) {
  const m = grid.length, n = grid[0].length;
  const target = grid[startY][startX];
  if (target === 1) return; // 点在线条上:不做处理(边界条件)
  const queue = [[startX, startY]];
  grid[startY][startX] = 2; // 2 = 已填充;先标记再入队,避免重复入队

  while (queue.length > 0) {
    const [x, y] = queue.shift();
    for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
      const nx = x + dx, ny = y + dy;
      // 越界、轮廓线、已填充,都不可扩散——形状边界由此被"挡住"
      if (nx < 0 || nx >= n || ny < 0 || ny >= m || grid[ny][nx] !== target) continue;
      grid[ny][nx] = 2;
      queue.push([nx, ny]);
    }
  }
}

"点形状内只填该形状、点形状外填整块背景"这一行为不需要任何特殊判断:BFS 遇到轮廓线自然停止,连通域被轮廓分割成不同区域,所以点击位置决定了填充哪个连通域。讲解时说明复杂度 O(mn) 时间、O(mn) 空间(最坏整网格入队),并与 Adobe 2023 年面过的"数岛屿"题(同样是网格 BFS/DFS)关联起来——两道题本质都是网格图上的连通域遍历,一套模板通吃。

系统设计题:非前端方向的 LRU 与 OOD

2025 年 6 月的面经明确指出 Adobe 的系统设计轮是 non-frontend system design,被考了两道:设计 LRU cache,以及为国际象棋(chess)游戏设计类与接口(OOD)。仓库的 系统设计框架章节 也提示 RADIO 框架"不仅适用于前端系统设计,后端系统设计同样可用",正好覆盖这一场景。

1. LRU Cache(JS 版参考实现)

LRU 的规范行为:get/put 均须 O(1),容量满时淘汰最久未使用的键。JS 中最简洁的实现利用 Map 保持插入序的语义:

class LRUCache {
  constructor(capacity) {
    this.cap = capacity;
    this.map = new Map(); // key -> value;迭代序即"从旧到新"的访问序
  }
  get(key) {
    if (!this.map.has(key)) return -1;
    const val = this.map.get(key);
    // 删除后重新 set,把该键移到"最新"位置
    this.map.delete(key);
    this.map.set(key, val);
    return val;
  }
  put(key, value) {
    if (this.map.has(key)) this.map.delete(key);
    else if (this.map.size >= this.cap) {
      const oldest = this.map.keys().next().value; // 队首即最久未使用
      this.map.delete(oldest);
    }
    this.map.set(key, value);
  }
}

面试中可补充两点深度:(1) 严格语言中 Map 队头删除仍算 O(1),但底层通常是"哈希表 + 双向链表",主动画出链表结构说明 get 时"摘到尾部"的操作能展示数据结构功底;(2) 追问真实场景:HTTP 响应缓存、React 的 memo 与 LRU 思想的关系(仓库 系统设计 cheatsheet 中的评估维度——架构拆解、技术深度、tradeoff 阐述——对这类题同样适用)。

2. 为 Chess 设计类与接口(OOD)

标准拆解思路(面试中边说边画 UML):

  • 实体Chessboard(8×8 状态)、Piece(基类)及其子类 King/Queen/Rook/Bishop/Knight/PawnPlayer(白/黑)、Move(起点、终点、是否吃子)、Game(回合状态、历史记录、胜负判定);
  • 接口Piece.canMove(from, to, board)Piece.generateMoves(board) 多态化——每种棋子的合法走法封装在自己身上,Game.makeMove(move) 统一校验 + 更新棋盘 + 切换回合;
  • 关键 tradeoff:走法校验放 Piece 还是 Game?前者符合开闭原则(新增棋子类型不改 Game),后者便于统一处理将军/应将等跨棋子规则——能主动说出这个权衡正是评估轴里"Exploration and tradeoffs"想看到的。

来自 GreatFrontEnd 社区的原始面经

以下四段为仓库文档收录的社区内推者原文经验(已按仓库记录的时间倒序整理),是本文所有考点判断的一手依据:

2025 年 9 月 25 日(Firefly 团队 FE loop,5 轮):

5 轮总计:1. Pre-onsite——快节奏问答覆盖他们看重的技术栈,外加调试一个应用并讨论性能改进。2. Architecture 轮。最后 10 分钟面试官问了一个"陷阱题":编码一个递归 API 调用。我过去 3 年一直在用 SSR GraphQL,卡在 setTimeout 中如何处理返回值(不能用副作用变量),没写完(他说我完成了 95%)。3. 行为/技术讨论,不写代码。4. 聊天 + 一个需要 setInterval 的自定义 React hook。面试官认为代码功能上能用,但 interval 无法正确清除、在编辑器里随机跑了两遍——React 的修复办法正是对 setInterval 用 useRef 而不是 useState。5. 经理聊。

反馈:所有人都喜欢我,但基于上述两个问题,他们说我在 React 和 REST API 上有缺口。总体比沉迷 LeetCode 的公司简单——把 React 的怪癖准备到位应该就没问题。

2025 年 6 月 19 日

参加了 Adobe 系统设计,是非前端系统设计。被要求设计 LRU cache、为国际象棋游戏设计类和接口。还有 hiring manager 轮,被要求实现 flood fill 算法:一个 m*n 网格上有若干形状的轮廓,点在形状内部则填充该形状,点在形状外部则填充整个网格(形状除外)。不过只要求写伪代码。

2025 年 6 月 16 日

参加了 Adobe 面试。在语言与 Web 基础轮,被问到"数组中的领导者"这道题。另一个问题就是 getAllLinks(url) 的子孙链接题,边界情况层层加码:页面间循环依赖怎么办?getAllLinks 返回 promise 怎么办?异步代码怎么更高效?时间复杂度?要注意的一点是,我不得不手工跑代码,因为测试用例没有预置,测试用例是面试官现场敲出来的而不是粘贴的。

2023 年 10 月 26 日(FE mid-level):

上周刚面完 Adobe FE mid-level。流程是 HackerRank + 与 HM 快速 Q&A,之后 5 轮,每轮 30 分钟技术 + 10 分钟行为 + 5 分钟 Q&A。技术题包括:LC medium——在 0 和 1 的嵌套数组中数岛屿总数;LC easy——把 Oct 1 23 转成 10-01-23;UI——用 JS 或 React 做 Accordion;手写 .reduce polyfill。难度偏简单,但当时只有一个 headcount,所以没拿到 offer。

备考策略小结

  1. 题型定位:Adobe 偏"solid fundamentals"而非 LeetCode hard——数组/网格一次遍历题(leaders、岛屿、flood fill)、polyfill(reduce)、设计稿还原(Accordion、Header)构成主体;
  2. React 机制是明确的评分项:自定义 hook、setInterval 清理、useRef vs useState 的取舍(参见 React hooks 章节),面经两次把失败原因直接归到 React 细节上;
  3. 系统设计按通用(非前端)准备:LRU、OOD 这类经典题,可借用仓库 RADIO 框架(framework 章节)结构化作答;
  4. 练习方式适配 Adobe 风格:至少一次"无测试用例、纯手工推演"的模拟——在纸上/白板上按面试官口述的输入逐步推演输出,这是文档面经中被单独强调的实战细节;
  5. 异步代码要习惯追问链:以 getAllLinks 为例,"循环 → promise → 并行化 → 复杂度"的追问顺序就是 Adobe 的出题习惯,回答时应逐层主动覆盖,而不是等面试官追问。
登录后查看全文
热门项目推荐
相关项目推荐