前端面试复习笔记:React 高频题 + JS 手写题

前端面试复习笔记:React 高频题 + JS 手写题

适合面试前快速过一遍。内容覆盖到 React 19(含 19.2 / 19.3 要点),按「能答清楚原理 + 能写出代码」整理。


目录

  • [一、React 高频面试题](#一、React 高频面试题 "#%E4%B8%80react-%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E9%A2%98")
  • [二、JavaScript 手写题](#二、JavaScript 手写题 "#%E4%BA%8Cjavascript-%E6%89%8B%E5%86%99%E9%A2%98")

一、React 高频面试题

1. React 18 / 19 有哪些重要更新?

React 18(仍常考的基础)
特性 一句话理解
并发渲染(Concurrent Rendering) 更新可中断、可恢复,输入等高优任务不易被长任务卡住
新的根 API ReactDOM.rendercreateRoot(...).render(...)
自动批处理 事件、定时器、Promise 里的多次更新默认合并
Suspense 增强 更好支持数据请求与 SSR 流式渲染
startTransition / useTransition 标记非紧急更新,保持交互流畅
useDeferredValue 推迟展示次要内容
useId 生成 SSR/CSR 一致的唯一 ID
js 复制代码
import { createRoot } from 'react-dom/client';

createRoot(document.getElementById('root')).render(<App />);
React 19(面试重点,建议优先背)

React 19 在并发能力之上,把 Actions(异步更新链路)表单use APIref 用法文档元数据 / 资源预加载 ,以及 Server Components 推到更可用的位置。

特性 一句话理解
Actions 在 Transition 里跑异步逻辑,自动管 pending / 错误 / 乐观更新
useActionState 专为 Action 设计的状态钩子(结果 + 提交函数 + pending)
useFormStatus 子组件读取父级 <form> 的提交状态
useOptimistic 先更新 UI,请求失败再回滚
use 在渲染中读取 Promise / Context(可配合 Suspense)
ref 作为普通 prop 函数组件可直接接收 ref,多数场景不再需要 forwardRef
ref 回调可返回 cleanup 类似 useEffect 清理函数
<Context> 直接当 Provider 可写 <ThemeContext value={...}>,不必再套 .Provider
Document Metadata 组件里直接写 <title> / <meta> 等,React 会提升到 document.head
样式表 / async script / preload 内置更好的资源加载与去重
Server Components / Server Actions RSC 更稳定;服务端函数配合 "use server"
Actions + 表单(最常考)

过去改名要自己管 isPending、错误、成功跳转。React 19 可用 Action 简化:

jsx 复制代码
import { useActionState } from 'react';

function ChangeName() {
  const [error, submitAction, isPending] = useActionState(
    async (_prev, formData) => {
      const err = await updateName(formData.get('name'));
      return err || null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input name="name" />
      <button disabled={isPending}>更新</button>
      {error && <p>{error}</p>}
    </form>
  );
}

子组件读表单状态:

jsx 复制代码
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>;
}

乐观更新:

jsx 复制代码
import { useOptimistic } from 'react';

function TodoList({ todos, sendTodo }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, sending: true }]
  );

  // 发送前先 addOptimistic,失败时 React 会回到真实 todos
}
use API
jsx 复制代码
import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // Promise pending 时触发最近的 Suspense
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}

<Suspense fallback={<p>加载中...</p>}>
  <Comments commentsPromise={fetchComments()} />
</Suspense>

注意:

  • use 可在条件分支里调用(和普通 Hook 规则不完全一样)
  • 不要 在渲染里临时 use(new Promise(...)) 这种每次新建的 Promise
  • 也可 use(SomeContext) 读取 Context
