自学内容网 自学内容网

前端面试题-手写篇-万字长文!

1. 手写实现EventBus

实现一个简单的 EventBus(事件总线)可以让我们在不同的组件或模块之间进行事件驱动的通信。下面是一个用 JavaScript 手写实现 EventBus 的基本例子:

class EventBus {
  constructor() {
    this.events = {}; // 存储事件名与对应的监听器
  }

  // 注册事件监听器
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = []; // 如果事件尚不存在,初始化为空数组
    }
    this.events[event].push(listener); // 添加监听器
  }

  // 触发事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => {
        listener(...args); // 执行所有对应事件的监听器
      });
    }
  }

  // 注销事件监听器
  off(event, listener) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(
        existingListener => existingListener !== listener
      );
    }
  }

  // 只执行一次的事件监听器
  once(event, listener) {
    const onceListener = (...args) => {
      listener(...args);
      this.off(event, onceListener); // 触发后立即注销监听器
    };
    this.on(event, onceListener);
  }
}

// 使用示例

const bus = new EventBus();

// 注册事件监听器
bus.on('message', (data) => {
  console.log('Received message:', data);
});

// 触发事件
bus.emit('message', 'Hello, EventBus!'); // 输出: "Received message: Hello, EventBus!"

// 注销事件监听器
const messageHandler = (data) => {
  console.log('This will not be called');
};
bus.on('message', messageHandler);
bus.off('message', messageHandler);
bus.emit('message', 'Hello again!'); // 监听器已注销,不会输出

// 只触发一次的监听器
bus.once('message', (data) => {
  console.log('This will be logged once:', data);
});
bus.emit('message', 'Once only'); // 输出: "This will be logged once: Once only"
bus.emit('message', 'This will not be logged'); // 不再触发

说明:

  1. on(event, listener): 注册事件监听器。
  2. emit(event, ...args): 触发事件,传递参数给监听器。
  3. off(event, listener): 注销某个事件的特定监听器。
  4. once(event, listener): 只触发一次的事件监听器,执行后会自动注销。

这个简单的 EventBus 类实现了基本的事件发布与订阅功能,可以根据实际需要进行扩展或优化。

2. LocalStorage实现60s后自动删除

要使用 localStorage 在 60 秒后自动删除存储的数据,我们可以通过存储一个时间戳,然后在每次访问时检查当前时间与存储时间的差值。如果超过 60 秒,就删除该数据。

下面是一个简单的实现:

// 设置数据并记录当前时间
function setWithExpiry(key, value, ttl = 60000) { // ttl 默认为 60 秒
  const item = {
    value: value,
    expiry: new Date().getTime() + ttl, // 设置过期时间
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// 获取数据,检查是否过期
function getWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  
  // 如果不存在该 key,则返回 null
  if (!itemStr) {
    return null;
  }

  const item = JSON.parse(itemStr);
  const now = new Date().getTime();
  
  // 如果已过期,则删除并返回 null
  if (now > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }

  return item.value;
}

// 使用示例

// 设置一个带过期时间的数据
setWithExpiry('userToken', 'abcdef123456');

// 获取数据(在 60 秒内都有效)
const userToken = getWithExpiry('userToken');
console.log(userToken); // 输出 'abcdef123456',如果未过期

// 60 秒后再尝试获取
setTimeout(() => {
  const expiredToken = getWithExpiry('userToken');
  console.log(expiredToken); // 如果超过 60 秒,输出 null
}, 61000); // 延时 61 秒

说明:

  1. setWithExpiry(key, value, ttl)

    :

    • 将数据存入 localStorage,并存储当前时间加上过期时间(默认为 60 秒)。
  2. getWithExpiry(key)

    :

    • localStorage 获取数据,并检查是否过期。如果过期则删除数据并返回 null,否则返回数据的值。

优点:

  • 简单易用,适用于需要过期的数据存储。
  • 不需要使用 setInterval 或其他定时器,避免了性能问题。

这种方法利用了时间戳和过期机制,确保数据能在指定时间后自动删除。

3. 手写截流函数

function throttle(func, wait, options = { leading: true, trailing: true }) {
  let timeout = null;
  let lastExec = 0;

  return function (...args) {
    const now = Date.now();
    const remainingTime = wait - (now - lastExec);

    if (remainingTime <= 0) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      lastExec = now;
      if (options.leading) {
        func(...args);
      }
    } else if (options.trailing && !timeout) {
      timeout = setTimeout(() => {
        lastExec = Date.now();
        timeout = null;
        if (options.trailing) {
          func(...args);
        }
      }, remainingTime);
    }
  };
}

