盘点前端经典手写代码题

在前端面试过程中,免不了一些与业务相挂钩的手写题,这些题目有时候考得比算法还多,毕竟作为前端来说,接触算法还是相对少一些。最近在面试,也是梳理了一波这种题目。接下来我会盘点面试经常遇到的手写题以及解决思路,题目来自于各个网站,时间比较久远了忘记引用了哪些链接了。

梳理成了一版类似于问答的编程版本,感兴趣的可以给个star github.com/wuquan1995/...

1. 版本比较

样例输入:versions = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'] 期望输出:['0.1.1', '0.302.1', '2.3.3', '4.3.4.5', '4.3.5']

核心思路:使用sort方法对数组元素两两比较,回调函数里面主要要体现分位比较的过程。

JavaScript 复制代码
function compareVersion(arr){
   let copyArr = [...arr];
    copyArr.sort((a,b)=>{
      const tempA = a.split('.');
      const tempB = b.split('.');
      const maxLen = Math.max(tempA.length, tempB.length);
      for(let i = 0; i < maxLen;i++){
           const  targetA = +tempA[i] || 0;
           const  targetB = +tempB[i] || 0;
           if(targetA === targetB) continue
           return targetA - targetB
      }
      return 0;
   })
   return copyArr;
}

测试案例

ini 复制代码
let  versions =  ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
let copyArr = compareVersion(versions);
console.log("copyArr",copyArr);
console.log("versions",versions);

2. 父子菜单数据结构转换

2.1 树转数组

JavaScript 复制代码
// 输入
const tree = {
  id: 1,
  name: "部门A",
  children: [
    {
      id: 2,
      name: "部门B",
     }
   ]
}
 // 期望转换
[{id:1,name:'部门A',parentId:null}, {id:2,name:'部门B',parentId:1 } 

核心思路:利用层次遍历,在每一层不断收集该节点进入队列,并为后续的子节点补充上parentId

JavaScript 复制代码
function treeToArray(tree){
   let result = [];
   let queue = [];
   result.push({ id:tree.id,name:tree.name, parentId:0});
   queue.unshift({id:tree.id,name:tree.name,children:tree.children})
   while(queue.length > 0 ){
      const currentNode = queue.shift();
      if(currentNode.children.length > 0){
            for(let i = 0; i < currentNode.children.length;i++){
                const childNode =  currentNode.children[i];
                const {id ,name } = childNode;
                queue.unshift(childNode);
                result.push({
                    id,
                    name,
                    parentId:currentNode.id
                })
            }
      }
   }
   return result;
}

const tree = {
  id: 1,
  name: "部门A",
  children: [
    {
      id: 2,
      name: "部门B",
      children: [
        {
          id: 4,
          name: "部门D",
          children: [
            {
              id: 7,
              name: "部门G",
              children: [
              ],
            },
            {
              id: 8,
              name: "部门H",
              children: [
              ],
            },
          ],
        },
        {
          id: 5,
          name: "部门E",
          children: [
          ],
        },
      ],
    },
    {
      id: 3,
      name: "部门C",
      children: [
        {
          id: 6,
          name: "部门F",
          children: [
          ],
        },
      ],
    },
  ],
}
console.log(treeToArray(tree));

2.2 数组转树

预期效果即是将上面的输入输出翻转一下。

核心思路: 先用map收集一轮id与name的对应关系,后续采用"回溯"的思路为父节点添加子节点。

JavaScript 复制代码
function arrayToTree(arr){
  let map  = new Map()
  let result = null;
  for(let i = 0; i < arr.length; i++){
    const { id , name } = arr[i]
    const children = [];
    map.set( id, { id, name, children})
  }

  for(let  i = 0;  i < arr.length;i++){
    const { parentId , id } = arr[i]
    if(parentId !== 0){
        const parentNode = map.get(parentId);
        parentNode.children.push(map.get(id));
    }else{
        result = map.get(id);
    }
  }
 return result;
}

const arr = [
    {id:1, name: '部门A', parentId: 0},
    {id:2, name: '部门B', parentId: 1},
    {id:3, name: '部门C', parentId: 1},
    {id:4, name: '部门D', parentId: 2},
    {id:5, name: '部门E', parentId: 2},
    {id:6, name: '部门F', parentId: 3},
    {id:7, name: '部门G', parentId: 4},
    {id:8, name: '部门H', parentId: 4}
]
console.log(arrayToTree(arr))

3. 编译DOM

JavaScript 复制代码
const vDom = {
    tag: 'DIV',
    attrs:{
     id:'app'
    },
    children: [
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] }
        ]
      },
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] },
          { tag: 'A', children: [] }
        ]
      }
    ]
  };
  // 期望转换成实际的DOM结构
  console.log(_render(vDom))