ref 作为 prop(React 19)
jsx 复制代码
function Input({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

// 多数新代码不再需要 forwardRef

ref 回调还能返回清理函数:

jsx 复制代码
<div
  ref={(node) => {
    // 挂载时拿到 node
    return () => {
      // 卸载 / ref 变化时清理
    };
  }}
/>
元数据与资源
jsx 复制代码
function BlogPost({ post }) {
  return (
    <>
      <title>{post.title}</title>
      <meta name="author" content={post.author} />
      <link rel="stylesheet" href="/blog.css" precedence="default" />
      <article>{post.body}</article>
    </>
  );
}

也可用 preload / preinit 等 API 预加载字体、脚本。

Server Components(概念题)
  • Server Components:默认在服务端跑,不把一大坨交互逻辑打进客户端包
  • Client Components :文件顶部 "use client",才能用 state / 事件
  • Server Actions"use server" 标记的服务端函数,常和表单 Action 一起用

面试别只背名词:强调「减少客户端 JS、数据靠近服务端、表单/变更用 Action 串起来」。

React 19.2 / 19.3 可补充的加分点
版本 值得提的点
19.2 <Activity>(隐藏/恢复 UI 与状态)、useEffectEvent(把"事件逻辑"从 Effect 依赖里拆出去)、部分预渲染 resume API、Performance Tracks
19.3 <ViewTransition> 稳定(进出场/移动动画)、Fragment refs 稳定、browser()(等浏览器环境再渲染)、Trusted Types、Server Components 可直接渲染 Context
jsx 复制代码
import { ViewTransition } from 'react';

<ViewTransition>
  <Page />
</ViewTransition>
jsx 复制代码
import { useEffectEvent, useEffect } from 'react';

function Chat({ roomId, onMessage }) {
  const onMsg = useEffectEvent((msg) => onMessage(msg));

  useEffect(() => {
    const conn = connect(roomId);
    conn.on('message', onMsg);
    return () => conn.disconnect();
  }, [roomId]); // 不必把 onMessage 塞进依赖
}

口述模板:

18 解决「怎么并发、怎么批处理」;19 解决「异步提交、表单、乐观更新、RSC 落地」;19.2/19.3 再补齐 Activity、Effect Event、View Transition 等体验与心智负担问题。


2. JSX 是什么?和 JS 有什么区别?

JSX 是 JavaScript 的语法扩展,让你用类似 HTML 的写法描述 UI。构建工具会把它编译成普通 JS(如 React.createElement 或新的 JSX 运行时)。

jsx 复制代码
<div className="sidebar" />

大致等价于:

js 复制代码
React.createElement('div', { className: 'sidebar' });

区别要点:

  • JSX 不是合法原生 JS,需要编译
  • 属性用 classNamehtmlFor,事件用驼峰(onClick
  • {} 里写 JS 表达式

3. 简述 React 生命周期(类组件)

三大阶段:挂载 → 更新 → 卸载

挂载
  1. constructor
  2. static getDerivedStateFromProps
  3. render
  4. componentDidMount
更新(setState / props 变化 / forceUpdate
  1. static getDerivedStateFromProps
  2. shouldComponentUpdate
  3. render
  4. getSnapshotBeforeUpdate
  5. componentDidUpdate
卸载
  • componentWillUnmount(清定时器、取消请求、解绑事件)

注意:componentWillMount / componentWillReceiveProps / componentWillUpdate 已过时,新项目别用。

函数组件对应关系:useEffect(() => {}, [])DidMount;带清理函数 ≈ WillUnmount;依赖变化 ≈ DidUpdate

getDerivedStateFromProps 应用场景很窄。常见误用:无脑把 props 拷进 state。多数情况直接用 props,或在事件里更新 state 即可。


4. React 事件机制和原生 DOM 事件有什么区别?

原生 DOM React 合成事件
绑定位置 具体 DOM 节点 React 17+ 绑在根容器 上(之前多在 document
事件对象 原生 Event SyntheticEvent(跨浏览器统一)
执行顺序 捕获 → 目标 → 冒泡 同一轮里,原生监听通常先于 React 委托逻辑(仍建议少混用)

React 用事件委托减少监听器数量,并统一行为。需要原生事件时可用 e.nativeEvent,或在 ref 上直接 addEventListener


5. Redux 工作原理

核心:单一 store + 纯函数 reducer + 不可变更新

流程:

  1. 组件 dispatch(action)
  2. store 把当前 state 和 action 交给 reducer
  3. reducer 返回新 state
  4. 订阅者(如 useSelector / connect)感知变化并重渲染

原则:

  • 唯一改状态的方式是 dispatch
  • reducer 必须是纯函数
  • 复杂异步常用中间件(redux-thunk / redux-saga)

现在很多项目也会用 Zustand / Jotai / React Query,但 Redux 的单向数据流思想仍常考。


6. React Router 工作原理?常用组件有哪些?

依赖 history(或自建 history)监听 URL 变化,再匹配路由表渲染对应组件。

常见 history 模式:

  • BrowserHistory :真实路径,靠 pushState,需服务端兜底
  • HashHistory#/path,兼容性好,不依赖服务端 rewrite
  • MemoryHistory:内存路由,适合 RN / 测试

常用组件(v6):

  • BrowserRouter / HashRouter
  • Routes / Route
  • Link / NavLink
  • Outlet(嵌套路由出口)
  • NavigateuseNavigateuseParamsuseSearchParams

7. Hooks 解决了什么问题?函数组件和类组件区别

Hooks 主要解决:

  1. 逻辑复用难:不用 HOC / render props 层层包裹
  2. 相关逻辑分散:同一业务散落在多个生命周期里
  3. 类组件心智负担this、绑定、生命周期规则多
类组件 函数组件 + Hooks
状态 this.state useState / useReducer
副作用 生命周期方法 useEffect / useLayoutEffect
复用 HOC、render props 自定义 Hook
代码形态 类、方法分散 按关注点组织

React 19 再补几个常考 Hook / API:useActionStateuseOptimisticuseFormStatususe,以及 19.2 的 useEffectEvent


8. setState 是同步还是异步?它做了什么?

面试标准答法:

  • 多数情况下表现为「异步」 :调用后立刻读 state 往往还是旧值
  • React 会把多次更新批处理合并,减少渲染次数
  • React 18 起,在 Promise、setTimeout 等里也会自动批处理
  • 需要基于最新值更新时用函数式写法:
js 复制代码
setCount((c) => c + 1);

内部大致流程:合并更新 → 进入 reconciler 算新树 → Diff → 提交到真实 DOM。


9. 什么是 Fiber?解决了什么问题?

React 15 的 Stack Reconciler 用递归 Diff,无法中断。一旦计算过久,会卡住主线程(输入掉帧)。

Fiber 做了这些事:

  1. 把工作单元改成可中断的链表结构(child / sibling / return
  2. 用循环替代不可打断的递归
  3. 结合优先级调度:紧急更新优先,低优更新可暂停

这是并发渲染、startTransition 等能力的基础。


10. React 中如何捕获渲染错误?

Error Boundary(错误边界),只能是类组件(或基于它的库封装):

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error(error, info);
  }

  render() {
    if (this.state.hasError) return <h1>出错了</h1>;
    return this.props.children;
  }
}

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

捕获不到

  • 事件处理函数里的错误(用 try/catch
  • 异步代码(setTimeout、Promise)------若用 React 19 的 use(promise) 且错误在渲染链路中,可被边界/Suspense 相关机制处理;普通事件里的 Promise 仍要自己 catch
  • 服务端渲染错误
  • 错误边界自身抛出的错误

11. 组件传值有哪些方式?

  1. 父 → 子:props
  2. 子 → 父:父传回调,子调用并带参
  3. 跨多层:Context
  4. 全局/复杂状态:Redux、Zustand 等
  5. URL 状态:路由 params / search
  6. 非响应式通信:EventBus(慎用,难追踪)

12. 无状态组件和类组件的区别

历史上「无状态」多指纯函数展示组件:只吃 props、无 state、无生命周期。

Hooks 出现后,函数组件也能有状态。现在更准确的对比是:

  • 展示型:主要负责 UI
  • 容器型:负责数据与副作用

函数组件通常更简洁,配合 React.memo、Hooks 也能做性能优化。


13. React 如何实现类似 Vue keep-alive 的缓存?

React 官方长期没有内置 keep-alive。常见做法:

  1. 第三方库 :如 react-activationkeepalive-for-react
  2. 自己做缓存 :隐藏时用 CSS/display 保留 DOM,或把状态存到上层/全局 store
  3. 路由级缓存:结合路由库做 outlet 缓存
  4. React 19.2+ :可关注官方 <Activity>------用于隐藏/恢复子树 UI 与内部状态(面试可提"方向上在补齐这类能力")

注意:缓存会占用内存,也要处理「返回页是否刷新数据」。


14. React 如何做路由监听?

React Router v6:

js 复制代码
import { useLocation, useNavigationType } from 'react-router-dom';
import { useEffect } from 'react';

function RouteListener() {
  const location = useLocation();

  useEffect(() => {
    console.log('当前路径', location.pathname);
  }, [location]);

  return null;
}

也可以监听原生:

js 复制代码
window.addEventListener('popstate', handler);
window.addEventListener('hashchange', handler);

15. 有哪些方式会触发组件更新 / 改变 state?

  1. setState / useState 的 setter
  2. useReducer 的 dispatch
  3. 父组件重渲染导致子组件 props 变化
  4. Context value 变化
  5. 类组件 forceUpdate(尽量少用)
  6. 改变列表项 key 强制重建

16. React 有哪几种创建组件的方式?

  1. 函数组件(主流)
  2. ES6 class 组件
  3. React.createClass(已淘汰)

17. props 和 state 有什么区别?

props state
来源 外部传入 组件内部
可变性 组件内不应直接改 可通过 setter 更新
用途 配置、数据下发 交互产生的内部状态

相同 props + 相同 state,渲染结果应可预期(纯渲染思想)。


18. key 的作用是什么?

帮助 React 在列表 Diff 时识别「是同一个元素,还是该增删改」。

jsx 复制代码
{todos.map((todo) => (
  <li key={todo.id}>{todo.text}</li>
))}

建议:

  • 用稳定唯一 ID
  • 不要用随机数当 key
  • 列表会乱序/增删时,尽量别只用 index

19. ref 的作用是什么?

用于拿到 DOM 节点或类组件实例(少数场景)。

jsx 复制代码
import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

原则:能用声明式就别用 ref。适合焦点、动画测量、与非 React 库集成。

React 19 变化(加分):

  1. 函数组件可直接把 ref 当普通 prop 接收,新代码多数不必再写 forwardRef
  2. ref 回调可以 return 清理函数
  3. 19.3 起 Fragment 也可挂 ref,方便操作一组 DOM 子节点

20. React Diff 的主要策略

  1. 同层比较:不跨层移动整棵树硬匹配
  2. 类型不同直接重建 :如 div 换成 p,整棵子树拆掉重建
  3. 列表靠 key:提高复用准确度
  4. 开发者可用 shouldComponentUpdate / React.memo 跳过子树

目标不是保证绝对最优 Diff,而是在常见 UI 场景下足够快。


21. 受控组件和非受控组件

受控:表单值由 React state 驱动。

jsx 复制代码
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />

非受控:值存在 DOM 里,通过 ref 读取。

jsx 复制代码
const inputRef = useRef();
<input defaultValue="hello" ref={inputRef} />

多数表单推荐受控,便于校验与统一数据流;和原生插件集成时非受控更方便。

React 19 补充: <form action={fn}> / formAction 可直接接 Action。非受控字段在 Action 成功后会自动 reset;需要手动重置可用 requestFormReset。复杂校验仍常用受控,简单提交场景 Action 更省事。


22. 为什么说虚拟 DOM 能提升性能?

更准确的说法是:虚拟 DOM 提升的是「可维护性 + 更新可控性」,不是「一定比手写 DOM 更快」。

流程:

  1. 用 JS 对象描述 UI(Virtual DOM)
  2. 状态变了生成新树
  3. Diff 出补丁
  4. 批量更新真实 DOM

收益:

  • 减少粗糙的整页重绘思维,尽量做最小更新
  • 把 UI 更新变成数据驱动,好写也好推演
  • 跨平台(RN 等)也可复用同一套思想

二、JavaScript 手写题

1. 数组去重

js 复制代码
// Set
const unique1 = (arr) => [...new Set(arr)];

// filter
const unique2 = (arr) => arr.filter((item, i) => arr.indexOf(item) === i);

// reduce
const unique3 = (arr) =>
  arr.reduce((acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), []);

对象/NaN 去重要另说;面试优先写 Set,再补充复杂度。


2. 数字千分位

js 复制代码
const num = 1234567.89;
num.toLocaleString('en-US'); // "1,234,567.89"

// 正则版
function formatNumber(n) {
  const [int, decimal] = String(n).split('.');
  const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return decimal ? `${formatted}.${decimal}` : formatted;
}

3. 防抖与节流

js 复制代码
function debounce(fn, wait) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

function throttle(fn, wait) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}
  • 防抖:停下来才执行(搜索框)
  • 节流:一段时间内最多一次(滚动)

4. 手写简化版 Promise

js 复制代码
function MyPromise(executor) {
  this.status = 'pending';
  this.value = undefined;
  this.reason = undefined;
  this.onFulfilled = [];
  this.onRejected = [];

  const resolve = (value) => {
    if (this.status !== 'pending') return;
    this.status = 'fulfilled';
    this.value = value;
    this.onFulfilled.forEach((cb) => cb());
  };

  const reject = (reason) => {
    if (this.status !== 'pending') return;
    this.status = 'rejected';
    this.reason = reason;
    this.onRejected.forEach((cb) => cb());
  };

  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    const handle = (cb, data) => {
      queueMicrotask(() => {
        try {
          const result = typeof cb === 'function' ? cb(data) : data;
          resolve(result);
        } catch (e) {
          reject(e);
        }
      });
    };

    if (this.status === 'fulfilled') handle(onFulfilled, this.value);
    else if (this.status === 'rejected') handle(onRejected, this.reason);
    else {
      this.onFulfilled.push(() => handle(onFulfilled, this.value));
      this.onRejected.push(() => handle(onRejected, this.reason));
    }
  });
};

说明:这是面试简化版,未完整实现 Promise/A+(如 thenable 递归解析)。


5. 深浅拷贝

js 复制代码
function shallowCopy(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  return Array.isArray(obj) ? [...obj] : { ...obj };
}

function deepCopy(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (map.has(obj)) return map.get(obj);

  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);

  for (const key of Reflect.ownKeys(obj)) {
    clone[key] = deepCopy(obj[key], map);
  }
  return clone;
}

6. 手写 new

js 复制代码
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);
  const result = Constructor.apply(obj, args);
  return result !== null && (typeof result === 'object' || typeof result === 'function')
    ? result
    : obj;
}

