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

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

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

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

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

🔥请求并发控制

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

js 复制代码
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异步调度器

上一题的其中一个变化

js 复制代码
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()

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

js 复制代码
function Person() {
  this.queue = [];
  this.lock = false;
}

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

Person.prototype.sleep = function(time, flag) {
  this.queue.push(() => new Promise(resolve => {
    setTimeout(() => {
      console.log('sleep', flag);
      resolve();
    }, time * 1000)
  }));
  // this.run();
  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();

方法二

js 复制代码
class Lazy {
    // 函数调用记录,私有属性
    #cbs = [];
    constructor(num) {
        // 当前操作后的结果
        this.res = num;
    }

    // output时,执行,私有属性
    #add(num) {
        this.res += num;
        console.log(this.res);
    }

    // output时,执行,私有属性
    #multipy(num) {
        this.res *= num;
        console.log(this.res)
    }

    add(num) {

        // 往记录器里面添加一个add函数的操作记录
        // 为了实现lazy的效果,所以没有直接记录操作后的结果,而是记录了一个函数
        this.#cbs.push({
            type: 'function',
            params: num,
            fn: this.#add
        })
        return this;
    }
    multipy(num) {

        // 和add函数同理
        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) {

        // 增加delay的记录
        this.#cbs.push({
            type: 'delay',

            // 因为需要在output调用是再做到延迟time的效果,利用了Promise来实现
            fn: () => {
                return new Promise(resolve => {
                    console.log(`等待${time}ms`);
                    setTimeout(() => {
                        resolve();
                    }, time);
                })
            }
        })
        return this;
    }

    // 关键性函数,区分#cbs中每项的类型,然后执行不同的操作
    // 因为需要用到延迟的效果,使用了async/await,所以output的返回值会是promise对象,无法链式调用
    // 如果需实现output的链式调用,把for里面函数的调用全部放到promise.then的方式
    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();
            }
        }

        // 执行完成后清空 #cbs,下次再调用output的,只需再输出本轮的结果
        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);

🔥函数柯里化

毫无疑问,需要记忆

js 复制代码
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实现方式

js 复制代码
// es6实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

lazy-load实现

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

js 复制代码
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树

js 复制代码
// 样例数据
let demoNode = ({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        ({tagName: 'li', children: ['douyin']}),
        ({tagName: 'li', children: ['toutiao']})
    ]
});

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

html 复制代码
<ul class="list">
  <li>douyin</li>
  <li>toutiao</li>
</ul>

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

js 复制代码
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 缓存失效策略

js 复制代码
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);

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

js 复制代码
// 闭包
function once(fn) {
  let called = false;
  return function _once() {
    if (called) {
      return _once.value;
    }
    called = true;
    _once.value = fn.apply(this, arguments);
  }
}

//ES6 的元编程 Reflect API 将其定义为函数的行为
Reflect.defineProperty(Function.prototype, 'once', {
  value () {
    return once(this);
  },
  configurable: true,
})

LRU 算法实现

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

js 复制代码
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) {
      // map.keys() 会返回 Iterator 对象
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

🔥发布-订阅

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

js 复制代码
class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
      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)
  }
}

观察者模式

js 复制代码
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属性拦截

js 复制代码
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函数

js 复制代码
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];
      // 这个next是外部的回调
      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的情况下,一般出一些常见的场景题的可能性更大,所以最好理解➕记忆,最后欢迎评论区分享一些你遇到的题目

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

相关推荐
C_心欲无痕几秒前
react - useTransition标记低优先级更新
前端·react.js·前端框架
捻tua馔...3 分钟前
antd3的表单实现(HOC解决方案)
前端·javascript·react.js
支付宝体验科技5 分钟前
支付宝 KJS Compose 动态化方案与架构设计
前端·客户端
AllinLin15 分钟前
JS中的call apply bind全面解析
前端·javascript·vue.js
阿乐去买菜20 分钟前
2025 年末 TypeScript 趋势洞察:AI Agent 与 TS 7.0 的原生化革命
前端
POLITE321 分钟前
Leetcode 438. 找到字符串中所有字母异位词 JavaScript (Day 4)
javascript·算法·leetcode
创思通信22 分钟前
STM32F103C8T6采 DS18B20,通过A7680C 4G模块不断发送短信到手机
javascript·stm32·智能手机
海绵宝龙25 分钟前
Vue 中的 Diff 算法
前端·vue.js·算法
zhougl99626 分钟前
vue中App.vue和index.html冲突问题
javascript·vue.js·html
止观止27 分钟前
告别全局污染:深入理解 ES Modules 模块化与构建工具
javascript·webpack·vite·前端工程化·es modules