核心思路:使用递归创建每个DOM节点及其子节点

JavaScript 复制代码
  // 真正的渲染函数
function _render(vnode) {
    // 如果是数字类型转化为字符串
    if (typeof vnode === "number") {
      vnode = String(vnode);
    }
    // 字符串类型直接就是文本节点
    if (typeof vnode === "string") {
      return document.createTextNode(vnode);
    }
    // 普通DOM
    const dom = document.createElement(vnode.tag);
    if (vnode.attrs) {
      // 遍历属性
      Object.keys(vnode.attrs).forEach((key) => {
        const value = vnode.attrs[key];
        dom.setAttribute(key, value);
      });
    }
    // 子数组进行递归操作 这一步是关键
    vnode.children.forEach((child) => dom.appendChild(_render(child)));
    return dom;
  }

const vDom = {
    tag: 'DIV',
    attrs:{
     id:'app'
    },
    children: [
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] }
        ]
      },
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] },
          { tag: 'A', children: [] }
        ]
      }
    ]
  };
  // 把上述虚拟Dom转化成下方真实Dom
 const realDom = `<div id="app">
    <span>
      <a></a>
    </span>
    <span>
      <a></a>
      <a></a>
    </span>
  </div>`;
  console.log(_render(vDom))

4. 一维数组展开

输入: let arr = [1, [2, [3, 4, 5]]]; 期望输出: [1, 2, 3, 4,5]

核心思路:递归判断每个数组元素本身是否为数组,并往结果里面添加单数组元素,也可以采用reduce去优化下写法。

JavaScript 复制代码
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
  let result = [];
  for(let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if(Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}
flatten(arr);  //  [1, 2, 3, 4,5]

5. 延迟输出结果

调用方式:(new LazyLog()).log(1).sleep(1000).log(2) 期望输出:先输出1,延迟1秒后输出2

核心思路:创建一个队列用于存储任务,后续从队列里面取出任务同步执行。

JavaScript 复制代码
class LazyLog {
    constructor(){
       this.queue = [] 
       setTimeout(()=>{
         this.run()
       },0)
    }
    async run(){
        for(let task of this.queue){
            await task();
        }
    }

    log(value){
        this.queue.push(()=>{
            console.log(value)
        })
        return this;
    }

    sleep(time){
        this.queue.push(()=>{
            return new Promise((resolve)=>{
                setTimeout(resolve,time)
            })
        })
        return this;
    }
}

console.log(new LazyLog().log(1).sleep(1000).log(2));

6. 模拟并发请求

模拟network面板,一次最大只能发N个请求,期望能使用以下案例跑通。

JavaScript 复制代码
 // 这个函数是模拟发送请求的,实际中你可能需要替换成真实的请求操作
  function sendRequest(url) {
    console.log(`Sending request to ${url}`);
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`Response received from ${url}`);
        resolve(`Result from ${url}`);
      }, Math.random() * 2000); // 随机延时以模拟请求处理时间
    });
  }
  
  // 使用 RequestQueue
  const requestQueue = new RequestQueue(3); // 假设我们限制最大并发数为3
  // 模拟批量请求
  const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6'];
  urls.forEach(url => {
    requestQueue.enqueue(url).then(result => {
      console.log(result);
    });
  });

核心思路:创建一个RequestQueue类,并创建一个存储任务的队列。每次进队列时将当前运作的请求数加1,如果当前的请求数小于最大可并发数,则执行任务。