7. 函数柯里化

js 复制代码
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...rest) => curried.apply(this, args.concat(rest));
  };
}

const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6

8. Promise 封装 AJAX

js 复制代码
function ajax({ url, method = 'GET', data = null, headers = {} }) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);

    Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.responseText);
      else reject(new Error(xhr.statusText));
    };
    xhr.onerror = () => reject(new Error('Network Error'));
    xhr.send(data);
  });
}

9. 不借助临时变量交换 a、b

js 复制代码
let a = 5;
let b = 10;
[a, b] = [b, a];

数字也可用加减/异或,但解构最清晰。


10. 数组求和

js 复制代码
arr.reduce((sum, n) => sum + n, 0);

11. 数组扁平化

js 复制代码
function flatten(arr) {
  return arr.reduce(
    (acc, cur) => acc.concat(Array.isArray(cur) ? flatten(cur) : cur),
    []
  );
}

// 现代 API
nested.flat(Infinity);

12. 实现 add(1)(2)(3) 累加(可扩展)

js 复制代码
function add(a) {
  let sum = a;
  function inner(b) {
    sum += b;
    return inner;
  }
  inner.valueOf = () => sum;
  inner.toString = () => String(sum);
  return inner;
}

console.log(+add(1)(2)(3)); // 6

13. 类数组转数组

