首页/文章/八股文

前端面试必须掌握的手写题:进阶篇

2023-11-30
9274 分钟
...

本文是前端面试必须掌握的手写题系列的最后一篇,这个系列几乎将我整理和遇到的题目都包含到了,这里还是想强调一下,对于特别常见的题目最好能“背”下来,不要眼高手低,在面试的时候不需要再进行推导分析直接一把梭,后续会整理分享一些其他的信息,希望对你能有所帮助

前端面试必须掌握的手写题:基础篇

前端面试必须掌握的手写题:场景篇

前端面试必须掌握的手写题:进阶篇

🔥请求并发控制

多次遇到的题目,而且有很多变种,主要就是同步改异步

function getUrlByFetch() {
  let idx = maxLoad;

  function getContention(index) {
    fetch(pics[index]).then(() => {
      idx++;
      if(idx < pics.length){
        getContention(idx);
      }
    });
  }
  function start() {
    for (let i = 0; i < maxLoad; i++) {
      getContention(i);
    }
  }
  start();
}

🔥带并发限制的promise异步调度器

上一题的其中一个变化

function taskPool() {
  this.tasks = [];
  this.pool = [];
  this.max = 2;
}

taskPool.prototype.addTask = function(task) {
  this.tasks.push(task);
  this.run();
}

taskPool.prototype.run = function() {
  if(this.tasks.length === 0) {
    return;
  }
  let min = Math.min(this.tasks.length, this.max - this.pool.length);
  for(let i = 0; i<min;i++) {
    const currTask = this.tasks.shift();
    this.pool.push(currTask);
    currTask().finally(() => {
      this.pool.splice(this.pool.indexOf(currTask), 1);
      this.run();
    })
  }
}

🔥🔥🔥实现lazy链式调用: person.eat().sleep(2).eat()

解法其实就是将所有的任务异步化,然后存到一个任务队列里

function Person() {
  this.queue = [];
  this.lock = false;
}

Person.prototype.eat = function () {
  this.queue.push(() => new Promise(resolve => { console.log('eat'); resolve(); }));
  
  return this;
}

Person.prototype.sleep = function(time, flag) {
  this.queue.push(() => new Promise(resolve => {
    setTimeout(() => {
      console.log('sleep', flag);
      resolve();
    }, time * 1000)
  }));
  
  return this;
}

Person.prototype.run = async function() {
  if(this.queue.length > 0 && !this.lock) {
    this.lock = true;
    const task = this.queue.shift();
    await task();
    this.lock = false;
    this.run();
  }
}

const person = new Person();
person.eat().sleep(1, '1').eat().sleep(3, '2').eat().run();

方法二

class Lazy {
    
    #cbs = [];
    constructor(num) {
        
        this.res = num;
    }

    
    #add(num) {
        this.res += num;
        console.log(this.res);
    }

    
    #multipy(num) {
        this.res *= num;
        console.log(this.res)
    }

    add(num) {

        
        
        this.#cbs.push({
            type: 'function',
            params: num,
            fn: this.#add
        })
        return this;
    }
    multipy(num) {

        
        this.#cbs.push({
            type: 'function',
            params: num,
            fn: this.#multipy
        })
        return this;
    }
    top (fn) {

        
        this.#cbs.push({
            type: 'callback',
            fn: fn
        })
        return this;
    }
    delay (time) {

        
        this.#cbs.push({
            type: 'delay',

            
            fn: () => {
                return new Promise(resolve => {
                    console.log(`等待${time}ms`);
                    setTimeout(() => {
                        resolve();
                    }, time);
                })
            }
        })
        return this;
    }

    
    
    
    async output() {
        let cbs = this.#cbs;
        for(let i = 0, l = cbs.length; i < l; i++) {
            const cb = cbs[i];
            let type = cb.type;
            if (type === 'function') {
                cb.fn.call(this, cb.params);
            }
            else if(type === 'callback') {
                cb.fn.call(this, this.res);
            }
            else if(type === 'delay') {
                await cb.fn();
            }
        }

        
        this.#cbs = [];
    }
}
function lazy(num) {
    return new Lazy(num);
}

