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

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

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

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

🔥请求并发控制

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

 1function getUrlByFetch() {
 2  let idx = maxLoad;
 3
 4  function getContention(index) {
 5    fetch(pics[index]).then(() => {
 6      idx++;
 7      if(idx < pics.length){
 8        getContention(idx);
 9      }
10    });
11  }
12  function start() {
13    for (let i = 0; i < maxLoad; i++) {
14      getContention(i);
15    }
16  }
17  start();
18}

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

上一题的其中一个变化

 1function taskPool() {
 2  this.tasks = [];
 3  this.pool = [];
 4  this.max = 2;
 5}
 6
 7taskPool.prototype.addTask = function(task) {
 8  this.tasks.push(task);
 9  this.run();
10}
11
12taskPool.prototype.run = function() {
13  if(this.tasks.length === 0) {
14    return;
15  }
16  let min = Math.min(this.tasks.length, this.max - this.pool.length);
17  for(let i = 0; i<min;i++) {
18    const currTask = this.tasks.shift();
19    this.pool.push(currTask);
20    currTask().finally(() => {
21      this.pool.splice(this.pool.indexOf(currTask), 1);
22      this.run();
23    })
24  }
25}

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

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

 1function Person() {
 2  this.queue = [];
 3  this.lock = false;
 4}
 5
 6Person.prototype.eat = function () {
 7  this.queue.push(() => new Promise(resolve => { console.log('eat'); resolve(); }));
 8  
 9  return this;
10}
11
12Person.prototype.sleep = function(time, flag) {
13  this.queue.push(() => new Promise(resolve => {
14    setTimeout(() => {
15      console.log('sleep', flag);
16      resolve();
17    }, time * 1000)
18  }));
19  
20  return this;
21}
22
23Person.prototype.run = async function() {
24  if(this.queue.length > 0 && !this.lock) {
25    this.lock = true;
26    const task = this.queue.shift();
27    await task();
28    this.lock = false;
29    this.run();
30  }
31}
32
33const person = new Person();
34person.eat().sleep(1, '1').eat().sleep(3, '2').eat().run();

方法二

 1class Lazy {
 2    
 3    #cbs = [];
 4    constructor(num) {
 5        
 6        this.res = num;
 7    }
 8
 9    
10    #add(num) {
11        this.res += num;
12        console.log(this.res);
13    }
14
15    
16    #multipy(num) {
17        this.res *= num;
18        console.log(this.res)
19    }
20
21    add(num) {
22
23        
24        
25        this.#cbs.push({
26            type: 'function',
27            params: num,
28            fn: this.#add
29        })
30        return this;
31    }
32    multipy(num) {
33
34        
35        this.#cbs.push({
36            type: 'function',
37            params: num,
38            fn: this.#multipy
39        })
40        return this;
41    }
42    top (fn) {
43
44        
45        this.#cbs.push({
46            type: 'callback',
47            fn: fn
48        })
49        return this;
50    }
51    delay (time) {
52
53        
54        this.#cbs.push({
55            type: 'delay',
56
57            
58            fn: () => {
59                return new Promise(resolve => {
60                    console.log(`等待${time}ms`);
61                    setTimeout(() => {
62                        resolve();
63                    }, time);
64                })
65            }
66        })
67        return this;
68    }
69
70    
71    
72    
73    async output() {
74        let cbs = this.#cbs;
75        for(let i = 0, l = cbs.length; i < l; i++) {
76            const cb = cbs[i];
77            let type = cb.type;
78            if (type === 'function') {
79                cb.fn.call(this, cb.params);
80            }
81            else if(type === 'callback') {
82                cb.fn.call(this, this.res);
83            }
84            else if(type === 'delay') {
85                await cb.fn();
86            }
87        }
88
89        
90        this.#cbs = [];
91    }
92}
93function lazy(num) {
94    return new Lazy(num);
95}
96
97const lazyFun = lazy(2).add(2).top(console.log).delay(1000).multipy(3)
98console.log('start');
99console.log('等待1000ms');
100setTimeout(() => {
101    lazyFun.output();
102}, 1000);

🔥函数柯里化

毫无疑问,需要记忆

 1function curry(fn, args) {
 2  let length = fn.length;
 3  args = args || [];
 4
 5  return function() {
 6    let subArgs = args.slice(0);
 7    subArgs = subArgs.concat(arguments);
 8    if(subArgs.length >= length) {
 9      return fn.apply(this, subArgs);
10    } else {
11      return curry.call(this, fn, subArgs);
12    }
13  }
14}
15
16
17function curry(func, arity = func.length) {
18  function generateCurried(preArgs) {
19    return function curried(nextArgs) {
20      const args = [...preArgs, ...nextArgs];
21      if(args.length >= arity) {
22        return func(...args);
23      } else {
24        return generateCurried(args);
25      }
26    }
27  }
28  return generateCurried([]);
29}

es6实现方式

 1
 2function curry(fn, ...args) {
 3  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
 4}
 5

lazy-load实现

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

 1const imgs = document.getElementsByTagName('img');
 2const viewHeight = window.innerHeight || document.documentElement.clientHeight;
 3
 4let num = 0;
 5
 6function lazyLoad() {
 7  for (let i = 0; i < imgs.length; i++) {
 8    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
 9    if(distance >= 0) {
10      imgs[i].src = imgs[i].getAttribute('data-src');
11      num = i+1;
12    }
13  }
14}
15window.addEventListener('scroll', lazyLoad, false);