// 使用示例
const handleScroll = throttle(() => {
  console.log('Scroll event triggered');
}, 200, { leading: true, trailing: false });

window.addEventListener('scroll', handleScroll);

实现数组翻转

例如:
[
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9, 10, 11, 12],
]变成
[
  [4, 8, 12],
  [3, 7, 11],
  [2, 6, 10],
  [1, 5, 9]
]

要实现数组翻转,将二维数组按列翻转(即,第一列变为最后一行,第二列变为倒数第二行,依此类推),可以通过以下步骤实现:

解决思路:

  1. 理解二维数组的转置:将数组的每一列变成新的数组的行,可以通过转置操作来实现。
  2. 翻转每一行的顺序:在转置后,反转每一行的顺序。

代码实现:

function flipArray(arr) {
  const result = [];
  const rows = arr.length;
  const cols = arr[0].length;

  for (let col = cols - 1; col >= 0; col--) {
    const newRow = [];
    for (let row = 0; row < rows; row++) {
      newRow.push(arr[row][col]);
    }
    result.push(newRow);
  }

  return result;
}
const input = [
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9, 10, 11, 12],
];

const output = flipArray(input);
console.log(output);

输出结果:

[
  [4, 8, 12],
  [3, 7, 11],
  [2, 6, 10],
  [1, 5, 9]
]

代码解释:

  1. 获取数组的行数和列数numRowsnumCols 分别表示原始数组的行数和列数。
  2. 创建一个新的数组 result:这个数组将保存我们处理过后的结果。
  3. 外层循环:遍历原始数组的列(即每个索引为 col 的列),每一列将被转置为一个新的行。
  4. 内层循环:遍历每一行的当前列元素,按顺序将每行的元素从后往前存入新行数组 newRow
  5. 将新行添加到结果数组result.push(newRow)

总结:

通过这种方式,原始数组的列会变成反转后的行,最终实现了按列翻转的效果。

4. 以下代码输出顺序

console.log("start");
setTimeout(() => {
  console.log("timer1");
  new Promise(function (resolve) {
    console.log("promise start");
    resolve();
  }).then(() => {
    console.log("promise1");
  });
  console.log("timer1 end");
}, 0);

setTimeout(() => {
  console.log("timer2");
  Promise.resolve().then(() => {
    console.log("promise2");
  });
}, 0);
console.log("end");

答案:

start -> end -> timer1 -> promise start -> timer1 end -> promise1 -> timer2 -> promise2

给定一个字符串如abc 打印出所有的可能出现的顺序

要打印出字符串中所有可能出现的顺序(即字符串的所有排列),可以使用 回溯法 来实现。这是一个经典的递归问题,目标是生成字符串的所有排列。

解决思路:

  1. 递归:我们可以通过交换字符来生成所有排列。每次固定一个字符,递归地处理剩下的字符。
  2. 回溯:在递归的过程中,如果交换过字符,就需要恢复原状,以便尝试其他的排列。

代码实现:

function permute(str) {
  const result = [];
  const arr = str.split(''); // 将字符串转换为字符数组

  function backtrack(start) {
    if (start === arr.length) {
      result.push(arr.join('')); // 到达排列末尾,加入结果
      return;
    }

    for (let i = start; i < arr.length; i++) {
      // 交换当前字符和下一个字符
      [arr[start], arr[i]] = [arr[i], arr[start]];

      // 递归产生下一个排列
      backtrack(start + 1);

      // 回溯,恢复交换前的状态
      [arr[start], arr[i]] = [arr[i], arr[start]];
    }
  }

  backtrack(0); // 从第一个字符开始排列
  return result;
}

// 使用示例
const input = "abc";
const permutations = permute(input);
console.log(permutations);

输出:

[ 'abc', 'acb', 'bac', 'bca', 'cab', 'cba' ]

代码解析:

  1. permute(str):这个函数接受一个字符串 str,并生成其所有排列。首先将字符串转换成字符数组,以便后续操作。

  2. backtrack(start)

    :这是递归函数。

    start
    

    是当前要处理的字符索引,表示当前排列的起始位置。

    • 如果 start === arr.length,说明已经处理完所有字符,将当前排列(通过 arr.join(''))加入到结果数组中。
    • 对于每一个位置的字符,我们交换字符并递归处理下一个位置,然后进行回溯,即恢复交换前的状态,以便进行其他的尝试。
  3. [arr[start], arr[i]] = [arr[i], arr[start]];:这行代码是交换数组元素的方式,生成新的排列。