const lazyFun = lazy(2).add(2).top(console.log).delay(1000).multipy(3)
console.log('start');
console.log('等待1000ms');
setTimeout(() => {
    lazyFun.output();
}, 1000);

🔥函数柯里化

毫无疑问,需要记忆

function curry(fn, args) {
  let length = fn.length;
  args = args || [];

  return function() {
    let subArgs = args.slice(0);
    subArgs = subArgs.concat(arguments);
    if(subArgs.length >= length) {
      return fn.apply(this, subArgs);
    } else {
      return curry.call(this, fn, subArgs);
    }
  }
}


function curry(func, arity = func.length) {
  function generateCurried(preArgs) {
    return function curried(nextArgs) {
      const args = [...preArgs, ...nextArgs];
      if(args.length >= arity) {
        return func(...args);
      } else {
        return generateCurried(args);
      }
    }
  }
  return generateCurried([]);
}

es6实现方式


function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

lazy-load实现

img标签默认支持懒加载只需要添加属性 loading="lazy",然后如果不用这个属性,想通过事件监听的方式来实现的话,也可以使用IntersectionObserver来实现,性能上会比监听scroll好很多

const imgs = document.getElementsByTagName('img');
const viewHeight = window.innerHeight || document.documentElement.clientHeight;

let num = 0;

function lazyLoad() {
  for (let i = 0; i < imgs.length; i++) {
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    if(distance >= 0) {
      imgs[i].src = imgs[i].getAttribute('data-src');
      num = i+1;
    }
  }
}
window.addEventListener('scroll', lazyLoad, false);

实现简单的虚拟dom

给出如下虚拟dom的数据结构,如何实现简单的虚拟dom,渲染到目标dom树


let demoNode = ({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        ({tagName: 'li', children: ['douyin']}),
        ({tagName: 'li', children: ['toutiao']})
    ]
});

构建一个render函数,将demoNode对象渲染为以下dom

<ul class="list">
  <li>douyin</li>
  <li>toutiao</li>
</ul>

通过遍历,逐个节点地创建真实DOM节点

function Element({tagName, props, children}){
   
    if(!(this instanceof Element)){
        return new Element({tagName, props, children})
    }
    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
}

Element.prototype.render = function(){
    var el = document.createElement(this.tagName),
        props = this.props,
        propName,
        propValue;
    for(propName in props){
        propValue = props[propName];
        el.setAttribute(propName, propValue);
    }
    this.children.forEach(function(child){
        var childEl = null;
        if(child instanceof Element){
            childEl = child.render();
        }else{
            childEl = document.createTextNode(child);
        }
        el.appendChild(childEl);
    });
    return el;
};


var elem = Element({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        Element({tagName: 'li', children: ['item1']}),
        Element({tagName: 'li', children: ['item2']})
    ]
});
document.querySelector('body').appendChild(elem.render());

实现SWR 机制

SWR 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略

const cache = new Map();

async function swr(cacheKey, fetcher, cacheTime) {
  let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
  cache.set(cacheKey, data);
  
  
  const isStaled = Date.now() - data.time > cacheTime;
  if (isStaled && !data.promise) {
    data.promise = fetcher()
      .then((val) => {
        data.value = val;
        data.time = Date.now();
      })
      .catch((err) => {
        console.log(err);
      })
      .finally(() => {
        data.promise = null;
      });
  }
  
  if (data.promise && !data.value) await data.promise;
  return data.value;
}

const data = await fetcher();
const data = await swr('cache-key', fetcher, 3000);

实现一个只执行一次的函数


function once(fn) {
  let called = false;
  return function _once() {
    if (called) {
      return _once.value;
    }
    called = true;
    _once.value = fn.apply(this, arguments);
  }
}


Reflect.defineProperty(Function.prototype, 'once', {
  value () {
    return once(this);
  },
  configurable: true,
})

LRU 算法实现

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

class LRUCahe {
  constructor(capacity) {
    this.cache = new Map();
    this.capacity = capacity;
  }