实现简单的虚拟dom

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

 1
 2let demoNode = ({
 3    tagName: 'ul',
 4    props: {'class': 'list'},
 5    children: [
 6        ({tagName: 'li', children: ['douyin']}),
 7        ({tagName: 'li', children: ['toutiao']})
 8    ]
 9});

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

 1<ul class="list">
 2  <li>douyin</li>
 3  <li>toutiao</li>
 4</ul>

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

 1function Element({tagName, props, children}){
 2   
 3    if(!(this instanceof Element)){
 4        return new Element({tagName, props, children})
 5    }
 6    this.tagName = tagName;
 7    this.props = props || {};
 8    this.children = children || [];
 9}
10
11Element.prototype.render = function(){
12    var el = document.createElement(this.tagName),
13        props = this.props,
14        propName,
15        propValue;
16    for(propName in props){
17        propValue = props[propName];
18        el.setAttribute(propName, propValue);
19    }
20    this.children.forEach(function(child){
21        var childEl = null;
22        if(child instanceof Element){
23            childEl = child.render();
24        }else{
25            childEl = document.createTextNode(child);
26        }
27        el.appendChild(childEl);
28    });
29    return el;
30};
31
32
33var elem = Element({
34    tagName: 'ul',
35    props: {'class': 'list'},
36    children: [
37        Element({tagName: 'li', children: ['item1']}),
38        Element({tagName: 'li', children: ['item2']})
39    ]
40});
41document.querySelector('body').appendChild(elem.render());

实现SWR 机制

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

 1const cache = new Map();
 2
 3async function swr(cacheKey, fetcher, cacheTime) {
 4  let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
 5  cache.set(cacheKey, data);
 6  
 7  
 8  const isStaled = Date.now() - data.time > cacheTime;
 9  if (isStaled && !data.promise) {
10    data.promise = fetcher()
11      .then((val) => {
12        data.value = val;
13        data.time = Date.now();
14      })
15      .catch((err) => {
16        console.log(err);
17      })
18      .finally(() => {
19        data.promise = null;
20      });
21  }
22  
23  if (data.promise && !data.value) await data.promise;
24  return data.value;
25}
26
27const data = await fetcher();
28const data = await swr('cache-key', fetcher, 3000);

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

 1
 2function once(fn) {
 3  let called = false;
 4  return function _once() {
 5    if (called) {
 6      return _once.value;
 7    }
 8    called = true;
 9    _once.value = fn.apply(this, arguments);
10  }
11}
12
13
14Reflect.defineProperty(Function.prototype, 'once', {
15  value () {
16    return once(this);
17  },
18  configurable: true,
19})
20

LRU 算法实现

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

 1class LRUCahe {
 2  constructor(capacity) {
 3    this.cache = new Map();
 4    this.capacity = capacity;
 5  }
 6
 7  get(key) {
 8    if (this.cache.has(key)) {
 9      const temp = this.cache.get(key);
10      this.cache.delete(key);
11      this.cache.set(key, temp);
12      return temp;
13    }
14    return undefined;
15  }
16
17  set(key, value) {
18    if (this.cache.has(key)) {
19      this.cache.delete(key);
20    } else if (this.cache.size >= this.capacity) {
21      
22      this.cache.delete(this.cache.keys().next().value);
23    }
24    this.cache.set(key, value);
25  }
26}

🔥发布-订阅

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

 1class EventEmitter {
 2  constructor() {
 3    
 4    this.handlers = {}
 5  }
 6
 7  
 8  on(eventName, cb) {
 9    
10    if (!this.handlers[eventName]) {
11      
12      this.handlers[eventName] = []
13    }
14
15    
16    this.handlers[eventName].push(cb)
17  }
18
19  
20  emit(eventName, ...args) {
21    
22    if (this.handlers[eventName]) {
23      
24      const handlers = this.handlers[eventName].slice()
25      
26      handlers.forEach((callback) => {
27        callback(...args)
28      })
29    }
30  }
31
32  
33  off(eventName, cb) {
34    const callbacks = this.handlers[eventName]
35    const index = callbacks.indexOf(cb)
36    if (index !== -1) {
37      callbacks.splice(index, 1)
38    }
39  }
40
41  
42  once(eventName, cb) {
43    
44    const wrapper = (...args) => {
45      cb(...args)
46      this.off(eventName, wrapper)
47    }
48    this.on(eventName, wrapper)
49  }
50}

观察者模式

 1const queuedObservers = new Set();
 2
 3const observe = fn => queuedObservers.add(fn);
 4const observable = obj => new Proxy(obj, {set});
 5
 6function set(target, key, value, receiver) {
 7  const result = Reflect.set(target, key, value, receiver);
 8  queuedObservers.forEach(observer => observer());
 9  return result;
10}

单例模式

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

 1function getSingleInstance(func) {
 2  let instance;
 3  let handler = {
 4    construct(target, args) {
 5      if(!instance) instance = Reflect.construct(func, args);
 6      return instance;
 7    }
 8  }
 9  return new Proxy(func, handler);
10}
11

洋葱圈模型compose函数

 1function compose(middleware) {
 2  return function(context, next) {
 3    let index = -1;
 4    return dispatch(0);
 5    function dispatch(i) {
 6      
 7      if(i <= index) return Promise.reject(new Error('next() called multiple times'));
 8      
 9      index = i;
10      let fn = middle[i];
11      
12      if(i === middle.length) fn = next;
13      if(!fn) return Promsie.resolve();
14      try{
15        return Promise.resove(fn(context, dispatch.bind(null, i+1)));
16      }catch(err){
17        return Promise.reject(err);
18      }
19    }
20  }
21}

总结

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

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

个人笔记记录 2021 ~ 2025