前端面试题-手写篇-万字长文!
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'); // 不再触发
说明:
on(event, listener)
: 注册事件监听器。emit(event, ...args)
: 触发事件,传递参数给监听器。off(event, listener)
: 注销某个事件的特定监听器。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 秒
说明:
-
setWithExpiry(key, value, ttl)
:
- 将数据存入
localStorage
,并存储当前时间加上过期时间(默认为 60 秒)。
- 将数据存入
-
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]
]
要实现数组翻转,将二维数组按列翻转(即,第一列变为最后一行,第二列变为倒数第二行,依此类推),可以通过以下步骤实现:
解决思路:
- 理解二维数组的转置:将数组的每一列变成新的数组的行,可以通过转置操作来实现。
- 翻转每一行的顺序:在转置后,反转每一行的顺序。
代码实现:
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]
]
代码解释:
- 获取数组的行数和列数:
numRows
和numCols
分别表示原始数组的行数和列数。 - 创建一个新的数组
result
:这个数组将保存我们处理过后的结果。 - 外层循环:遍历原始数组的列(即每个索引为
col
的列),每一列将被转置为一个新的行。 - 内层循环:遍历每一行的当前列元素,按顺序将每行的元素从后往前存入新行数组
newRow
。 - 将新行添加到结果数组:
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 打印出所有的可能出现的顺序
要打印出字符串中所有可能出现的顺序(即字符串的所有排列),可以使用 回溯法 来实现。这是一个经典的递归问题,目标是生成字符串的所有排列。
解决思路:
- 递归:我们可以通过交换字符来生成所有排列。每次固定一个字符,递归地处理剩下的字符。
- 回溯:在递归的过程中,如果交换过字符,就需要恢复原状,以便尝试其他的排列。
代码实现:
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' ]
代码解析:
-
permute(str)
:这个函数接受一个字符串str
,并生成其所有排列。首先将字符串转换成字符数组,以便后续操作。 -
backtrack(start)
:这是递归函数。
start
是当前要处理的字符索引,表示当前排列的起始位置。
- 如果
start === arr.length
,说明已经处理完所有字符,将当前排列(通过arr.join('')
)加入到结果数组中。 - 对于每一个位置的字符,我们交换字符并递归处理下一个位置,然后进行回溯,即恢复交换前的状态,以便进行其他的尝试。
- 如果
-
[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
的执行时机是关键。
执行过程
- 循环会首先执行并立即开始,
i
的值从 0 递增到 2。 - 由于
var
是 函数作用域(而不是块级作用域),每次循环中的i
都是同一个变量。setTimeout
里的回调函数会在异步执行时访问这个变量。 - 当
setTimeout
的回调函数最终执行时,i
已经变成了 3(因为循环结束时,i
已经增到 3)。 - 因此,
console.log(i)
将打印三次3
。
输出结果:
3
3
3
为什么会这样?
var
的作用域问题:var
声明的变量是函数作用域或者全局作用域,而不是块级作用域。这意味着,i
在循环内的每次赋值,都会直接影响i
的值。由于setTimeout
是异步的,它不会立刻执行,而是等到整个循环执行完后才会执行回调。- 在循环结束后,
i
的值变成了 3。所以所有的setTimeout
回调函数访问的i
都是最终的值 3,而不是每次循环时的值。
如何解决?
为了解决这个问题,确保每个 setTimeout
回调函数访问到不同的 i
,你可以使用 let
替代 var
。let
具有 块级作用域,它会为每次循环创建一个新的 i
。
修改后的代码:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
});
}
结果:
0
1
2
解释:
- 使用
let
使得每次循环中的i
都是 块级作用域,因此每个setTimeout
的回调函数都有一个独立的i
。 - 在循环的每次迭代中,
i
会被捕获并保存到回调函数中,因此异步执行时,setTimeout
能正确访问每次迭代时的i
。
其他解决方案:
-
使用闭包: 如果你仍然希望使用
var
,可以通过使用闭包来捕获当前i
的值。在每次循环中传递当前的i
给setTimeout
:for (var i = 0; i < 3; i++) { (function(i) { setTimeout(function() { console.log(i); }); })(i); }
在这个解决方案中,立即调用的函数表达式(IIFE)创建了一个新的作用域,使得每次循环中的
i
被传递到闭包中,从而确保回调函数可以正确访问当前的i
。 -
使用
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. 减少阻塞代码的影响(避免同步阻塞)
最根本的解决方法是 避免长时间的同步阻塞代码。尽量将阻塞的代码改为异步执行,例如使用 setTimeout
、Promise
、async/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();
解释:
- 使用
await
让simulateAsyncTask
异步执行,避免了阻塞主线程。setTimeout
仍然会准确地按照时间间隔执行,而不会被阻塞代码影响。
3. 使用 requestAnimationFrame
或 setTimeout
对于需要精确计时的任务,或者需要进行动画更新的场景,可以使用 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
可以确保定时器的精确间隔,而不会因为阻塞代码而错过执行时间。 - 最佳实践:尽量避免同步阻塞代码,改为使用异步操作(如
setTimeout
、Promise
、async/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)!