js 复制代码
Array.prototype.slice.call(arrayLike);
Array.from(arrayLike);
[...arrayLike]; // 需可迭代

注意:普通 {0:'a', length:1} 不能直接 [...],用 Array.from 更稳。


14. 扁平数据转树

js 复制代码
function buildTree(items, parentId = null) {
  return items
    .filter((item) => item.parentId === parentId)
    .map((item) => {
      const children = buildTree(items, item.id);
      return children.length ? { ...item, children } : { ...item };
    });
}

15. 红灯 3s、绿灯 1s、黄灯 2s 循环

js 复制代码
function light(color, ms) {
  return new Promise((resolve) => {
    console.log(color);
    setTimeout(resolve, ms);
  });
}

async function traffic() {
  while (true) {
    await light('红', 3000);
    await light('绿', 1000);
    await light('黄', 2000);
  }
}

traffic();

16. Promise 异步加载图片

js 复制代码
function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = url;
  });
}

17. 发布订阅

js 复制代码
class EventBus {
  constructor() {
    this.events = {};
  }

  on(type, cb) {
    (this.events[type] || (this.events[type] = [])).push(cb);
  }

  off(type, cb) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter((fn) => fn !== cb);
  }

  emit(type, ...args) {
    (this.events[type] || []).forEach((fn) => fn(...args));
  }
}