复杂度:

  • 时间复杂度:O(n!),因为对于一个长度为 n 的字符串,所有排列的数量是 n!
  • 空间复杂度:O(n),由于递归栈的深度为 n

注意事项:

  • 这个方法没有去重,所以如果字符串中有重复字符,它会生成重复的排列。如果需要去重,可以将结果存储在一个 Set 中,确保每个排列是唯一的:
function permute(str) {
  const result = new Set();
  const arr = str.split('');

  function backtrack(start) {
    if (start === arr.length) {
      result.add(arr.join('')); // 使用 Set 来去重
      return;
    }

    for (let i = start; i < arr.length; i++) {
      [arr[start], arr[i]] = [arr[i], arr[start]];
      backtrack(start + 1);
      [arr[start], arr[i]] = [arr[i], arr[start]];
    }
  }

  backtrack(0);
  return Array.from(result);
}

const input = "aab";
const permutations = permute(input);
console.log(permutations);

这样,Set 可以确保结果中没有重复的排列。

5. for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }); } 打印什么 为什么 能怎么解决

问题描述

你提供的代码是:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  });
}

预期输出

我们来分析一下这段代码的行为,特别是在 setTimeout 中使用 var 的情况下。首先,setTimeout 是异步执行的,它将回调函数排入任务队列,在当前执行栈空闲后才会执行。因此,代码的执行顺序和 setTimeout 的执行时机是关键。

执行过程

  1. 循环会首先执行并立即开始,i 的值从 0 递增到 2。
  2. 由于 var函数作用域(而不是块级作用域),每次循环中的 i 都是同一个变量。setTimeout 里的回调函数会在异步执行时访问这个变量。
  3. setTimeout 的回调函数最终执行时,i 已经变成了 3(因为循环结束时,i 已经增到 3)。
  4. 因此,console.log(i) 将打印三次 3

输出结果:

3
3
3

为什么会这样?

  • var 的作用域问题var 声明的变量是函数作用域或者全局作用域,而不是块级作用域。这意味着,i 在循环内的每次赋值,都会直接影响 i 的值。由于 setTimeout 是异步的,它不会立刻执行,而是等到整个循环执行完后才会执行回调。
  • 在循环结束后,i 的值变成了 3。所以所有的 setTimeout 回调函数访问的 i 都是最终的值 3,而不是每次循环时的值。

如何解决?

为了解决这个问题,确保每个 setTimeout 回调函数访问到不同的 i,你可以使用 let 替代 varlet 具有 块级作用域,它会为每次循环创建一个新的 i

修改后的代码:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  });
}

结果:

0
1
2

解释:

  • 使用 let 使得每次循环中的 i 都是 块级作用域,因此每个 setTimeout 的回调函数都有一个独立的 i
  • 在循环的每次迭代中,i 会被捕获并保存到回调函数中,因此异步执行时,setTimeout 能正确访问每次迭代时的 i

其他解决方案:

  1. 使用闭包: 如果你仍然希望使用 var,可以通过使用闭包来捕获当前 i 的值。在每次循环中传递当前的 isetTimeout

    for (var i = 0; i < 3; i++) {
      (function(i) {
        setTimeout(function() {
          console.log(i);
        });
      })(i);
    }
    

    在这个解决方案中,立即调用的函数表达式(IIFE)创建了一个新的作用域,使得每次循环中的 i 被传递到闭包中,从而确保回调函数可以正确访问当前的 i

  2. 使用 forEach(如果 i 是数组元素): 如果你使用的是数组,并且每次循环处理数组的元素,可以使用 forEach,它在每次迭代时提供了正确的作用域:

    [0, 1, 2].forEach(function(i) {
      setTimeout(function() {
        console.log(i);
      });
    });
    

    这时,forEach 会确保每个回调函数都有正确的作用域和变量 i

总结:

  • 使用 var 时,所有的 setTimeout 回调都会共享同一个 i 变量,因此打印的是最终的 i 值(即 3)。
  • 使用 let 可以确保每次循环创建新的作用域,解决了这个问题,输出正确的值(0, 1, 2)。
  • 另外,也可以使用闭包或其他方法来确保每次循环的值被捕获。

6. 写一个定时器,用了 setInterval,1000ms 轮询一次,在第 5 的时候会不会变成 6s ,如果有大量同步代码阻塞了会发生,怎么解决

