STUDY / FULL STACK

FULL STACK / LEARNING NOTES

全栈学习提升清单

312道题 · 29 个主题

点击题目查看解题思路、参考代码和示例结果;需要数据库、Redis、浏览器或框架的示例已在题内注明。

01 / 29

高频手写精选

13 题
  1. 实现 Promise.all

    Promise.all:将可迭代对象转成数组,空数组立即兑现 [];逐个 Promise.resolve,按原下标保存结果,计数归零后兑现;任一拒绝立即拒绝,其他任务不会自动取消。

    参考代码 · js
    function all(items) {
      return new Promise((resolve, reject) => {
        const values = Array.from(items), out = Array(values.length);
        if (!values.length) return resolve([]);
        let left = values.length;
        values.forEach((value, i) => Promise.resolve(value).then(
          result => { out[i] = result; if (--left === 0) resolve(out); }, reject));
      });
    }
    all([Promise.resolve(1), 2]).then(console.log);

    结果:输出 `[1, 2]`;拒绝时返回首个拒绝原因。

  2. JSON2DOM = react的render函数

    JSON2DOM:递归创建元素;文本节点用 createTextNode,元素用 createElement,属性需区分 className、事件和普通属性,再递归追加 children。React 的 render 还包含协调、状态与更新,不能等同于简单 JSON2DOM。

    参考代码 · js
    function json2dom(node) {
      if (typeof node === 'string' || typeof node === 'number') return document.createTextNode(String(node));
      const el = document.createElement(node.type);
      for (const [key, value] of Object.entries(node.props ?? {})) {
        if (key === 'className') el.className = value;
        else if (key.startsWith('on') && typeof value === 'function') el.addEventListener(key.slice(2).toLowerCase(), value);
        else el.setAttribute(key, String(value));
      }
      for (const child of node.children ?? []) el.append(json2dom(child));
      return el;
    }
    document.body.append(json2dom({type:'p', children:['Hello']}));

    结果:页面新增 `<p>Hello</p>`;这里只实现首次创建 DOM。

  3. 树形结构转成列表

    树转列表:深度优先遍历,把节点的 children 拆掉,输出 {...node, parentId};记录访问过的节点以防循环引用,保留顺序。

    参考代码 · js
    function flattenTree(nodes, parentId = null, seen = new WeakSet()) {
      return nodes.flatMap(node => {
        if (seen.has(node)) throw Error('cycle');
        seen.add(node);
        const {children = [], ...rest} = node;
        const out = [{...rest, parentId}, ...flattenTree(children, node.id, seen)];
        seen.delete(node);
        return out;
      });
    }
    console.log(flattenTree([{id:1, children:[{id:2}]}]));

    结果:`[{id:1,parentId:null},{id:2,parentId:1}]`;循环引用会抛错。

  4. 列表转成树形结构

    列表转树:先用 Map(id → 节点副本) 建索引,再按 parentId 挂到父节点 children;根节点单独收集。时间 O(n),需约定孤儿节点和重复 id 的处理。

    参考代码 · js
    function toTree(rows) {
      const nodes = new Map(rows.map(({id, ...rest}) => [id, {id, ...rest, children:[]}]));
      const roots = [];
      for (const row of rows) {
        const node = nodes.get(row.id);
        if (row.parentId == null) roots.push(node);
        else if (nodes.has(row.parentId)) nodes.get(row.parentId).children.push(node);
        else throw Error(`missing parent ${row.parentId}`);
      }
      return roots;
    }
    console.log(toTree([{id:1,parentId:null},{id:2,parentId:1}]));

    结果:根节点 1 的 `children` 包含节点 2;重复 id、环应另行拒绝。

  5. Array.prototype.flat

    flat(depth):递归或显式栈遍历;遇数组且剩余深度大于 0 则展开,否则加入结果。原生 flat 会跳过稀疏数组空位且不修改原数组。

    参考代码 · js
    function flat(input, depth = 1) {
      const out = [];
      function visit(arr, left) {
        for (let i = 0; i < arr.length; i++) if (i in arr) {
          if (Array.isArray(arr[i]) && left > 0) visit(arr[i], left - 1);
          else out.push(arr[i]);
        }
      }
      visit(input, depth);
      return out;
    }
    console.log(flat([1,[2,[3]]], 1));

    结果:`[1, 2, [3]]`。

  6. instanceof

    instanceof:先检查右侧对象的 Symbol.hasInstance,若存在可调用方法就由它决定结果;否则右侧必须是函数,沿左侧值的原型链查找该函数的 prototype。自定义方法也可能让基本值返回 true。

    参考代码 · js
    function myInstanceof(value, rhs) {
      if (rhs == null) throw TypeError('invalid right operand');
      const method = rhs[Symbol.hasInstance];
      if (method != null) {
        if (typeof method !== 'function') throw TypeError('@@hasInstance must be callable');
        return !!method.call(rhs, value);
      }
      if (typeof rhs !== 'function') throw TypeError('right operand is not callable');
      const prototype = rhs.prototype;
      if (Object(prototype) !== prototype) throw TypeError('invalid prototype');
      if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false;
      for (let p = Object.getPrototypeOf(value); p; p = Object.getPrototypeOf(p)) if (p === prototype) return true;
      return false;
    }
    console.log(myInstanceof([], Array), myInstanceof(3, {[Symbol.hasInstance]: x => x === 3}));

    结果:`true true`;无自定义 `Symbol.hasInstance` 时,基本值返回 `false`。

  7. call apply bind

    call/apply/bind:call 逐项传参,apply 传数组状参数,bind 返回固定 this 与前置参数的新函数。手写版还须处理严格模式、构造调用、原型和 length,临时属性方案并不完全等价。

    参考代码 · js
    function callWith(fn, thisArg, ...args) { return Reflect.apply(fn, thisArg, args); }
    function applyWith(fn, thisArg, args) { return Reflect.apply(fn, thisArg, args ?? []); }
    function bindWith(fn, thisArg, ...bound) {
      return function boundFn(...rest) {
        return new.target ? Reflect.construct(fn, [...bound,...rest], fn) : Reflect.apply(fn, thisArg, [...bound,...rest]);
      };
    }
    const add=function(x){return this.n+x;};
    console.log(callWith(add,{n:2},3),applyWith(add,{n:2},[3]),bindWith(add,{n:2})(3));

    结果:`5 5 5`;以 `Reflect.apply/construct` 为底层原语,未覆盖原生 bound 函数的全部元数据。

  8. Array.prototype.map

    map:遍历 0..length-1,只在索引存在时调用回调 callback.call(thisArg, value, index, array),输出等长数组并保留空位;先检查回调可调用。

    参考代码 · js
    function mapLike(array, callback, thisArg) {
      if (typeof callback !== 'function') throw TypeError('callback');
      const source = Object(array), out = new Array(source.length >>> 0);
      for (let i = 0; i < out.length; i++) if (i in source) out[i] = callback.call(thisArg, source[i], i, source);
      return out;
    }
    console.log(mapLike([1,,3], x => x * 2));

    结果:`[2, <空位>, 6]`。

  9. 用正则实现模板字符串

    模板字符串替换:如 /\{\{\s*([\w.]+)\s*\}\}/g 匹配 {{user.name}},回调用安全的路径读取函数取值;若插入 HTML,需做上下文转义,不能 eval。

    参考代码 · js
    function template(text, data) {
      return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) =>
        String(path.split('.').reduce((obj, key) => obj?.[key], data) ?? ''));
    }
    console.log(template('Hi {{user.name}}', {user:{name:'Ada'}}));

    结果:`Hi Ada`;若写入 HTML,必须按 HTML 上下文转义。

  10. lodash.get

    lodash.get:把点号、方括号或数组路径解析为键序列后逐层读取;最终解析值为 undefined 时返回默认值,路径不存在也同样返回默认值,不必区分两者。此题只读数据,不应额外执行路径表达式。

    参考代码 · js
    function get(object, path, fallback) {
      const keys = Array.isArray(path) ? path : String(path).replace(/\[(\d+)\]/g, '.$1').split('.');
      const value = keys.reduce((current, key) => current?.[key], object);
      return value === undefined ? fallback : value;
    }
    console.log(get({a:[{b:0}]}, 'a[0].b', 9));

    结果:`0`;复杂 Lodash 路径语法不在这个简化版中。

  11. 深拷贝

    深拷贝:普通结构优先 structuredClone;手写时用 WeakMap 处理循环引用,并针对 Date、RegExp、Map、Set、数组等逐类复制。函数、DOM 节点和原型行为须单独约定。

    参考代码 · js
    function clone(value, seen = new WeakMap()) {
      if (value === null || typeof value !== 'object') return value;
      if (seen.has(value)) return seen.get(value);
      if (value instanceof Date) return new Date(value);
      if (value instanceof RegExp) return new RegExp(value.source, value.flags);
      const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
      seen.set(value, out);
      for (const key of Reflect.ownKeys(value)) out[key] = clone(value[key], seen);
      return out;
    }
    const a = {}; a.self = a; const b = clone(a); console.log(b !== a, b.self === b);

    结果:`true true`;Map/Set 等需专门处理。

  12. 寄生组合式继承

    寄生组合继承:Child.prototype = Object.create(Parent.prototype, {constructor:{value:Child,writable:true,configurable:true}});在 Child 中 Parent.call(this, ...) 初始化实例属性,避免重复调用父构造函数。

    参考代码 · js
    function Parent(name) { this.name = name; }
    Parent.prototype.greet = function() { return `Hi ${this.name}`; };
    function Child(name) { Parent.call(this, name); }
    Child.prototype = Object.create(Parent.prototype, {constructor:{value:Child, writable:true, configurable:true}});
    console.log(new Child('Ada').greet());

    结果:`Hi Ada`;父构造函数每个实例只执行一次。

  13. 发布订阅者模式

    发布订阅:维护 Map<事件, Set<回调>>;on 加入并返回退订函数,emit 用快照遍历,off 删除。约定回调抛错是否中断后续订阅者。

    参考代码 · js
    function emitter() {
      const events = new Map();
      return {
        on(name, fn) { const set = events.get(name) ?? new Set(); set.add(fn); events.set(name,set); return () => set.delete(fn); },
        emit(name, ...args) { for (const fn of [...(events.get(name) ?? [])]) fn(...args); }
      };
    }
    const bus = emitter(); const off = bus.on('x', console.log); bus.emit('x', 1); off();

    结果:输出 `1` 一次;取消订阅后不再输出。

02 / 29

实现 Promise (hot)

