实现一个简单的 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
类实现了基本的事件发布与订阅功能,可以根据实际需要进行扩展或优化。
要使用 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
或其他定时器,避免了性能问题。这种方法利用了时间戳和过期机制,确保数据能在指定时间后自动删除。
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)
。通过这种方式,原始数组的列会变成反转后的行,最终实现了按列翻转的效果。
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
要打印出字符串中所有可能出现的顺序(即字符串的所有排列),可以使用 回溯法 来实现。这是一个经典的递归问题,目标是生成字符串的所有排列。
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
可以确保结果中没有重复的排列。
你提供的代码是:
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
)。你提到的是使用 setInterval
每 1000 毫秒(1 秒)轮询一次,但是在第 5 次(即 5 秒后)可能会变成 6 秒,这个问题通常是因为 阻塞代码 的执行,导致定时器的回调函数没有按预期时间间隔触发。我们来详细分析这个问题的原因。
setInterval
的回调函数在 事件循环 中等待执行。当你设置了一个定时器,理论上它会每隔指定时间(例如 1000ms)执行一次回调。但是,如果在回调函数执行过程中有 大量同步代码 阻塞了 JavaScript 线程(如长时间运行的同步计算、网络请求等),那么 下一个回调就无法按时执行,可能会延迟执行。
这可能导致定时器的回调间隔不精确。比如:
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 秒内无法执行任何其他的代码。为了解决这个问题,有几种常见的方法:
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
会重新设置下一次回调。这样即使当前回调存在阻塞,下一次回调也会确保在指定时间后执行,而不是受前一次执行时长的影响。最根本的解决方法是 避免长时间的同步阻塞代码。尽量将阻塞的代码改为异步执行,例如使用 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
仍然会准确地按照时间间隔执行,而不会被阻塞代码影响。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
等),以提高代码的可扩展性和执行的精度。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;
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()
})
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);
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" }
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