问题分析

你提到的是使用 setInterval 每 1000 毫秒(1 秒)轮询一次,但是在第 5 次(即 5 秒后)可能会变成 6 秒,这个问题通常是因为 阻塞代码 的执行,导致定时器的回调函数没有按预期时间间隔触发。我们来详细分析这个问题的原因。

为什么会发生这种情况?

setInterval 的回调函数在 事件循环 中等待执行。当你设置了一个定时器,理论上它会每隔指定时间(例如 1000ms)执行一次回调。但是,如果在回调函数执行过程中有 大量同步代码 阻塞了 JavaScript 线程(如长时间运行的同步计算、网络请求等),那么 下一个回调就无法按时执行,可能会延迟执行。

这可能导致定时器的回调间隔不精确。比如:

  • 在第一次执行回调后,可能因为阻塞的代码,回调没有在 1000ms 后被执行。
  • 直到这个阻塞代码执行完,回调才会执行,这样就错过了定时器的精确间隔,导致第 5 次的定时器回调会变成第 6 秒。

举个例子:

let count = 0;
let intervalId = setInterval(() => {
  console.log(`Tick ${count++}`);

  // 模拟阻塞代码(例如长时间同步计算)
  if (count === 5) {
    let start = Date.now();
    while (Date.now() - start < 3000) {} // 阻塞 3 秒
  }

}, 1000);

运行时的情况:

  • setInterval 的第 5 次执行时,count === 5,代码进入了一个阻塞的 while 循环,导致 3 秒内无法执行任何其他的代码。
  • 由于阻塞,定时器的回调无法按照原定的 1000ms 时间间隔执行,导致总共的时间变长,第 5 次的回调会变成 6 秒之后才执行。

如何解决?

为了解决这个问题,有几种常见的方法:

1. 使用 setTimeout 替代 setInterval(递归定时器)

通过递归调用 setTimeout,我们可以避免 setInterval 的回调函数堆积问题。这种方式可以确保每次回调之间的时间间隔是准确的,而不会受到阻塞代码的影响。

let count = 0;

function recursiveInterval() {
  console.log(`Tick ${count++}`);

  // 模拟阻塞代码(例如长时间同步计算)
  if (count === 5) {
    let start = Date.now();
    while (Date.now() - start < 3000) {} // 阻塞 3 秒
  }

  // 递归调用 setTimeout,确保回调间隔是准确的
  setTimeout(recursiveInterval, 1000);
}

recursiveInterval();

解释

  • 每次回调完成后,setTimeout 会重新设置下一次回调。这样即使当前回调存在阻塞,下一次回调也会确保在指定时间后执行,而不是受前一次执行时长的影响。
  • 这避免了多个回调函数被堆积在事件队列中。
2. 减少阻塞代码的影响(避免同步阻塞)

最根本的解决方法是 避免长时间的同步阻塞代码。尽量将阻塞的代码改为异步执行,例如使用 setTimeoutPromiseasync/await 等方式。

比如,将 while 循环改为异步操作:

let count = 0;

function simulateAsyncTask() {
  return new Promise(resolve => {
    setTimeout(resolve, 3000); // 使用异步的 3 秒延迟
  });
}

async function interval() {
  console.log(`Tick ${count++}`);

  if (count === 5) {
    await simulateAsyncTask(); // 异步等待,避免阻塞
  }

  setTimeout(interval, 1000); // 确保定时器间隔为 1000ms
}

interval();

解释

  • 使用 awaitsimulateAsyncTask 异步执行,避免了阻塞主线程。setTimeout 仍然会准确地按照时间间隔执行,而不会被阻塞代码影响。
3. 使用 requestAnimationFramesetTimeout

对于需要精确计时的任务,或者需要进行动画更新的场景,可以使用 requestAnimationFrame(如果任务与动画帧有关)或递归的 setTimeout 来实现比 setInterval 更精确的定时。

let count = 0;

function animationFrameLoop() {
  console.log(`Tick ${count++}`);

  // 通过异步任务(比如 setTimeout)避免阻塞
  if (count === 5) {
    let start = Date.now();
    while (Date.now() - start < 3000) {} // 阻塞 3 秒
  }

  requestAnimationFrame(animationFrameLoop); // 下一帧继续
}

requestAnimationFrame(animationFrameLoop);

requestAnimationFrame 会在浏览器下一次重绘之前执行,因此适用于需要在特定时间精度下执行的任务。