7 题
  1. 完整实现Promise A+

    Promise/A+:实现 pending/fulfilled/rejected 三态、只能首次落定、异步执行 then 回调和 then 返回新 Promise;核心 resolvePromise 要处理 thenable、递归吸收、自解析拒绝与防止多次调用。A+ 不规定 finally/all,完整原生兼容还需遵循 ECMAScript 微任务语义。

    参考代码 · js
    class APromise {
      #state='pending'; #value; #listeners=[];
      constructor(executor) {
        let sealed=false;
        const settle=(state,value)=>{
          if(this.#state!=='pending')return;
          this.#state=state;this.#value=value;
          for(const listener of this.#listeners)queueMicrotask(listener);
          this.#listeners=[];
        };
        const resolve=value=>{if(sealed)return;sealed=true;resolvePromise(this,value,x=>settle('fulfilled',x),e=>settle('rejected',e));};
        const reject=error=>{if(sealed)return;sealed=true;settle('rejected',error);};
        try{executor(resolve,reject);}catch(error){reject(error);}
      }
      then(onFulfilled,onRejected){
        let next;
        next=new APromise((resolve,reject)=>{
          const run=()=>{
            try{
              const fulfilled=this.#state==='fulfilled';
              const handler=fulfilled?onFulfilled:onRejected;
              if(typeof handler!=='function')return fulfilled?resolve(this.#value):reject(this.#value);
              resolvePromise(next,handler(this.#value),resolve,reject);
            }catch(error){reject(error);}
          };
          this.#state==='pending'?this.#listeners.push(run):queueMicrotask(run);
        });
        return next;
      }
    }
    function resolvePromise(promise,value,resolve,reject){
      if(value===promise)return reject(new TypeError('self resolution'));
      if(value!==null && (typeof value==='object'||typeof value==='function')){
        let then;try{then=value.then;}catch(error){return reject(error);}
        if(typeof then==='function'){
          let called=false;
          try{then.call(value,y=>{if(!called){called=true;resolvePromise(promise,y,resolve,reject);}},e=>{if(!called){called=true;reject(e);}});}
          catch(error){if(!called)reject(error);}
          return;
        }
      }
      resolve(value);
    }
    new APromise(resolve=>resolve({then(ok){ok(3);ok(4);}})).then(console.log);

    结果:只输出 `3`;包含状态、异步 `then`、链式返回、thenable 吸收和自解析拒绝,尚未运行官方 Promises/A+ 测试套件。

  2. 实现Promise.prototype.finally

    finally:若 onFinally 不是函数,直接透传原结果;否则通过 then 在兑现/拒绝两条路径调用它,等待其返回值落定后继续传原值或原错误,清理失败则以新错误覆盖。简化版可用 Promise.resolve(onFinally()),完整兼容还须考虑 Promise 子类的 Symbol.species。

    参考代码 · js
    function finallyLike(promise, onFinally) {
      if (typeof onFinally !== 'function') return promise.then(onFinally, onFinally);
      return promise.then(
        value => Promise.resolve(onFinally()).then(() => value),
        error => Promise.resolve(onFinally()).then(() => { throw error; })
      );
    }
    finallyLike(Promise.resolve(3), () => console.log('cleanup')).then(console.log);

    结果:先输出 `cleanup`,再输出 `3`;子类 species 语义未覆盖。

  3. 实现Promise.allSettled

    allSettled:对每项 Promise.resolve,按输入顺序存 {status:'fulfilled',value} 或 {status:'rejected',reason},全部完成才兑现;空输入兑现 []。

    参考代码 · js
    function allSettled(items) {
      return Promise.all(Array.from(items, item => Promise.resolve(item).then(
        value => ({status:'fulfilled', value}), reason => ({status:'rejected', reason}))));
    }
    allSettled([1, Promise.reject('x')]).then(console.log);

    结果:`[{status:'fulfilled',value:1},{status:'rejected',reason:'x'}]`。

  4. 实现 Promise.race

    race:对每项 Promise.resolve(item).then(resolve,reject);第一个落定结果决定输出。空输入保持 pending。

    参考代码 · js
    function race(items) {
      return new Promise((resolve, reject) => {
        for (const item of items) Promise.resolve(item).then(resolve, reject);
      });
    }
    race([Promise.resolve('first'), new Promise(r => setTimeout(r, 10, 'later'))]).then(console.log);

    结果:`first`;空输入永不落定。

  5. 实现 Promise.prototype.catch

    catch:等价于 this.then(undefined, onRejected);会返回一个新的 Promise。

    参考代码 · js
    function catchLike(promise, onRejected) { return promise.then(undefined, onRejected); }
    catchLike(Promise.reject('error'), e => `handled ${e}`).then(console.log);

    结果:`handled error`。

  6. Promise.resolve

    resolve:Promise.resolve(value) 吸收 thenable;同构造器的 Promise 可直接返回。手写时防止 thenable 的多次落定和自引用。

    参考代码 · js
    function resolveLike(value) { return new Promise(resolve => resolve(value)); }
    resolveLike({then(resolve){resolve(3)}}).then(console.log);

    结果:`3`;原生 Promise 的 resolve 过程会吸收 thenable。

  7. Promise.reject

    reject:Promise.reject(reason) 返回已拒绝的新 Promise,不吸收传入的 Promise。

    参考代码 · js
    function rejectLike(reason) { return new Promise((_, reject) => reject(reason)); }
    rejectLike('bad').catch(console.log);

    结果:`bad`;传入 Promise 时不会吸收其状态。

03 / 29

Promise 周边场景题(hot)

31 题
  1. 交通灯

    交通灯:把每个灯封装成 await sleep(duration) 后切换状态,外层 while (!signal.aborted) 顺序运行;离开页面时取消计时器。

    参考代码 · js
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    async function traffic(signal) {
      for (const [color, ms] of [['red',3000],['green',2000],['yellow',1000]]) {
        if (signal.aborted) return;
        console.log(color); await sleep(ms);
      }
    }
    traffic(new AbortController().signal);

    结果:依次打印 `red`、`green`、`yellow`;示例只跑一轮,取消需同时清理正在等待的定时器。

  2. 封装异步的fetch,使用async await方式来使用

    异步 fetch:async function request(url, options){ const r=await fetch(url,options); if(!r.ok) throw new Error(String(r.status)); return r.json(); };调用方用 try/catch,并支持 signal。

    参考代码 · js
    async function request(url, options = {}) {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    }
    request('/api/user').then(console.log).catch(console.error);

    结果:2xx JSON 响应解析为对象;HTTP 错误进入 `catch`。

  3. repeat(console.log, 5, 1000)

    repeat(fn,5,1000):循环 5 次,每次 await sleep(1000) 后调用 fn;明确是否应立即执行第一次,并返回可取消句柄。

    参考代码 · js
    async function repeat(fn, count, interval) {
      for (let i = 0; i < count; i++) {
        await new Promise(r => setTimeout(r, interval));
        fn(i + 1);
      }
    }
    repeat(console.log, 5, 1000);

    结果:约每秒输出 `1` 到 `5`,首次输出在约 1 秒后。

  4. 封装一个工具函数输入一个promiseA返回一个promiseB如果超过1s没返回则抛出异常如果正常则输出正确的值

    一秒超时:Promise.race([promiseA, timeoutPromise]) 可让等待方超时,但不能中断原任务;若能控制请求,应传 AbortSignal.timeout(1000)。

    参考代码 · js
    function withTimeout(promise, ms = 1000) {
      let timer;
      return Promise.race([promise, new Promise((_, reject) => {
        timer = setTimeout(() => reject(new Error('timeout')), ms);
      })]).finally(() => clearTimeout(timer));
    }
    withTimeout(Promise.resolve(3)).then(console.log);

    结果:输出 `3`;超时只终止等待,不取消原异步工作。

  5. 请求5s未完成就终止

    五秒未完成终止:创建 AbortController,setTimeout(() => controller.abort(), 5000),传 signal 给 fetch,finally 清除定时器;不要只做 Promise.race。

    参考代码 · js
    async function fetchWithin5s(url) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 5000);
      try { return await fetch(url, {signal:controller.signal}); }
      finally { clearTimeout(timer); }
    }

    结果:5 秒未完成会取消 `fetch` 并以 AbortError 拒绝。

  6. 实现一个sleep函数

    sleep(ms):new Promise(resolve => setTimeout(resolve, ms));如需取消,再监听 AbortSignal 并清除定时器。

    参考代码 · js
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    sleep(10).then(() => console.log('done'));

    结果:约 10 毫秒后输出 `done`。

  7. js每隔一秒打印1,2,3,4,5

    每秒打印 1 到 5:for(let i=1;i<=5;i++){ await sleep(1000); console.log(i); },要立即输出 1 则把打印放到等待之前。

    参考代码 · js
    async function printOneToFive() {
      for (let i = 1; i <= 5; i++) {
        await new Promise(r => setTimeout(r, 1000)); console.log(i);
      }
    }
    printOneToFive();

    结果:每隔约 1 秒依次输出 `1,2,3,4,5`。

  8. 使用 setTimeout 实现 setInterval

    setTimeout 模拟 setInterval:在回调结束后再次 setTimeout(tick, interval);返回取消函数。此方式不会重叠,但间隔包含回调耗时。

    参考代码 · js
    function intervalByTimeout(fn, ms) {
      let stopped = false, id;
      function tick() { if (stopped) return; fn(); id = setTimeout(tick, ms); }
      id = setTimeout(tick, ms);
      return () => { stopped = true; clearTimeout(id); };
    }
    const stop = intervalByTimeout(() => console.log('tick'), 1000);
    setTimeout(stop, 3100);

    结果:约输出三次 `tick` 后停止。

  9. promise实现图片异步加载

    图片异步加载:new Promise((resolve,reject)=>{const img=new Image(); img.onload=()=>resolve(img); img.onerror=reject; img.src=url;});先绑事件再设 src。

    参考代码 · js
    function loadImage(url) {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => resolve(image);
        image.onerror = () => reject(new Error('image load failed'));
        image.src = url;
      });
    }
    loadImage('/logo.png').then(img => console.log(img.naturalWidth));

    结果:成功时输出图片自然宽度;失败时 Promise 拒绝。

  10. 使用Promise封装AJAX请求

    Promise 封装 AJAX:创建 XMLHttpRequest,注册 load/error/timeout/abort,open/send;只把允许的 HTTP 状态当成功,错误时拒绝,支持取消与超时。

    参考代码 · js
    function ajax(url) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve(xhr.responseText) : reject(new Error(`HTTP ${xhr.status}`));
        xhr.onerror = () => reject(new Error('network error'));
        xhr.send();
      });
    }
    ajax('/api').then(console.log).catch(console.error);

    结果:成功返回响应文本,网络错误或非 2xx 拒绝。

  11. 我们能反过来使用 setinterval 模拟实现 settimeout 吗?

    setInterval 模拟 setTimeout:首次触发时立即 clearInterval(id) 再执行回调;可实现近似效果,但计时精度和页面节流仍受环境影响。

    参考代码 · js
    function timeoutByInterval(fn, ms) {
      const id = setInterval(() => { clearInterval(id); fn(); }, ms);
      return () => clearInterval(id);
    }
    timeoutByInterval(() => console.log('once'), 1000);

    结果:约 1 秒后仅输出一次 `once`。

  12. 异步任务:依次发送3次网络请求,拿到服务器数据

    三次串行请求:for (const input of inputs) { const response = await fetch(...); results.push(await response.json()); };后一请求需要前一结果时用其结果构造参数。

    参考代码 · js
    async function serial(urls) {
      const out = [];
      for (const url of urls) {
        const response = await fetch(url);
        if (!response.ok) throw Error(`HTTP ${response.status}`);
        out.push(await response.json());
      }
      return out;
    }

    结果:三个 URL 时顺序发三次请求,按请求顺序返回三项结果。

  13. 实现网络请求超时判断,超过三秒视为超时

    三秒请求超时:fetch(url,{signal:AbortSignal.timeout(3000)});区分超时、用户取消和 HTTP 非 2xx 响应。

    参考代码 · js
    async function timedRequest(url) {
      const response = await fetch(url, {signal:AbortSignal.timeout(3000)});
      if (!response.ok) throw Error(`HTTP ${response.status}`);
      return response.json();
    }

    结果:超过 3 秒会超时拒绝;2xx JSON 返回解析值。

  14. promise中断请求

    Promise 无通用“中断”;Promise 只是结果容器。请求取消要用 AbortController 并把 signal 传给可取消的底层 API。

    参考代码 · js
    const controller = new AbortController();
    const request = fetch('/api/slow', {signal:controller.signal});
    controller.abort();
    request.catch(error => console.log(error.name));

    结果:输出 `AbortError`;取消的是支持 AbortSignal 的请求。

  15. 给定一系列的api,测量上传速度(实现的时候用的GET请求)并选择一个加载时间最短的api

    测量 API:对每个候选发独立请求,以 performance.now() 差值计时,处理超时/错误,再按成功请求耗时选最小;GET 测的是延迟与下载,不等于上传速度。上传速度须用实际 POST/PUT 字节数除以耗时。

    参考代码 · js
    async function fastest(urls) {
      const times = await Promise.all(urls.map(async url => {
        const start = performance.now();
        try { const response = await fetch(url, {cache:'no-store'}); if (!response.ok) throw Error(); await response.arrayBuffer(); return {url, ms:performance.now()-start}; }
        catch { return {url, ms:Infinity}; }
      }));
      return times.reduce((best, item) => item.ms < best.ms ? item : best);
    }

    结果:返回 GET 下载耗时最短的地址;这不等于上传速度。

  16. settimeout系统补偿时间

    补偿计时:记录计划执行时间 next += interval,每次使用 setTimeout(tick, Math.max(0,next-performance.now()));后台节流无法完全补偿。

    参考代码 · js
    function accurateInterval(fn, interval) {
      let next = performance.now() + interval, id, stopped = false;
      function tick() { if (stopped) return; fn(); next += interval; id = setTimeout(tick, Math.max(0,next-performance.now())); }
      id = setTimeout(tick, interval);
      return () => { stopped = true; clearTimeout(id); };
    }

    结果:以绝对计划时间补偿单次回调耗时;后台节流仍可能延迟。

  17. setTimeout准时

    setTimeout 无法保证“准时”;可按绝对时间重排定时器并在回调中计算漂移,主线程阻塞或浏览器节流时仍会迟到。

    参考代码 · js
    const target = performance.now() + 1000;
    setTimeout(() => console.log(Math.round(performance.now() - target)), 1000);

    结果:输出实际漂移毫秒数,通常非严格的 `0`。

  18. 并发多个请求,返回先得到response的。函数输入为url数组,输出为第一个返回的response的结果

    并发请求取最先成功/最先完成需区分:Promise.race 取最先落定,Promise.any 取最先成功;请求完成后可取消其他 fetch。

    参考代码 · js
    async function firstResponse(urls) {
      const controllers = urls.map(() => new AbortController());
      const stopOthers = i => controllers.forEach((c,j) => { if (j !== i) c.abort(); });
      return Promise.race(urls.map((url, i) => fetch(url, {signal:controllers[i].signal}).then(
        response => { stopOthers(i); return response; },
        error => { stopOthers(i); throw error; }
      )));
    }

    结果:返回首个完成的响应;首个失败也会直接拒绝。

  19. JS异步数据流,实现并发异步请求,结果顺序输出

    并发发起、顺序输出:先 const promises=urls.map(request),再按顺序 for (const p of promises) console.log(await p);提前为每项附拒绝处理,避免未处理拒绝。

    参考代码 · js
    async function ordered(urls) {
      const pending = urls.map(url => fetch(url).then(r => r.text()).then(value => ({value}), error => ({error})));
      for (const task of pending) {
        const item = await task;
        if (item.error) throw item.error;
        console.log(item.value);
      }
    }

    结果:请求并发开始,打印顺序与 `urls` 一致。

  20. Promise串行

    Promise 串行:tasks.reduce((p,task)=>p.then(()=>task()), Promise.resolve());tasks 必须是函数,不能是已启动的 Promise。

    参考代码 · js
    async function runSerial(tasks) {
      const out = [];
      for (const task of tasks) out.push(await task());
      return out;
    }
    runSerial([() => Promise.resolve(1), () => Promise.resolve(2)]).then(console.log);

    结果:输出 `[1,2]`;任务函数在轮到时才启动。

  21. 处理高并发, 100 条数据,带宽为 10, 跑满带宽

    100 项、并发 10:维护最多 10 个 worker,各 worker 从共享下标领取下一任务,完成后继续领取;结果按输入下标存储,最后 await Promise.all(workers)。

    参考代码 · js
    async function limitAll(tasks, limit) {
      if (!Number.isInteger(limit) || limit < 1) throw RangeError('limit');
      const out = Array(tasks.length); let next = 0;
      async function worker() { while (next < tasks.length) { const i = next++; out[i] = await tasks[i](); } }
      await Promise.all(Array.from({length:Math.min(limit,tasks.length)}, worker));
      return out;
    }
    limitAll([() => Promise.resolve(1), () => Promise.resolve(2)], 10).then(console.log);

    结果:`[1,2]`;同时运行数不超过 `limit`。

  22. 设计一个简单的任务队列, 要求分别在 1,3,4 秒后打印出 "1", "2", "3";

    1、3、4 秒后打印:如果是相对起点,分别设置三个 setTimeout;若指相邻间隔,则顺序 await sleep(1000/3000/4000),需先确认题意。

    参考代码 · js
    for (const [ms, value] of [[1000,'1'],[3000,'2'],[4000,'3']]) {
      setTimeout(() => console.log(value), ms);
    }

    结果:相对开始时刻约 1、3、4 秒分别输出 `1`、`2`、`3`。

  23. 实现有并行限制的 Promise 调度器

    并行限制调度器:队列保存任务函数,active < limit 时启动,任务在 finally 中递减 active 并启动下一项;add 返回该任务结果 Promise。

    参考代码 · js
    function scheduler(limit) {
      if (!Number.isInteger(limit) || limit < 1) throw RangeError('limit');
      let active = 0; const queue = [];
      function drain() {
        while (active < limit && queue.length) {
          const {task,resolve,reject} = queue.shift(); active++;
          Promise.resolve().then(task).then(resolve,reject).finally(() => { active--; drain(); });
        }
      }
      return task => new Promise((resolve,reject) => { queue.push({task,resolve,reject}); drain(); });
    }
    const add = scheduler(2); add(() => Promise.resolve(3)).then(console.log);

    结果:输出 `3`;同时运行的任务最多 2 个。

  24. 实现 Scheduler

    Scheduler:与上一题相同,核心是不超过并发上限、每项结果和错误独立传回,并处理空队列与同步抛错。

    参考代码 · js
    class Scheduler {
      #queue=[]; #active=0;
      constructor(limit) { if(!Number.isInteger(limit)||limit<1)throw RangeError('limit');this.limit=limit; }
      add(task){return new Promise((resolve,reject)=>{this.#queue.push({task,resolve,reject});this.#drain();});}
      #drain(){while(this.#active<this.limit&&this.#queue.length){const {task,resolve,reject}=this.#queue.shift();this.#active++;Promise.resolve().then(task).then(resolve,reject).finally(()=>{this.#active--;this.#drain();});}}
    }
    new Scheduler(2).add(() => Promise.resolve('ok')).then(console.log);

    结果:`ok`;同时运行的任务最多 2 个。

  25. 有并发限制的Promise.all(ts类型)

    TypeScript 版限流 all:签名可用 function limitAll<T>(tasks: Array<() => Promise<T>>, limit:number): Promise<T[]>;校验 limit > 0,worker 并发领取索引,按原顺序写结果,任一失败拒绝。

    参考代码 · ts
    async function limitAll<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]> {
      if (!Number.isInteger(limit) || limit < 1) throw new RangeError('limit');
      const out = new Array<T>(tasks.length); let next = 0;
      async function worker() { while (next < tasks.length) { const i = next++; out[i] = await tasks[i](); } }
      await Promise.all(Array.from({length:Math.min(limit,tasks.length)}, worker));
      return out;
    }

    结果:最多 `limit` 项并发,返回值按输入顺序组成 `T[]`。

  26. 实现 如果上一次的没请求完,之后的就无响应

    忽略后续请求:用 inFlight 锁;若已在请求则直接返回既定结果(如 undefined),否则 try { return await request() } finally { inFlight=false }。需明确“无响应”是忽略还是复用当前 Promise。

    参考代码 · js
    function ignoreWhilePending(request) {
      let busy = false;
      return async (...args) => {
        if (busy) return undefined;
        busy = true;
        try { return await request(...args); }
        finally { busy = false; }
      };
    }

    结果:前次未完成时后续调用直接得到 `undefined`;前次结束后可重新请求。

  27. 使用 Promise 实现每隔三秒输出时间

    每隔三秒输出时间:循环 await sleep(3000); console.log(new Date());提供停止标志或 AbortSignal 以释放定时器。

    参考代码 · js
    async function printTime(signal) {
      while (!signal.aborted) {
        await new Promise(r => setTimeout(r, 3000));
        if (!signal.aborted) console.log(new Date().toISOString());
      }
    }

    结果:约每 3 秒输出一次当前时间;示例停止后仍要等当前计时器结束。

  28. 使用 Promise 改写回调地狱

    回调地狱:先把每个 Node 风格回调接口包装为 Promise,再用 async/await 依赖顺序执行,并在 try/catch 统一处理失败。

    参考代码 · js
    const read = file => new Promise((resolve,reject) => require('node:fs').readFile(file,'utf8',(e,data) => e ? reject(e) : resolve(data)));
    async function load() { try { return await read('a.txt'); } catch (error) { console.error(error); throw error; } }

    结果:文件存在时返回文本,失败时记录并抛出错误。

  29. 设计一个函数,该函数的参数为可同时发送请求的大小,返回一个函数,该函数的参数为要请求的url。 实现的效果为,同时发送n个请求,当有请求返回后往请求队列里push新的请求,并输出刚刚结束的请求的返回值

    返回限流请求函数:内部维护 queue 和 active;调用时把 {url,resolve,reject} 入队,空位立即发请求,完成后输出结果、落定调用方 Promise 并继续取队列。

    参考代码 · js
    function requestQueue(limit) {
      if(!Number.isInteger(limit)||limit<1)throw RangeError('limit');
      let active=0;const queue=[];
      function drain(){while(active<limit&&queue.length){const {url,resolve,reject}=queue.shift();active++;fetch(url).then(r=>{if(!r.ok)throw Error(`HTTP ${r.status}`);return r.json();}).then(value=>{console.log(value);resolve(value);},reject).finally(()=>{active--;drain();});}}
      return url=>new Promise((resolve,reject)=>{queue.push({url,resolve,reject});drain();});
    }

    结果:同时最多发 `limit` 个请求;每个完成时打印并返回该请求结果。

  30. Promise.retry 超时重新请求,并在重试一定次数依然失败时输出缓存内容

    重试与缓存:每次用独立超时信号请求,失败后最多重试 N 次,可指数退避;全部失败时返回事先存在的缓存,否则抛最后错误。非幂等写操作不能盲目重试。

    参考代码 · js
    async function retry(task, attempts, fallback) {
      if (!Number.isInteger(attempts) || attempts < 1) throw RangeError('attempts');
      let last;
      for (let i = 0; i < attempts; i++) {
        try { return await task(); } catch (error) { last = error; }
      }
      if (fallback !== undefined) return fallback;
      throw last;
    }
    retry(() => Promise.reject('offline'), 2, 'cached').then(console.log);

    结果:尝试两次后输出 `cached`;每次请求超时需在 `task` 内实现。

  31. 写一个 mySetInterVal(fn, a, b),每次间隔 a,a+b,a+2b 的时间,然后写一个 myClear,停止上面的 mySetInterVal

    递增间隔:保存次数 n,每次执行后安排下一次 a+n*b 毫秒;myClear 设置停止标志并 clearTimeout(id)。首轮是等待 a 后执行。

    参考代码 · js
    function mySetInterVal(fn, a, b) {
      let count = 0, stopped = false, id;
      function tick() { if (stopped) return; fn(); count++; id = setTimeout(tick, a + count*b); }
      id = setTimeout(tick, a);
      return () => { stopped = true; clearTimeout(id); };
    }
    const myClear = mySetInterVal(() => console.log('tick'), 1000, 500);

    结果:约在 1、2.5、4.5 秒输出,调用 `myClear()` 停止。

04 / 29

JavaScript 常考手写题

77 题
  1. 产生一个不重复的随机数组

    假设从 [min,max] 取 n 个不重复整数:验证 n <= max-min+1,可先建候选数组再 Fisher–Yates 洗牌并取前 n 项;避免无限随机重抽。

    参考代码 · js
    function uniqueRandom(min, max, count) {
      const size = max - min + 1;
      if (!Number.isInteger(size) || !Number.isInteger(count) || count < 0 || count > size) throw RangeError('range/count');
      const pool = Array.from({length:size}, (_,i) => min + i);
      for (let i = 0; i < count; i++) { const j = i + Math.floor(Math.random()*(size-i)); [pool[i],pool[j]] = [pool[j],pool[i]]; }
      return pool.slice(0,count);
    }
    console.log(uniqueRandom(1,3,3).sort());

    结果:一定包含 `1,2,3` 各一次;抽取顺序随机。

  2. await async 如何实现

    async/await 可理解为把生成器的每一步包装成 Promise:next(value) 得到 {value,done},未结束时 Promise.resolve(value).then(step,throwStep),结束时兑现返回值。

    参考代码 · js
    function run(generator) {
      return new Promise((resolve,reject) => {
        const iterator = generator();
        function step(method, value) {
          let state;
          try { state = iterator[method](value); } catch (error) { reject(error); return; }
          if (state.done) { resolve(state.value); return; }
          Promise.resolve(state.value).then(x => step('next',x), e => step('throw',e));
        }
        step('next');
      });
    }
    run(function*(){ return (yield Promise.resolve(2)) + 1; }).then(console.log);

    结果:`3`;展示 async/await 的生成器驱动核心,不等于引擎内部实现。

  3. 使用递归完成 1 到 100 的累加

    sum(n)=n<=0?0:n+sum(n-1),所以 sum(100)=5050;实际大范围求和可用 n*(n+1)/2 避免栈溢出。

    参考代码 · js
    function sum(n) { return n <= 0 ? 0 : n + sum(n-1); }
    console.log(sum(100));

    结果:`5050`。

  4. 打印出 1~10000 以内的对称数

    遍历 1 到 10000,把数字转字符串并与反转字符串比较;“对称数”按十进制回文数解释,若 10000 包含与否按题目约定。

    参考代码 · js
    const palindromes = Array.from({length:10000}, (_,i) => i+1)
      .filter(n => String(n) === [...String(n)].reverse().join(''));
    console.log(palindromes.slice(0,12));

    结果:前 12 项为 `1,2,3,4,5,6,7,8,9,11,22,33`。

  5. 实现一个字符串匹配算法indexOf

    indexOf(text,pattern):外层枚举起点 0..text.length-pattern.length,内层逐字符比较,首次全匹配返回起点;空模式返回 0,无匹配返回 -1。

    参考代码 · js
    function indexOfText(text, pattern) {
      for (let i = 0; i <= text.length - pattern.length; i++) {
        let j = 0;
        while (j < pattern.length && text[i+j] === pattern[j]) j++;
        if (j === pattern.length) return i;
      }
      return -1;
    }
    console.log(indexOfText('banana','ana'));

    结果:`1`;空模式返回 `0`。

  6. 请实现一个模块 math,支持链式调用math.add(2,4).minus(3).times(2);

    链式 math:对象持有当前值,add/minus/times 修改值并返回 this,用 value() 取结果;示例需约定 add(2,4) 为初始加法,结果 (2+4-3)*2=6。

    参考代码 · js
    const math = {
      current: 0,
      add(...values) { this.current += values.reduce((a,b)=>a+b,0); return this; },
      minus(value) { this.current -= value; return this; },
      times(value) { this.current *= value; return this; },
      value() { return this.current; }
    };
    console.log(math.add(2,4).minus(3).times(2).value());

    结果:`6`;再次调用会延续当前值。

  7. 手写用 ES6proxy 如何实现 arr[-1] 的访问(滴滴2020)

    new Proxy(arr,{get(target,key,receiver){if(typeof key==='string' && /^-[1-9]\d*$/.test(key)){const i=target.length+Number(key);return i>=0?Reflect.get(target,String(i),receiver):undefined} return Reflect.get(target,key,receiver)}});必须先排除 Symbol 键,数组迭代等操作才不会因 Number(Symbol) 抛错。

    参考代码 · js
    function negativeIndex(array) {
      return new Proxy(array, {get(target,key,receiver) {
        if (typeof key === 'string' && /^-[1-9]\d*$/.test(key)) {
          const i = target.length + Number(key);
          return i < 0 ? undefined : Reflect.get(target,String(i),receiver);
        }
        return Reflect.get(target,key,receiver);
      }});
    }
    console.log(negativeIndex([10,20])[-1]);

    结果:`20`;Symbol 键正常传给 `Reflect.get`。

  8. 有一堆整数,请把他们分成三份,确保每一份和尽量相等(11,42,23,4,5,6 4 5 6 11 23 42 56 78 90)

    三组尽量等和属于三路划分,通常是组合优化题。小数组可回溯搜索并剪枝,最小化三组和的最大差;大数组可按数值降序放入当前最小和组作近似,需说明不保证最优。

    参考代码 · js
    function threeGroups(numbers) {
      const groups = [[],[],[]], totals = [0,0,0];
      for (const value of [...numbers].sort((a,b)=>b-a)) {
        const i = totals.indexOf(Math.min(...totals));
        groups[i].push(value); totals[i] += value;
      }
      return {groups, totals};
    }
    console.log(threeGroups([11,42,23,4,5,6]).totals);

    结果:示例为 `[42,23,26]`;这是贪心近似,不能保证最优。

  9. 之字形打印矩阵

    假设按行“之”字打印:偶数行从左到右、奇数行从右到左;若题目指对角线之字遍历,需另定遍历方向。

    参考代码 · js
    function zigzagRows(matrix) {
      return matrix.flatMap((row,i) => i % 2 ? [...row].reverse() : row);
    }
    console.log(zigzagRows([[1,2],[3,4]]));

    结果:按行蛇形输出 `[1,2,4,3]`;对角线之字形需另一算法。

  10. 数组中的最大值

    非空数组用 Math.max(...arr) 或 reduce((max,x)=>Math.max(max,x));大数组避免展开参数过多,空数组需约定返回值。

    参考代码 · js
    function maximum(items) {
      if (!items.length) throw RangeError('empty');
      return items.reduce((a,b) => a > b ? a : b);
    }
    console.log(maximum([3,8,2]));

    结果:`8`。

  11. 用尾递归实现斐波那契数列

    尾递归 fib(n,a=0,b=1){return n===0?a:fib(n-1,b,a+b)};JS 引擎不普遍保证尾调用优化,大 n 建议改循环或 BigInt。

    参考代码 · js
    function fib(n, a = 0, b = 1) { return n === 0 ? a : fib(n-1,b,a+b); }
    console.log(fib(7));

    结果:`13`;大输入改循环,JS 不保证尾调用优化。

  12. 实现简单路由

    简单路由:监听 popstate,调用 history.pushState 后主动渲染当前 location.pathname 对应视图;直达路径需服务器回退到 index.html。

    参考代码 · js
    const routes = {'/': () => 'Home', '/about': () => 'About'};
    function renderRoute() { document.querySelector('#app').textContent = (routes[location.pathname] ?? (()=>'404'))(); }
    function go(path) { history.pushState(null,'',path); renderRoute(); }
    addEventListener('popstate',renderRoute); renderRoute();

    结果:`go('/about')` 后 `#app` 显示 `About`;服务器需把直达路径回退到页面入口。

  13. 封装一个localstorage的setItem和getItem方法

    setItem 用 JSON.stringify,getItem 用 JSON.parse;包装 try/catch 处理禁用存储、配额与坏 JSON,并约定缺失值为 null。

    参考代码 · js
    const store = {
      set(key,value) { localStorage.setItem(key,JSON.stringify(value)); },
      get(key) { const raw = localStorage.getItem(key); return raw === null ? null : JSON.parse(raw); }
    };
    store.set('a',{n:1}); console.log(store.get('a'));

    结果:`{n:1}`;生产代码还要处理存储禁用、配额和损坏 JSON。

  14. 1-1000回文数

    同第 4 题,限定 1 到 1000:String(n) === [...String(n)].reverse().join('')。

    参考代码 · js
    const result = Array.from({length:1000},(_,i)=>i+1)
      .filter(n => String(n) === [...String(n)].reverse().join(''));
    console.log(result.at(-1));

    结果:`999`,是 1–1000 范围最后一个回文数。

  15. 随机生成字符串

    若用于口令/令牌,用 crypto.getRandomValues 取随机字节,并通过拒绝采样映射到字符表以避免取模偏差;Math.random 仅适合非安全用途。

    参考代码 · js
    function randomHex(bytes = 4) {
      const values = crypto.getRandomValues(new Uint8Array(bytes));
      return [...values].map(x => x.toString(16).padStart(2,'0')).join('');
    }
    console.log(randomHex(4).length);

    结果:`8`;具体字符每次随机变化。

  16. 判断一个字符串是否为驼峰字符串, judge('ByteDance','BD') -> true judge('Bytedance','BD') -> false

    按大写字母提取缩写:[...s].filter(ch=>/[A-Z]/.test(ch)).join('')===abbr;ByteDance 得 BD,Bytedance 得 B。此定义仅适用于 ASCII 驼峰。

    参考代码 · js
    function judge(text, abbreviation) {
      return [...text].filter(ch => /[A-Z]/.test(ch)).join('') === abbreviation;
    }
    console.log(judge('ByteDance','BD'), judge('Bytedance','BD'));

    结果:`true false`。

  17. 压缩字符串

    例如游程编码:连续相同字符写成“字符+次数”,aaabb → a3b2;需约定数字或分隔符出现在原文时如何消歧。

    参考代码 · js
    function runLength(text) {
      let out = '';
      for (let i = 0; i < text.length;) {
        let j = i + 1; while (j < text.length && text[j] === text[i]) j++;
        out += text[i] + (j-i); i = j;
      }
      return out;
    }
    console.log(runLength('aaabb'));

    结果:`a3b2`;原文含数字时需加分隔符防歧义。

  18. Map 场景题:按 categories 将商品归类,输出每个分类下的商品名称

    原讨论给出商品 {name,categories} 数组,目标是按分类反向归组。用 Map<分类, 商品名称数组>,遍历每个商品的每个分类并追加名称,最后转为 [{name:'热门',categories:['可乐','苹果']}, ...];分类首次出现的顺序可由 Map 保留。

    参考代码 · js
    function groupProducts(products) {
      const groups = new Map();
      for (const {name,categories} of products) for (const category of categories) {
        if (!groups.has(category)) groups.set(category,[]);
        groups.get(category).push(name);
      }
      return [...groups].map(([name,categories]) => ({name,categories}));
    }
    console.log(groupProducts([{name:'苹果',categories:['水果','热门']}]));

    结果:`[{name:'水果',categories:['苹果']},{name:'热门',categories:['苹果']}]`。

  19. 输入50a6we8y20x 输出50个a,6个we,8个y,20个x

    需要给多字符片段定义分界,否则 50a6we8y20x 有歧义。若约定“数字后直到下一段数字前全是重复片段”,用 /\d+\D+/g 分段,分别取数量和片段后 repeat。

    参考代码 · js
    function expand(text) {
      return (text.match(/\d+\D+/g) ?? []).map(part => {
        const [,count,word] = part.match(/^(\d+)(\D+)$/);
        return word.repeat(Number(count));
      }).join('');
    }
    console.log(expand('3a2we'));

    结果:`aaawewe`;格式非法时应拒绝,而不是悄悄忽略残余片段。

  20. 手写defineProperty

    原讨论实际在探讨用 Object.defineProperty 实现属性监听:遍历对象自有键,为每个键定义 get 收集依赖、set 在值变化时通知订阅者,并递归监听新赋入的对象。需说明新增/删除属性和部分数组操作无法仅靠现有属性的 getter/setter 自动捕获;若题意是“复刻原生 defineProperty”,纯 JS 无法完全实现。

    参考代码 · js
    function observe(object, onChange) {
      for (const key of Object.keys(object)) {
        let value = object[key];
        Object.defineProperty(object,key,{enumerable:true,configurable:true,
          get() { return value; },
          set(next) { if (!Object.is(value,next)) { value = next; onChange(key,next); } }
        });
      }
      return object;
    }
    const state = observe({n:1}, console.log); state.n = 2;

    结果:输出 `n 2`;新增属性不会自动被监听。

  21. String string 值一样返回true Object Object 返回true function function 都是声明的一个新的变量 返回false

    String 原始值按值相等;对象、函数按引用相等。两个不同对象即使结构相同也不相等;同一引用的对象或函数相等,故原题“Object Object 返回 true”要明确是否同一引用。

    参考代码 · js
    const x = {}, same = x;
    console.log('a' === 'a', x === same, {} === {}, (()=>{}) === (()=>{}));

    结果:`true true false false`;对象和函数按引用比较。

  22. 对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'

    先删除所有 b,再反复删除相邻 ac,直到稳定;若删除会产生新邻接,可用栈在线处理,每见 c 且栈顶为 a 则弹出。

    参考代码 · js
    function removeBAndAC(text) {
      const stack = [];
      for (const ch of text) {
        if (ch === 'b') continue;
        if (ch === 'c' && stack.at(-1) === 'a') stack.pop();
        else stack.push(ch);
      }
      return stack.join('');
    }
    console.log(removeBAndAC('abacbc'));

    结果:空字符串 `''`;删除后新相邻的 `ac` 也会消除。

  23. 用一行代码,将数组中的字符串和字符串对象(new String(123))直接判定出来

    arr.filter(x => typeof x === 'string' || x instanceof String);跨 realm 字符串对象时 instanceof 不可靠,可用 Object.prototype.toString.call(x)==='[object String]'。

    参考代码 · js
    const values = ['x', new String('y'), 3, {}];
    console.log(values.filter(x => typeof x === 'string' || Object.prototype.toString.call(x) === '[object String]'));

    结果:包含 `'x'` 和 `String {'y'}` 两项。

  24. 实现 before(n, fn):fn 最多执行 n 次,此后返回最后一次结果

    按原讨论中的示例,闭包记录次数和最近结果;前 n 次调用执行 fn 并保存结果,之后直接返回最近结果。注意这与 lodash 的 before(n,fn) 只执行前 n−1 次不同,必须按本题示例实现。

    参考代码 · js
    function before(n, fn) {
      let count = 0, last;
      return function(...args) { if (count < n) { count++; last = fn.apply(this,args); } return last; };
    }
    const f = before(2, x => x*2);
    console.log(f(1),f(2),f(3));

    结果:`2 4 4`;按原讨论示例执行前 n 次。

  25. 实现一下 console.log

    原讨论中的实现是包装调用:function myLog(...args){ console.log(...args) };若要求从零实现浏览器 DevTools 控制台,纯 JS 无法做到,需区分这两种题意。

    参考代码 · js
    function myLog(...args) { console.log(...args); }
    myLog('hello', 2);

    结果:控制台输出 `hello 2`;只是调用包装器。

  26. 实现(5).add(3).minus(2)功能

    可给 Number.prototype 定义 add/minus 返回普通数,但污染全局原型且与装箱有关;更稳妥是 num(5).add(3).minus(2).value(),结果 6。

    参考代码 · js
    function num(value) {
      return {add(x){value+=x; return this;}, minus(x){value-=x; return this;}, value(){return value;}};
    }
    console.log(num(5).add(3).minus(2).value());

    结果:`6`;避免污染 `Number.prototype`。

  27. 将十进制数字转为二进制数字字符串

    非负安全整数可用 n.toString(2);手写时不断取 n%2 并整除 2,逆序拼接,0 返回 '0'。负数的补码位宽需另外指定。

    参考代码 · js
    function binary(n) {
      if (!Number.isSafeInteger(n) || n < 0) throw RangeError('n');
      if (n === 0) return '0';
      let out = ''; while (n) { out = (n % 2) + out; n = Math.floor(n/2); }
      return out;
    }
    console.log(binary(13));

    结果:`1101`。

  28. 封装remove child.remove()销毁自身

    child.remove() 已是标准 DOM API;兼容写法 child.parentNode?.removeChild(child)。删除节点前应解除应用自行注册的外部监听或资源。

    参考代码 · js
    function removeSelf(node) { node.parentNode?.removeChild(node); }
    const item = document.createElement('p'); document.body.append(item); removeSelf(item);
    console.log(item.isConnected);

    结果:`false`;现代浏览器也可直接 `item.remove()`。

  29. 字符串中字母的出现次数

    遍历字符串,用 Map 或对象计数;“字母”是否含 Unicode、是否区分大小写需约定。

    参考代码 · js
    function countLetters(text) {
      const counts = new Map();
      for (const ch of text) if (/[A-Za-z]/.test(ch)) counts.set(ch,(counts.get(ch) ?? 0)+1);
      return Object.fromEntries(counts);
    }
    console.log(countLetters('aAb!a'));

    结果:`{a:2,A:1,b:1}`;示例区分大小写,仅计 ASCII 字母。

  30. 输出一个等腰三角形

    高度 n 的第 i 行输出 n-i 个空格加 2*i-1 个星号;用 repeat 拼每行。

    参考代码 · js
    function triangle(height) {
      return Array.from({length:height},(_,i) => ' '.repeat(height-i-1)+'*'.repeat(2*i+1)).join('\n');
    }
    console.log(triangle(3));

    结果:输出三行:` *`、` ***`、`*****`。

  31. 实现一个函数a,使其奇数次调用时返回1,偶数次调用时返回2(不能使用全局变量)

    闭包保存调用次数:const a=(()=>{let n=0;return ()=>++n%2?1:2})();无需全局变量。

    参考代码 · js
    const a = (() => { let count = 0; return () => ++count % 2 ? 1 : 2; })();
    console.log(a(),a(),a());

    结果:`1 2 1`。

  32. 从数组中找出最接近目标数的值

    给定非空数组 values 与目标数 target,遍历并保留 Math.abs(x-target) 最小的元素;原讨论的例子目标为 37,[3,56,56,23,7,76,-2,345,45,76,3] 的结果是 45。距离相同保留先出现者,空数组需另定返回或抛错。

    参考代码 · js
    function nearest(values,target) {
      if (!values.length) throw RangeError('empty');
      return values.reduce((best,x) => Math.abs(x-target) < Math.abs(best-target) ? x : best);
    }
    console.log(nearest([3,56,56,23,7,76,-2,345,45,76,3],37));

    结果:`45`;并列时保留先出现的值。

  33. 不用循环求和

    若求 1 到 n,直接 n*(n+1)/2;若是任意数组且禁用循环语句,可用递归或 reduce,但底层仍会遍历。

    参考代码 · js
    function sumTo(n) { return n * (n+1) / 2; }
    console.log(sumTo(100));

    结果:`5050`;适用于 1 到 n 的整数求和。

  34. 连续赋值操作

    a=b=c=1 从右向左赋值;a.x = a = {} 会先确定左侧引用,再计算右侧赋值,可能导致旧对象的 x 指向新对象。需给出具体表达式才能唯一推导。

    参考代码 · js
    let a = {old:true}, b = a, c = null;
    a.x = a = {new:true};
    console.log(a.new, b.x === a, c);

    结果:`true true null`;先固定旧对象的 `a.x` 引用,再执行右侧的 `a={...}`。

  35. 输入一串字符串,根据字符串求出每个字母的数量并返回结果对象。(数字为1时可省略

    遍历字符串计频并返回对象,例 aab → {a:2,b:1};“数字为 1 可省略”暗示输出可能是编码字符串,原题格式不完整。

    参考代码 · js
    function frequencies(text) {
      return [...text].reduce((out,ch) => (out[ch]=(out[ch]??0)+1,out),Object.create(null));
    }
    console.log({...frequencies('aab')});

    结果:`{a:2,b:1}`;原题“1 可省略”的输出格式未确定。

  36. 创建包含10个1的数组 多种方法

    Array(10).fill(1)、Array.from({length:10},()=>1)、[...Array(10)].map(()=>1);注意 Array(10).map(()=>1) 因空位不会执行回调。

    参考代码 · js
    console.log(Array(10).fill(1), Array.from({length:10},()=>1), [...Array(10)].map(()=>1));

    结果:三种写法都产生 10 个 `1`;`Array(10).map` 本身不会访问空位。

  37. 将 ['zm', 'za', 'b', 'lm', 'ln', 'k'] 按首字母分组,并将组内字符串排序

    遍历字符串,以首字母为键收集到对象或 Map,再对每个分组调用 sort();示例输出 {b:['b'],k:['k'],l:['lm','ln'],z:['za','zm']}。原讨论已补充这一目标。

    参考代码 · js
    function groupSorted(words) {
      const out = {};
      for (const word of words) (out[word[0]] ??= []).push(word);
      for (const group of Object.values(out)) group.sort();
      return out;
    }
    console.log(groupSorted(['zm','za','b','lm','ln','k']));

    结果:`{z:['za','zm'],b:['b'],l:['lm','ln'],k:['k']}`;对象键顺序按首次出现。

  38. 将升序整数数组 [0,1,2,4,5,7,13,15,16] 压缩为 ["0->2", "4->5", "7", "13", "15->16"]

    输入必须是升序整数数组:扫描每段连续的 +1 区间,记录起点和终点;单个数输出自身字符串,连续段输出 起点->终点。给定输入应得到题目所示五段;空数组返回 [],重复数如何处理需另定。

    参考代码 · js
    function ranges(nums) {
      const out = [];
      for (let i = 0; i < nums.length;) {
        let j = i; while (j+1 < nums.length && nums[j+1] === nums[j]+1) j++;
        out.push(i === j ? String(nums[i]) : `${nums[i]}->${nums[j]}`);
        i = j+1;
      }
      return out;
    }
    console.log(ranges([0,1,2,4,5,7,13,15,16]));

    结果:`['0->2','4->5','7','13','15->16']`。

  39. ['ab', 'c', 'ab', 'd', 'c'] => ['ab1', 'c1' ,'ab2', 'd', 'c2']

    先计每个值总出现次数,再遍历维护已出现次数;仅重复的值追加序号,得到题示结果。

    参考代码 · js
    function numberDuplicates(values) {
      const total = new Map(), seen = new Map();
      values.forEach(x => total.set(x,(total.get(x)??0)+1));
      return values.map(x => {
        seen.set(x,(seen.get(x)??0)+1);
        return total.get(x) > 1 ? `${x}${seen.get(x)}` : x;
      });
    }
    console.log(numberDuplicates(['ab','c','ab','d','c']));

    结果:`['ab1','c1','ab2','d','c2']`。

  40. 移除空属性

    过滤对象条目;“空”需定义。常见仅删 null/undefined/'':Object.fromEntries(Object.entries(obj).filter(([,v])=>v!==null&&v!==undefined&&v!==''))。

    参考代码 · js
    function removeEmpty(object) {
      return Object.fromEntries(Object.entries(object).filter(([,value]) => value !== null && value !== undefined && value !== ''));
    }
    console.log(removeEmpty({a:0,b:'',c:null,d:false}));

    结果:`{a:0,d:false}`;“空”在此限定为 `null/undefined/''`。

  41. 判断两个对象是否相等

    先定义浅比较还是深比较。深比较需处理类型、键集合、数组顺序、循环引用、Date/Map/Set 和原型;通常用配对 WeakMap 避免无限递归。

    参考代码 · js
    function equal(a,b) {
      if (Object.is(a,b)) return true;
      if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
      const ak = Reflect.ownKeys(a), bk = Reflect.ownKeys(b);
      return ak.length === bk.length && ak.every(key => bk.includes(key) && equal(a[key],b[key]));
    }
    console.log(equal({x:[1,2]},{x:[1,2]}));

    结果:`true`;仅适用于无循环的普通对象和数组,其他内建类型需特判。

  42. 一个数组,找出每个数组元素右侧第一个比当前数大的数的下标,时间复杂度O(N)

    单调递减栈保存未找到答案的下标;遍历新元素时反复弹出比它小的下标并填当前下标,剩余填 -1;时间 O(n)。

    参考代码 · js
    function nextGreaterIndexes(nums) {
      const out = Array(nums.length).fill(-1), stack = [];
      nums.forEach((value,i) => {
        while (stack.length && nums[stack.at(-1)] < value) out[stack.pop()] = i;
        stack.push(i);
      });
      return out;
    }
    console.log(nextGreaterIndexes([2,1,3]));

    结果:`[2,2,-1]`,时间 O(n)。

  43. 寻找出现次数最多的三个标签

    用 Map 计频,再按次数降序、平局按字典序排序取前 3;数据量大可用容量 3 的最小堆。

    参考代码 · js
    function topThree(tags) {
      const counts = new Map(); for (const tag of tags) counts.set(tag,(counts.get(tag)??0)+1);
      return [...counts].sort((a,b)=>b[1]-a[1] || a[0].localeCompare(b[0])).slice(0,3);
    }
    console.log(topThree(['a','b','a','c','c','a']));

    结果:`[['a',3],['c',2],['b',1]]`。

  44. 素数

    n>1 且不存在 2..sqrt(n) 的整数因子即素数;可跳过偶数,生成区间素数可用埃氏筛。

    参考代码 · js
    function isPrime(n) {
      if (!Number.isInteger(n) || n < 2) return false;
      for (let d = 2; d*d <= n; d++) if (n%d === 0) return false;
      return true;
    }
    console.log([1,2,17,18].map(isPrime));

    结果:`[false,true,true,false]`。

  45. 实现日期格式化函数

    用 Date 取年月日时分秒,padStart(2,'0') 补齐,按令牌替换;明确使用本地时区还是 UTC,避免把月份从 0 开始当成 1。

    参考代码 · js
    function formatDate(date) {
      const pad = n => String(n).padStart(2,'0');
      return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
    }
    console.log(formatDate(new Date(2026,0,2,3,4)));

    结果:本地时区下输出 `2026-01-02 03:04`。

  46. 实现 jsonp

    JSONP 仅能 GET:创建唯一全局回调名,拼入 URL 查询参数,插入 <script>;回调或出错/超时后清理脚本和全局函数。只能向可信源使用,响应会执行脚本。

    参考代码 · js
    function jsonp(url, timeout = 5000) {
      return new Promise((resolve,reject) => {
        const name = `jsonp_${crypto.randomUUID().replaceAll('-','')}`;
        const script = document.createElement('script');
        const timer = setTimeout(() => done(new Error('timeout')),timeout);
        function done(error,data) { clearTimeout(timer); script.remove(); delete window[name]; error ? reject(error) : resolve(data); }
        window[name] = data => done(null,data);
        script.onerror = () => done(new Error('load failed'));
        const target = new URL(url,location.href); target.searchParams.set('callback',name);
        script.src = target.href; document.head.append(script);
      });
    }

    结果:服务端调用指定回调时返回数据;仅对可信脚本源使用。

  47. 将 www.baidu.taobao.com 按点号分段反转为 com.taobao.baidu.www

    原讨论要求反转点号分隔的域名片段,而不是反转字符或 URL 路径:www.baidu.taobao.com → com.taobao.baidu.www。可用 split('.').reverse().join('.');若禁用这些方法,就从右向左扫描片段并拼接。

    参考代码 · js
    function reverseDomain(host) { return host.split('.').reverse().join('.'); }
    console.log(reverseDomain('www.baidu.taobao.com'));

    结果:`com.taobao.baidu.www`。

  48. 解析 URL Params 为对象

    new URL(url).searchParams 解析,重复键可用 getAll 保留数组;URLSearchParams 会处理百分号解码。不能把 ?a=1&a=2 默默丢成单值。

    参考代码 · js
    function queryObject(url) {
      const out = Object.create(null);
      for (const [key,value] of new URL(url).searchParams) {
        if (key in out) out[key] = Array.isArray(out[key]) ? [...out[key],value] : [out[key],value];
        else out[key] = value;
      }
      return out;
    }
    console.log(queryObject('https://x.test/?a=1&a=2'));

    结果:`{a:['1','2']}`。

  49. 调用计数器(支持重置)

    闭包持有计数,count() 增加并返回值,count.reset() 将其设回初值;不暴露可随意修改的全局状态。

    参考代码 · js
    function counter() {
      let n = 0;
      const count = () => ++n;
      count.reset = () => { n = 0; };
      return count;
    }
    const c = counter(); console.log(c(),c()); c.reset(); console.log(c());

    结果:`1 2`,然后 `1`。

  50. 颜色生成

    原题未说明颜色格式。若生成随机 #RRGGBB,从 crypto.getRandomValues(new Uint8Array(3)) 取 3 字节,各自转两位十六进制后拼接;若仅装饰用途也可用 Math.random。

    参考代码 · js
    function randomColor() {
      return '#' + [...crypto.getRandomValues(new Uint8Array(3))]
        .map(byte => byte.toString(16).padStart(2,'0')).join('');
    }
    console.log(/^#[0-9a-f]{6}$/.test(randomColor()));

    结果:`true`;具体颜色随机。

  51. JavaScript怎么清空数组

    arr.length=0 会原地清空并影响同一数组的其他引用;arr=[] 只是让当前变量指向新数组;splice(0) 也会原地清空。

    参考代码 · js
    const list = [1,2], alias = list;
    list.length = 0;
    console.log(list,alias);

    结果:两个引用都看到空数组 `[]`;`list=[]` 只会改变一个变量的引用。

  52. 判断A、B数组的包含关系(值和数量),A属于B返回1,B属于A返回2,两者相等返回0,其他返回-1

    用计数 Map 比较每个值出现次数。两边计数完全相同返回 0;A 计数均不大于 B 返回 1;B 属于 A 返回 2;否则 -1。对象元素按引用比较。

    参考代码 · js
    function relation(a,b) {
      const count = xs => xs.reduce((m,x) => m.set(x,(m.get(x)??0)+1),new Map());
      const x = count(a), y = count(b);
      const subset = (p,q) => [...p].every(([key,n]) => n <= (q.get(key)??0));
      const ab = subset(x,y), ba = subset(y,x);
      return ab && ba ? 0 : ab ? 1 : ba ? 2 : -1;
    }
    console.log(relation([1],[1,1]), relation([1,2],[2,1]));

    结果:`1 0`。

  53. 对象的合并

    浅合并用 {...a,...b} 或 Object.assign({},a,b),后者覆盖同名键;深合并需单独规定数组、空值和循环引用策略,避免原型污染。

    参考代码 · js
    const a = {x:1,shared:'a'}, b = {y:2,shared:'b'};
    console.log({...a,...b});

    结果:`{x:1,shared:'b',y:2}`;这是浅合并。

  54. 实现一个可无限延伸的数组

    若题意是按需得到无限序列,可用生成器 function* numbers(){let i=0;while(true) yield i++},逐次 next() 而不一次存入内存;原讨论也有用 Proxy 为越界索引提供默认值的不同理解。JS 数组本身不能真正占用无限内存,仍需明确目标接口。

    参考代码 · js
    function* integers() { let n = 0; while (true) yield n++; }
    const it = integers(); console.log(it.next().value,it.next().value,it.next().value);

    结果:`0 1 2`;按需产生值,不建立真正无限长数组。

  55. 将按空格分列、换行分行的多行字符串转为二维数组

    原讨论的输入是以换行分行、连续空白分列的数字文本,可用 s.trim().split(/\r?\n/).map(line=>line.trim().split(/\s+/)) 得到字符串二维数组;若要数字,再对每项用 Number 转换。空输入需单独返回 []。

    参考代码 · js
    function matrix(text) {
      if (!text.trim()) return [];
      return text.trim().split(/\r?\n/).map(line => line.trim().split(/\s+/));
    }
    console.log(matrix('1  2\n3 4'));

    结果:`[['1','2'],['3','4']]`;要数字时再逐项 `Number`。

  56. 请实现一个通用的Array解构赋值

    JS 已有 [a,b]=iterable;手写通用解构要遵守迭代协议、默认值、剩余元素和迭代器关闭规则。仅针对数组可按下标取值,但不等价于通用解构。

    参考代码 · js
    function take(iterable,count) {
      const iterator=iterable[Symbol.iterator](),out=[];
      try { for(let i=0;i<count;i++){const step=iterator.next();if(step.done)break;out.push(step.value);} }
      finally { iterator.return?.(); }
      return out;
    }
    console.log(take(new Set([10,20]),2));

    结果:`[10,20]`;这是按迭代协议提取前两项,默认值和剩余元素需按具体模式扩展。

  57. 数组合并

    浅合并 [...a,...b] 或 a.concat(b);是否去重、是否深拷贝元素另行约定。

    参考代码 · js
    const a = [1,2], b = [3,4];
    console.log([...a,...b]);

    结果:`[1,2,3,4]`,元素仍保持原引用。

  58. 数组交集,并集,差集

    集合语义先用 const sa=new Set(A), sb=new Set(B) 去重;交集 new Set([...sa].filter(x=>sb.has(x))),并集 new Set([...sa,...sb]),差集 new Set([...sa].filter(x=>!sb.has(x)))。若题目要求保留重复次数,则改用计数 Map。

    参考代码 · js
    function sets(a,b) {
      const A = new Set(a), B = new Set(b);
      return {intersection:[...A].filter(x=>B.has(x)), union:[...new Set([...A,...B])], difference:[...A].filter(x=>!B.has(x))};
    }
    console.log(sets([1,1,2],[2,3]));

    结果:交集 `[2]`、并集 `[1,2,3]`、A−B `[1]`。

  59. 多维数组全排列

    多维数组笛卡尔积:arrays.reduce((acc,arr)=>acc.flatMap(prefix=>arr.map(x=>[...prefix,x])),[[]]);任一维为空则结果为空。

    参考代码 · js
    function product(arrays) {
      return arrays.reduce((rows,items) => rows.flatMap(row => items.map(item => [...row,item])),[[]]);
    }
    console.log(product([[1,2],['a','b']]));

    结果:`[[1,'a'],[1,'b'],[2,'a'],[2,'b']]`。

  60. 判断对象是否存在循环引用

    DFS 用“当前递归路径”WeakSet 检测回边;退出节点时删除。仅用全局 visited 会把共享引用误判为循环。

    参考代码 · js
    function hasCycle(value, active = new WeakSet()) {
      if (!value || typeof value !== 'object') return false;
      if (active.has(value)) return true;
      active.add(value);
      const found = Reflect.ownKeys(value).some(key => hasCycle(value[key],active));
      active.delete(value); return found;
    }
    const a = {}; a.self = a; console.log(hasCycle(a));

    结果:`true`;共享子对象但无回边时返回 `false`。

  61. 找出数组中最小的 k 个数,并保持它们在原数组中的相对顺序

    先取最小 k 个数的多重计数,再原顺序遍历数组,遇到仍需的值就加入并减计数;排序阈值法时间 O(n log n),可用选择算法优化。

    参考代码 · js
    function smallestInOriginalOrder(values,k) {
      if (!Number.isInteger(k) || k < 0 || k > values.length) throw RangeError('k');
      const need = new Map();
      for (const x of [...values].sort((a,b)=>a-b).slice(0,k)) need.set(x,(need.get(x)??0)+1);
      return values.filter(x => { const n = need.get(x)??0; if (!n) return false; need.set(x,n-1); return true; });
    }
    console.log(smallestInOriginalOrder([5,1,3,2],2));

    结果:`[1,2]`,保持原数组相对顺序。

  62. 逆对象扁平

    假设把点路径键还原为嵌套对象:按 . 分割路径逐层创建对象并赋值;要处理数组索引、路径冲突并拒绝 __proto__/constructor/prototype。

    参考代码 · js
    function unflatten(entries) {
      const out = Object.create(null);
      for (const [path,value] of Object.entries(entries)) {
        const keys = path.split('.'); if (keys.some(k => ['__proto__','prototype','constructor'].includes(k))) throw Error('unsafe key');
        let node = out; for (const key of keys.slice(0,-1)) node = node[key] ??= Object.create(null);
        node[keys.at(-1)] = value;
      }
      return out;
    }
    console.log(unflatten({'a.b':1}).a.b);

    结果:`1`;数组索引和路径冲突未实现。

  63. 对象扁平化

    DFS 遍历对象并把路径连接成 a.b 键;数组可选择索引路径。循环引用、点号本身出现在键中、空对象都需约定表示法。

    参考代码 · js
    function flattenObject(object, prefix = '', out = {}) {
      for (const [key,value] of Object.entries(object)) {
        const path = prefix ? `${prefix}.${key}` : key;
        if (value && typeof value === 'object' && !Array.isArray(value)) flattenObject(value,path,out);
        else out[path] = value;
      }
      return out;
    }
    console.log(flattenObject({a:{b:1}}));

    结果:`{'a.b':1}`;循环引用与键本身含点号需另定规则。

  64. 实现 执行一次的函数

    once(fn) 用闭包存 called 与 result,第一次 fn.apply(this,args),之后返回缓存值;若函数抛错,通常不标记成功,以便下次重试。

    参考代码 · js
    function once(fn) {
      let called = false, result;
      return function(...args) { if (!called) { result = fn.apply(this,args); called = true; } return result; };
    }
    const f = once(x => x*2); console.log(f(2),f(9));

    结果:`4 4`;同步抛错时可重试。

  65. 链式调用

    方法返回 this 或新包装对象以继续调用,最后用 value() 取结果;需明确是可变链还是不可变链。

    参考代码 · js
    function chain(value) { return {add(n){value+=n;return this;},times(n){value*=n;return this;},value(){return value;}}; }
    console.log(chain(2).add(3).times(4).value());

    结果:`20`。

  66. 偏函数

    偏函数先保存部分参数,调用时合并后续参数并以调用时 this 调原函数;如 partial(fn,...bound){return function(...rest){return fn.apply(this,[...bound,...rest])}}。

    参考代码 · js
    function partial(fn,...bound) { return function(...rest) { return fn.apply(this,[...bound,...rest]); }; }
    console.log(partial((a,b,c)=>a+b+c,1,2)(3));

    结果:`6`。

  67. 实现管道函数

    管道函数按从左到右执行:fns.reduce((value,fn)=>fn(value),initial);要支持异步时用 Promise 链。

    参考代码 · js
    function pipe(...functions) { return input => functions.reduce((value,fn)=>fn(value),input); }
    console.log(pipe(x=>x+1,x=>x*2)(3));

    结果:`8`,从左到右执行。

  68. 手写事件代理(委托)

    在共同父节点监听事件,使用 event.target.closest(selector) 找匹配子元素,并确认它仍属于父节点;适用于动态子元素,卸载时移除监听。

    参考代码 · js
    function delegate(root,selector,handler) {
      const listener = event => { const target = event.target.closest?.(selector); if (target && root.contains(target)) handler(event,target); };
      root.addEventListener('click',listener);
      return () => root.removeEventListener('click',listener);
    }
    const off = delegate(document.body,'button',(_,button)=>console.log(button.textContent));

    结果:点击 `body` 内的按钮会输出按钮文本;调用 `off()` 后停止。

  69. 数据类型判断

    typeof 识别基本值与函数,但 typeof null==='object';对象具体类型可用 Object.prototype.toString.call(value),跨 realm 和 Symbol.toStringTag 需注意。

    参考代码 · js
    function typeOf(value) { return Object.prototype.toString.call(value).slice(8,-1).toLowerCase(); }
    console.log(typeOf(null),typeOf([]),typeOf(new Date()));

    结果:`null array date`;自定义 `Symbol.toStringTag` 可影响结果。

  70. 类数组转数组

    Array.from(arrayLike) 或 Array.prototype.slice.call(arrayLike);前者同时支持可迭代对象并可映射。

    参考代码 · js
    function toArray(arrayLike) { return Array.from(arrayLike); }
    console.log(toArray({0:'a',1:'b',length:2}));

    结果:`['a','b']`。

  71. 预加载

    图片预加载可创建 Image 并设 src,关键资源可用 <link rel="preload">;仅预加载确实马上会用的资源,避免争抢带宽。

    参考代码 · js
    function preloadImage(url) { const image = new Image(); image.src = url; return image; }
    const image = preloadImage('/banner.webp');

    结果:浏览器开始加载图片;`image.complete` 可检查加载完成状态。

  72. 图片懒加载

    优先原生 <img loading="lazy">;需自定义进入视口行为时用 IntersectionObserver,加载后取消观察并处理失败占位。

    参考代码 · js
    const observer = new IntersectionObserver(entries => {
      for (const entry of entries) if (entry.isIntersecting) {
        const image = entry.target; image.src = image.dataset.src; observer.unobserve(image);
      }
    });
    document.querySelectorAll('img[data-src]').forEach(image => observer.observe(image));

    结果:图片进入视口附近时设置 `src` 并取消观察。

  73. 数组去重

    基本值去重 Array.from(new Set(arr));对象按引用去重,按字段去重用 Map(key → item) 并明确保留首项还是末项。

    参考代码 · js
    function unique(values) { return [...new Set(values)]; }
    console.log(unique([1,1,NaN,NaN]));

    结果:`[1,NaN]`;对象按引用去重。

  74. 防抖 节流

    防抖在连续触发停止一段时间后执行;节流在窗口内最多执行一次。实现时保存计时器/上次时间,提供 cancel,并处理首次与末次执行需求。

    参考代码 · js
    function debounce(fn,ms) { let id; return (...args) => { clearTimeout(id); id=setTimeout(()=>fn(...args),ms); }; }
    function throttle(fn,ms) { let last=-Infinity; return (...args) => { const now=Date.now(); if(now-last>=ms){last=now;fn(...args);} }; }

    结果:防抖在停止触发后执行;节流在时间窗口内最多执行一次。

  75. 函数组合 compose redux-saga koa 洋葱模型

    普通 compose(f,g)(x)=f(g(x)) 从右往左;Koa 洋葱模型是中间件 await next() 前后包裹下游,需防止 next 被调用两次。

    参考代码 · js
    function compose(...fns) { return x => fns.reduceRight((value,fn)=>fn(value),x); }
    console.log(compose(x=>x+1,x=>x*2)(3));

    结果:`7`;Koa 洋葱模型的 `await next()` 需单独实现中间件调度。

  76. sum(x,y)和sum(x)(y)

    可让 sum(x,y) 在第二参存在时直接返回和,否则返回 y=>x+y;判断参数个数用 arguments.length,避免把合法的 0 当作缺失。

    参考代码 · js
    function sum(x,y) { return arguments.length === 2 ? x+y : next => x+next; }
    console.log(sum(2,3),sum(2)(3),sum(2,0));

    结果:`5 5 2`。

  77. curry柯里化

    柯里化把多参数函数拆成逐步接收参数的函数,收集到原函数形参个数后执行;默认参数、剩余参数会影响 fn.length,需约定完成条件。

    参考代码 · js
    function curry(fn,...collected) {
      return (...args) => {
        const all = [...collected,...args];
        return all.length >= fn.length ? fn(...all) : curry(fn,...all);
      };
    }
    console.log(curry((a,b,c)=>a+b+c)(1)(2)(3));

    结果:`6`;默认参数和剩余参数会影响 `fn.length`。

05 / 29

设计模式相关

1 题
  1. 观察者模式

    观察者模式:被观察者持有观察者集合,状态变化时逐个通知;与发布订阅相比,观察者通常直接订阅特定对象,发布订阅常通过事件中心解耦双方。退订和通知中抛错策略要明确。

    参考代码 · js
    class Subject {
      #observers = new Set();
      subscribe(fn) { this.#observers.add(fn); return () => this.#observers.delete(fn); }
      notify(value) { for (const fn of [...this.#observers]) fn(value); }
    }
    const subject = new Subject(); const off = subject.subscribe(console.log); subject.notify(3); off();

    结果:输出 `3` 一次。

06 / 29

树-场景题(hot)

9 题
  1. DOM2JSON

    DOM2JSON:递归读取 nodeType;文本节点输出文本,元素节点输出小写标签、选定属性和 childNodes。避免读取事件处理器、私有数据或过大的 DOM 子树。

    参考代码 · js
    function dom2json(node) {
      if (node.nodeType === Node.TEXT_NODE) return node.textContent;
      if (node.nodeType !== Node.ELEMENT_NODE) return null;
      return {type:node.tagName.toLowerCase(), attributes:Object.fromEntries([...node.attributes].map(a=>[a.name,a.value])), children:[...node.childNodes].map(dom2json).filter(x=>x!==null)};
    }
    console.log(dom2json(document.createElement('div')).type);

    结果:`div`;文本节点返回文本。

  2. 计算目录树的深度

    树深度:空树为 0,叶子为 1,depth(node)=1+max(depth(child));极深树可用显式栈避免调用栈溢出。

    参考代码 · js
    function depth(node) { return node ? 1 + Math.max(0,...(node.children??[]).map(depth)) : 0; }
    console.log(depth({children:[{children:[{}]}]}));

    结果:`3`。

  3. 树形结构获取路径名

    路径名:DFS 时传入祖先路径数组,到目标节点返回 [...path,node.name];需要返回所有匹配时不要找到首项就终止。

    参考代码 · js
    function findNames(nodes,id,path=[]) {
      for (const node of nodes) {
        const next = [...path,node.name];
        if (node.id === id) return next;
        const found = findNames(node.children??[],id,next); if (found) return found;
      }
      return null;
    }
    console.log(findNames([{id:1,name:'A',children:[{id:2,name:'B'}]}],2));

    结果:`['A','B']`。

  4. 对象树遍历

    对象树遍历:DFS 可用栈,BFS 可用队列;对每个节点处理后遍历 children,用 WeakSet 防循环。

    参考代码 · js
    function walk(nodes,visit) { for (const node of nodes) { visit(node); walk(node.children??[],visit); } }
    walk([{name:'A',children:[{name:'B'}]}],node=>console.log(node.name));

    结果:依次输出 `A`、`B`;循环引用需额外检测。

  5. 获取树对象属性

    “获取树对象属性”缺少目标结构。若按指定键收集属性,遍历节点并 result.push(node[key]),未定义值按需求跳过。

    参考代码 · js
    function collect(nodes,key) { const out=[]; (function visit(items){ for(const node of items){ if(key in node) out.push(node[key]); visit(node.children??[]); } })(nodes); return out; }
    console.log(collect([{name:'A',children:[{name:'B'}]}],'name'));

    结果:`['A','B']`;这里假设要收集全树指定属性。

  6. 查找json中的children路径

    查找 children 路径:递归访问子节点,维护索引或名称路径;命中目标后返回路径,重复目标时需决定返回一个还是全部。

    参考代码 · js
    function childPath(nodes,id,path=[]) {
      for (let i=0;i<nodes.length;i++) { const next=[...path,i]; if(nodes[i].id===id) return next; const found=childPath(nodes[i].children??[],id,next); if(found) return found; }
      return null;
    }
    console.log(childPath([{id:1,children:[{id:2}]}],2));

    结果:`[0,0]`,表示从根数组到 `children` 的索引路径。

  7. 对象字符串转化成树形结构

    对象字符串转树:若输入是 JSON 字符串,先 JSON.parse,再验证节点结构;若是缩进文本,按缩进层级用栈构树。原题缺少样例,无法确定格式。

    参考代码 · js
    function parseTreeJSON(text) {
      const value = JSON.parse(text);
      if (!Array.isArray(value)) throw TypeError('expected node array');
      return value;
    }
    console.log(parseTreeJSON('[{"name":"A","children":[]}]')[0].name);

    结果:`A`;假设输入是 JSON 字符串,缩进文本需另一解析器。

  8. 判断有无符合路径和 -> 打印所有路径

    DFS 累加沿途数值,命中目标和时输出当前路径;负数存在时不能仅凭当前和大于目标剪枝。

    参考代码 · js
    function pathsWithSum(root,target) {
      const out=[];
      function visit(node,sum,path) { if(!node) return; const next=[...path,node.value], total=sum+node.value; if(!node.children?.length && total===target) out.push(next); for(const child of node.children??[]) visit(child,total,next); }
      visit(root,0,[]); return out;
    }
    console.log(pathsWithSum({value:1,children:[{value:2},{value:3}]},3));

    结果:`[[1,2]]`;这里统计根到叶的路径。

  9. 获取树结构中的name:getName

    DFS 遍历每个节点,收集 name;若仅取目标节点名称,应先说明按 id、路径还是其他条件查找。

    参考代码 · js
    function getNames(root) { return [root.name,...(root.children??[]).flatMap(getNames)]; }
    console.log(getNames({name:'A',children:[{name:'B'}]}));

    结果:`['A','B']`;假设题意是获取整棵树的名称。

07 / 29

实现 JS 原生方法

20 题
  1. Array.prototype.forEach

    forEach:转换接收者为对象,取长度快照,只访问存在的索引,调用 callback.call(thisArg,value,index,object);返回 undefined。

    参考代码 · js
    function forEachLike(array,callback,thisArg) {
      if(typeof callback!=='function') throw TypeError('callback');
      const obj=Object(array), len=obj.length>>>0;
      for(let i=0;i<len;i++) if(i in obj) callback.call(thisArg,obj[i],i,obj);
    }
    forEachLike([1,,3],console.log);

    结果:只回调索引 0 和 2,函数返回 `undefined`。

  2. Array.prototype.filter

    filter:同样跳过空位,回调为真时将原值追加到新数组;结果数组是密集的。

    参考代码 · js
    function filterLike(array,callback,thisArg) {
      if(typeof callback!=='function') throw TypeError('callback');
      const obj=Object(array), out=[], len=obj.length>>>0;
      for(let i=0;i<len;i++) if(i in obj && callback.call(thisArg,obj[i],i,obj)) out.push(obj[i]);
      return out;
    }
    console.log(filterLike([1,,2,3],x=>x>1));

    结果:`[2,3]`。

  3. Array.prototype.reduce

    reduce:有初始值从索引 0 开始;无初始值时查找首个存在的索引作为累加器,空数组且无初始值抛 TypeError。

    参考代码 · js
    function reduceLike(array,callback,...initial) {
      if(typeof callback!=='function') throw TypeError('callback');
      const obj=Object(array), len=obj.length>>>0; let i=0, acc;
      if(initial.length) acc=initial[0];
      else { while(i<len && !(i in obj)) i++; if(i===len) throw TypeError('empty'); acc=obj[i++]; }
      for(;i<len;i++) if(i in obj) acc=callback(acc,obj[i],i,obj);
      return acc;
    }
    console.log(reduceLike([1,,3],(a,b)=>a+b,0));

    结果:`4`;空数组无初值会抛 `TypeError`。

  4. Array.prototype.fill

    fill(value,start,end):把负索引换算成从末尾计,范围截在 [0,length],逐个写入原数组并返回原数组;会填补空位。

    参考代码 · js
    function fillLike(array,value,start=0,end=array.length) {
      const len=array.length, index=n=>Math.min(len,Math.max(0,n<0?len+n:n));
      for(let i=index(start);i<index(end);i++) array[i]=value;
      return array;
    }
    console.log(fillLike([1,2,3],9,-2));

    结果:`[1,9,9]`,原数组也被修改。

  5. Array.prototype.includes

    includes:按 SameValueZero 比较,因此 NaN 可找到,+0/-0 相等;空位按 undefined 处理,支持负起始索引。

    参考代码 · js
    function includesLike(array,search,from=0) {
      const len=array.length; let i=from<0?Math.max(len+from,0):from;
      for(;i<len;i++){const value=array[i]; if(value===search || (value!==value && search!==search)) return true;}
      return false;
    }
    console.log(includesLike([,NaN],undefined),includesLike([NaN],NaN));

    结果:`true true`,空位按 `undefined` 比较。

  6. Array.prototype.push

    push(...items):从当前 length 起写入各项,更新长度并返回新长度;数组接近最大长度时可能抛错。

    参考代码 · js
    function pushLike(array,...items) { for(const item of items) array[array.length]=item; return array.length; }
    const a=[1]; console.log(pushLike(a,2,3),a);

    结果:`3 [1,2,3]`。

  7. Array.prototype.unshift

    unshift(...items):从后向前移动旧元素,再写入新元素,返回新长度;需保留原有空位。

    参考代码 · js
    function unshiftLike(array,...items) {
      const old=array.length, count=items.length;
      for(let i=old-1;i>=0;i--) { if(i in array) array[i+count]=array[i]; else delete array[i+count]; }
      for(let i=0;i<count;i++) array[i]=items[i];
      return array.length;
    }
    const a=[2,3]; console.log(unshiftLike(a,1),a);

    结果:`3 [1,2,3]`。

  8. Array.prototype.copy

    标准数组原型没有 copy 方法;可能指 copyWithin:把数组内区间复制到目标位置,重叠时选择正确方向,原地修改并返回数组。

    参考代码 · js
    function copyWithinLike(array,target,start,end=array.length) {
      const len=array.length, norm=n=>Math.min(len,Math.max(0,n<0?len+n:n));
      let to=norm(target), from=norm(start), count=Math.min(norm(end)-from,len-to);
      const step=from<to && to<from+count ? -1 : 1;
      if(step<0){from+=count-1;to+=count-1;}
      while(count-->0){ if(from in array) array[to]=array[from]; else delete array[to]; from+=step; to+=step; }
      return array;
    }
    console.log(copyWithinLike([1,2,3,4],1,2));

    结果:`[1,3,4,4]`;题名 `copy` 非标准,此处按 `copyWithin` 解释。

  9. Array.prototype.getLevel

    标准数组原型没有 getLevel;若指数组嵌套深度,可递归求 1+max(子数组深度),空数组/非数组的基准层数先约定。

    参考代码 · js
    function getLevel(value) { return Array.isArray(value) ? 1 + Math.max(0,...value.map(getLevel)) : 0; }
    console.log(getLevel([1,[2,[3]]]));

    结果:`3`;此处约定最外层数组深度为 1。

  10. 实现es6的set集合

    Set 可用 Map 模拟增删查与迭代,值比较需用 SameValueZero 且维持插入顺序;直接使用对象键会把不同对象混为一谈。

    参考代码 · js
    class MiniSet {
      #items=new Map();
      add(x){this.#items.set(x,true);return this;}
      has(x){return this.#items.has(x);}
      delete(x){return this.#items.delete(x);}
      get size(){return this.#items.size;}
      [Symbol.iterator](){return this.#items.keys();}
    }
    console.log([...new MiniSet().add(1).add(1).add(2)]);

    结果:`[1,2]`;用 `Map` 复用键身份和迭代顺序。

  11. 实现es6的map集合

    Map 可用原生 Map 作为说明其行为的参考;纯 JS 完整复刻需对象键身份、插入顺序、迭代器与删除再插入语义,不能只用普通对象代替。

    参考代码 · js
    class MiniMap {
      #entries=[];
      #index(key){return this.#entries.findIndex(([k])=>Object.is(k,key) || k===key);}
      set(key,value){const i=this.#index(key);i<0?this.#entries.push([key,value]):this.#entries[i][1]=value;return this;}
      get(key){const i=this.#index(key);return i<0?undefined:this.#entries[i][1];}
      has(key){return this.#index(key)>=0;}
      delete(key){const i=this.#index(key);if(i<0)return false;this.#entries.splice(i,1);return true;}
      [Symbol.iterator](){return this.#entries[Symbol.iterator]();}
    }
    const key={};console.log(new MiniMap().set(key,3).get(key));

    结果:`3`;这是教学简化版,完整原生迭代和性能语义尚未覆盖。

  12. String.prototype.zpadStart

    zpadStart 非标准方法;若意为左补零,String(value).padStart(targetLength,'0'),超过长度保持原字符串。

    参考代码 · js
    function zpadStart(value,length){return String(value).padStart(length,'0');}
    console.log(zpadStart(12,5));

    结果:`00012`;`zpadStart` 不是标准方法。

  13. Object.assign

    Object.assign 逐个源对象拷贝自身可枚举的字符串与 Symbol 键到目标,会触发 getter/setter,浅拷贝并返回目标;null/undefined 源跳过。

    参考代码 · js
    function assignLike(target,...sources){
      if(target==null) throw TypeError('target');
      const out=Object(target);
      for(const source of sources) if(source!=null) for(const key of Reflect.ownKeys(Object(source))) {
        const descriptor=Object.getOwnPropertyDescriptor(Object(source),key);
        if(descriptor.enumerable) out[key]=source[key];
      }
      return out;
    }
    console.log(assignLike({}, {a:1},{b:2}));

    结果:`{a:1,b:2}`,为浅拷贝。

  14. Object.is

    Object.is 类似严格相等,但 NaN 与自身相等,+0 与 -0 不相等;可用 x===y ? x!==0 || 1/x===1/y : x!==x && y!==y。

    参考代码 · js
    function isLike(x,y){return x===y ? x!==0 || 1/x===1/y : x!==x && y!==y;}
    console.log(isLike(NaN,NaN),isLike(0,-0));

    结果:`true false`。

  15. JSON.stringify

    JSON.stringify 要处理转义、数组和对象、toJSON、replacer、缩进、循环引用及 undefined/BigInt 等规则;不宜声称简单递归版等价于原生。

    参考代码 · js
    function stringifySubset(value){
      const seen=new WeakSet();
      function encode(x){
        if(x===null || typeof x==='boolean' || typeof x==='number') return JSON.stringify(x);
        if(typeof x==='string') return JSON.stringify(x);
        if(typeof x!=='object') return undefined;
        if(seen.has(x)) throw TypeError('cycle'); seen.add(x);
        const out=Array.isArray(x) ? `[${Array.from(x,v=>encode(v)??'null').join(',')}]`
          : `{${Object.keys(x).flatMap(k=>{const v=encode(x[k]);return v===undefined?[]:[`${JSON.stringify(k)}:${v}`]}).join(',')}}`;
        seen.delete(x); return out;
      }
      return encode(value);
    }
    console.log(stringifySubset({a:1,b:[true,null]}));

    结果:`{"a":1,"b":[true,null]}`;不覆盖 `toJSON`、replacer 等完整原生语义。

  16. JSON.parse

    JSON.parse 需词法分析与递归下降解析字符串、数字、布尔、null、数组和对象,并拒绝非法语法;绝不使用 eval 解析不可信 JSON。

    参考代码 · js
    function parseJSON(text){
      let i=0;
      const space=()=>{while(/\s/.test(text[i]??'' ) && i<text.length)i++;};
      function value(){
        space();const ch=text[i];
        if(ch==='"'){let end=i+1;while(end<text.length){if(text[end]==='\\'){end+=2;continue;}if(text[end++]==='"'){const raw=text.slice(i,end);i=end;return JSON.parse(raw);}}throw SyntaxError('string');}
        if(ch==='['){i++;const out=[];space();if(text[i]===']'){i++;return out;}do{out.push(value());space();if(text[i]===']'){i++;return out;}if(text[i++]!==',')throw SyntaxError('array');}while(true);}
        if(ch==='{'){i++;const out={};space();if(text[i]==='}'){i++;return out;}do{space();if(text[i]!=='"')throw SyntaxError('key');const key=value();space();if(text[i++]!==':')throw SyntaxError('colon');const item=value();Object.defineProperty(out,key,{value:item,enumerable:true,writable:true,configurable:true});space();if(text[i]==='}'){i++;return out;}if(text[i++]!==',')throw SyntaxError('object');}while(true);}
        for(const [word,item] of [['true',true],['false',false],['null',null]])if(text.startsWith(word,i)){i+=word.length;return item;}
        const number=text.slice(i).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
        if(number){i+=number[0].length;return Number(number[0]);}
        throw SyntaxError('value');
      }
      const out=value();space();if(i!==text.length)throw SyntaxError('trailing');return out;
    }
    console.log(parseJSON('{"a":[1,true,null]}').a);

    结果:`[1,true,null]`;字符串转义委托原生 `JSON.parse`,对象与数组由递归下降解析。

  17. trim

    trim:删除字符串两端的 ECMAScript 空白和换行字符,不改变中间内容;/^\s+|\s+$/g 是常见近似实现。

    参考代码 · js
    function trimLike(text){return String(text).replace(/^\s+|\s+$/g,'');}
    console.log(trimLike('  hi \n'));

    结果:`hi`;这是常见近似版。

  18. 实现new

    new:创建以 Constructor.prototype 为原型的对象,用该对象调用构造函数;若构造函数返回对象/函数就采用返回值,否则返回新对象。箭头函数不能被 new。

    参考代码 · js
    function newLike(Constructor,...args){
      if(typeof Constructor!=='function') throw TypeError('constructor');
      const instance=Object.create(Constructor.prototype);
      const returned=Constructor.apply(instance,args);
      return returned!==null && (typeof returned==='object'||typeof returned==='function') ? returned : instance;
    }
    function Person(name){this.name=name;} console.log(newLike(Person,'Ada').name);

    结果:`Ada`;类构造函数不能通过 `.apply` 调用,完整语义应使用 `Reflect.construct`。

  19. String.prototype.repeat

    repeat(n):验证 n 为非负有限数并转整数,重复字符串 n 次;可用倍增拼接减少次数,过大输出应抛范围错误。

    参考代码 · js
    function repeatLike(text,count){
      if(!Number.isFinite(count)||count<0) throw RangeError('count');
      let n=Math.trunc(count),chunk=String(text),out='';
      while(n){if(n%2)out+=chunk;n=Math.floor(n/2);if(n)chunk+=chunk;}
      return out;
    }
    console.log(repeatLike('ab',3));

    结果:`ababab`。

  20. String.prototype.includes

    字符串 includes(search,position) 可用 indexOf(search,position)!==-1,但需拒绝正则入参,并处理位置转换与空串。

    参考代码 · js
    function includesText(text,search,position=0){
      if(search instanceof RegExp) throw TypeError('RegExp');
      return String(text).indexOf(String(search),Math.max(0,Math.trunc(position)||0))!==-1;
    }
    console.log(includesText('hello','ell'));

    结果:`true`;跨 realm 正则和 `Symbol.match` 需完整原生语义处理。

08 / 29

JS 库函数实现

1 题
  1. lodash.chunk

    chunk(array,size):要求 size 为正整数,循环 i += size 并 slice(i,i+size);空数组返回 [],不修改输入。

    参考代码 · js
    function chunk(array,size){
      if(!Number.isInteger(size)||size<1) throw RangeError('size');
      const out=[];for(let i=0;i<array.length;i+=size)out.push(array.slice(i,i+size));return out;
    }
    console.log(chunk([1,2,3,4,5],2));

    结果:`[[1,2],[3,4],[5]]`。

09 / 29

JS 工具函数

1 题
  1. 判断一个对象是否是isPlainObject

    isPlainObject:先排除 null 和非对象,再检查原型为 Object.prototype 或 null;跨 realm 对象需要更谨慎,不能仅看 constructor === Object。

    参考代码 · js
    function isPlainObject(value){
      if(value===null||typeof value!=='object') return false;
      const proto=Object.getPrototypeOf(value);
      return proto===null||proto===Object.prototype;
    }
    console.log(isPlainObject({}),isPlainObject([]));

    结果:`true false`;跨 realm 需更完整判断。

10 / 29

手写 nodejs 模块

1 题
  1. promisify

    promisify(fn):返回函数,在原参数后追加 Node 风格 (err,value)=>err?reject(err):resolve(value) 回调;调用原函数时保留 this,同步抛错也应拒绝。

    参考代码 · js
    function promisify(fn){return function(...args){return new Promise((resolve,reject)=>{
      fn.call(this,...args,(error,value)=>error?reject(error):resolve(value));
    });};}
    const read=promisify(require('node:fs').readFile);
    read('package.json','utf8').then(text=>console.log(text.length));

    结果:文件存在时输出长度;失败时 Promise 拒绝。

11 / 29

正则相关

9 题
  1. 正则判断手机号,qq,颜色,邮箱

    手机号、QQ、颜色、邮箱只能按明确业务规则写正则;例如 CSS 六位十六进制色 ^#[0-9a-fA-F]{6}$。手机号号段会变化,邮箱完整规范不适合单个简短正则,优先格式初筛加实际验证。

    参考代码 · js
    const checks={qq:/^[1-9]\d{4,}$/,hexColor:/^#[0-9a-fA-F]{6}$/,email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/};
    console.log(checks.qq.test('12345'),checks.hexColor.test('#aBc123'),checks.email.test('[email protected]'));

    结果:`true true true`;手机号号段和邮箱有效性需按业务规则与实际验证处理。

  2. 字符串的大小写取反

    遍历字符,将小写字母转大写、大写转小写,其他不变;只针对 ASCII 可用 /[a-zA-Z]/g。

    参考代码 · js
    function swapCase(text){return text.replace(/[A-Za-z]/g,ch=>ch===ch.toLowerCase()?ch.toUpperCase():ch.toLowerCase());}
    console.log(swapCase('Ab1'));

    结果:`aB1`。

  3. 检验字符串首尾是否含有数字

    ^\d|\d$ 测首或尾为数字;若要求首尾都为数字,用 ^\d.*\d$ 并单独处理单字符数字。

    参考代码 · js
    const startsOrEndsWithDigit = text => /^\d|\d$/.test(text);
    console.log(startsOrEndsWithDigit('1a'),startsOrEndsWithDigit('a2'),startsOrEndsWithDigit('ab'));

    结果:`true true false`;这里是“首或尾”。

  4. 去除字符串空格

    只去两端空格用 trim();删除全部空白可用 replace(/\s/g,''),仅普通空格用 / /g。

    参考代码 · js
    console.log(' a b '.trim(), ' a b '.replace(/\s/g,''));

    结果:分别是 `'a b'` 与 `'ab'`;前者只去两端。

  5. 去除字符串中的字母

    ASCII 字母用 replace(/[A-Za-z]/g,'');Unicode 字母可考虑 /\p{L}/gu,需目标环境支持。

    参考代码 · js
    console.log('a1中B'.replace(/[A-Za-z]/g,''));

    结果:`1中`;仅删除 ASCII 字母。

  6. 正则 驼峰转换

    驼峰转下划线:先处理连续大写缩写与单词边界,再 /([a-z0-9])([A-Z])/g 插 _ 并转小写;仅简单正则会把 XMLHttp 拆错。

    参考代码 · js
    function snake(text){return text.replace(/([A-Z]+)([A-Z][a-z])/g,'$1_$2').replace(/([a-z0-9])([A-Z])/g,'$1_$2').toLowerCase();}
    console.log(snake('XMLHttpRequest'));

    结果:`xml_http_request`。

  7. 对象key的驼峰转下划线

    递归转换对象键,同时保留数组结构;只处理自身属性,遇循环引用用 WeakMap,并约定键冲突时如何处理。

    参考代码 · js
    function snakeKeys(value){
      if(Array.isArray(value)) return value.map(snakeKeys);
      if(value===null||typeof value!=='object') return value;
      return Object.fromEntries(Object.entries(value).map(([key,item])=>[snake(key),snakeKeys(item)]));
    }
    function snake(text){return text.replace(/([A-Z]+)([A-Z][a-z])/g,'$1_$2').replace(/([a-z0-9])([A-Z])/g,'$1_$2').toLowerCase();}
    console.log(snakeKeys({userName:'Ada'}));

    结果:`{user_name:'Ada'}`;循环引用需加 `WeakMap`。

  8. 判断字符串中是否存在连续的三个数

    若指三个连续数字字符,用 /\d{3}/;若指数值连续递增如 123,需逐位比较相邻数字差是否为 1。

    参考代码 · js
    const hasThreeDigits = text => /\d{3}/.test(text);
    console.log(hasThreeDigits('a123b'),hasThreeDigits('a12b'));

    结果:`true false`;这里指连续三个数字字符,不要求数值递增。

  9. ((2+3)+(3*4))+2---->['(2 + 3)+(3 * 4)', '2 + 3', '3 * 4']

    用栈匹配左右括号,记录每对括号对应的原串范围,从内到外抽取;只用正则难以处理任意嵌套层数,输出的空格格式另定。

    参考代码 · js
    function innerParentheses(text){
      const stack=[],out=[];
      for(let i=0;i<text.length;i++){
        if(text[i]==='(')stack.push(i);
        else if(text[i]===')'){if(!stack.length)throw Error('unbalanced');const start=stack.pop();out.push(text.slice(start,i+1));}
      }
      if(stack.length)throw Error('unbalanced');return out;
    }
    console.log(innerParentheses('((2+3)+(3*4))+2'));

    结果:`['(2+3)','(3*4)','((2+3)+(3*4))']`;输出格式与原题样例不同,需先确认括号保留规则。

12 / 29

排序算法

10 题
  1. 冒泡排序

    冒泡:反复比较相邻元素,逆序则交换;一轮无交换可提前结束。平均/最坏 O(n²),稳定。

    参考代码 · js
    function bubble(a){a=[...a];for(let end=a.length-1;end>0;end--){let changed=false;for(let i=0;i<end;i++)if(a[i]>a[i+1]){[a[i],a[i+1]]=[a[i+1],a[i]];changed=true;}if(!changed)break;}return a;}
    console.log(bubble([3,1,2]));

    结果:`[1,2,3]`。

  2. 选择排序

    选择:每轮找到剩余元素最小值并与当前位置交换;O(n²),通常不稳定。

    参考代码 · js
    function selection(a){a=[...a];for(let i=0;i<a.length;i++){let min=i;for(let j=i+1;j<a.length;j++)if(a[j]<a[min])min=j;[a[i],a[min]]=[a[min],a[i]];}return a;}
    console.log(selection([3,1,2]));

    结果:`[1,2,3]`。

  3. 快速排序

    快排:选枢轴,把小于与大于枢轴的元素分区,再递归两边;平均 O(n log n),最坏 O(n²),原地版通常不稳定。

    参考代码 · js
    function quick(a){if(a.length<2)return [...a];const [pivot,...rest]=a;return [...quick(rest.filter(x=>x<pivot)),pivot,...quick(rest.filter(x=>x>=pivot))];}
    console.log(quick([3,1,2]));

    结果:`[1,2,3]`;示例用额外数组,非原地快排。

  4. 插入排序

    插入:依次把当前元素插入前面已排序部分;O(n²),近乎有序时接近 O(n),稳定。

    参考代码 · js
    function insertion(a){a=[...a];for(let i=1;i<a.length;i++){const value=a[i];let j=i-1;while(j>=0&&a[j]>value){a[j+1]=a[j];j--;}a[j+1]=value;}return a;}
    console.log(insertion([3,1,2]));

    结果:`[1,2,3]`。

  5. 归并排序

    归并:二分递归排序后合并两个有序序列;O(n log n),额外 O(n) 空间,可稳定。

    参考代码 · js
    function mergeSort(a){if(a.length<2)return [...a];const mid=a.length>>1,left=mergeSort(a.slice(0,mid)),right=mergeSort(a.slice(mid)),out=[];let i=0,j=0;while(i<left.length&&j<right.length)out.push(left[i]<=right[j]?left[i++]:right[j++]);return [...out,...left.slice(i),...right.slice(j)];}
    console.log(mergeSort([3,1,2]));

    结果:`[1,2,3]`,相等元素优先取左边保持稳定。

  6. 基数排序

    基数:按个位、十位等从低到高做稳定分桶;适用于整数或固定编码键,时间与位数及基数有关,负数需分开处理。

    参考代码 · js
    function radix(a){if(a.some(x=>!Number.isSafeInteger(x)||x<0))throw RangeError('nonnegative integers only');let out=[...a];for(let place=1;out.some(x=>x>=place);place*=10){const buckets=Array.from({length:10},()=>[]);for(const x of out)buckets[Math.floor(x/place)%10].push(x);out=buckets.flat();}return out;}
    console.log(radix([21,3,12]));

    结果:`[3,12,21]`;仅处理非负安全整数。

  7. 计数排序

    计数:统计有限整数范围内各值频次,前缀和可用于稳定输出;时间 O(n+值域),值域大时不适合。

    参考代码 · js
    function counting(a){if(!a.length)return [];const min=Math.min(...a),max=Math.max(...a);if(!a.every(Number.isInteger)||max-min>1e6)throw RangeError('range');const counts=Array(max-min+1).fill(0);for(const x of a)counts[x-min]++;return counts.flatMap((n,i)=>Array(n).fill(i+min));}
    console.log(counting([3,1,2,1]));

    结果:`[1,1,2,3]`;大值域不适合该实现。

  8. 希尔排序

    希尔:按递减 gap 对各组做插入排序,最后 gap=1;性能依赖 gap 序列,通常不稳定。

    参考代码 · js
    function shell(a){a=[...a];for(let gap=Math.floor(a.length/2);gap>0;gap=Math.floor(gap/2))for(let i=gap;i<a.length;i++){const x=a[i];let j=i;while(j>=gap&&a[j-gap]>x){a[j]=a[j-gap];j-=gap;}a[j]=x;}return a;}
    console.log(shell([3,1,2]));

    结果:`[1,2,3]`。

  9. 桶排序

    桶:把值映射到若干桶,桶内排序后依次拼接;分布均匀时较快,偏斜时可能退化,需明确桶边界。

    参考代码 · js
    function bucket(a){if(!a.length)return [];const buckets=Array.from({length:10},()=>[]);for(const x of a){if(x<0||x>=1)throw RangeError('[0,1) only');buckets[Math.floor(x*10)].push(x);}return buckets.flatMap(b=>b.sort((x,y)=>x-y));}
    console.log(bucket([0.31,0.12,0.25]));

    结果:`[0.12,0.25,0.31]`;此例只支持 `[0,1)`。

  10. 堆排序

    堆:建最大堆后反复把堆顶换到末尾并下沉;O(n log n)、额外 O(1) 空间、不稳定。

    参考代码 · js
    function heapSort(a){a=[...a];const down=(start,end)=>{let root=start;while(root*2+1<end){let child=root*2+1;if(child+1<end&&a[child]<a[child+1])child++;if(a[root]>=a[child])break;[a[root],a[child]]=[a[child],a[root]];root=child;}};for(let i=Math.floor(a.length/2)-1;i>=0;i--)down(i,a.length);for(let end=a.length-1;end>0;end--){[a[0],a[end]]=[a[end],a[0]];down(0,end);}return a;}
    console.log(heapSort([3,1,2]));

    结果:`[1,2,3]`。

13 / 29

自定义 Hooks 手写题

7 题
  1. Hooks怎么封装手势逻辑

    手势 Hook 在 pointerdown/move/up/cancel 记录起点、位移和时间,计算方向/速度,设阈值避免误触;在 effect 清理监听,并考虑触屏滚动冲突与键盘替代操作。

    参考代码 · tsx
    function useSwipe(onSwipe: (direction:'left'|'right')=>void) {
      const start = React.useRef<number|null>(null);
      return {
        onPointerDown:(e:React.PointerEvent)=>{ start.current=e.clientX; },
        onPointerUp:(e:React.PointerEvent)=>{ if(start.current!==null && Math.abs(e.clientX-start.current)>40) onSwipe(e.clientX<start.current?'left':'right'); start.current=null; },
        onPointerCancel:()=>{start.current=null;}
      };
    }

    结果:横向移动超过 40px 时回调 `left` 或 `right`;需按实际设备补速度和滚动冲突处理。

  2. Hooks实现移动端的滑动轮播插件

    轮播 Hook 管理当前索引、拖动位移和动画状态;手势结束按距离/速度切页,索引循环,自动播放计时器在悬停、失焦及卸载时暂停/清理。

    参考代码 · tsx
    function useCarousel(length:number){
      const [index,setIndex]=React.useState(0);
      const next=()=>setIndex(i=>(i+1)%length);
      const prev=()=>setIndex(i=>(i-1+length)%length);
      return {index,next,prev};
    }
    // const {index,next,prev}=useCarousel(images.length);

    结果:`next()`/`prev()` 在 `0..length-1` 循环;要求 `length>0`,完整移动端轮播需再接手势和动画。

  3. 如何用 Hooks 模拟 componentDidMount 与componentWillUnmount

    useEffect(()=>{ setup(); return cleanup; },[]) 对应挂载后的副作用和清理;开发模式 Strict Mode 可能执行“建立→清理→建立”检查,不能假设只运行一次。

    参考代码 · tsx
    React.useEffect(() => {
      console.log('setup');
      return () => console.log('cleanup');
    }, []);

    结果:挂载后建立、卸载时清理;开发 Strict Mode 可能额外执行一次建立和清理。

  4. 实现一个useBodyScrollLock ,当出现弹窗时 阻止背景滚动

    弹窗打开时在 effect 保存 body.style.overflow 原值并设 hidden,清理时恢复;多个弹窗并存需共享锁计数,移动端还要处理滚动穿透与焦点管理。

    参考代码 · tsx
    function useBodyScrollLock(open:boolean){
      React.useEffect(()=>{
        if(!open)return;
        const previous=document.body.style.overflow;
        document.body.style.overflow='hidden';
        return ()=>{document.body.style.overflow=previous;};
      },[open]);
    }

    结果:单弹窗打开时背景滚动锁定,关闭或卸载时恢复;多弹窗需共享锁计数。

  5. ts 实现 hooks: useInterval

    useInterval(callback,delay) 用 ref 保存最新回调,effect 随 delay 创建/清除 interval;delay=null 暂停,避免闭包读到旧状态。

    参考代码 · tsx
    function useInterval(callback:()=>void,delay:number|null){
      const latest=React.useRef(callback);
      React.useEffect(()=>{latest.current=callback;},[callback]);
      React.useEffect(()=>{if(delay===null)return;const id=setInterval(()=>latest.current(),delay);return ()=>clearInterval(id);},[delay]);
    }

    结果:按 `delay` 调用最新回调;传 `null` 暂停。

  6. 实现 useQuery

    useQuery 需先定义是读取 URL 参数还是数据查询。URL 场景可订阅路由变化并从 URLSearchParams 派生值;数据场景需缓存键、请求状态和失效规则。

    参考代码 · tsx
    function useQuery(name:string){
      const [value,setValue]=React.useState(()=>new URLSearchParams(location.search).get(name));
      React.useEffect(()=>{const update=()=>setValue(new URLSearchParams(location.search).get(name));addEventListener('popstate',update);return ()=>removeEventListener('popstate',update);},[name]);
      return value;
    }

    结果:读取当前 URL 查询参数,并响应浏览器前进/后退;这里把 `useQuery` 解释为 URL 查询 Hook。

  7. 实现useRequest

    useRequest 至少返回 {data,error,loading,run,cancel};每次请求持有 AbortController 或版本号,卸载/新请求时防止旧结果覆盖,处理错误与重复提交。

    参考代码 · tsx
    function useRequest<T>(load:(signal:AbortSignal)=>Promise<T>){
      const [state,setState]=React.useState<{data?:T;error?:Error;loading:boolean}>({loading:false});
      const controller=React.useRef<AbortController|null>(null);
      const run=React.useCallback(async()=>{controller.current?.abort();const c=new AbortController();controller.current=c;setState({loading:true});try{const data=await load(c.signal);if(!c.signal.aborted)setState({data,loading:false});}catch(error){if(!c.signal.aborted)setState({error:error as Error,loading:false});}},[load]);
      React.useEffect(()=>()=>controller.current?.abort(),[]);
      return {...state,run,cancel:()=>controller.current?.abort()};
    }

    结果:`run()` 更新加载、成功或错误状态;新请求或卸载取消旧请求。

14 / 29

组件设计题(Vue/React/JS均可)

13 题
  1. 全选

    全选:源状态是已选 id 集合;全选取当前可选项 id,反选移除它们;“部分选择”状态从数量派生,并设置原生 checkbox 的 indeterminate。

    参考代码 · tsx
    function SelectAll({ids}:{ids:string[]}){
      const [selected,setSelected]=React.useState<Set<string>>(new Set());
      const all=ids.length>0 && ids.every(id=>selected.has(id));
      return <><input type="checkbox" aria-label="全选" checked={all} onChange={()=>setSelected(all?new Set():new Set(ids))}/>{ids.map(id=><label key={id}><input type="checkbox" checked={selected.has(id)} onChange={()=>setSelected(old=>{const next=new Set(old);next.has(id)?next.delete(id):next.add(id);return next;})}/>{id}</label>)}</>;
    }

    结果:勾选全选选中所有 id,再次点击清空;部分选择的 `indeterminate` 需用 ref 设置。

  2. 轮播图

    轮播图:索引、上一张/下一张、自动播放和交互暂停;按钮可键盘操作,有可访问名称;动画尊重减少动态效果偏好。

    参考代码 · tsx
    function Carousel({images}:{images:string[]}){
      const [index,setIndex]=React.useState(0);
      return <div><button onClick={()=>setIndex(i=>(i-1+images.length)%images.length)}>上一张</button><img src={images[index]} alt={`第 ${index+1} 张`}/><button onClick={()=>setIndex(i=>(i+1)%images.length)}>下一张</button></div>;
    }

    结果:点击按钮在图片列表中循环切换;需保证 `images` 非空。

  3. 根据response渲染table

    表格:校验响应结构,列配置决定渲染,稳定 id 作 key;提供加载、空、错误、分页状态,不将不可信字段当 HTML 注入。

    参考代码 · tsx
    function DataTable({rows}:{rows:Array<{id:string;name:string}>}){
      return <table><thead><tr><th scope="col">名称</th></tr></thead><tbody>{rows.map(row=><tr key={row.id}><td>{row.name}</td></tr>)}</tbody></table>;
    }

    结果:逐行显示 `name`,React 默认转义文本;加载、空、错误、分页由外层状态管理。

  4. 歌词滚动功能(hot)

    歌词滚动:解析时间戳并按时间排序,用二分查找当前播放时间对应行;只在行变化时滚到可视区域,用户手动滚动时暂停自动跟随。

    参考代码 · js
    function lyricAt(lines,seconds){
      let left=0,right=lines.length-1,result=-1;
      while(left<=right){const mid=(left+right)>>1;if(lines[mid].time<=seconds){result=mid;left=mid+1;}else right=mid-1;}
      return result;
    }
    console.log(lyricAt([{time:0,text:'A'},{time:5,text:'B'}],6));

    结果:`1`,表示当前高亮第二行;滚动时只在索引变化时调用 `scrollIntoView`。

  5. 实现一个 百度搜索框 (搜索提示)

    搜索提示:输入防抖,给每次请求版本号或取消前次请求,确保只展示最新结果;键盘上下/回车/Escape、空结果与 combobox/listbox 语义要完整。

    参考代码 · tsx
    function Suggest({search}:{search:(q:string,signal:AbortSignal)=>Promise<string[]>}){
      const [query,setQuery]=React.useState(''),[items,setItems]=React.useState<string[]>([]);
      React.useEffect(()=>{if(!query.trim()){setItems([]);return;}const c=new AbortController();const id=setTimeout(()=>search(query,c.signal).then(setItems).catch(e=>{if(e.name!=='AbortError')setItems([]);}),250);return ()=>{clearTimeout(id);c.abort();};},[query,search]);
      return <><input role="combobox" aria-expanded={items.length>0} value={query} onChange={e=>setQuery(e.target.value)}/><ul role="listbox">{items.map(x=><li role="option" aria-selected="false" key={x}>{x}</li>)}</ul></>;
    }

    结果:输入停止 250ms 后搜索;旧请求被取消。完整组合框还需键盘导航和焦点管理。

  6. 实现todos

    Todos:任务 id、文本、完成状态为核心状态;支持增删改查与过滤,持久化时处理坏数据;列表项用稳定 id。

    参考代码 · tsx
    function Todos(){
      const [items,setItems]=React.useState<Array<{id:string;text:string;done:boolean}>>([]),[text,setText]=React.useState('');
      return <><form onSubmit={e=>{e.preventDefault();if(text.trim())setItems(old=>[...old,{id:crypto.randomUUID(),text,done:false}]);setText('');}}><input value={text} onChange={e=>setText(e.target.value)}/><button>添加</button></form><ul>{items.map(item=><li key={item.id}><label><input type="checkbox" checked={item.done} onChange={()=>setItems(old=>old.map(x=>x.id===item.id?{...x,done:!x.done}:x))}/>{item.text}</label><button onClick={()=>setItems(old=>old.filter(x=>x.id!==item.id))}>删除</button></li>)}</ul></>;
    }

    结果:可添加、切换完成和删除任务;编辑和持久化仍需补充。

  7. 计时器或倒计时组件

    倒计时:记录结束时刻,用 Date.now() 差值计算剩余,而不是每秒减 1;卸载清理 timer,切后台后回来仍能校准。

    参考代码 · tsx
    function Countdown({end}:{end:number}){
      const [remaining,setRemaining]=React.useState(()=>Math.max(0,end-Date.now()));
      React.useEffect(()=>{const id=setInterval(()=>setRemaining(Math.max(0,end-Date.now())),250);return ()=>clearInterval(id);},[end]);
      return <output>{Math.ceil(remaining/1000)} 秒</output>;
    }

    结果:显示剩余秒数;后台恢复后根据结束时间校准。

  8. 设计一个公会入驻信息提交页

    入驻提交页:把字段分组、必填与格式验证、保存草稿、提交中禁重复、失败可恢复;上传材料和隐私告知依业务要求设计。

    参考代码 · tsx
    function JoinForm({submit}:{submit:(name:string)=>Promise<void>}){
      const [name,setName]=React.useState(''),[busy,setBusy]=React.useState(false),[error,setError]=React.useState('');
      return <form onSubmit={async e=>{e.preventDefault();if(!name.trim()||busy)return;setBusy(true);setError('');try{await submit(name.trim());}catch(err){setError(String(err));}finally{setBusy(false);}}}><label>公会名称<input required value={name} onChange={e=>setName(e.target.value)}/></label><button disabled={busy}>{busy?'提交中':'提交'}</button>{error&&<p role="alert">{error}</p>}</form>;
    }

    结果:必填校验、提交中禁重复、失败提示;完整入驻字段与隐私文案需按业务规则补齐。

  9. 编码实现宽高不相同图片的自适配排列

    不同尺寸图片:保留原始宽高比,可用 CSS Grid/Masonry 或按原尺寸计算布局;图片预留尺寸避免 CLS,懒加载并限制单张最大宽度。

    参考代码 · html
    <style>.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}.gallery img{width:100%;height:auto;display:block}</style>
    <div class="gallery"><img src="a.jpg" width="800" height="600" alt="示例一"><img src="b.jpg" width="600" height="900" alt="示例二"></div>

    结果:不同宽高图片在网格中保持比例;宽高属性帮助预留空间。

  10. 列表子元素顺序反转

    反转列表顺序:若只改变显示顺序用 [...items].reverse(),不要原地改 props;交互和屏幕阅读顺序应与视觉顺序一致,谨慎使用 flex-direction:row-reverse。

    参考代码 · tsx
    function Reversed({items}:{items:Array<{id:string;text:string}>}){
      return <ul>{[...items].reverse().map(item=><li key={item.id}>{item.text}</li>)}</ul>;
    }

    结果:视觉和 DOM 顺序都反转,原始 `items` 不变。

  11. 遍历树组件

    树组件:递归节点组件或扁平可见节点列表,按 id 管理展开/选中;大树用虚拟列表,键盘上下/左右导航和 aria-expanded 要支持。

    参考代码 · tsx
    type TreeNode={id:string;name:string;children?:TreeNode[]};
    function Tree({nodes}:{nodes:TreeNode[]}){
      return <ul role="tree">{nodes.map(node=><li role="treeitem" key={node.id}>{node.name}{node.children?.length?<Tree nodes={node.children}/>:null}</li>)}</ul>;
    }

    结果:递归显示节点;完整树组件还需展开状态与箭头键导航。

  12. 选项卡

    选项卡:管理 active id,按钮用 role=tab、内容用 tabpanel,箭头键切换焦点并支持 Home/End;切换时决定是否保留面板状态。

    参考代码 · tsx
    function Tabs({tabs}:{tabs:Array<{id:string;label:string;content:string}>}){
      const [active,setActive]=React.useState(tabs[0]?.id);
      return <><div role="tablist">{tabs.map(tab=><button role="tab" aria-selected={active===tab.id} key={tab.id} onClick={()=>setActive(tab.id)}>{tab.label}</button>)}</div>{tabs.map(tab=>active===tab.id?<div role="tabpanel" key={tab.id}>{tab.content}</div>:null)}</>;
    }

    结果:点击标签切换面板;完整无障碍模式需加焦点和方向键处理。

  13. 拖拽

    拖拽:原生 Drag and Drop 或 Pointer Events;更新列表必须依据稳定 id,提供键盘移动方式、拖拽预览、取消和越界处理。

    参考代码 · tsx
    function MoveList({items}:{items:string[]}){
      const [order,setOrder]=React.useState(items);
      function move(from:number,to:number){setOrder(old=>{const next=[...old];next.splice(to,0,...next.splice(from,1));return next;});}
      return <ul>{order.map((x,i)=><li key={x}>{x}<button disabled={i===0} onClick={()=>move(i,i-1)}>上移</button></li>)}</ul>;
    }

    结果:可用键盘操作的按钮重排列表;指针拖拽需再接 Pointer Events。

15 / 29

HTML CSS 手写题

1 题
  1. 实现圆形环状进度条

    环形进度条可用 SVG <circle> 的 stroke-dasharray/stroke-dashoffset 按比例绘制;提供文本百分比与 role="progressbar"、aria-valuenow/min/max,不要只靠颜色表达进度。

    参考代码 · html
    <svg viewBox="0 0 120 120" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75" aria-label="完成进度"><circle cx="60" cy="60" r="50" fill="none" stroke="#ddd" stroke-width="10"/><circle cx="60" cy="60" r="50" fill="none" stroke="#315ac7" stroke-width="10" stroke-dasharray="314.16" stroke-dashoffset="78.54" transform="rotate(-90 60 60)"/><text x="60" y="66" text-anchor="middle">75%</text></svg>

    结果:绘制约 75% 的环形进度并提供可读文本。

16 / 29

JavaScript 与浏览器原理

10 题
  1. 事件循环中,宏任务、微任务和浏览器绘制的执行顺序是什么?

    一轮事件循环取一个任务执行,随后清空微任务队列,再有机会更新渲染;实际调度由宿主环境决定,不能认为每个宏任务之后必定绘制。

    参考代码 · js
    console.log('sync');
    setTimeout(()=>console.log('task'),0);
    Promise.resolve().then(()=>console.log('microtask'));

    结果:依次输出 `sync`、`microtask`、`task`;浏览器是否在两个任务间绘制由宿主决定。

  2. 持续追加微任务为什么会让页面无法响应输入?如何处理长任务?

    微任务会在渲染前持续清空,递归追加可能使输入和绘制饿死;把大工作分片并主动让出主线程,可用任务队列、scheduler.yield()(有兼容性处理)或 Worker。

    参考代码 · js
    async function work(items){
      for(let i=0;i<items.length;i+=100){items.slice(i,i+100).forEach(expensive);await new Promise(r=>setTimeout(r,0));}
    }

    结果:每处理 100 项后让出主线程,给输入和绘制机会;`expensive` 是项目实际工作函数。

  3. 闭包如何形成?在实际项目中怎样排查闭包导致的内存占用?

    函数保留创建处词法环境即形成闭包;内存排查用 Heap Snapshot 比较保留对象与引用链,检查未清理监听、计时器和长生命周期缓存。

    参考代码 · js
    function makeCounter(){let count=0;return ()=>++count;}
    const counter=makeCounter();console.log(counter(),counter());

    结果:`1 2`;返回函数保留了创建处的 `count`,长生命周期引用可用 Heap Snapshot 排查。

  4. 原型链、class 和继承之间是什么关系?

    class 是基于原型的语法;实例沿 [[Prototype]] 链查找方法,extends 建立实例和构造器继承链,方法通常放在原型而非每个实例上。

    参考代码 · js
    class Parent{greet(){return 'hi';}}
    class Child extends Parent{}
    const child=new Child();console.log(child.greet(),Object.getPrototypeOf(Child.prototype)===Parent.prototype);

    结果:`hi true`,方法沿原型链找到。

  5. 箭头函数与普通函数的 this、arguments 和构造行为有何不同?

    箭头函数词法绑定 this,无自己的 arguments,不能作构造函数;普通函数的 this 由调用方式决定,可用 new,有自己的 arguments。

    参考代码 · js
    const owner={n:2,arrow:()=>this?.n,regular(){return this.n;}};
    console.log(owner.regular(),owner.regular.call({n:3}));

    结果:`2 3`;箭头函数的 `this` 取决于定义处环境,不因 `.call` 改变。

  6. ESM 与 CommonJS 的加载、导出和循环依赖行为有何区别?

    ESM 静态声明、实时绑定且支持异步模块;CommonJS 运行时 require、导出对象,循环依赖可能得到尚未初始化的部分导出。两者互操作依运行环境。

    参考代码 · js
    // a.mjs: export let count=0; export function inc(){count++}
    // b.mjs: import {count,inc} from './a.mjs'; inc(); console.log(count);

    结果:ESM 的 `b.mjs` 输出 `1`,因为导入是实时绑定;CommonJS 模块需在 Node 环境另写 `require` 示例。

  7. 浏览器从输入 URL 到页面可交互,经历了哪些关键步骤?

    URL 解析后检查缓存/解析 DNS,建立连接与 TLS,发请求收响应;浏览器解析 HTML/CSS、加载关键资源、布局绘制并执行影响交互的脚本。真实顺序含并行与缓存分支。

    参考代码 · js
    performance.getEntriesByType('navigation').forEach(n=>console.log({dns:n.domainLookupEnd-n.domainLookupStart,connect:n.connectEnd-n.connectStart,response:n.responseEnd-n.responseStart}));

    结果:输出当前导航的 DNS、连接、响应阶段耗时;缓存命中时某些阶段为 0。

  8. 浏览器如何构建 DOM/CSSOM,并完成布局、绘制与合成?

    HTML 构建 DOM、CSS 构建 CSSOM,再形成渲染树;计算样式与布局、绘制图层并合成。样式/几何变化可能触发重算或重排,transform 常只需合成。

    参考代码 · js
    const box=document.querySelector('#box');
    box.style.transform='translateX(20px)';

    结果:元素平移;可在 DevTools Performance 面板观察是否只需合成,具体由浏览器实现决定。

  9. 如何用 DevTools 定位内存泄漏、长任务和频繁重排?

    Performance 面板看长任务和 Layout,Memory Heap Snapshot 看保留链,Performance Monitor 看节点/堆增长;用可复现步骤和前后对比确认原因。

    参考代码 · js
    new PerformanceObserver(list=>{for(const entry of list.getEntries())console.log(entry.name,entry.duration);}).observe({type:'longtask',buffered:true});

    结果:浏览器支持时输出长任务名称与耗时;内存泄漏另用 Heap Snapshot 查保留链。

  10. localStorage、sessionStorage、IndexedDB 和 Cookie 应如何选择?

    Cookie 随请求发送,适合少量会话数据并设安全属性;localStorage 持久同步,sessionStorage 按标签页会话,IndexedDB 异步且适合较大结构化数据。敏感令牌避免暴露给脚本。

    参考代码 · js
    localStorage.setItem('theme','dark');
    sessionStorage.setItem('draft','hello');
    console.log(localStorage.getItem('theme'),sessionStorage.getItem('draft'));

    结果:`dark hello`;大结构化数据改用 IndexedDB,服务端会话通常用安全 Cookie。

17 / 29

TypeScript

8 题
  1. any、unknown 和 never 分别适用于什么场景?

    any 跳过检查,仅边界迁移时谨慎用;unknown 是安全的未知值,使用前须收窄;never 表示不可能值,可做穷尽检查。

    参考代码 · ts
    function label(value:unknown){if(typeof value==='string')return value.toUpperCase();return 'unknown';}
    function assertNever(value:never):never{throw new Error(String(value));}
    console.log(label('ok'));

    结果:运行输出 `OK`;`unknown` 用前需收窄,`never` 用于不可能分支。

  2. 如何用类型守卫和判别联合安全地收窄联合类型?

    用 typeof、in、自定义谓词或判别字段收窄联合;判别联合给每种分支不同字面量 kind,避免直接断言类型。

    参考代码 · ts
    type Shape={kind:'circle';radius:number}|{kind:'square';side:number};
    function area(shape:Shape){if(shape.kind==='circle')return Math.PI*shape.radius**2;return shape.side**2;}
    console.log(area({kind:'square',side:3}));

    结果:`9`;判别字段将类型收窄到方形分支。

  3. 如何用 never 检查联合类型的分支是否穷尽?

    switch(value.kind) 的 default 中写 const unreachable: never = value;新增联合成员时编译器会指出遗漏分支。

    参考代码 · ts
    type Result={kind:'ok';value:number}|{kind:'error';message:string};
    function show(result:Result){switch(result.kind){case 'ok':return result.value;case 'error':return result.message;default:{const unreachable:never=result;return unreachable;}}}

    结果:新增 `kind` 分支但未处理时,`never` 赋值产生编译错误。

  4. 泛型约束、条件类型和 infer 如何用于设计可复用 API?

    T extends Constraint 限制泛型能力;条件类型按类型关系选择分支,infer 从匹配形状提取内部类型,例如从函数提取返回值。

    参考代码 · ts
    type Return<F>=F extends (...args:any[])=>infer R?R:never;
    function first<T extends {id:string}>(items:T[]):T|undefined{return items[0];}
    type Value=Return<()=>number>; // number

    结果:`first([{id:'a'}])` 返回该对象;缺少 `id` 的输入编译失败。

  5. 实现 Partial、Pick、Omit、ReturnType 和 Awaited 中任意两个工具类型。

    例如 type MyPartial<T>={[K in keyof T]?:T[K]};type MyPick<T,K extends keyof T>={[P in K]:T[P]}。原生工具类型还涉及一些细节,学习时应弄清边界。

    参考代码 · ts
    type MyPartial<T>={[K in keyof T]?:T[K]};
    type MyPick<T,K extends keyof T>={[P in K]:T[P]};
    type User={id:number;name:string};
    const patch:MyPartial<User>={name:'Ada'};
    const identity:MyPick<User,'id'>={id:1};

    结果:两段声明可通过类型检查;未知键会报错。

  6. interface 与 type 在扩展和声明合并方面有何区别?

    interface 可声明合并且方便扩展对象形状;type 可表达联合、交叉、元组、条件类型等,不能同名声明合并。二者多数对象类型场景可互用。

    参考代码 · ts
    interface User{ id:number }
    interface User{ name:string } // 声明合并
    type Id=number|string;
    const user:User={id:1,name:'Ada'};

    结果:`User` 同时要求 `id` 与 `name`;`type` 可表达联合类型。

  7. 如何为异步请求建模成功、加载、失败三种状态,避免非法状态?

    用判别联合:{status:'loading'} | {status:'success',data:T} | {status:'error',error:Error};只有成功分支才有 data,避免多个独立布尔量形成非法组合。

    参考代码 · ts
    type Load<T>={status:'loading'}|{status:'success';data:T}|{status:'error';error:Error};
    const state:Load<number>={status:'success',data:3};
    if(state.status==='success')console.log(state.data);

    结果:输出 `3`;加载分支不能意外读取 `data`。

  8. 为什么 Vite 能转换 TypeScript,却仍需要单独执行类型检查?

    Vite 开发时用转译器去掉 TS 类型以求速度,不执行完整语义类型检查;CI 仍需 tsc --noEmit 或 vue-tsc --noEmit。

    参考代码 · json
    {"scripts":{"build":"vite build","typecheck":"tsc --noEmit","verify":"npm run typecheck && npm run build"}}

    结果:`verify` 先做完整类型检查,再构建;Vite 单独转译不等于类型检查。

18 / 29

HTML、CSS 与无障碍

8 题
  1. Flexbox 与 Grid 分别适合哪些布局?实现响应式卡片网格。

    Flex 适合单轴排布,Grid 适合二维行列;响应式卡片可用 grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr))。

    参考代码 · css
    .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));gap:1rem}
    .card-actions{display:flex;align-items:center;gap:.5rem}

    结果:卡片二维响应式排布,卡片内操作用单轴 Flex。

  2. BFC 是什么?它如何影响外边距折叠和浮动?

    BFC 是独立的块格式化上下文,内部浮动参与高度、与外部浮动布局隔离,内部外边距与外部不折叠;可用 display:flow-root 创建。

    参考代码 · html
    <style>.container{display:flow-root}.float{float:left;width:40px;height:40px}</style>
    <div class="container"><div class="float"></div>正文</div>

    结果:容器高度包含内部浮动元素,并形成独立 BFC。

  3. 如何避免字体、图片和异步内容造成布局偏移?

    图片声明宽高或 aspect-ratio,字体预加载并考虑替换指标,异步区域预留空间;用真实 CLS 数据找移动元素。

    参考代码 · html
    <img src="hero.webp" width="1200" height="600" alt="展示图">
    <div style="min-height:160px" aria-busy="true">加载中…</div>

    结果:图片和异步区域预留空间,减少布局偏移。

  4. 如何实现暗色模式,并兼顾系统偏好与用户手动选择?

    用 prefers-color-scheme 作默认值、用户设置作覆盖并持久化;保证对比度,更新 color-scheme 让原生控件匹配。

    参考代码 · css
    :root{color-scheme:light dark;--bg:#fff;--fg:#111}
    @media(prefers-color-scheme:dark){:root{--bg:#111;--fg:#fff}}
    :root[data-theme="light"]{--bg:#fff;--fg:#111;color-scheme:light}
    :root[data-theme="dark"]{--bg:#111;--fg:#fff;color-scheme:dark}
    body{background:var(--bg);color:var(--fg)}

    结果:默认跟随系统;设置 `data-theme` 可覆盖系统偏好。

  5. 如何让弹窗具备正确的焦点管理、键盘操作和语义?

    优先原生 <dialog>;打开时聚焦合适元素,限制焦点在模态内容内,Escape 可关闭,关闭后回到触发按钮,并标注标题。

    参考代码 · html
    <button id="open" onclick="dialog.showModal()">打开弹窗</button>
    <dialog id="dialog" aria-labelledby="title"><h2 id="title">设置</h2><form method="dialog"><button>关闭</button></form></dialog>

    结果:原生模态对话框支持 Escape 和焦点约束;复杂场景仍要确认关闭后焦点回到触发按钮。

  6. 何时使用原生 HTML 元素,何时才需要 ARIA 属性?

    原生按钮、链接、输入自带键盘和语义,优先使用;只有原生语义无法表达时补 ARIA,ARIA 不会自动添加交互行为。

    参考代码 · html
    <button type="button" onclick="save()">保存</button>
    <a href="/help">帮助</a>

    结果:按钮和链接自带键盘与语义;`save` 为应用中的保存函数。

  7. 如何验证表单错误提示对键盘用户和屏幕阅读器可用?

    标签关联控件,错误文本用 aria-describedby 关联,提交后聚焦错误摘要或首个错误;仅颜色变化不足够,用键盘和屏幕阅读器实测。

    参考代码 · html
    <label for="email">邮箱</label><input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
    <p id="email-error" role="alert">请输入有效邮箱</p>

    结果:输入与标签和错误文本关联;实际提交后还需聚焦错误位置。

  8. 如何实现宽屏到移动端的自适应布局,而不依赖固定设备宽度?

    用内容驱动的 Grid/Flex、相对单位与容器查询,让内容自然换行;窄视口、缩放到 200% 和长文案都需检查溢出。

    参考代码 · css
    .page{container-type:inline-size}
    .layout{display:grid;grid-template-columns:minmax(0,16rem) minmax(0,1fr);gap:1rem}
    @container (max-width:40rem){.layout{grid-template-columns:1fr}}

    结果:`.layout` 的父容器 `.page` 宽度小于 40rem 时改为单列。

19 / 29

网络、缓存与安全

10 题
  1. HTTP 强缓存与协商缓存如何配合?Cache-Control 和 ETag 分别解决什么问题?

    Cache-Control 指定可缓存性与新鲜期,命中强缓存可不发请求;过期后带 If-None-Match 与 ETag 协商,未变则 304。带哈希的静态资源适合长期缓存。

    参考代码 · http
    GET /app.abc123.js HTTP/1.1
    
    HTTP/1.1 200 OK
    Cache-Control: public, max-age=31536000, immutable
    ETag: "abc123"

    结果:新鲜期内可用本地强缓存;过期后带 `If-None-Match` 可协商得到 304。

  2. CORS 为什么由浏览器执行?预检请求在什么情况下出现?

    CORS 是浏览器同源策略的一部分,服务器通过响应头授予跨源读取权限;非简单方法/头或某些内容类型会先发 OPTIONS 预检,服务器间请求不受浏览器 CORS 限制。

    参考代码 · http
    OPTIONS /api/data HTTP/1.1
    Origin: https://app.example
    Access-Control-Request-Method: POST
    
    HTTP/1.1 204 No Content
    Access-Control-Allow-Origin: https://app.example
    Access-Control-Allow-Methods: POST

    结果:浏览器在跨源非简单请求前预检,许可后才发实际 POST。

  3. 从 DNS、TCP/TLS 到 HTTP 响应,HTTPS 请求的关键步骤是什么?

    先解析 DNS、建立传输连接(TCP 或 QUIC)、TLS 验证证书并协商密钥,然后发送 HTTP 请求与接收响应;可复用连接并有缓存/CDN 分支。

    参考代码 · js
    const n=performance.getEntriesByType('navigation')[0];
    console.log({dns:n.domainLookupEnd-n.domainLookupStart,tls:n.connectEnd-n.secureConnectionStart,ttfb:n.responseStart-n.requestStart});

    结果:输出当前导航部分阶段耗时;复用连接或缓存时可能为 0。

  4. HTTP/2 与 HTTP/3 对前端资源加载有什么影响?

    HTTP/2 多路复用和头压缩减少请求开销,但单 TCP 丢包仍可能影响流;HTTP/3 基于 QUIC,可减少部分队头阻塞及连接迁移成本。仍应控制资源大小和优先级。

    参考代码 · js
    const response=await fetch('/api');
    console.log(response.ok);
    // DevTools Network 的 Protocol 列可查看 h2 / h3;JS fetch 语义通常不因此变化。

    结果:输出请求是否成功;协议版本需在网络面板或服务端协商信息中确认。

  5. SSE、WebSocket 与普通 HTTP 请求应如何选择?

    普通 HTTP 适合一次性请求;SSE 适合服务端单向事件流与自动重连;WebSocket 适合持续双向低延迟通信,需处理心跳与断线恢复。

    参考代码 · js
    const stream=new EventSource('/events');stream.onmessage=e=>console.log(e.data);
    const socket=new WebSocket('wss://example.com/ws');socket.onopen=()=>socket.send('ping');

    结果:SSE 单向接收事件,WebSocket 可双向发送;普通 `fetch` 适合单次请求。

  6. 如何实现 Token 刷新时的并发去重和失败请求重放?

    用单个 refreshPromise 让并发 401 共享刷新;刷新成功后重放可安全重试的请求并防无限循环,失败则清理登录态。请求体不可重放时需特殊处理。

    参考代码 · js
    let refreshPromise=null;
    async function api(url,options={}){
      let response=await fetch(url,options);
      if(response.status!==401)return response;
      refreshPromise??=fetch('/auth/refresh',{method:'POST',credentials:'include'}).then(r=>{if(!r.ok)throw Error('refresh failed');}).finally(()=>{refreshPromise=null;});
      await refreshPromise;
      response=await fetch(url,options);return response;
    }

    结果:并发 401 共用一次刷新;重放非幂等请求前需确认请求体可重放和权限状态。

  7. 如何防止 XSS?为何只设置 CSP 仍不够?

    默认对不可信文本做上下文编码,避免 innerHTML,对允许的 HTML 用可信清洗库;CSP 是纵深防御,不能替代正确编码和 DOM API 使用。

    参考代码 · js
    const message='<img src=x onerror=alert(1)>';
    document.querySelector('#message').textContent=message;

    结果:页面显示字面文本,不执行 `onerror`;需要富文本时用受信任清洗器。

  8. Cookie 的 HttpOnly、Secure 和 SameSite 分别解决什么问题?

    HttpOnly 阻止 JS 读取 Cookie,Secure 要求 HTTPS 传输,SameSite 限制跨站请求携带;仍需考虑 CSRF、XSS 与会话过期。

    参考代码 · http
    Set-Cookie: session=opaque; HttpOnly; Secure; SameSite=Lax; Path=/

    结果:脚本不能读取该 Cookie;仅经 HTTPS 发送,并限制部分跨站携带场景。

  9. CSRF 的触发条件是什么?前后端如何配合防护?

    站外页面诱导浏览器携带受害者凭据发送有副作用请求,即可形成 CSRF;用 SameSite、CSRF token、Origin/Referer 检查及避免 GET 改数据共同防护。

    参考代码 · js
    // 服务端伪代码:有副作用请求校验 Origin 和 CSRF token。
    if(req.method==='POST' && (req.headers.origin!==allowedOrigin || req.headers['x-csrf-token']!==session.csrfToken)){
      res.statusCode=403;res.end('Forbidden');
    }

    结果:不满足同源来源和有效 token 的提交返回 403;服务端仍需安全管理 token。

  10. 如何设计上传大文件的分片、断点续传、校验和重试?

    客户端切块并记录上传 id、分片索引和校验值,服务端幂等接收、查缺补漏与最终合并校验;失败只重传缺失块,限制并发并支持取消。

    参考代码 · js
    async function uploadParts(file,uploadId,chunkSize=1024*1024){
      for(let start=0,index=0;start<file.size;start+=chunkSize,index++){
        const part=file.slice(start,start+chunkSize);
        const response=await fetch(`/upload/${uploadId}/${index}`,{method:'PUT',body:part});
        if(!response.ok)throw Error(`part ${index} failed`);
      }
    }

    结果:按索引顺序上传分片;断点续传需先查询服务器已收到的分片并校验最终文件。

20 / 29

React 与 React 19

11 题
  1. React 的 render 与 commit 阶段分别做什么?状态更新何时触发重新渲染?

    render 阶段计算下一棵 UI,可被中断/重试;commit 阶段把变化提交到 DOM 并运行相关 effect。状态更新安排重新渲染,同值更新可能被跳过。

    参考代码 · tsx
    function Counter(){const [n,setN]=React.useState(0);console.log('render');return <button onClick={()=>setN(x=>x+1)}>{n}</button>;}

    结果:点击安排重新 render 并在 commit 后更新按钮文字;开发 Strict Mode 可能多次调用 render。

  2. 列表 key 为什么必须稳定?错误的 key 会造成什么问题?

    key 标识同级列表项身份,重排时帮助保留正确组件状态;用索引作 key 在插入/删除/排序时可能让输入状态错位。

    参考代码 · tsx
    function Rows({items}:{items:Array<{id:string;text:string}>}){return <ul>{items.map(x=><li key={x.id}><input defaultValue={x.text}/></li>)}</ul>;}

    结果:重排列表时输入状态跟随稳定 `id`,而非错误地跟随数组位置。

  3. useEffect 的依赖、清理和闭包过期问题如何排查?

    effect 应列全读取的响应式依赖,返回清理函数;闭包过期可通过正确依赖、函数式更新或 ref 解决,避免靠禁用 lint 掩盖问题。

    参考代码 · tsx
    function Ticker({id}:{id:string}){React.useEffect(()=>{const timer=setInterval(()=>console.log(id),1000);return()=>clearInterval(timer);},[id]);return null;}

    结果:`id` 改变时清理旧计时器、建立新计时器,不继续输出旧 `id`。

  4. useMemo、useCallback 和 React.memo 在什么条件下才有收益?

    只有计算昂贵、引用稳定性确实影响子组件,且测量证明收益时才用;记忆化本身也有成本,先用 React Profiler 定位。

    参考代码 · tsx
    const Expensive=React.memo(function Expensive({items}:{items:number[]}){return <p>{items.reduce((a,b)=>a+b,0)}</p>;});
    function Parent({items}:{items:number[]}){const sorted=React.useMemo(()=>[...items].sort((a,b)=>a-b),[items]);return <Expensive items={sorted}/>;}

    结果:`items` 引用不变时复用排序结果并可能跳过子组件渲染;收益须用 Profiler 测量。

  5. useTransition 与 useDeferredValue 分别适合什么交互?

    useTransition 把特定状态更新标为非紧急并给 pending 状态;useDeferredValue 让某个值的消费延后,适合输入即时响应、结果稍后更新。

    参考代码 · tsx
    function Search({items}:{items:string[]}){const [query,setQuery]=React.useState('');const deferred=React.useDeferredValue(query);return <><input value={query} onChange={e=>setQuery(e.target.value)}/><p>{items.filter(x=>x.includes(deferred)).length}</p></>;}

    结果:输入值立即变化,较慢的结果统计可延后;`useTransition` 适合把指定状态更新标为非紧急。

  6. 如何在 React 中处理表单提交的 pending、错误和乐观更新?

    提交时禁重复或标 pending,显示可恢复错误;乐观更新需在失败时回滚或重新同步,尤其注意并发提交的顺序。

    参考代码 · tsx
    function Submit({send}:{send:()=>Promise<void>}){const [pending,setPending]=React.useState(false),[error,setError]=React.useState('');return <><button disabled={pending} onClick={async()=>{setPending(true);setError('');try{await send();}catch(e){setError(String(e));}finally{setPending(false);}}}>提交</button>{error&&<p role="alert">{error}</p>}</>;}

    结果:提交期间按钮禁用,错误可见;乐观更新需在失败时回滚。

  7. useActionState、useFormStatus 和 useOptimistic 各承担什么职责?

    useActionState 管理 action 结果与 pending,useFormStatus 读取所属表单提交状态,useOptimistic 在异步期间提供临时乐观视图。

    参考代码 · tsx
    function SaveForm(){const [state,action,pending]=React.useActionState<{saved:string|null},FormData>(async(_old,form)=>({saved:String(form.get('name')??'')}),{saved:null});return <form action={action}><input name="name"/><button disabled={pending}>保存</button><output>{state.saved??''}</output></form>;}

    结果:提交后 `state.saved` 显示名称;`useFormStatus` 可在子组件读取状态,`useOptimistic` 可显示临时值。

  8. React 中何时使用 useState、Context 或外部状态库?

    局部状态用 useState;跨层共享少量稳定值可用 Context;复杂跨页面共享或高频更新再评估外部状态库,避免把所有状态集中。

    参考代码 · tsx
    const Theme=React.createContext('light');
    function App(){const [theme,setTheme]=React.useState('light');return <Theme.Provider value={theme}><button onClick={()=>setTheme('dark')}>切换</button><View/></Theme.Provider>;}
    function View(){return <p>{React.useContext(Theme)}</p>;}

    结果:点击后 `View` 显示 `dark`;局部状态仍由 `useState` 管理。

  9. 如何定位大型 React 应用的无效渲染并量化优化效果?

    用 React Profiler 找提交次数和耗时,确定导致更新的 state/props,改动后对同一交互量化比较;开发模式数据不等于生产模式。

    参考代码 · tsx
    function Measured(){const renderStart=performance.now();React.useEffect(()=>console.log('commit elapsed',performance.now()-renderStart));return <div>内容</div>;}

    结果:输出近似渲染到 effect 的时间;正式比较需用 React Profiler 和相同交互样本。

  10. 如何设计一个支持取消、竞态处理和错误状态的异步数据 Hook?

    Hook 在 effect 中创建 AbortController,请求开始设 loading,完成时只让当前版本写入 data/error,清理时 abort;给调用方 retry 与取消入口。

    参考代码 · tsx
    function useJson(url:string){const [state,setState]=React.useState<{data?:unknown;error?:string;loading:boolean}>({loading:true});React.useEffect(()=>{const c=new AbortController();setState({loading:true});fetch(url,{signal:c.signal}).then(r=>{if(!r.ok)throw Error(String(r.status));return r.json();}).then(data=>setState({data,loading:false})).catch(e=>{if(!c.signal.aborted)setState({error:String(e),loading:false});});return()=>c.abort();},[url]);return state;}

    结果:URL 改变时取消旧请求,返回加载、成功或失败状态。

  11. React Compiler 的自动记忆化与手动 useMemo、useCallback 有什么关系?如何验证它是否生效?

    React Compiler 可自动记忆化组件和 Hook 中部分计算;手动 useMemo/useCallback 仍可精确控制特定值的稳定性,但不能把记忆化当作程序正确性的前提。检查构建配置与编译诊断,用 Profiler 对比提交次数和耗时。

    参考代码 · tsx
    function Total({prices}:{prices:number[]}){return <output>{prices.reduce((a,b)=>a+b,0)}</output>;}

    结果:总价正确性来自计算本身;若项目启用 React Compiler,可用编译诊断与 Profiler 检查是否自动记忆化。

21 / 29

Vue 3

8 题
  1. Vue 3 的 reactive 和 ref 如何跟踪依赖并触发更新?

    reactive 通过 Proxy 跟踪对象属性读写,ref 通过 .value 跟踪读写;effect 读取时收集依赖,写入触发调度更新。

    参考代码 · vue
    <script setup>
    import {ref,reactive} from 'vue'
    const count=ref(0),user=reactive({name:'Ada'})
    </script>
    <template><button @click="count++">{{count}} {{user.name}}</button></template>

    结果:点击后 `count` 增加,模板自动更新。

  2. computed、watch 和 watchEffect 的使用场景及执行时机有何不同?

    computed 缓存派生值;watch 监控指定源并可取新旧值;watchEffect 自动追踪同步读取的依赖。默认 watcher 回调通常在父更新后、组件 DOM 更新前运行,需 DOM 更新后用 flush:'post'。

    参考代码 · vue
    <script setup>
    import {ref,computed,watch} from 'vue'
    const n=ref(1),double=computed(()=>n.value*2)
    watch(n,(next,prev)=>console.log(prev,next))
    </script>
    <template><button @click="n++">{{double}}</button></template>

    结果:初始显示 `2`,点击后显示 `4`,watch 输出 `1 2`。

  3. Vue 组件之间如何选择 props、emit、provide/inject 和 Pinia?

    父子数据用 props/emit,深层依赖注入用 provide/inject,跨页面共享业务状态可用 Pinia;避免把局部临时状态全局化。

    参考代码 · vue
    <!-- Parent.vue -->
    <Child :value="count" @change="count=$event" />
    <!-- Child.vue: defineProps<{value:number}>(); const emit=defineEmits<{change:[value:number]}>() -->

    结果:子组件发 `change` 后父组件更新 `count`;跨多层共享可用 provide/inject。

  4. v-model 在原生表单和自定义组件上分别如何工作?

    原生表单 v-model 映射值属性和输入事件;自定义组件默认用 modelValue prop 和 update:modelValue 事件,也可声明命名模型。

    参考代码 · vue
    <script setup>
    const model=defineModel<string>({default:''})
    </script>
    <template><input v-model="model" /></template>

    结果:父组件 `<Child v-model="name"/>` 与子组件双向同步值。

  5. Vue 的 nextTick 解决了什么问题?什么时候不该依赖它?

    nextTick 等待 Vue 批量 DOM 更新完成,适合更新数据后读取新 DOM;不应用它代替正确的数据流或异步业务完成通知。

    参考代码 · vue
    <script setup>
    import {ref,nextTick} from 'vue'
    const n=ref(0)
    async function update(){n.value++;await nextTick();console.log(document.querySelector('#n')?.textContent)}
    </script>
    <template><button @click="update">更新</button><span id="n">{{n}}</span></template>

    结果:点击后日志为新的 DOM 文本 `1`。

  6. Vue 2 项目迁移 Vue 3 时,如何分阶段处理 API 与依赖兼容?

    先升级依赖并运行迁移构建,处理不兼容 API/插件;可逐模块迁移组件,覆盖关键交互测试,再切换应用入口。

    参考代码 · js
    // 迁移时先检查依赖,再让新入口使用 Vue 3 createApp。
    import {createApp} from 'vue';
    import App from './App.vue';
    createApp(App).mount('#app');

    结果:Vue 3 应用挂载到 `#app`;实际迁移还需逐项处理插件和不兼容 API。

  7. 为什么解构 reactive 对象可能丢失响应性?如何修复?

    直接解构普通变量会失去 Proxy 属性读取链;使用 toRefs/toRef 保留关联,或在消费处继续读取 state.key。

    参考代码 · vue
    <script setup>
    import {reactive,toRefs} from 'vue'
    const state=reactive({count:0})
    const {count}=toRefs(state)
    </script>
    <template><button @click="count++">{{count}}</button></template>

    结果:解构后仍保留响应性,点击更新数字。

  8. Vue 列表渲染中 key 的作用是什么?如何避免组件状态错位?

    key 帮助 Vue 在列表变动时识别节点与组件身份;用稳定 id,不在重排列表中用索引作 key。

    参考代码 · vue
    <template><ul><li v-for="item in items" :key="item.id"><input :value="item.name"></li></ul></template>

    结果:按稳定 `id` 识别节点;重排时输入不应错绑到其他项。

22 / 29

Next.js 与服务端渲染

8 题
  1. CSR、SSR、SSG 与按需重新生成页面分别适用于什么场景?

    CSR 适合强交互且 SEO 要求低的界面;SSR 适合请求时个性化内容;SSG 适合变化少的公开内容;按需重新生成适合可容忍短时间旧内容的公开页面。

    参考代码 · tsx
    // Next.js App Router:静态公开页面可在构建时生成。
    export default async function Page(){const posts=await fetch('https://example.com/posts').then(r=>r.json());return <ul>{posts.map((p:{id:string;title:string})=><li key={p.id}>{p.title}</li>)}</ul>;}

    结果:页面在服务端生成 HTML;具体缓存/重验证由项目版本和配置决定。

  2. Server Component 与 Client Component 的边界如何划分?

    Server Component 负责服务端数据读取、保密逻辑和减少客户端 JS;需要 state、事件、浏览器 API 的边界用 Client Component,尽量让客户端边界靠近叶子。

    参考代码 · tsx
    // app/page.tsx (Server Component)
    import Counter from './Counter';
    export default async function Page(){const data=await loadPublicData();return <><h1>{data.title}</h1><Counter/></>;}
    // Counter.tsx 以 'use client' 开头并使用 useState。

    结果:标题由服务端输出,计数交互只给客户端组件加载 JS。

  3. Hydration mismatch 有哪些常见原因?如何定位和修复?

    服务端与客户端首屏输出不一致会报错,常见于时间、随机数、浏览器状态或非法 HTML 嵌套;定位具体节点,保证首次输出一致,浏览器专有值放 effect 后处理。

    参考代码 · tsx
    'use client';
    function Clock(){const [now,setNow]=React.useState<string|null>(null);React.useEffect(()=>setNow(new Date().toISOString()),[]);return <time>{now??'加载时间…'}</time>;}

    结果:首屏服务端和客户端都先显示占位文案,挂载后更新,避免时间造成 hydration mismatch。

  4. 哪些数据可以在服务端读取,哪些交互必须留在客户端?

    密钥、数据库读取只在服务端;交互、DOM、localStorage 在客户端。服务端返回给客户端的 props 必须可序列化且不含秘密。

    参考代码 · tsx
    // Server Component 内读取服务端环境变量,不把 secret 传给客户端。
    export default async function Page(){const data=await fetchPrivate(process.env.API_SECRET);return <ClientWidget title={data.publicTitle}/>;}

    结果:客户端只收到 `publicTitle`;密钥不进入可序列化 props。

  5. 如何用 Suspense 和流式渲染改善慢数据页面的体验?

    用 Suspense 为慢数据区块设 fallback,让外层内容先到达;流式响应逐步发送边界内容,仍需有错误边界和可用的加载状态。

    参考代码 · tsx
    import {Suspense} from 'react';
    export default function Page(){return <><h1>文章</h1><Suspense fallback={<p>正文加载中…</p>}><SlowArticle/></Suspense></>;}

    结果:标题和 fallback 可先返回,慢正文随后流式到达;`SlowArticle` 是异步服务端组件。

  6. 如何设计公开页面与个性化页面的缓存策略,避免数据串用?

    公共数据可按明确标签/时效缓存;含用户身份的数据按请求隔离或不缓存,缓存键必须包含影响输出的身份/权限维度,防止串用。

    参考代码 · tsx
    async function userProfile(userId:string){return fetch(`/api/users/${encodeURIComponent(userId)}`,{cache:'no-store'}).then(r=>r.json());}

    结果:个性化资料每次读取,不作为公开共享缓存;服务端仍须校验当前用户权限。

  7. 如何让服务端渲染页面同时具备良好的 SEO 和可交互性?

    服务端输出可抓取的标题、正文与元数据,客户端对需要的区域进行 hydration;减少阻塞脚本并确保交互在加载前有可理解的状态。

    参考代码 · tsx
    export const metadata={title:'产品说明',description:'公开的产品介绍'};
    export default function Page(){return <main><h1>产品说明</h1><p>可抓取的正文</p><ClientAction/></main>;}

    结果:服务端 HTML 有标题与正文,`ClientAction` 承担交互。

  8. Next.js 的 Cache Components 与 use cache 如何划分静态、缓存和请求时数据?

    Cache Components 让可缓存区域显式使用 use cache 并设置生命周期/失效策略;依请求变化的区域保持动态,使用 Suspense 边界。具体行为随 Next.js 版本和配置变化,应以项目版本验证。

    参考代码 · tsx
    import {Suspense} from 'react';
    async function getPublicPosts(){'use cache';const response=await fetch('https://example.com/posts');return response.json();}
    async function PostList(){const posts=await getPublicPosts();return <ul>{posts.map((p:{id:string;title:string})=><li key={p.id}>{p.title}</li>)}</ul>;}
    export default function Page(){return <Suspense fallback={<p>加载中…</p>}><PostList/></Suspense>;}

    结果:公开帖子数据可缓存;真实缓存寿命与失效策略须按项目 Next.js 版本配置。

23 / 29

性能、工程化与测试

10 题
  1. LCP、INP、CLS 各衡量什么?如何用真实用户数据定位问题?

    LCP 衡量最大内容渲染,INP 衡量交互响应,CLS 衡量意外布局偏移;按设备/页面分组看真实用户分位数,再用实验室工具找根因。

    参考代码 · js
    new PerformanceObserver(list=>{for(const e of list.getEntries())console.log(e.name,e.value??e.duration);}).observe({type:'largest-contentful-paint',buffered:true});

    结果:浏览器支持时输出 LCP 候选;INP、CLS 需采集对应指标并按真实用户分组统计。

  2. 首屏图片、字体与脚本分别如何优化,并如何验证收益?

    图片用合适尺寸/格式并优先加载 LCP 图,字体减少字重并控制交换,脚本按需拆分/延后;用同口径真实用户指标和网络瀑布图验证。

    参考代码 · html
    <link rel="preload" as="image" href="hero.webp">
    <img src="hero.webp" width="1200" height="600" fetchpriority="high" alt="首页主图">
    <script type="module" src="app.js"></script>

    结果:主图更早请求且预留尺寸;收益需用同口径 LCP 和网络瀑布验证。

  3. 长列表和聊天记录应如何虚拟化?动态高度会带来什么问题?

    只渲染视口及缓冲区项目;动态高度需测量并维护位置索引,插入历史消息时保持滚动锚点,避免跳动。

    参考代码 · js
    function visibleRange(scrollTop,rowHeight,viewportHeight,total){const start=Math.max(0,Math.floor(scrollTop/rowHeight)-3);const end=Math.min(total,Math.ceil((scrollTop+viewportHeight)/rowHeight)+3);return {start,end};}
    console.log(visibleRange(500,50,300,1000));

    结果:`{start:7,end:19}`;动态高度时要测量行高并维持滚动锚点。

  4. Vite 开发服务器与生产构建的工作方式有何区别?

    Vite 开发时按需通过原生 ESM 提供模块并快速 HMR;生产构建打包、优化、拆分和压缩资源。TypeScript 类型检查需另跑。

    参考代码 · json
    {"scripts":{"dev":"vite","build":"vite build","typecheck":"tsc --noEmit"}}

    结果:开发走按需模块与 HMR,构建生成优化产物;类型检查需单独运行。

  5. Tree shaking 失效的常见原因是什么?如何检查打包体积?

    副作用标记错误、CommonJS、不支持静态分析的动态访问或整个库导入可影响 tree shaking;用构建分析报告和产物确认实际体积。

    参考代码 · js
    const add=(a,b)=>a+b;
    const unused=()=>42;
    console.log(add(1,2));

    结果:`3`;生产构建可能移除未调用的 `unused`,需检查实际产物确认。

  6. 如何为组件设计单元测试、交互测试和端到端测试的边界?

    纯逻辑用单元测试,组件行为用用户交互测试,跨页面关键流程用 E2E;避免三层重复测试同一实现细节。

    参考代码 · tsx
    // Testing Library 示例:验证用户可见行为。
    function Counter(){const [count,setCount]=React.useState(0);return <button onClick={()=>setCount(x=>x+1)}>{count}</button>;}
    render(<Counter/>);
    await user.click(screen.getByRole('button',{name:'增加'}));
    expect(screen.getByRole('button',{name:'1'})).toBeVisible();

    结果:点击后按钮显示 `1`;需在 Testing Library 测试环境运行。

  7. 如何测试异步请求的加载、失败、取消和重复提交?

    用可控的请求 mock 验证 loading→success/error、取消后不更新、重复提交行为;断言用户可见状态与请求次数。

    参考代码 · js
    const controller=new AbortController();
    const promise=fetch('/api',{signal:controller.signal});
    controller.abort();
    await expect(promise).rejects.toMatchObject({name:'AbortError'});

    结果:测试验证取消分支;`expect` 需在 Vitest/Jest 环境中运行,网络请求应使用 mock。

  8. Monorepo 中如何划分共享包、版本与依赖边界?

    共享包只放跨应用稳定能力,明确包之间依赖方向与发布版本;避免应用互相引用内部源码,CI 按变更范围构建测试。

    参考代码 · json
    {"name":"@study/ui","private":true,"exports":{".":"./src/index.ts"},"peerDependencies":{"react":"^19"}}

    结果:共享 UI 包只暴露指定入口,React 由使用方提供;版本范围需与实际工作区匹配。

  9. 如何制定前端错误监控、Source Map 和发布回滚方案?

    采集错误与发布版本、匿名会话线索;Source Map 私有上传并限权限;发布采用可回退产物/开关,白屏率异常时快速回滚。

    参考代码 · js
    window.addEventListener('error',e=>report({message:e.message,release:APP_RELEASE}));
    window.addEventListener('unhandledrejection',e=>report({message:String(e.reason),release:APP_RELEASE}));

    结果:上报带版本的错误;`report` 须脱敏,Source Map 应受控上传,故障时回滚对应版本。

  10. 如何量化一次性能优化,避免只看本地 Lighthouse 分数?

    明确目标指标与样本,固定页面、设备和网络条件比较实验室数据,同时看线上同口径 p75/p95 与错误率,避免只凭一次 Lighthouse 分数。

    参考代码 · js
    const p75=values=>[...values].sort((a,b)=>a-b)[Math.ceil(values.length*.75)-1];
    console.log(p75([100,200,300,400]));

    结果:`300`;比较优化前后需同设备、同页面和同流量口径。

24 / 29

AI 应用前端与流式交互

8 题
  1. 如何用 fetch、ReadableStream 和 TextDecoder 逐块解析模型流式响应?

    fetch 后从 response.body.getReader() 循环读块,用 TextDecoder.decode(value,{stream:true}) 保留跨块字符;SSE 还需缓冲到完整事件边界再解析,不能假定一块就是一条消息。

    参考代码 · js
    async function readStream(response,onText){
      const reader=response.body.getReader(),decoder=new TextDecoder();
      try{while(true){const {done,value}=await reader.read();if(done)break;onText(decoder.decode(value,{stream:true}));}onText(decoder.decode());}
      finally{reader.releaseLock();}
    }

    结果:逐块得到解码文本;SSE 的 `data:` 边界还需跨块缓冲解析。

  2. SSE 流断开、用户取消和网络重连时,界面状态如何恢复?

    维护连接状态与最后事件 id;断线按可重放协议重连并去重,用户取消用 AbortController 且不自动重连;服务端不支持重放时不能假称续传。

    参考代码 · js
    const controller=new AbortController();
    async function start(){try{const response=await fetch('/events',{signal:controller.signal});if(!response.ok)throw Error(String(response.status));await readStream(response,console.log);}catch(error){if(controller.signal.aborted)console.log('cancelled');else console.error('retryable error',error);}}

    结果:用户取消显示 `cancelled`,网络失败进入重试状态;续传需服务器提供游标。

  3. 模型输出 Markdown 时,如何处理不完整片段和不可信内容?

    对不完整 Markdown 可先按文本或增量解析展示,完成后再稳定渲染;禁止直接注入模型生成 HTML,链接与代码展示也要做安全处理。

    参考代码 · js
    const partialMarkdown='**未结束的流';
    const preview=document.createElement('pre');
    preview.textContent=partialMarkdown;

    结果:流未结束时安全显示原始 Markdown 文本;最终富文本渲染需可信解析和 HTML 清洗。

  4. 聊天消息持续增长时,如何控制渲染成本和自动滚动行为?

    长列表虚拟化或分段,流式文本按帧/批次更新;仅当用户停留底部时自动跟随,用户向上阅读则显示“跳到底部”按钮。

    参考代码 · js
    function shouldStick(container){return container.scrollHeight-container.scrollTop-container.clientHeight<80;}
    const stick=shouldStick(chat);appendMessage(message);if(stick)requestAnimationFrame(()=>chat.scrollTop=chat.scrollHeight);

    结果:用户原本接近底部时自动跟随;向上阅读历史时不抢滚动位置。

  5. 如何避免旧请求的流式内容覆盖用户最新发起的会话?

    每次会话/请求分配版本 id 并取消旧流;收到片段时先核对当前 id,再写入对应消息,切换会话时清理 reader。

    参考代码 · js
    let version=0,controller;
    async function ask(prompt){const current=++version;controller?.abort();controller=new AbortController();const response=await fetch('/chat',{method:'POST',body:prompt,signal:controller.signal});if(current!==version)return;await readStream(response,text=>{if(current===version)append(text);});}

    结果:新会话取消旧请求,旧流片段不会覆盖当前内容。

  6. 工具调用或 Agent 运行较久时,如何展示进度、错误和可取消状态?

    明确“排队/运行/等待工具/完成/失败/已取消”状态,显示可理解的进度与重试入口;取消要传到底层并处理服务端仍在执行的情况。

    参考代码 · tsx
    function RunStatus({run,cancel}:{run:{state:'running'|'error'|'done';step?:string;error?:string};cancel:()=>void}){return <section aria-live="polite"><p>{run.state==='running'?`正在执行:${run.step}`:run.state==='error'?`失败:${run.error}`:'已完成'}</p>{run.state==='running'&&<button onClick={cancel}>取消</button>}</section>;}

    结果:显示步骤、错误或完成状态;运行中可取消。

  7. 如何审查 AI 生成代码的正确性、安全性与可维护性?

    先核对需求与数据边界,再审查输入校验、依赖、授权、错误处理与复杂度;运行测试和真实交互,对关键逻辑保留人工复核。

    参考代码 · js
    // 对 AI 生成的纯函数先做可复现断言,再做依赖和安全审查。
    const generated=(a,b)=>a+b;
    console.assert(generated(0,0)===0);
    console.assert(generated(-2,3)===1);

    结果:两个边界断言通过;不能据此证明所有输入或外部依赖安全。

  8. 你如何在实际项目中使用 AI 工具,并验证它产出的代码?

    记录具体任务、给 AI 的上下文范围、所采用与拒绝的建议,以及测试/审查证据;学习重点是如何验证,而非仅说“用 AI 提效”。

    参考代码 · js
    // 工作流:保留输入、候选补丁、检查结果和人工审查结论。
    const checks=['typecheck','unit','build'],results={typecheck:'passed',unit:'passed',build:'passed'};
    console.log(checks.every(name=>results[name]==='passed'));

    结果:`true`;真实 `results` 必须来自实际 CI,不可凭模型描述代替。

25 / 29

系统设计与项目深挖

8 题
  1. 如何设计一个高并发搜索建议框,并处理防抖、取消和结果乱序?

    输入防抖并设最小长度,取消旧请求或用版本号丢弃旧结果;缓存短期热门查询,限制服务端速率,完整处理键盘操作、加载和空态。

    参考代码 · js
    let timer,controller,version=0;
    function suggest(query){clearTimeout(timer);controller?.abort();const current=++version;timer=setTimeout(async()=>{controller=new AbortController();try{const r=await fetch(`/suggest?q=${encodeURIComponent(query)}`,{signal:controller.signal});if(current===version)render(await r.json());}catch(e){if(e.name!=='AbortError')showError(e);}},250);}

    结果:防抖、取消和版本校验共同避免旧结果覆盖新输入;`render/showError` 为界面函数。

  2. 如何设计后台系统的路由、按钮和数据权限?前后端职责如何划分?

    前端路由/按钮权限仅控制体验,后端对每个资源和操作强制鉴权;权限模型可用角色加资源动作,并避免把敏感数据先发给无权用户。

    参考代码 · js
    // 服务端必须独立校验;前端权限只影响展示。
    app.delete('/users/:id',authenticate,requirePermission('users:delete'),async(req,res)=>{await deleteUser(req.params.id);res.sendStatus(204);});

    结果:无权限请求在服务端被拒绝;按钮隐藏不能替代接口鉴权。

  3. 如何设计大型前端应用的组件边界和状态管理方案?

    按业务领域划分组件与状态,局部状态留局部,跨域状态经明确接口共享;区分服务端缓存数据与 UI 临时状态,防止全局 store 成为耦合中心。

    参考代码 · tsx
    function SearchPage(){const [query,setQuery]=React.useState('');return <><SearchBox value={query} onChange={setQuery}/><SearchResults query={query}/></>;}

    结果:只在最近共同父组件共享查询状态;跨页面共享再评估 Context 或状态库。

  4. 如何解释一次真实项目的性能瓶颈、改动、指标和取舍?

    用“场景与基线→证据→改动→前后指标→取舍”叙述真实案例,例如 p75 LCP 从多少降到多少;没有实测数据就如实说明。

    参考代码 · js
    const before={lcpP75:3200,inpP75:280},after={lcpP75:2400,inpP75:190};
    console.log(before.lcpP75-after.lcpP75,before.inpP75-after.inpP75);

    结果:样例改善 `800ms` 与 `90ms`;真实项目需填真实同口径数据并说明取舍。

  5. 如何设计稳定的文件上传体验,包括进度、失败恢复和重复提交?

    分片上传、进度计算、断点查询和幂等完成接口;网络失败可重试单片,重复提交用上传 id/幂等键防止重复文件,最终校验完整性。

    参考代码 · js
    async function upload(file,id,onProgress){const chunk=1024*1024;for(let start=0,i=0;start<file.size;start+=chunk,i++){const response=await fetch(`/upload/${id}/${i}`,{method:'PUT',body:file.slice(start,start+chunk)});if(!response.ok)throw Error(`failed part ${i}`);onProgress(Math.min(file.size,start+chunk)/file.size);}}

    结果:逐片上传并更新进度;服务端需要上传 id 幂等和最终校验。

  6. 如何设计实时聊天页面的消息顺序、断线重连和历史分页?

    消息用服务端序号或时间加稳定 id 排序,发送中临时 id 与回执对账;断线按游标补消息,历史分页向上插入并保持滚动位置。

    参考代码 · js
    const byId=new Map();
    function receive(message){byId.set(message.id,message);return [...byId.values()].sort((a,b)=>a.seq-b.seq);}

    结果:按服务端序号展示且重复消息不会重复;断线时用最后序号请求缺失消息。

  7. WebView 与原生页面通信时,如何设计 JS Bridge 的权限和错误处理?

    JS Bridge 采用白名单方法、版本化请求/响应 id、来源校验与权限控制;每次调用有超时和结构化错误,敏感能力必须由原生端复核授权。

    参考代码 · js
    const pending=new Map();let nextId=0;
    function callNative(method,payload){if(!['openCamera'].includes(method))return Promise.reject(Error('denied'));const id=++nextId;return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'));},5000);pending.set(id,{resolve,reject,timer});window.NativeBridge.postMessage(JSON.stringify({id,method,payload}));});}

    结果:只允许白名单方法,5 秒无回执时拒绝;原生侧仍须校验来源和权限。

  8. 当线上页面出现白屏时,你会如何定位、止损和复盘?

    先确认影响范围并回滚/切流止损,再查发布版本、静态资源加载、JS 错误、接口和 CDN;复现根因、补监控与回归测试,记录恢复时间和预防措施。

    参考代码 · js
    // 线上白屏排查用版本与资源加载证据定位,回滚命令按实际平台执行。
    window.addEventListener('error',e=>report({release:APP_RELEASE,url:e.filename,message:e.message}));

    结果:错误事件携带发布版本和资源 URL;止损后检查部署、CDN、接口,再复盘根因。

26 / 29

蜜雪冰城前端场景(公开面经线索)

4 题
  1. 点单小程序在低端设备上首屏慢、切换商品规格卡顿,如何定位并优化?

    先用真机性能面板区分网络、脚本和渲染耗时,并记录首屏与交互基线;按需分包、压缩图片、减少 setData 的字段和频次,对长列表按可见区域渲染。每次只改一个瓶颈,再用同设备、同网络和真实使用数据复测。

    参考代码 · js
    const page = {
      data: { selectedSku: 'A', quantity: 1 },
      setData(patch) { Object.assign(this.data, patch); console.log(JSON.stringify(patch)); }
    };
    const next = { selectedSku: 'A', quantity: 2 };
    const patch = Object.fromEntries(
      Object.entries(next).filter(([key, value]) => page.data[key] !== value)
    );
    if (Object.keys(patch).length) page.setData(patch);

    结果:只输出并更新 `{"quantity":2}`,避免把未变化的规格字段重复发送到视图层;真实优化仍需先测量。

  2. 点单小程序的商品规格、购物车和服务端价格分别应如何管理状态,避免跨页面数据不一致?

    规格选择留在当前页,购物车数量和条目由一个共享状态源维护,商品与价格以服务端响应为准;跨页返回时订阅或重新读取最新购物车快照。提交订单前由服务端重新计算价格和库存,前端缓存不能作为结算依据。

    参考代码 · js
    const cart = new Map();
    const listeners = new Set();
    const snapshot = () => [...cart].map(([skuId, quantity]) => ({ skuId, quantity }));
    function setQuantity(skuId, quantity) {
      if (!Number.isInteger(quantity) || quantity < 0) throw Error('invalid quantity');
      if (quantity === 0) cart.delete(skuId); else cart.set(skuId, quantity);
      for (const listener of listeners) listener(snapshot());
    }
    listeners.add(items => console.log(JSON.stringify(items)));
    setQuantity('ice-cream', 2);

    结果:输出 `[{"skuId":"ice-cream","quantity":2}]`;多个页面可订阅同一购物车状态,结算价格仍须由服务端核算。

  3. 促销期间订单和库存消息高频到达,如何避免每条消息都触发一次完整页面渲染?

    消息先按订单或商品 id 合并到最新状态,同一帧最多安排一次视图更新;对不可合并的事件保留顺序和确认机制。只更新受影响的组件,设置缓冲上限,并在积压时请求服务端快照校准,避免漏更新或无限占用内存。

    参考代码 · js
    const frames = [];
    const requestFrame = callback => frames.push(callback); // 示例中手动推进一帧
    const pendingChanges = new Map();
    let scheduled = false;
    function onStockMessage(message) {
      pendingChanges.set(message.id, message);
      if (scheduled) return;
      scheduled = true;
      requestFrame(() => {
        scheduled = false;
        const batch = [...pendingChanges.values()];
        pendingChanges.clear();
        console.log(JSON.stringify(batch));
      });
    }
    onStockMessage({ id: 1, stock: 9 });
    onStockMessage({ id: 1, stock: 8 });
    onStockMessage({ id: 2, stock: 3 });
    frames.shift()();

    结果:一帧只输出 `[{"id":1,"stock":8},{"id":2,"stock":3}]`;只适用于可用最新值覆盖旧值的状态消息,不可合并的订单事件必须逐条处理。

  4. 门店后台要展示数万条订单,如何实现流畅滚动,并处理动态行高与实时插入?

    使用虚拟列表只渲染视口及缓冲区,稳定 id 作为 key;固定行高可直接计算起止索引,动态行高需要测量、缓存高度和累计位置。向上插入历史订单时补偿滚动位置,实时新单仅在用户位于顶部时自动跟随;过滤与排序尽量由服务端分页处理。

    参考代码 · js
    function visibleRange(total, scrollTop, rowHeight, viewportHeight, overscan = 2) {
      const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
      const end = Math.min(total, Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan);
      return { start, end }; // end 为不包含的边界
    }
    const { start, end } = visibleRange(10000, 400, 40, 120);
    console.log(`${start}..${end - 1} 共${end - start}条`);

    结果:输出 `8..14 共7条`,只渲染这 7 条;动态行高需测量并维护位置索引,不能直接使用固定行高公式。

27 / 29

MySQL 查询与事务(近期面经方向)

6 题
  1. 订单列表按用户、状态筛选并按创建时间倒序分页,如何设计联合索引并用执行计划验证?

    等值筛选列放在索引前面,再接排序列和稳定的 id;例如 (user_id,status,created_at DESC,id DESC)。用 EXPLAIN 看实际选中的索引、扫描行数和排序方式,再用 EXPLAIN ANALYZE 比较实际耗时;索引是否划算还受选择性、返回列和写入成本影响。

    参考代码 · sql
    CREATE INDEX idx_orders_user_status_time
      ON orders(user_id, status, created_at DESC, id DESC);
    EXPLAIN SELECT id, created_at, total_amount
    FROM orders WHERE user_id = 7 AND status = 'paid'
    ORDER BY created_at DESC, id DESC LIMIT 20;

    结果:检查计划是否使用 `idx_orders_user_status_time`、扫描行数及是否出现额外排序;真实计划取决于表统计信息和数据分布,需在数据库运行验证。

  2. InnoDB 的可重复读如何处理普通查询?为什么 `SELECT ... FOR UPDATE` 可能看到不同的值?

    InnoDB 默认可重复读下,普通一致性查询通常复用事务首次一致性读建立的快照;锁定读是当前读,会等待并读取较新的已提交版本。不要把所有 SELECT 都说成读同一快照;还要区分隔离级别和事务是否已做过写入。

    参考代码 · sql
    -- 会话 A:InnoDB 默认 REPEATABLE READ
    START TRANSACTION;
    SELECT quantity FROM stock WHERE sku_id = 1; -- 假设读到 5,建立快照
    -- 会话 B 此时将 quantity 更新为 4 并 COMMIT
    SELECT quantity FROM stock WHERE sku_id = 1; -- 仍读快照中的 5
    SELECT quantity FROM stock WHERE sku_id = 1 FOR UPDATE; -- 当前读,可读到 4
    COMMIT;

    结果:普通一致性读与锁定读可能分别得到 `5` 和 `4`;需用两个真实连接按注释顺序演示,具体锁等待取决于并发时序。

  3. 高并发扣减库存时,如何用 SQL 避免超卖,并判断扣减是否成功?

    用单条条件更新 UPDATE stock SET quantity=quantity-? WHERE sku_id=? AND quantity>=?,依据受影响行数判断成功或库存不足;这一步是原子的。订单写入与库存扣减应在同一 MySQL 事务中,失败时回滚。

    参考代码 · sql
    START TRANSACTION;
    UPDATE stock SET quantity = quantity - 2
    WHERE sku_id = 1 AND quantity >= 2;
    SELECT ROW_COUNT() AS deducted;
    -- deducted 为 1 时写入订单并 COMMIT;为 0 时 ROLLBACK

    结果:库存充足且行存在时 `deducted=1`,不足时为 `0`;订单写入必须放在同一事务里。此处未连接数据库执行。

  4. 如何用稳定的排序键实现深分页,避免大 OFFSET 带来的扫描成本和翻页重复?

    用 (created_at,id) 作为总排序键,下一页带上上一页最后一条的两个值,用小于游标的条件继续查询;相应复合索引可避免跳过大量 OFFSET 行。实时新增/删除时还需说明快照一致性要求。

    参考代码 · sql
    CREATE INDEX idx_orders_time_id ON orders(created_at DESC, id DESC);
    SELECT id, created_at FROM orders
    WHERE created_at < '2026-10-01 10:00:00'
       OR (created_at = '2026-10-01 10:00:00' AND id < 501)
    ORDER BY created_at DESC, id DESC LIMIT 20;

    结果:取得给定游标之后最多 20 条订单;实际是否充分利用索引需用 `EXPLAIN` 核对,游标应来自上一页最后一条。

  5. 两个事务以相反顺序更新两行时为什么会死锁?应用应如何处理?

    两个事务若先后持有对方要更新的行锁,会形成等待环;数据库会选一个事务回滚。所有调用路径按固定顺序加锁,缩短事务,并只对死锁/可重试错误做有限次数的整事务重试,操作本身须幂等。

    参考代码 · sql
    -- 分别在两个连接执行,按 A1、B1、A2、B2 的顺序交错
    -- A1
    START TRANSACTION;
    UPDATE stock SET quantity=9 WHERE sku_id=1;
    -- B1(另一个连接)
    START TRANSACTION;
    UPDATE stock SET quantity=9 WHERE sku_id=2;
    -- A2
    UPDATE stock SET quantity=8 WHERE sku_id=2;
    -- B2
    UPDATE stock SET quantity=8 WHERE sku_id=1;

    结果:若两会话交错执行到第二次更新,可能形成死锁,InnoDB 回滚其中一个事务;应用应按固定行顺序加锁并对死锁做有限整事务重试。

  6. 重试下单请求时,如何用唯一约束保证订单只创建一次?

    给客户端请求 ID 建唯一索引,在同一事务中插入订单;重复键时读取已有订单并返回原结果,不重复扣库存。不要仅靠先查询再插入判断重复,因为并发请求会同时通过查询。

    参考代码 · sql
    CREATE TABLE orders (
      id BIGINT PRIMARY KEY AUTO_INCREMENT,
      request_id VARCHAR(64) NOT NULL UNIQUE,
      sku_id BIGINT NOT NULL
    ) ENGINE=InnoDB;
    START TRANSACTION;
    INSERT INTO orders(request_id, sku_id) VALUES ('req-001', 1);
    -- 仅首次插入成功时,继续执行同事务的库存扣减
    COMMIT;

    结果:相同 `request_id` 的第二次插入报重复键,业务回滚该次事务并读取已有订单;真实接口还需核对重复请求的参数是否一致。

28 / 29

Redis 缓存与并发(近期面经方向)

6 题
  1. MySQL 为准、Redis 做缓存时,更新数据和失效缓存的顺序如何设计?仍有哪些一致性窗口?

    常见 cache-aside 写法是先提交数据库,再删除缓存;读请求未命中时从数据库加载并回填,设置 TTL。删除失败或并发读在写入前回填旧值仍可能造成短暂不一致,应按业务需要重试/补偿或使用版本校验;强一致读直接走权威数据源。

    参考代码 · js
    async function updateProduct(db, redis, id, patch) {
      await db.updateProduct(id, patch); // 权威数据源先提交
      await redis.del(`product:${id}`); // 删除失败须记录并补偿
    }

    结果:后续缓存未命中时从 MySQL 重建;并发读回填旧值或删缓存失败仍会造成短暂不一致,不能声称强一致。示例依赖实际数据库与 Redis 客户端。

  2. 缓存穿透、击穿、雪崩分别是什么?应如何按场景处理?

    穿透是反复请求不存在的数据,可用参数校验、短 TTL 空值缓存或布隆过滤器;击穿是热点键过期后并发回源,可用单飞/互斥和预热;雪崩是大量键同时失效,可加 TTL 抖动、限流和降级。空值缓存也必须过期,以便新数据可见。

    参考代码 · js
    const ttlWithJitter = (baseSeconds, random = Math.random) =>
      baseSeconds + Math.floor(random() * 60);
    console.log(ttlWithJitter(300, () => 0.5));

    结果:输出 `330` 秒;TTL 抖动可分散集中失效,但热点击穿还需单飞/互斥,不存在的键还需短期空值缓存或过滤。

  3. 使用 Redis 实现分布式锁时,为什么需要过期时间、随机令牌和原子校验后释放?

    获取锁用 SET key token NX PX ttl,只在成功时进入临界区;释放时原子比较 token 后删除,防止误删别人的锁。租期过后旧持锁者仍可能执行,关键写入还需 fencing token 或数据库约束;锁不能替代业务幂等。

    参考代码 · redis
    SET lock:order:1 random-token NX PX 30000
    EVAL "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:order:1 random-token

    结果:首次获取成功返回 `OK`,持有相同 token 的释放返回 `1`;令牌不匹配返回 `0`。需在 Redis 实例运行,锁租期不保证业务写入仍有独占权。

  4. 如何用 Redis 原子地实现固定窗口限流?窗口边界有什么缺点?

    在 Lua 脚本中组合 INCR 和首次 EXPIRE,作为一次原子操作返回当前计数;超过阈值拒绝。固定窗口在相邻窗口交界可出现双倍突发,要求平滑限流时改用滑动窗口或令牌桶,并处理 key 的 TTL 与限流失败策略。

    参考代码 · redis
    EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; return n" 1 rate:user:7:minute:123 60

    结果:同窗口连续调用返回 `1、2、3…`;若限制为 2 次,第 3 次应拒绝。脚本保证计数与首次过期设置原子执行;固定窗口边界仍可能突发。

  5. Redis Pub/Sub 与 Streams 消费组在断线恢复和消息确认方面有什么区别?

    Pub/Sub 适合在线即时广播,订阅端离线时没有持久化待处理队列;Streams 可保留记录,并以消费组分发、XACK 确认和 pending 记录做失败恢复。消费处理仍要幂等,并设置保留与积压治理策略。

    参考代码 · redis
    XGROUP CREATE orders workers 0 MKSTREAM
    XADD orders * order_id 1001
    XREADGROUP GROUP workers consumer-1 COUNT 1 STREAMS orders >
    XACK orders workers <上一步返回的消息ID>

    结果:消费组读到订单消息并确认;未确认消息留在 pending 中,可检查并重新分配。命令需要 Redis 实例,重复处理必须由业务保证幂等。

  6. 如何用 Redis 数据结构维护实时排行榜?分数相同时如何定义稳定次序?

    用 Sorted Set 的 score 表示积分,ZINCRBY 更新,按倒序范围取榜单;相同 score 默认按成员字典序打破平局。若业务要求按达成时间或 ID 排序,应定义额外规则,而非依赖未声明的次序。

    参考代码 · redis
    ZADD leaderboard 100 user:1 80 user:2
    ZINCRBY leaderboard 30 user:2
    ZRANGE leaderboard 0 1 REV WITHSCORES

    结果:`user:2` 为 110 分、`user:1` 为 100 分,倒序返回两人;同分时需明确业务平局规则。命令需要 Redis 实例。

29 / 29

Node.js 服务端实践(近期面经方向)

6 题
  1. `process.nextTick`、Promise 微任务和定时器在 Node.js 中如何调度?为什么不能只背固定输出顺序?

    当前同步栈先执行;process.nextTick 队列通常先于 Promise 微任务,之后才进入事件循环阶段中的定时器等回调。setTimeout(0) 与 setImmediate 的相对顺序依上下文和运行时版本而变,不能把单次打印顺序当成通用保证;递归 nextTick 会饿死 I/O。

    参考代码 · js
    console.log('sync');
    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    setTimeout(() => console.log('timer'), 0);

    结果:从 Node.js 顶层脚本运行时依次输出 `sync`、`nextTick`、`promise`、`timer`;I/O 回调内和不同调度 API 的顺序不能套用这个单例。

  2. CPU 密集任务为什么会阻塞请求?什么情况下使用 `worker_threads`?

    JavaScript 主线程执行长时间同步计算时无法及时处理其他回调;CPU 密集且可独立拆分的计算可交给 Worker,并复用 Worker 池控制创建成本。异步 I/O 通常不用 Worker;任务取消、错误、超时和进程退出也需管理。

    参考代码 · js
    const { Worker } = require('node:worker_threads');
    const worker = new Worker(
      "const {parentPort}=require('node:worker_threads'); let a=0,b=1; for(let i=0;i<10;i++) [a,b]=[b,a+b]; parentPort.postMessage(a)",
      { eval: true }
    );
    worker.once('message', value => console.log(value));
    worker.once('error', error => { console.error(error); process.exitCode = 1; });

    结果:Worker 计算并输出 `55`,主线程无需运行该计算;生产环境应复用 Worker 池并处理超时与退出。

  3. 处理大文件或大响应时,如何用 Stream 和 `pipeline` 控制背压与错误传播?

    流分块处理避免一次性加载整个文件;pipeline 串联可读、转换和可写流,处理背压并把错误传给调用者。手工调用 write 时必须在返回 false 后等待 drain,同时限制输入大小和处理速率。

    参考代码 · js
    const { Readable, Writable } = require('node:stream');
    const { pipeline } = require('node:stream/promises');
    let output = '';
    pipeline(
      Readable.from(['a', 'b']),
      new Writable({ write(chunk, _, done) { output += chunk.toString(); done(); } })
    ).then(() => console.log(output)).catch(error => { console.error(error); process.exitCode = 1; });

    结果:输出 `ab`;`pipeline` 管理背压与流错误,真实大文件场景仍须限制输入大小和资源占用。

  4. 调用下游接口时,如何设置超时、取消请求并释放相关资源?

    用 AbortSignal.timeout 或 AbortController 为下游请求设置期限,失败时区分超时、用户取消和服务错误;对响应体进行消费或取消,并对重试设置总预算。超时只取消本端等待,不保证远端业务已停止,写操作需幂等键。

    参考代码 · js
    const { setTimeout: sleep } = require('node:timers/promises');
    sleep(100, undefined, { signal: AbortSignal.timeout(5) })
      .catch(error => console.log(error.name));

    结果:输出 `AbortError`;请求超时应取消本地操作并处理远端可能仍执行的情况。

  5. Node.js 服务收到退出信号后,如何停止接收新请求并等待已有请求结束?

    收到 SIGTERM 后先标记为不就绪,停止接收新连接,再等待已有请求与后台任务完成;设置最长宽限期并释放数据库/队列连接。不要在每次信号里直接 process.exit(),也不要无限等待卡住的连接。

    参考代码 · js
    const http = require('node:http');
    const server = http.createServer((_, response) => response.end('ok'));
    function shutdown() {
      server.close(error => {
        if (error) { console.error(error); process.exitCode = 1; return; }
        console.log('closed');
      });
    }
    process.once('SIGTERM', shutdown);
    server.once('error', error => { console.error(error); process.exitCode = 1; });
    server.listen(0, shutdown); // 示例直接触发;生产中只由退出信号触发

    结果:示例输出 `closed`;`server.close` 停止新连接并等待已接收请求完成,生产中还要设置宽限期并关闭其他资源。

  6. 如何在异步调用链中传递请求 ID,避免并发日志串线?

    每个请求创建唯一 ID,用 AsyncLocalStorage.run 将上下文带过 Promise/回调链,日志从当前 store 读取 ID。只记录必要元数据,避免把凭据或个人信息写入日志;跨进程调用仍需显式传递追踪标识。

    参考代码 · js
    const { AsyncLocalStorage } = require('node:async_hooks');
    const context = new AsyncLocalStorage();
    context.run({ requestId: 'req-1' }, async () => {
      await Promise.resolve();
      console.log(context.getStore().requestId);
    });

    结果:输出 `req-1`;并发请求应各自调用 `run`,跨服务仍要显式传递追踪 ID。