JavaScript 复制代码
class RequestQueue {
    constructor(maxConcurrent) {
      this.maxConcurrent = maxConcurrent; // 设置最大并发数
      this.currentRunning = 0; // 当前正在运行的请求数
      this.queue = []; // 等待执行的请求队列
    }
  
    // 将请求封装成一个函数,推入队列,并尝试执行
    enqueue(url) {
      return new Promise((resolve, reject) => {
        const task = () => {
          // 当请求开始时,currentRunning 加 1
          this.currentRunning++;
          sendRequest(url).then(resolve).catch(reject).finally(() => {
            // 请求结束后,currentRunning 减 1,并尝试执行下一个请求
            this.currentRunning--;
            this.dequeue();
          });
        };
        this.queue.push(task);
        this.dequeue(); // 每次添加请求后尝试执行请求
      });
    }
  
    dequeue() {
      // 如果当前运行的请求小于最大并发数,并且队列中有待执行的请求
      if (this.currentRunning < this.maxConcurrent && this.queue.length) {
        // 从队列中取出一个请求并执行
        const task = this.queue.shift();
        task();
      }
    }
  }
  
  // 这个函数是模拟发送请求的,实际中你可能需要替换成真实的请求操作
  function sendRequest(url) {
    console.log(`Sending request to ${url}`);
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`Response received from ${url}`);
        resolve(`Result from ${url}`);
      }, Math.random() * 2000); // 随机延时以模拟请求处理时间
    });
  }
  
  // 使用 RequestQueue
  const requestQueue = new RequestQueue(3); // 假设我们限制最大并发数为3
  
  // 模拟批量请求
  const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6'];
  urls.forEach(url => {
    requestQueue.enqueue(url).then(result => {
      console.log(result);
    });
  });

7. 函数科里化

函数科里化的大名应该不用做过多解释了,其特点主要就是延迟调用。

JavaScript 复制代码
  // 使用示例
  function sum(a, b, c) {
    return a + b + c;
  }
  
  // 将 sum 函数柯里化
  const curriedSum = curry(sum);
  
  // 现在可以这样使用柯里化的函数
  const addOneAndTwo = curriedSum(1);
  const addThree = addOneAndTwo(2);
  const result = addThree(3); // 结果为 6
  console.log('result',result);

核心思路:创建一个工具函数(curry),不断收集传入进来的参数。当传入进来的参数与目标函数(sum)参数个数相等时,则执行目前函数。

JavaScript 复制代码
function curry(fn) {
    // 返回一个函数,这个函数接受一个参数
    return function curried(...args) {
      // 如果当前函数已经收集了足够的参数,就调用原函数
      if (args.length >= fn.length) {
        return fn.apply(this, args);
      } else {
        // 否则,返回一个新的函数,这个函数会接受下一个参数
        return function(...args2) {
          return curried.apply(this, args.concat(args2));
        };
      }
    };
  }

8. 选择排序

核心思路:选择排序比较简单,注意是用后面的数和每一轮的第一个数相比较,每一轮的第一个数会跟着下标的移动而移动。

JavaScript 复制代码
function selectSort(nums){
    // 冒泡排序是左右两个数相比较
  for(let i  = 0 ; i < nums.length; i++){
    for(let j = i + 1; j < nums.length;j++){
        if(nums[j] < nums[i]){
           [nums[i],nums[j]] = [nums[j],nums[i]];
        } 
    }

  }
return nums;
}
console.log(selectSort([2,8,6,4,5,7,1]))

9. 冒泡排序

核心思路:冒泡排序的每轮过程是左右两个数相比较,比较的过程中会随着下标的移动固定住末尾的数。

JavaScript 复制代码
function bubbleSort(nums){
    for(let i = 0;  i < nums.length; i++){
        for(let j = 0; j < nums.length - i -1 ; j++){
            if(nums[j+1] < nums[j]){
                [nums[j],nums[j+1]] = [nums[j+1],nums[j]]
            }
        }
    }
    return nums;
}

console.log(bubbleSort([2,8,6,4,5,7,1]))

10. 快速排序