总结:

  • setInterval 与阻塞代码setInterval 的回调函数受阻塞代码的影响,如果存在长时间的同步代码,可能会导致回调函数没有按预期时间间隔执行。
  • 解决方案:使用递归的 setTimeout 替代 setInterval 可以确保定时器的精确间隔,而不会因为阻塞代码而错过执行时间。
  • 最佳实践:尽量避免同步阻塞代码,改为使用异步操作(如 setTimeoutPromiseasync/await 等),以提高代码的可扩展性和执行的精度。

7. 用react实现一个倒计时器组件,使用用户传入的格式比如hh/mm/ss进行显示。

import React, { useState, useEffect } from 'react';

// 格式化倒计时的函数
const formatTime = (time, format) => {
  const hours = String(Math.floor(time / 3600)).padStart(2, '0');
  const minutes = String(Math.floor((time % 3600) / 60)).padStart(2, '0');
  const seconds = String(time % 60).padStart(2, '0');
  
  return format
    .replace('hh', hours)
    .replace('mm', minutes)
    .replace('ss', seconds);
};

const CountdownTimer = ({ totalSeconds, format = 'hh:mm:ss' }) => {
  const [remainingTime, setRemainingTime] = useState(totalSeconds);
  
  useEffect(() => {
    if (remainingTime <= 0) return;

    const intervalId = setInterval(() => {
      setRemainingTime(prevTime => {
        if (prevTime <= 1) {
          clearInterval(intervalId); // 清除定时器
          return 0;
        }
        return prevTime - 1;
      });
    }, 1000);

    return () => clearInterval(intervalId); // 清理副作用
  }, [remainingTime]);

  return <div>{formatTime(remainingTime, format)}</div>;
};

export default CountdownTimer;

8. 手写防抖函数

const debounce = (func, wait, leading) => {
  let timerId, result

  function debounced() {
    const context = this
    const args = arguments

    if (timerId) clearTimeout(timerId)
    if (leading === true) {
      if (!timerId) result = func.apply(context, args)

      timerId = setTimeout(() => {
        // 重置 timerId 的值。
        timerId = null
      }, wait)
    } else {
      timerId = setTimeout(() => {
        result = func.apply(context, args)
      }, wait)
    }
    return result
  }

  // 取消
  debounced.cancel = function() {
    clearTimeout(timerId)
    timerId = null
  }

  return debounced
}


// 测试:
function getCity(e) {
  console.log('鼠标移动了,事件对象:', e)
}
const container = document.getElementById('container')
const btnCancel = document.getElementById('container')
const handleMouseMove = debounce(getCity, 2000, true)
container.addEventListener('mousemove', handleMouseMove)

btnCancel.addEventListener('click', () => {
  handleMouseMove.cancel()
})

9. 手写bind

Function.prototype.myBind = function (ctx, ...args) {
  ctx = ctx === null || ctx === undefined ? globalThis : ctx
  const fn = this;
  return function (...restArgs) {
    if (new.target) {
      return new fn(...args, ...restArgs);
    }
    return fn.call(ctx, ...args, ...restArgs);
  }
}
const person1 = {
  name: 'person1',
}
const person2 = {
  name: 'person2',
  sayName: function (a, b, c) {
    console.log(this, ...arguments);
  }
}
const newFn = person2.sayName.bind(null, 1, 2) // global | Window 1 2 3 4
// const newFn = person2.sayName.bind(person1, 1, 2); // { name: 'person1' } 1 2 3 4
// const newFn = person2.sayName.myBind(person1, 1, 2); // { name: 'person1' } 1 2 3 4
newFn(3, 4);

10. 手写call

Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
  const fn = this;
  const key = Symbol();
  Object.defineProperty(ctx, key, {
    value: fn,
    enumerable: false,
  });
  const r = ctx[key](...args);
  delete ctx[key];
  return r;
};

function method(a, b) {
  console.log(a, b);
  console.log(this);
}
method.myCall({ name: "123" }, 1, 2); // 1 2 { name: "123" }

11. 手写apply

Function.prototype.myApply = function (ctx, args) {
  ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
  const fn = this;
  const key = Symbol();
  Object.defineProperty(ctx, key, {
    value: fn,
    enumerable: false,
  });
  const r = ctx[key](args);
  delete ctx[key];
  return r;
};

function method(a) {
  console.log(a);
  console.log(this.name);
}
method.myApply({ name: "123" }, [1, 2]); // [1, 2] 123

原文地址:https://blog.csdn.net/qq_44704740/article/details/145241167

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!