取消订阅时必须传入同一个函数引用


18. async/await 封装 fetch

js 复制代码
async function fetchJSON(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

19. 极简双向绑定思路(Object.defineProperty / Proxy)

面试能讲清链路即可:依赖收集 → 设置值通知 → 更新视图

js 复制代码
function bindInput(input, obj, key) {
  input.value = obj[key];

  Object.defineProperty(obj, key, {
    configurable: true,
    get() {
      return input.value;
    },
    set(v) {
      input.value = v;
    },
  });

  input.addEventListener('input', (e) => {
    obj[key] = e.target.value;
  });
}

现代框架多用 Proxy;React 本身是单向数据流,表单「受控组件」是显式同步。


20. 简易 Hash 路由

js 复制代码
const routes = {
  '/': () => 'Home',
  '/about': () => 'About',
};

function render() {
  const path = window.location.hash.slice(1) || '/';
  document.getElementById('app').textContent = (routes[path] || routes['/'])();
}

window.addEventListener('hashchange', render);
window.addEventListener('load', render);

21. 斐波那契

js 复制代码
function fib(n) {
  if (n <= 1) return n;
  let a = 0;
  let b = 1;
  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b];
  }
  return b;
}

递归能写,但要主动提时间复杂度与记忆化。


22. 最长无重复子串长度