可以查下快速排序的过程是什么样子的(最好是动画的比较具象一些),这里大致描述下过程。

核心思路 :在每一轮排序的过程中找到一个中间数作为基准,将数组分割成两边,小于这个基准的划分到左边为新数组,大于这个基准的划分到右边也为新数组。然后将左右两边被分割出来的新数组递归这个过程,注意递归要有出口,出口即是仅为一个数组元素无人可以比较时

快速排序比起选择排序和冒泡排序来说要相对复杂一些。

JavaScript 复制代码
function quickSort(nums){
    if(nums.length <=1){
        return nums;
    }
    let left = 0 , right = nums.length -1;
    let mid = (right + left) / 2;
    let leftArr = [], rightArr = [];
    const privot = nums.splice(mid,1)[0];
    for(let i = 0 ; i < nums.length; i++){
        if(nums[i] > privot){
            rightArr.push(nums[i])
        }else{
            leftArr.push(nums[i])
        }
    }
    return [...quickSort(leftArr),privot,...quickSort(rightArr)];
}

console.log(quickSort([2,8,6,4,5,7,1]))

11. 节流函数

节流函数也是前端手写的经典题目了,一般会有两种写法,这里只介绍根据时间来判断的写法。

核心思路:记录下上一次实际触发函数的时机,如果下次触发的函数时机超出了delay则执行,否则则不执行

JavaScript 复制代码
function throttle(fn, delay) {
  let curDate = 0;
  return function (...args) {
    const context = this;
    if (Date.now() - curDate > delay) {
      curDate = Date.now();
      fn.apply(this, context);
    }
  };
}

function testThrottle() {
  let count = 0;
  const throttledFunction = throttle(function () {
    count++;
    console.log("count", count);
  }, 1000);

  for (let i = 0; i < 10; i++) {
    throttledFunction();
  }

  setTimeout(() => {
    console.log("Count after 3000ms:", count); // 应该接近1,因为1秒内只执行一次
  }, 3000);
}

12. 防抖函数

防抖函数也是个经典手写题目,下面提供的是一个可以立即执行的防抖函数。

核心思路:防抖与节流最大的区别在同一段时间触发时,防抖会以最后一次作为结果,节流则是会以第一次作为结果。

JavaScript 复制代码
function debounce(fn, wait, immediate = false){
    let timer = null;
    return function(...args){
        let that = this;
        if(immediate && !timer){
           fn.apply(that,args)
        }else if(timer){
            clearTimeout(timer);
            timer = null;
            timer = setTimeout(()=>{
                fn.apply(that,args)
              },wait)
        }else{
            timer = setTimeout(()=>{
                fn.apply(that,args)
              },wait)
        }
    }
}

function testDebounceWithConsecutiveCalls() {
    let count = 0;
    const increment = () => count++;
    const debouncedIncrement = debounce(increment, 500);

    console.log('Test 2 start: count should be 0', count); // 初始值应为0

    // 在防抖时间内连续调用
    debouncedIncrement();
    setTimeout(debouncedIncrement, 300);
    setTimeout(debouncedIncrement, 600);

    // 等待足够长的时间以确保所有调用都被处理
    setTimeout(() => {
        console.log('Test 2 after wait: count should be 1', count); // 等待后应为1
    }, 1500);
}
testDebounceWithConsecutiveCalls();

13. 事件发布订阅

核心思路:需要实现一个函数负责订阅(on),监听函数负责收集回调函数。一个函数负责发布(emit),发布的过程即是取出该事件相关的订阅函数执行。

JavaScript 复制代码
class EventEmitter {
    constructor() {
        this.listeners = {};
    }

    on(type, callback) {
        if (!this.listeners[type]) {
            this.listeners[type] = [];
        }
        this.listeners[type].push(callback);
    }

    emit(type, ...args) {
        if (this.listeners[type]) {
            this.listeners[type].forEach(callback => {
                callback(...args);
            });
        }
    }
}

// 使用示例
const emitter = new EventEmitter();

// 注册事件监听器
emitter.on('event', (...args) => {
    console.log(...args);
});

// 触发事件
emitter.emit('event', ['Hello', 'World']);

