一、手写计时器
1.用setTimeout实现倒计时
function countDown(seconds, callback) {
let remaining = seconds;
const timer = setTimeout(function tick() {
if (remaining <= 0) {
callback && callback();
clearTimeout(timer);
} else {
console.log(`剩余时间: ${remaining} 秒`);
remaining--;
setTimeout(tick, 1000); // 1秒后再次执行
}
}, 1000);
}
// 使用示例
countDown(5, () => console.log("倒计时结束!"));
2.用setInterval实现正计时
function startTimer(duration, callback) {
let elapsed = 0;
const timer = setInterval(() => {
elapsed++;
console.log(`已过去: ${elapsed} 秒`);
if (elapsed >= duration) {
clearInterval(timer);
callback && callback();
}
}, 1000);
}
// 使用示例
startTimer(5, () => console.log("计时结束!"));
3.基于performance.now实现更精确的计时器
setTimeout 和 setInterval 受浏览器事件循环影响,可能有延迟。使用 performance.now() 可以获取更高精度的计时:
function preciseTimer(duration, callback) {
const startTime = performance.now();
function checkTime() {
const elapsed = (performance.now() - startTime) / 1000; // 转为秒
if (elapsed >= duration) {
callback && callback();
} else {
console.log(`精确计时: ${elapsed.toFixed(2)} 秒`);
requestAnimationFrame(checkTime); // 使用 requestAnimationFrame 优化
}
}
checkTime();
}
// 使用示例
preciseTimer(5, () => console.log("精确计时结束!"));
requestAnimationFrame() 是浏览器提供的一个专为动画优化设计的 API,它会在浏览器下一次重绘(repaint)之前执行指定的回调函数,通常以 60 FPS(帧/秒) 的速率运行(即每帧约 16.7ms)。它的核心目标是实现更流畅、更高效的动画渲染,同时减少不必要的性能开销。
二、手撕解析URL为JS对象
function parseURLParams(url) {
// 创建一个正则表达式来匹配查询字符串
const regex = /[?&]([^=#]+)=([^&#]*)/g;
const params = {};
let match;
// 使用正则表达式循环匹配查询字符串
while ((match = regex.exec(url))) {
const key = decodeURIComponent(match[1]);
const value = decodeURIComponent(match[2]);
// 如果参数名已经存在,将值存储为数组
if (params[key]) {
if (!Array.isArray(params[key])) {
params[key] = [params[key]];
}
params[key].push(value);
} else {
params[key] = value;
}
}
return params;
}
const url = "https://example.com?name=John&name=Jane&age=30";
const params = parseURLParams(url);
console.log(params); // { name: ["John", "Jane"], age: "30" }
三、手撕getType函数(获取详细的变量类型)
function getType(data) {
// 获取到 "[object Type]",其中 Type 是 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等。
const originType = Object.prototype.toString.call(data)
// 可以直接截取第8位和倒数第一位,这样就获得了 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等
const type = originType.slice(8, -1)
// 再转小写,得到 null、undefined、array、function 等
return type.toLowerCase()
}
四、手撕call和apply
Function.prototype.call2 = function (context) {
var context = context || window
context.fn = this
var args = []
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']')
}
var result = eval('context.fn(' + args + ')')
delete context.fn
return result
}
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window
context.fn = this
var result
if (!arr) {
result = context.fn()
} else {
var args = []
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']')
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result
}
五、手写红绿灯
模拟一个红绿灯变化,红灯 1 秒,绿灯 1 秒,黄灯 1 秒,然后循环
function red() {
console.log('red')
}
function green() {
console.log('green')
}
function yellow() {
console.log('yellow')
}
function light(cb, wait) {
return new Promise((resolve) => {
setTimeout(() => {
cb()
resolve()
}, wait)
})
}
function start() {
return Promise.resolve()
.then(() => {
return light(red, 1000)
})
.then(() => {
return light(green, 1000)
})
.then(() => {
return light(yellow, 1000)
})
.finally(() => {
return start()
})
}
start()
六、手撕LRU缓存
LRU(Least Recently Used)是一种缓存淘汰策略,它会优先删除最近最少使用的数据。下面提供两种实现方式:使用 Map 的简单实现和不使用 Map 的基础实现。
1.使用 Map 的实现
class LRUCache {
constructor(capacity) {
this.cache = new Map()
this.capacity = capacity
}
get(key) {
if (!this.cache.has(key)) return -1
// 将访问的元素移到最新使用的位置
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
put(key, value) {
// 如果 key 已存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key)
}
// 如果达到容量限制,删除最久未使用的元素
else if (this.cache.size >= this.capacity) {
// Map 的 keys() 会按插入顺序返回键
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1) // 缓存是 {1=1}
cache.put(2, 2) // 缓存是 {1=1, 2=2}
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2,缓存是 {1=1, 3=3}
console.log(cache.get(2)) // 返回 -1 (未找到)
2.使用双向链表的实现(不依赖 Map)
// 双向链表节点
class Node {
constructor(key, value) {
this.key = key
this.value = value
this.prev = null
this.next = null
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = {} // 哈希表用于O(1)查找
this.count = 0
// 创建头尾哨兵节点
this.head = new Node(0, 0)
this.tail = new Node(0, 0)
this.head.next = this.tail
this.tail.prev = this.head
}
// 将节点移到双向链表头部
moveToHead(node) {
this.removeNode(node)
this.addToHead(node)
}
// 从链表中删除节点
removeNode(node) {
node.prev.next = node.next
node.next.prev = node.prev
}
// 在链表头部添加节点
addToHead(node) {
node.prev = this.head
node.next = this.head.next
this.head.next.prev = node
this.head.next = node
}
// 删除链表尾部节点
removeTail() {
const node = this.tail.prev
this.removeNode(node)
return node
}
get(key) {
if (key in this.cache) {
const node = this.cache[key]
this.moveToHead(node)
return node.value
}
return -1
}
put(key, value) {
if (key in this.cache) {
// 如果 key 存在,更新值并移到头部
const node = this.cache[key]
node.value = value
this.moveToHead(node)
} else {
// 创建新节点
const newNode = new Node(key, value)
this.cache[key] = newNode
this.addToHead(newNode)
this.count++
// 如果超过容量,删除最久未使用的
if (this.count > this.capacity) {
const tail = this.removeTail()
delete this.cache[tail.key]
this.count--
}
}
}
}
// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2
console.log(cache.get(2)) // 返回 -1 (未找到)
cache.put(4, 4) // 删除 key 1
console.log(cache.get(1)) // 返回 -1 (未找到)
console.log(cache.get(3)) // 返回 3
console.log(cache.get(4)) // 返回 4
实现原理说明:
1.Map 实现版本:
·利用 Map 的特性,它能够记住键的原始插入顺序
·get 操作时将访问的元素移到最后(最新使用)
·put 操作时如果超出容量,删除第一个元素(最久未使用)
2.双向链表实现版本:
·使用哈希表实现 O(1) 的查找
·使用双向链表维护数据的使用顺序
·最近使用的数据放在链表头部
·最久未使用的数据在链表尾部
性能分析:
1.时间复杂度:
·get 操作:O(1)
·put 操作:O(1)
2.空间复杂度:
·O(capacity),其中 capacity 是缓存的容量
七、手写 VNode 对象,表示如下 DOM 节点
<div class="container">
<img src="x1.png" />
<p>hello</p>
</div>
const vnode = {
tag: 'div',
props: {
class: 'container',
},
children: [
{
tag: 'img',
props: {
src: 'x1.png',
},
},
{
tag: 'p',
props: {},
children: ['hello'],
},
],
}
八、手写 compose 函数
compose 函数是函数式编程中的一个重要概念,它将多个函数组合成一个函数,从右到左执行。
1.基础实现(使用 reduce)
function compose(...fns) {
if (fns.length === 0) return (arg) => arg
if (fns.length === 1) return fns[0]
return fns.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)
}
// 使用示例
const add1 = (x) => x + 1
const multiply2 = (x) => x * 2
const addThenMultiply = compose(multiply2, add1)
console.log(addThenMultiply(5)) // (5 + 1) * 2 = 12
九、手写curry函数,实现函数柯里化
function sum(...initialArgs) {
let total = initialArgs.reduce((acc, val) => acc + val, 0);
const curried = function(...args) {
if (args.length === 0) {
return curried;
}
total += args.reduce((acc, val) => acc + val, 0);
return curried;
};
curried.value = function() {
return total;
};
return curried;
}
// 测试用例
console.log(sum(1, 2)(3, 4).value()); // 输出: 10
console.log(sum(1)(2)(3)(4).value()); // 输出: 10
console.log(sum(1, 2, 3)(4).value()); // 输出: 10
console.log(sum(1)(2, 3, 4).value()); // 输出: 10
十、手写Promise
class MyPromise {
// 构造方法
constructor(executor) {
// 初始化值
this.initValue()
// 初始化this指向
this.initBind()
// 执行传进来的函数
executor(this.resolve, this.reject)
}
initBind() {
// 初始化this
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
initValue() {
// 初始化值
this.PromiseResult = null // 终值
this.PromiseState = 'pending' // 状态
}
resolve(value) {
// 如果执行resolve,状态变为fulfilled
this.PromiseState = 'fulfilled'
// 终值为传进来的值
this.PromiseResult = value
}
reject(reason) {
// 如果执行reject,状态变为rejected
this.PromiseState = 'rejected'
// 终值为传进来的reason
this.PromiseResult = reason
}
}
十一、手写Promise.all
static all(promises) {
const result = []
let count = 0
return new MyPromise((resolve, reject) => {
const addData = (index, value) => {
result[index] = value
count++
if (count === promises.length) resolve(result)
}
promises.forEach((promise, index) => {
if (promise instanceof MyPromise) {
promise.then(res => {
addData(index, res)
}, err => reject(err))
} else {
addData(index, promise)
}
})
})
}
十二、手写Promise.race
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
if (promise instanceof MyPromise) {
promise.then(res => {
resolve(res)
}, err => {
reject(err)
})
} else {
resolve(promise)
}
})
})
}
十三、手写Promise.allSettled
static allSettled(promises) {
return new Promise((resolve, reject) => {
const res = []
let count = 0
const addData = (status, value, i) => {
res[i] = {
status,
value
}
count++
if (count === promises.length) {
resolve(res)
}
}
promises.forEach((promise, i) => {
if (promise instanceof MyPromise) {
promise.then(res => {
addData('fulfilled', res, i)
}, err => {
addData('rejected', err, i)
})
} else {
addData('fulfilled', promise, i)
}
})
})
}
十四、手写防抖Debounce
function debounce(func,delay){
let debounceTimer;
return function(){
const context=this;
const args=arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(()=>{
func.apply(context,args);
},delay)
}
}
windows.addEventListener('resize',debounce(()=>{
console.log('resizing...')
},500))
十五、手写节流Throttle
function throttle(func,limit){
let inThrottle;
return function(){
const context = this;
const args = arguments;
if(!inThrottle){
func.apply(context,args);
inThrottle=True;
setTimeout(()=>{
inThrottle=false;
},limit)
}
}
}
window.addEventListener('scroll',throttle(()=>{
console.log('Scrolling')
},1000))
十六、输入一个字符串,将字符串中的-_字符后面的字母变成大写字母
function capitalizeAfterHyphenOrUnderscore(str) {
return str.replace(/[-_](\w)/g, (match, char) => char.toUpperCase());
}
// 测试用例
console.log(capitalizeAfterHyphenOrUnderscore("hello-world")); // 输出: "helloWorld"
console.log(capitalizeAfterHyphenOrUnderscore("foo_bar_baz")); // 输出: "fooBarBaz"
console.log(capitalizeAfterHyphenOrUnderscore("a-b-c-d")); // 输出: "aBCD"
十七、手写new操作符
在 JavaScript 中,new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。要实现一个类似 new 操作符的功能,可以通过以下步骤来模拟:
1.创建一个空对象:这个空对象将作为新对象的实例。
2.设置原型:将新对象的原型指向构造函数的 prototype。
3.执行构造函数:将构造函数的 this 指向新创建的对象,并执行构造函数。
4.返回新对象:返回新创建的对象。
以下是实现 new 操作符的代码:
function myNew(constructor, ...args) {
// 创建一个空对象,并将其原型指向构造函数的 prototype
const obj = Object.create(constructor.prototype);
// 将构造函数的 this 指向新创建的对象,并执行构造函数
const result = constructor.apply(obj, args);
// 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
return result instanceof Object ? result : obj;
}
// 测试用例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
const person1 = myNew(Person, "Alice", 25);
console.log(person1); // 输出: Person { name: 'Alice', age: 25 }
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.
const person2 = myNew(Person, "Bob", 30);
console.log(person2); // 输出: Person { name: 'Bob', age: 30 }
person2.sayHello(); // 输出: Hello, my name is Bob and I am 30 years old.
十八、对版本号数组进行排序,比如[0.1.2.3, 1.2.1.0, 4.2.1.0, 0.1.2.0]
function compareVersions(a, b) {
// 将版本号按点号分割成数组
var aParts = a.split('.');
var bParts = b.split('.');
// 比较每个部分
for (var i = 0; i < Math.max(aParts.length, bParts.length); i++) {
// 如果一个版本号较短,我们可以认为缺失的部分为0
var aPart = aParts[i] || 0;
var bPart = bParts[i] || 0;
// 将部分转换为数字并进行比较
var aNum = parseInt(aPart, 10);
var bNum = parseInt(bPart, 10);
if (aNum > bNum) {
return 1;
} else if (aNum < bNum) {
return -1;
}
}
// 如果所有部分都相等,则版本号相等
return 0;
}
// 使用比较函数对版本号数组进行排序
var versions = ['0.1.2.3', '1.2.1.0', '4.2.1.0', '0.1.2.0'];
versions.sort(compareVersions);
console.log(versions); // 输出排序后的数组
十九、手写 call 和 apply
Function.prototype.call2 = function (context) {
var context = context || window
context.fn = this
var args = []
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']')
}
var result = eval('context.fn(' + args + ')')
delete context.fn
return result
}
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window
context.fn = this
var result
if (!arr) {
result = context.fn()
} else {
var args = []
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']')
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result
}
二十、手写 EventBus 自定义事件
class EventBus {
constructor() {
this.eventObj = {}
this.callbcakId = 0
}
$on(name, callbcak) {
if (!this.eventObj[name]) {
this.eventObj[name] = {}
}
const id = this.callbcakId++
this.eventObj[name][id] = callbcak
return id
}
$emit(name, ...args) {
const eventList = this.eventObj[name]
for (const id in eventList) {
eventList[id](...args)
if (id.indexOf('D') !== -1) {
delete eventList[id]
}
}
}
$off(name, id) {
delete this.eventObj[name][id]
if (!Object.keys(this.eventObj[name]).length) {
delete this.eventObj[name]
}
}
$once(name, callbcak) {
if (!this.eventObj[name]) {
this.eventObj[name] = {}
}
const id = 'D' + this.callbcakId++
this.eventObj[name][id] = callbcak
return id
}
}
二十一、怎么实现将一个图标拖拽到另一个地方?
实现图标拖拽功能可以通过 HTML5 的拖放 API(Drag and Drop API)来完成。以下是一个简单的示例,展示如何将一个图标拖拽到另一个地方。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖拽示例</title>
<style>
.draggable {
width: 50px;
height: 50px;
background-color: blue;
color: white;
text-align: center;
line-height: 50px;
cursor: pointer;
margin: 10px;
}
.dropzone {
width: 200px;
height: 200px;
background-color: lightgray;
margin: 10px;
padding: 10px;
border: 2px dashed black;
}
</style>
</head>
<body>
<div class="draggable" draggable="true" id="draggable">拖拽我</div>
<div class="dropzone" id="dropzone">拖到这里</div>
<script src="script.js"></script>
</body>
</html>
script.js
document.addEventListener("DOMContentLoaded", () => {
const draggable = document.getElementById("draggable");
const dropzone = document.getElementById("dropzone");
// 拖拽开始时触发
draggable.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/plain", event.target.id); // 传递被拖拽元素的 ID
});
// 拖拽进入目标区域时触发
dropzone.addEventListener("dragover", (event) => {
event.preventDefault(); // 阻止默认行为,允许放置
});
// 拖拽离开目标区域时触发
dropzone.addEventListener("dragleave", (event) => {
console.log("离开目标区域");
});
// 拖拽结束时触发
dropzone.addEventListener("drop", (event) => {
event.preventDefault(); // 阻止默认行为
const draggableId = event.dataTransfer.getData("text/plain"); // 获取被拖拽元素的 ID
const draggableElement = document.getElementById(draggableId); // 获取被拖拽的元素
dropzone.appendChild(draggableElement); // 将被拖拽的元素移动到目标区域
});
});
二十二、手写实现sleep
用setTimeout
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用示例
async function example() {
console.log("Hello");
await sleep(2000); // 等待2秒
console.log("World");
}
example();