js 复制代码
function lengthOfLongestSubstring(s) {
  const seen = new Set();
  let left = 0;
  let max = 0;

  for (let right = 0; right < s.length; right++) {
    while (seen.has(s[right])) {
      seen.delete(s[left++]);
    }
    seen.add(s[right]);
    max = Math.max(max, right - left + 1);
  }
  return max;
}

23. 用 setTimeout 模拟 setInterval

js 复制代码
function mySetInterval(fn, delay) {
  let timer = null;
  let stopped = false;

  const run = () => {
    if (stopped) return;
    fn();
    timer = setTimeout(run, delay);
  };

  timer = setTimeout(run, delay);

  return () => {
    stopped = true;
    clearTimeout(timer);
  };
}

const cancel = mySetInterval(() => console.log('tick'), 1000);
// cancel();

优点:上一次执行完再预约下一次,比 setInterval 更不容易「重叠堆积」。


写在最后

如果你也在准备面试,建议按这个顺序刷:

  1. 先把 React 原理题讲顺(Fiber、diff、setState、Hooks)
  2. 再把 React 19 :Actions / use / ref as prop / RSC 讲清楚
  3. 每天手写 2~3 道 JS(防抖节流、深拷贝、Promise、扁平化、发布订阅)
  4. 每题准备「是什么 / 为什么 / 怎么用 / 坑点」四句话

有写错或想补充的题,欢迎评论交流。

相关推荐
holidaypenguin1 小时前
Windows 本地 HTTPS 证书生成指南
前端
酷酷的逗逗乐1 小时前
前端转 Agent 开发 · 第六节
前端·程序员
回家吃饭去吧1 小时前
WebAssembly 深度解析:前端性能的最后一块拼图
前端
爱勇宝1 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
尤小小1 小时前
Vite-SSG 实践:Vue项目预渲染落地完整方案
前端·vue.js
默_笙1 小时前
🍕 AI 的嘴巴装了水管(上):从"等它说完"到"边说边听"的流式输出指南
前端·javascript
lichenyang4531 小时前
让 VK 小程序调用 HarmonyOS 原生能力:壳子 SDK 的实现思路
前端
梨想橙汁2 小时前
Vue3 组合式 API 深度解析:ref/reactive 响应式,计算属性与侦听器
前端·vue.js
暖焰核心2 小时前
继承全解——继承、默认成员函数、切片、隐藏与虚继承
java·前端·javascript