14. 手写Promise

这个应该是很多人的噩梦了,而且我也觉得这个题目不是很必要,惯例给出一版网上的代码,具体思路看注释吧。

JavaScript 复制代码
class MyPromise {
  constructor(executor) {
    this.status = "pending";
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === "pending") {
        this.status = "fulfilled";
        this.value = value;
        this.onResolvedCallbacks.forEach((callback) => callback());
      }
    };

    const reject = (reason) => {
      if (this.status === "pending") {
        this.status = "rejected";
        this.reason = reason;
        this.onRejectedCallbacks.forEach((callback) => callback());
      }
    };

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

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected = typeof onRejected === "function" ? onRejected : (reason) => {
      throw reason;
    };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.status === "fulfilled") {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.status === "rejected") {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.status === "pending") {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });

    return promise2;
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach((promise) => {
        promise.then(resolve, reject);
      });
    });
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let resolvedCount = 0;

      promises.forEach((promise, index) => {
        promise.then((value) => {
          results[index] = value;
          resolvedCount++;

          if (resolvedCount === promises.length) {
            resolve(results);
          }
        }, reject);
      });
    });
  }
}

// resolvePromise函数,用于处理then方法中的返回值
function resolvePromise(promise2, x, resolve, reject) {
  // 如果promise2和x相等,则抛出循环引用的错误
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise"));
  }

  let called;
  // 如果x不是null且x是一个对象或函数
  if (x != null && (typeof x === "object" || typeof x === "function")) {
    try {
      // 获取x的then属性
      let then = x.then;
      // 如果then是一个函数
      if (typeof then === "function") {
        // 调用then函数,并传入x作为this,onFulfilled和onRejected作为参数
        then.call(
          x,
          (y) => {
            // 如果called为true,则直接返回
            if (called) return;
            called = true;
            // 递归调用resolvePromise函数,处理y的值
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) => {
            // 如果called为true,则直接返回
            if (called) return;
            called = true;
            // 如果onFulfilled函数抛出异常,则调用reject函数
            reject(r);
          }
        );
      } else {
        // 如果then不是一个函数,则直接resolve(x)
        resolve(x);
      }
    } catch (error) {
      // 如果called为true,则直接返回
      if (called) return;
      called = true;
      // 如果onFulfilled函数抛出异常,则调用reject函数
      reject(error);
    }
  } else {
    // 如果x不是对象或函数,则直接resolve(x)
    resolve(x);
  }
}

// 测试用例
const promise1 = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve("promise1");
  }, 1000);
});

const promise2 = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve("promise2");
  }, 2000);
});

const promise3 = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve("promise3");
  }, 3000);
});

MyPromise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // ["promise1", "promise2", "promise3"]
});

MyPromise.race([promise1, promise2, promise3]).then((value) => {
  console.log(value); // "promise1"
});
相关推荐
南方kenny几秒前
用HTML+CSS+JS复刻了水果忍者——Vibe Coding活动摸鱼实录
前端·aigc·vibecoding
&白帝&5 分钟前
vue中常用的api($set,$delete,$nextTick..)
前端·javascript·vue.js
要加油哦~10 分钟前
vue | async-validator 表单验证库 第三方库安装与使用
前端·javascript·vue.js
阿酷tony23 分钟前
视频点播web端AI智能大纲(自动生成视频内容大纲)的代码与演示
前端·人工智能·视频ai·视频智能大纲·ai智能大纲
小李小李不讲道理33 分钟前
「Ant Design 组件库探索」三:Select组件
前端·javascript·react.js
二闹33 分钟前
TypeScript核心玩法,顺便附赠面试通关秘籍!
前端·typescript
诗和远方149395623273436 分钟前
KSCrash中僵尸对象监控原理与实现
前端
XXXFIRE36 分钟前
前端必学:💻Mac + Nginx 部署 Vue3 静态项目
运维·前端
aiweker38 分钟前
python web开发-Flask 重定向与URL生成完全指南
前端·python·flask
Onion38 分钟前
CodeWave集成wujie微前端路由跳转思路
前端