  get(key) {
    if (this.cache.has(key)) {
      const temp = this.cache.get(key);
      this.cache.delete(key);
      this.cache.set(key, temp);
      return temp;
    }
    return undefined;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

🔥发布-订阅

发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式

class EventEmitter {
  constructor() {
    
    this.handlers = {}
  }

  
  on(eventName, cb) {
    
    if (!this.handlers[eventName]) {
      
      this.handlers[eventName] = []
    }

    
    this.handlers[eventName].push(cb)
  }

  
  emit(eventName, ...args) {
    
    if (this.handlers[eventName]) {
      
      const handlers = this.handlers[eventName].slice()
      
      handlers.forEach((callback) => {
        callback(...args)
      })
    }
  }

  
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  
  once(eventName, cb) {
    
    const wrapper = (...args) => {
      cb(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}

观察者模式

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

单例模式

核心要点: 用闭包和Proxy属性拦截

function getSingleInstance(func) {
  let instance;
  let handler = {
    construct(target, args) {
      if(!instance) instance = Reflect.construct(func, args);
      return instance;
    }
  }
  return new Proxy(func, handler);
}

洋葱圈模型compose函数

function compose(middleware) {
  return function(context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      
      if(i <= index) return Promise.reject(new Error('next() called multiple times'));
      
      index = i;
      let fn = middle[i];
      
      if(i === middle.length) fn = next;
      if(!fn) return Promsie.resolve();
      try{
        return Promise.resove(fn(context, dispatch.bind(null, i+1)));
      }catch(err){
        return Promise.reject(err);
      }
    }
  }
}

总结

当你看到这里的时候,几乎前端面试中常见的手写题目基本都覆盖到了,对于社招的场景下,其实手写题的题目是越来越务实的,尤其是真的有hc的情况下,一般出一些常见的场景题的可能性更大,所以最好理解➕记忆,最后欢迎评论区分享一些你遇到的题目

至此,手写题系列分享结束,希望对你有所帮助

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-08-27
定时器按顺序播放多个音频,切后台音频会乱
本身不会补触发,但 它会被系统级冻结 ——iOS Safari 后台完全停止计时,切回前台后 只补执行一次 (不是不补,而是"缺失的中间状态补不上")。间隔短的不会出大问题, 间隔长的会出现"音频流被压缩" ——切回来后本该播 5 分钟的间隙,实际只过了 3 分钟,结果整段对不上。 核心原则 :定时器只能用来"提醒一次", 真正决定"该不该播"的是绝对时间戳 。 setTimeout 在后台会发生什么(按平台) | 平台 | 后台行为...
面试
八股文2026-08-27
线上项目白屏的原因
白屏的本质是 渲染管线某一环断了 ——可能是资源、JS、接口、路由、样式、兼容性任一环节出问题。排查按"控制台 → 网络 → DOM → 环境"四步定位。 本质(前端类比) 把网页想成一栋楼: 白屏 ≠ 一定是同一种原因——这是面试想听的层次。 六大类原因(按出现频率) | 类别 | 典型表现 | 真实案例 | | : | : | : | | ① 资源加载失败 | DOM 是空的 | 入口 JS 404、CDN 挂了、CSS 阻塞 |...
面试
八股文2026-08-27
背景图就是 1MB 大图,怎么优化
1MB 大背景图优化分 三步 :压缩体积(10 30x)、按需加载(按设备/视口/网速)、渲染期优化(GPU 合成)。背景图跟 不同——它是 CSS,不走浏览器的原生懒加载机制,得手动处理。 背景图 vs 的关键区别(先讲清楚这个) 这是面试官想听的"针对性认知"——背景图不是普通图片,不能套通用方案。 三步优化(按优先级) 第 1 步:压缩体积(最重要,立竿见影) 1MB 的来源一般是这几种 ,对应解决方案: | 原始问题 | 体积来...
面试
八股文2026-08-27
项目里很多图片和视频,怎么优化
图片视频优化分 四层 :网络层(CDN/格式)、加载层(懒加载/预加载)、渲染层(解码/缓存)、业务层(按需/降级)。面试要把这四层都讲到位才算有体系。 四层优化模型 第 1 层:网络层(省钱、省时间) 核心目标:让资源体积小、让用户拿到资源快 | 手段 | 作用 | 关键点 | | : | : | : | | 图片格式 | WebP/AVIF 比 JPEG 小 25 50% | 兼容 fallback | | 视频格式 | H.265...
面试
八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试

评论

请登录后发表评论

去登录
加载评论中...

目录