JS手撕
输出题
异步
new Promise((resolve, reject) => {
console.log(1);
resolve(true);
console.log(2);
throw new Error('err');
reject(false);
console.log(3);
})
.catch(ex => console.log(ex))
.then(res => console.log(res));
打印:1, 2, true
//请写出输出内容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
this
var length = 10;
function fn() {
return this.length + 1;
}
var obj = {
length: 5,
test1: function () {
return fn();
},
};
obj.test2 = fn;
//下面代码输出是什么
console.log(obj.test1());
console.log(fn());
console.log(obj.test2());
11,11,6
闭包
var a = 0;
var b = 0;
var c = 0;
function fn(a) {
console.log(a++, c);
function fn2(b) {
console.log(a, b, c);
}
var c = 4;
fn = fn2;
}
fn(1);
fn(2);
1,undefined
2,2,4
手写布局
三栏布局,右侧自适应
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flex 三栏布局</title>
<style>
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100vh;
}
.left, .middle {
width: 200px; /* 左侧和中间栏固定宽度 */
background-color: lightblue;
padding: 20px;
}
.right {
flex-grow: 1; /* 右侧栏自适应宽度 */
background-color: lightgreen;
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="left">左侧栏</div>
<div class="middle">中间栏</div>
<div class="right">右侧栏自适应宽度</div>
</div>
</body>
</html>
过渡消失
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点击左侧栏使其消失</title>
<style>
body {
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100vh;
}
.left {
width: 200px;
background-color: lightblue;
padding: 20px;
transition: width 0.5s ease, opacity 0.5s ease; /* 添加过渡效果 */
}
.middle, .right {
flex: 1;
background-color: lightgreen;
padding: 20px;
}
.hidden {
width: 0;
opacity: 0; /* 渐变透明度 */
padding: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="left" id="left-bar">点击我隐藏左侧栏</div>
<div class="middle" id="mid-bar">中间栏</div>
<div class="right">右侧栏</div>
</div>
<script>
// 获取左侧栏元素
const leftBar = document.getElementById('left-bar');
const midBar = document.getElementById('mid-bar');
// 点击事件监听器
leftBar.addEventListener('click', function() {
leftBar.classList.add('hidden'); // 添加 'hidden' 类,触发过渡效果
});
// 点击中间栏恢复左侧
midBar.addEventListener('click', ()=>{
leftBar.classList.remove('hidden');
})
</script>
</body>
</html>
button 样式
要让一个 div
标签看起来像一个按钮,你可以使用 CSS 来添加样式。以下是一个简单的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Div as Button</title>
<style>
.button {
display: inline-block;
padding: 10px 20px; /* 内边距 */
background-color: #007BFF; /* 背景颜色 */
color: white; /* 文字颜色 */
border: none; /* 去掉边框 */
border-radius: 5px; /* 圆角 */
cursor: pointer; /* 鼠标指针变成手型 */
text-align: center; /* 文字居中 */
font-size: 16px; /* 字体大小 */
transition: background-color 0.3s; /* 背景色过渡效果 */
}
.button:hover {
background-color: #0056b3; /* 鼠标悬停时的背景颜色 */
}
.button:active {
background-color: #004085; /* 点击时的背景颜色 */
}
</style>
</head>
<body>
<div class="button" onclick="alert('Button clicked!')">Click Me</div>
</body>
</html>
CSS 样式:
display: inline-block;
:使div
像块级元素一样显示,同时可以设置宽高。padding
:增加内边距,让按钮看起来更大。background-color
和color
:设置背景颜色和文字颜色。border
和border-radius
:去掉默认边框并添加圆角。cursor: pointer;
:鼠标悬停时显示手型指针。transition
:为背景颜色变化添加平滑过渡效果。:hover
和:active
状态的样式:实现鼠标悬停和点击时的视觉效果。
HTML 结构:一个
div
标签,带有onclick
事件,可以在点击时触发 JavaScript 函数。
左中右布局
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
}
.container {
display: flex;
height: 100vh; /* 设置容器高度 */
}
.left-sidebar,
.right-sidebar {
width: 250px; /* 侧栏宽度 */
background-color: #f0f0f0;
padding: 20px;
}
.main-content {
flex: 1; /* 填充剩余空间 */
background-color: #fff;
padding: 20px;
}
闭包
函数柯里化
题目:
// 写一个函数,满足:
function add(a , b , c ){
return a+b+c;
}
let add1 = curry(add, 1);
console.log(add1(2,3)); // 6
console.log(curry(add)(0)(5)(6)); // 11
实现:
function curry(func) {
return function curried(...args) {
// 如果已经收集到足够参数,直接调用原函数
if (args.length >= func.length) {
return func(...args);
}
// 否则返回一个新函数,继续收集参数
return function (...newArgs) {
return curried(...args, ...newArgs);
};
};
}
防抖与节流
在网络请求的场景中,通常推荐使用节流。
防抖:n 秒后再执行某一事件,若在 n 秒内被再次触发,则重新计时
function debounce(func, wait) {
let timeout;
return function (...args) {
let context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
function print(event) {
console.log('mouse move!', event.clientX, event.clientY);
}
const debouncedPrint = debounce(print, 1000);
document.addEventListener('mousemove', debouncedPrint);
节流:n 秒内只运行一次,若在 n 秒内重复触发,则只有一次生效
function throttle(func, wait) {
let preTime = 0;
return function (...args) {
let now = Date.now();
if (now - preTime > wait) {
func.apply(this, args); // 确保正确的上下文和参数传递
preTime = now;
}
};
}
function print() {
console.log('mouse move!');
}
const throttledPrint = throttle(print, 1000);
document.addEventListener('mousemove', throttledPrint);
once 修饰器
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
// 示例用法
const sayHello = once(function(name) {
console.log(`Hello, ${name}!`);
});
sayHello('Alice'); // 输出: Hello, Alice!
sayHello('Bob'); // 不会输出任何内容
- 闭包:
once
函数返回一个新函数,内部使用一个called
标志来跟踪是否已经调用过。 - 调用控制:第一次调用时执行原始函数并记录结果;之后的调用会直接返回之前的结果。
- 参数处理:使用
...args
来支持任意数量的参数传递给原始函数。
这个 once
修饰器能够有效地限制函数只被调用一次,后续的调用将不会产生任何效果。
时间间隔重复执行函数
const repeat = function (func, times, wait) {
return (theStr) => {
for (let i = 1; i <= times; i++) {
setTimeout(() => {
func(theStr);
}, i * wait);
}
};
};
function myPrint(theStr) {
const timeStap = new Date();
console.log(theStr + timeStap);
}
const repeatPrint = repeat(myPrint, 4, 3000);
repeatPrint('helloworld');
异步
创建 Promise
// 传入执行器函数,该函数的参数为 resolve 和 reject
const lotteryPromise = new Promise((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('WIN! 🏆');
} else {
reject('LOSE...💩');
}
});
实现 sleep(n)
function sleep(sec) {
return new Promise(resolve => {
setTimeout(() => {
console.log(`I sleep for ${sec} seconds💤`);
resolve();
}, sec * 1000);
});
}
sleep(0.5).then(() => sleep(1));
手写 Promise.all()
Promise.all = function (promises) {
let results = [];
let length = promises.length;
let promiseCount = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(res => {
results[i] = res;
promiseCount++;
if (promiseCount === length) {
resolve(results);
}
}, err => {
reject(err);
})
}
})
}
// example
let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(2);
let promise3 = Promise.resolve(3);
promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3]
})
.catch(reason => {
console.log(reason);
});
复杂版本:
function customPromiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completedPromises = 0;
// 如果传入的不是数组,抛出错误
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
// 如果传入的是空数组,立即resolve空数组
if (promises.length === 0) {
return resolve([]);
}
promises.forEach((promise, index) => {
// 确保每个元素都是一个 Promise
Promise.resolve(promise)
.then(result => {
results[index] = result;
completedPromises++;
// 当所有 Promise 都完成时,resolve 最终的结果数组
if (completedPromises === promises.length) {
resolve(results);
}
})
.catch(error => {
// 一旦有一个 Promise 被 reject,立即 reject 整个 customPromiseAll
reject(error);
});
});
});
}
异步任务控制并发
class AsyncPool {
constructor(maxConcurrency) {
this.maxConcurrency = maxConcurrency; // 最大并发数
this.currentCount = 0; // 当前正在执行的任务数量
this.taskQueue = []; // 任务队列
}
// 执行一个任务
async run(task, ...args) {
if (this.currentCount >= this.maxConcurrency) {
// 如果当前执行任务数达到最大并发数,则等待
await new Promise(resolve => this.taskQueue.push(resolve));
}
this.currentCount++;
try {
// 执行实际任务
return await task(...args);
} finally {
this.currentCount--;
if (this.taskQueue.length > 0) {
// 如果队列中有等待的任务,释放一个
const next = this.taskQueue.shift();
next();
}
}
}
}
fetch 超时处理
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('请求超时'));
}, timeout);
fetch(url, options)
.then(response => {
clearTimeout(timeoutId); // 清除超时定时器
if (!response.ok) {
reject(new Error('网络响应不符合规范'));
} else {
resolve(response);
}
})
.catch(err => {
clearTimeout(timeoutId); // 清除超时定时器
reject(err);
});
});
}
// 示例用法
fetchWithTimeout('https://jsonplaceholder.typicode.com/posts', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('错误:', error));
hardMan
function HardMan(name) {
this.name = name;
this.taskQueue = [];
this.study = function () {
this.taskQueue.push(() => {
console.log('I am studying.');
});
return this;
};
this.rest = function (sec) {
this.taskQueue.push(async () => {
await wait(sec);
console.log(`rest for ${sec} seconds`);
});
console.log(this);
return this;
};
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, sec * 1000);
});
}
Promise.resolve().then(() => {
let sequence = Promise.resolve();
console.log(this);
this.taskQueue.forEach((item) => {
sequence = sequence.then(item);
});
});
}
// 测试代码
let jiaqi = new HardMan('jiaqi');
jiaqi.study().rest(3).study().rest(2);
原型链
判断是否有重复元素
Array.prototype.hasDuplicates = function () {
// this 指向条用这个方法的数组
let theSet = new Set();
for(let element of this){ // 不能用 forEach,因为 forEach 不受 return 影响
if (theSet.has(element)) {
return true;
} else {
theSet.add(element);
}
}
return false;
};
console.log([1, 2, 2].hasDuplicates()); // true
用 function 实现 class
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 添加方法到原型
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建实例
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
// 调用方法
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.
实现常用函数
flat 拍平数组
let nums = [1, 2, [3, 4, [6, 7]], 5];
function myFlat(arr, depth = 1) {
// 递归结束条件
if (depth < 1) {
return arr;
}
let res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
res = res.concat(myFlat(item, depth - 1));
} else {
res.push(item);
}
});
return res;
}
console.log(myFlat(nums, 2)); // [1, 2, 3, 4, 6, 7, 5]
解析 URL
let url = "http://www.xxx.com?a=1&b=2&c=3"
function parse(url) {
let obj = {};
url
.slice(url.indexOf("?") + 1)
.split("&")
.map(item => {
let [key, val] = item.split("=");
obj[key] = val;
});
return obj;
}
let result = parse(url);
console.log(result);
// { a: '1', b: '2', c: '3' }
手写深拷贝
WeakMap 起到了检测和处理循环引用的关键作用,防止无限递归:
- WeakMap 是 JavaScript 中的一种集合类型,键必须是对象,值可以是任意类型。
- WeakMap 的键是弱引用,不会阻止键被垃圾回收。WeakMap 的键是弱引用,如果键的对象被垃圾回收机制回收了,该键值对会自动删除。
- 没有遍历方法,例如 keys 或 values,这是为了确保弱引用的安全性。
其中映射关系为:
原对象 -> 拷贝的对象
function deepClone(srcObj, seen = new WeakMap()) {
if (srcObj === null || typeof srcObj !== 'object') {
return srcObj;
}
// 检查 weakmap 中是否存在
if (seen.has(srcObj)) {
return seen.get(srcObj); // 如果存在,直接返回已拷贝的对象
}
const copiedObj = Array.isArray(srcObj) ? [] : {};
// 将当前对象存储到 weakmap 中
seen.set(srcObj, copiedObj);
for (let key in srcObj) {
copiedObj[key] = deepClone(srcObj[key], seen); // 递归拷贝
}
return copiedObj;
}
手写 Math.pow()
function customPow(base, exponent) {
let result = 1;
if (exponent === 0) {
return result;
}
const positiveExponent = Math.abs(exponent);
for (let i = 0; i < positiveExponent; i++) {
result *= base;
}
return exponent < 0 ? 1 / result : result;
}
优化:快速幂算法,指数分治法
function customPow(base, exponent) {
if (exponent === 0) return 1; // 任何数的 0 次方是 1
if (exponent < 0) return 1 / customPow(base, -exponent); // 处理负指数
// 快速幂算法
let half = customPow(base, Math.floor(exponent / 2));
if (exponent % 2 === 0) {
return half * half; // 如果指数是偶数
} else {
return half * half * base; // 如果指数是奇数
}
}
手写 instanceOf
function myInstanceof(left, right) {
// 获取右侧构造函数的原型对象
let prototype = right.prototype;
// 获取左侧对象的原型
left = left.__proto__;
// 不断沿着原型链向上查找
while (true) {
// 到达原型链的顶端,仍未找到
if (left === null) return false;
// 找到相同的原型对象
if (left === prototype) return true;
// 继续向上查找
left = left.__proto__;
}
}
// 示例
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Array)); // false
实现 myBind
可以通过实现一个 myBind
方法来模拟 JavaScript 的 Function.prototype.bind
方法。以下是一个简单的实现:
Function.prototype.myBind = function(context, ...args) {
const fn = this; // 保存原函数
return function(...newArgs) {
// 使用 apply 调用原函数
return fn.apply(context, [...args, ...newArgs]);
};
};
// 示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
const greetAlice = greet.myBind(person, 'Hello');
greetAlice('!'); // 输出: Hello, Alice!
- 保存原函数:将
this
保存到fn
中。 - 返回新函数:返回一个新函数,该函数可以接收额外的参数。
- 调用原函数:在新函数中,使用
apply
方法调用原函数,将context
和合并后的参数传递给它。
这样,你就可以实现一个基本的 myBind
方法,模拟 bind
的行为。
手写 myReduce
Array.prototype.myReduce = function (callback, initialValue) {
// 检查调用者是否为数组
if (!Array.isArray(this)) {
throw new TypeError(`${this} is not an array`);
}
// 检查回调函数是否是一个函数
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const array = this; // 当前数组
let accumulator = initialValue; // 累积值
let startIndex = 0; // 初始索引
// 如果未提供初始值
if (accumulator === undefined) {
if (array.length === 0) {
throw new TypeError('Reduce of empty array with no initial value');
}
// 使用数组的第一个值作为累积器初始值
accumulator = array[0];
startIndex = 1; // 从第二个元素开始遍历
}
// 遍历数组
for (let i = startIndex; i < array.length; i++) {
if (i in array) { // 跳过稀疏数组中的空值
accumulator = callback(accumulator, array[i], i, array); // 调用回调函数
}
}
return accumulator; // 返回最终结果
};
callbackFn
:必需,一个回调函数,为数组中每个元素执行的函数。其返回值将作为下一次调用callbackFn
时的accumulator
参数。对于最后一次调用,返回值将作为reduce()
的返回值。该函数被调用时将传入以下参数:
function callback(accumulator, currentValue, index, array) {
// 逻辑
}
accumulator
: 上一次调用callbackFn
的结果。在第一次调用时,如果指定了initialValue
则为指定的值,否则为array[0]
的值。currentValue
: 当前元素的值。在第一次调用时,如果指定了initialValue
,则为array[0]
的值,否则为array[1]
。index
: 在数组中的索引位置。在第一次调用时,如果指定了initialValue
则为0
,否则为1
。array
: 数组本身。
initialValue
: 可选,第一次调用回调时初始化accumulator
的值。如果指定了initialValue
,则callbackFn
从数组中的第一个值作为currentValue
开始执行。如果没有指定initialValue
,则accumulator
初始化为数组中的第一个值,并且callbackFn
从数组中的第二个值作为currentValue
开始执行。在这种情况下,如果数组为空(没有第一个值可以作为accumulator
返回),则会抛出错误。
listToTree
function listToTree(list) {
const map = {};
const tree = [];
// 将每个节点存入映射表
list.forEach(item => {
map[item.id] = { ...item, children: [] }; // 添加 children 属性
});
// 构建树形结构
list.forEach(item => {
if (item.parentId === null) {
tree.push(map[item.id]); // 根节点
} else {
if (map[item.parentId]) {
map[item.parentId].children.push(map[item.id]); // 添加子节点
}
}
});
return tree;
}
// 示例
const list = [
{ id: 1, name: 'Node 1', parentId: null },
{ id: 2, name: 'Node 2', parentId: 1 },
{ id: 3, name: 'Node 3', parentId: 1 },
{ id: 4, name: 'Node 4', parentId: 2 },
{ id: 5, name: 'Node 5', parentId: null }
];
const tree = listToTree(list);
console.log(JSON.stringify(tree, null, 2));
数组去重
一维数组:
let a = [1,2,2,3];
a = [...new Set(a)];
console.log(a); // [1,2,3]
二维数组:
let aa = [[1,2], [2,3], [1,2]];
let obj = {};
aa.forEach((item)=> obj[item]=item);
aa = Object.values(obj);
console.log(aa);
反转键值
function inverse(obj){
var retobj = {};
for(var key in obj){
retobj[obj[key]] = key;
}
return retobj;
}
去除数组前0与后0
去除前侧 0 :
const height = [0, 0, 3, 9, 0];
const result = height.slice(height.findIndex((num) => num !== 0));
console.log(result); // [3, 9, 0]
去除后侧 0:
const height = [0, 0, 3, 9, 0];
const lastNonZeroIndex = height.lastIndexOf(
[...height].reverse().find((num) => num !== 0)
);
const result = height.slice(0, lastNonZeroIndex + 1);
console.log(result); // [0, 0, 3, 9]
设计模式
发布订阅模式
class Event {
constructor() {
// 存储事件的数据对象
// 为了迅速查找,使用字典
this.events = {};
}
// 绑定事件
on(eventName, callBack) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callBack);
}
// 派发事件:遍历事件列表,对每个事件执行相应的回调函数
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach((callBack) => callBack(...args));
}
}
// 取消订阅事件,从事件列表中删除指定的回调函数
off(eventName, callBack) {
if (this.events[eventName]) {
if (callBack) {
// 如果指定了回调函数,就删除该函数
this.events[eventName] = this.events[eventName].filter(
(cb) => cb != callBack
);
} else {
// 如果没指定回调函数,就删除所有绑定的函数
this.events[eventName] = [];
}
}
}
// 为事件绑定一个只执行一次的回调函数
once(eventName, callBack) {
// 将回调函数与取消订阅事件封装起来,成为一个新的函数
const func = () => {
callBack();
this.off(eventName, func);// 注意这里传入的参数是新定义的 func 而不是 callBack
};
this.on(eventName, func);
}
}
function test() {
console.log("in test");
}
const test2 = function () {
console.log("in test2");
};
myEvent = new Event();
myEvent.on("test", test);
myEvent.once("test2", test2);
myEvent.emit("test2");
Object.defineProperty
实现 Observer
Object.defineProperty(obj, 'count', decrip);
const obj = {
a: 1,
b: 2,
c: {
c1: 1,
c2: 2,
},
};
function _isObject(v) {
return typeof v === 'object' && v !== null;
}
// 由于是对属性的监听,所以必须深度遍历所有属性。
function observe(obj) {
for (let k in obj) {
let v = obj[k];
if (_isObject(v)) {
observe(v);
}
Object.defineProperty(obj, k, {
get() {
console.log(k, '读取');
return v;
},
set(val) {
if (val !== v) {
console.log(k, '更改');
v = val;
}
},
});
}
}
observe(obj);
obj.c.c1;
可以看出,Object.defineProperty
通过递归能够实现深层次数据的监听,但是无法实现对于新增的属性的监听。
Proxy
实现 Observer
new Proxy(obj, handler);
const obj = {
a: 1,
b: 2,
c: {
c1: 1,
c2: 2,
},
};
function _isObject(v) {
return typeof v === 'object' && v !== null;
}
function observe(obj) {
const proxy = new Proxy(obj, {
get(target, k) {
let v = target[k];
if (_isObject(v)) {
v = observe(v);
}
console.log(k, '读取');
return v;
},
set(target, k, val) {
let v = target[k];
if (val !== v) {
target[k] = val;
console.log(k, '更改');
}
},
});
return proxy;
}
const proxy = observe(obj);
proxy.c.c1;
其他
LRU
class Node {
constructor(key = 0, value = 0) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.dummy = new Node();
this.dummy.prev = this.dummy;
this.dummy.next = this.dummy;
this.keyToNode = new Map(); // 映射 key 与 Node 的 map
}
// 如果已经有这个 key 则更新 value
put(key, value) {
let node = this.getNode(key);
if (node) {
// 如果key存在,更新value
node.value = value;
return;
}
// 如果 key 不存在,新建键值对
let newNode = new Node(key, value);
this.keyToNode.set(key, newNode);
// 将新 node 放到链表头部
this.moveToHead(newNode);
// 当超过 capacity 的时候,去掉最后一个
if (this.keyToNode.size > this.capacity) {
const lastNode = this.dummy.prev;
this.keyToNode.delete(lastNode.key);
this.removeNode(lastNode);
}
}
get(key) {
const node = this.getNode(key);
return node ? node.value : -1;
}
// 获取对应 key 的 node
getNode(key) {
if (!this.keyToNode.has(key)) {
return null;
}
const node = this.keyToNode.get(key);
this.removeNode(node);
this.moveToHead(node);
return node;
}
// 将一个节点放到链表头部
moveToHead(node) {
node.prev = this.dummy;
node.next = this.dummy.next;
node.prev.next = node;
node.next.prev = node;
}
// 删除一个节点
removeNode(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
事件捕获和事件冒泡
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件冒泡与事件捕获</title>
<style>
.outer {
width: 300px;
height: 300px;
background-color: lightblue;
display: flex;
justify-content: center;
align-items: center;
}
.inner {
width: 150px;
height: 150px;
background-color: lightcoral;
}
</style>
</head>
<body>
<div class="outer" id="outer">外层DIV
<div class="inner" id="inner">内层DIV</div>
</div>
<script>
// 事件捕获阶段
document.getElementById('outer').addEventListener('click', function() {
alert('外层DIV 捕获阶段');
}, true); // 第三个参数 true 表示捕获阶段
document.getElementById('inner').addEventListener('click', function() {
alert('内层DIV 捕获阶段');
}, true); // 第三个参数 true 表示捕获阶段
// 事件冒泡阶段
document.getElementById('outer').addEventListener('click', function() {
alert('外层DIV 冒泡阶段');
}, false); // 第三个参数 false 表示冒泡阶段
document.getElementById('inner').addEventListener('click', function() {
alert('内层DIV 冒泡阶段');
}, false); // 第三个参数 false 表示冒泡阶段
</script>
</body>
</html>
图片懒加载
<!-- HTML -->
<img data-src="example.jpg" alt="Example Image" class="lazy">
<!-- JavaScript -->
<script>
// 选择所有带 data-src 属性的图片
const lazyImages = document.querySelectorAll('img.lazy');
// 创建 Intersection Observer
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 将 data-src 替换为 src
img.classList.remove('lazy'); // 移除懒加载类
observer.unobserve(img); // 停止观察当前图片
}
});
});
// 观察每个图片
lazyImages.forEach(img => observer.observe(img));
</script>
滚动事件监听:
<!-- HTML -->
<img data-src="example.jpg" alt="Example Image" class="lazy">
<!-- JavaScript -->
<script>
// 获取所有懒加载图片
const lazyImages = document.querySelectorAll('img.lazy');
// 检测图片是否进入可视区域
function lazyLoad() {
const windowHeight = window.innerHeight;
lazyImages.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top <= windowHeight && rect.bottom >= 0) {
img.src = img.dataset.src; // 加载图片
img.classList.remove('lazy'); // 移除懒加载类
}
});
}
// 绑定滚动事件
window.addEventListener('scroll', lazyLoad);
// 初始化检测
lazyLoad();
</script>
创建带有过期时间的 localStorage
// 存储数据到 LocalStorage
function setItemWithExpiry(key, value, expiryInMinutes) {
const now = new Date();
const item = {
value: value,
expiry: now.getTime() + expiryInMinutes * 60 * 1000, // 设置过期时间
};
localStorage.setItem(key, JSON.stringify(item));
}
// 从 LocalStorage 获取数据
function getItemWithExpiry(key) {
const itemStr = localStorage.getItem(key);
// 如果不存在,返回 null
if (!itemStr) {
return null;
}
const item = JSON.parse(itemStr);
const now = new Date();
// 检查是否过期
if (now.getTime() > item.expiry) {
localStorage.removeItem(key); // 删除过期项
return null; // 返回 null
}
return item.value; // 返回有效值
}
// 页面加载时检查过期数据
window.onload = function() {
const value = getItemWithExpiry('myKey');
if (value) {
console.log('有效数据:', value);
} else {
console.log('数据已过期或不存在');
}
};
// 示例使用
setItemWithExpiry('myKey', 'myValue', 1); // 设置1分钟后过期
- 存储数据:使用
setItemWithExpiry
存储数据和过期时间。 - 加载时检查:在
window.onload
事件中调用getItemWithExpiry
,当页面加载时检查是否有有效数据。 - 处理过期:如果数据已过期,则会从
LocalStorage
中删除并返回null
。
获取 cookie 的值
// 获取 Cookie 值的函数
function getCookieValue(name) {
const cookieText = document.cookie; // 获取所有 cookie
const cookies = cookieText.split(';').map(cookie => cookie.trim()); // 按分号分隔并去除空格
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === name) {
return decodeURIComponent(value); // 返回解码后的值
}
}
return null;
}
根据虚拟 dom 生成真实 dom
const vdom = {
tag: 'div',
props: { id: 'app' },
children: [
{
tag: 'h1',
props: {},
children: ['Hello, World!']
},
{
tag: 'p',
props: {},
children: ['This is a simple example.']
}
]
};
function createElement(vnode) {
// 创建元素
const element = document.createElement(vnode.tag);
// 设置属性
for (const key in vnode.props) {
element.setAttribute(key, vnode.props[key]);
}
// 添加子节点
vnode.children.forEach(child => {
if (typeof child === 'string') {
// 如果是文本,添加文本节点
element.appendChild(document.createTextNode(child));
} else {
// 否则递归创建子元素
element.appendChild(createElement(child));
}
});
return element;
}
// 使用虚拟 DOM 创建真实 DOM
const realDOM = createElement(vdom);
// 将生成的真实 DOM 添加到页面中
document.body.appendChild(realDOM);
比较版本号
function compareVersion(version1, version2) {
const v1Parts = version1.split('.');
const v2Parts = version2.split('.');
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1 = parseInt(v1Parts[i]) || 0; // 使用 parseInt 转换,默认值为 0
const v2 = parseInt(v2Parts[i]) || 0;
if (v1 > v2) return 1; // version1 大于 version2
if (v1 < v2) return -1; // version1 小于 version2
}
return 0; // 两个版本相等
}
// 示例用法
console.log(compareVersion('1.0.1', '1.0.0')); // 输出: 1
console.log(compareVersion('1.0.0', '1.0.1')); // 输出: -1
console.log(compareVersion('1.0', '1.0.0')); // 输出: 0
数组转 dom 并响应式 render
要将一个数组变成 DOM 结构,并实现数组变化时对应的 DOM 也改变,可以使用 JavaScript 来操作 DOM。以下是一个简单的示例,演示如何实现这一点:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Array to DOM</title>
</head>
<body>
<div id="app"></div>
<button id="add">添加项</button>
<button id="remove">移除项</button>
<script>
const app = document.getElementById('app');
let dataArray = ['项 1', '项 2', '项 3'];
function render() {
app.innerHTML = ''; // 清空当前内容
dataArray.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
app.appendChild(div);
});
}
// 添加新项
document.getElementById('add').addEventListener('click', () => {
dataArray.push(`项 ${dataArray.length + 1}`);
render();
});
// 移除最后一项
document.getElementById('remove').addEventListener('click', () => {
dataArray.pop();
render();
});
render(); // 初始渲染
</script>
</body>
</html>
代码说明:
- HTML 结构:包含一个
div
用于显示数组内容,以及两个按钮用于添加和移除数组项。 dataArray
:初始化一个数组。render
函数:清空当前 DOM 内容,并根据dataArray
创建新的 DOM 元素,显示在页面上。- 事件监听:添加和移除按钮的点击事件,通过修改
dataArray
并调用render
函数来更新 DOM。
这样,当数组发生变化时,页面中的 DOM 结构也会相应更新。
vdom 和简单 diff 算法
实现一个简单的 Virtual DOM 和 diff 算法的基础版本可以帮助理解如何处理 UI 更新。以下是一个示例,包括 Virtual DOM 的创建和 diff 算法的实现。
Virtual DOM 实现
首先,定义一个简单的 Virtual DOM 结构和创建元素的函数:
function createElement(tag, props = {}, ...children) {
return {
tag,
props,
children: children.flat(), // 扁平化子节点
};
}
Diff 算法实现
接下来,实现一个简单的 diff 函数,比较两个 Virtual DOM 节点并返回变化的部分:
function diff(oldNode, newNode) {
const patches = [];
function walk(oldNode, newNode, index) {
if (!newNode) {
patches.push({ type: 'REMOVE', index });
return;
}
if (!oldNode) {
patches.push({ type: 'ADD', newNode, index });
return;
}
if (oldNode.tag !== newNode.tag) {
patches.push({ type: 'REPLACE', newNode, index });
return;
}
if (oldNode.tag) {
const propsPatches = diffProps(oldNode.props, newNode.props);
if (propsPatches) {
patches.push({ type: 'UPDATE_PROPS', index, props: propsPatches });
}
}
const maxLength = Math.max(oldNode.children.length, newNode.children.length);
for (let i = 0; i < maxLength; i++) {
walk(oldNode.children[i], newNode.children[i], i + 1);
}
}
function diffProps(oldProps, newProps) {
const patches = {};
let hasChanges = false;
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches[key] = newProps[key];
hasChanges = true;
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patches[key] = undefined; // 标记删除
hasChanges = true;
}
}
return hasChanges ? patches : null;
}
walk(oldNode, newNode, 0);
return patches;
}
应用示例
结合 Virtual DOM 创建和 diff 算法,我们可以测试功能:
const oldVDOM = createElement('div', { id: 'app' },
createElement('h1', {}, 'Hello World'),
createElement('p', {}, 'This is a virtual DOM example.')
);
const newVDOM = createElement('div', { id: 'app' },
createElement('h1', {}, 'Hello Virtual DOM'),
createElement('p', {}, 'This is an updated virtual DOM example.')
);
const patches = diff(oldVDOM, newVDOM);
console.log(patches);
输出解释
patches
数组将包含需要对 DOM 进行的所有操作,例如:
REMOVE
:删除节点ADD
:添加节点REPLACE
:替换节点UPDATE_PROPS
:更新节点属性
判断矩形相交
要判断两个矩形是否相交,可以使用矩形的边界坐标来进行比较。以下是一个简单的实现方法:
矩形的表示
假设每个矩形用一个对象表示,包含左上角和右下角的坐标:
// 矩形格式: { x1, y1, x2, y2 }
const rect1 = { x1: 1, y1: 1, x2: 4, y2: 4 }; // 矩形1
const rect2 = { x1: 2, y1: 2, x2: 5, y2: 5 }; // 矩形2
判断相交的函数
可以使用以下函数来判断两个矩形是否相交:
function doRectanglesIntersect(rect1, rect2) {
// 如果一个矩形在另一个矩形的左侧、右侧、上方或下方,则不相交
if (
rect1.x2 < rect2.x1 || // rect1 在 rect2 的左侧
rect1.x1 > rect2.x2 || // rect1 在 rect2 的右侧
rect1.y2 < rect2.y1 || // rect1 在 rect2 的上方
rect1.y1 > rect2.y2 // rect1 在 rect2 的下方
) {
return false; // 不相交
}
return true; // 相交
}
// 示例用法
console.log(doRectanglesIntersect(rect1, rect2)); // 输出: true
解释
- 矩形边界比较:函数检查两个矩形的四个边界。若一个矩形完全在另一个的边界外,则它们不相交。
- 返回结果:如果满足任何一个条件,返回
false
,否则返回true
,表示两个矩形相交。
这种方法可以高效地判断两个矩形是否相交。
手写 useFetch()
import { useState, useEffect } from 'react';
const useFetch = (url, options = {}) => {
const [data, setData] = useState(null); // 存储返回的数据
const [loading, setLoading] = useState(true); // 加载状态
const [error, setError] = useState(null); // 错误状态
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true); // 开始加载
setError(null); // 清除之前的错误
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result); // 设置返回的数据
} catch (err) {
setError(err.message); // 设置错误信息
} finally {
setLoading(false); // 加载完成
}
};
fetchData();
}, [url, options]); // 依赖项:URL 和 options 改变时重新调用
return { data, loading, error };
};
export default useFetch;
WebSocket
class WebSocketManager {
constructor(url, maxRetries = 5) {
this.url = url;
this.socket = null;
this.messageQueue = []; // 消息队列
this.isConnected = false;
this.reconnectAttempts = 0; // 当前重连次数
this.maxRetries = maxRetries; // 最大重连次数
this.init();
}
// 初始化
init() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket connected!');
this.isConnected = true;
this.reconnectAttempts = 0; // 重连成功后重置计数
this.flushQueue(); // 重连后发送消息队列中的消息
};
this.socket.onclose = () => {
console.warn('WebSocket disconnect!');
this.isConnected = false;
this.reconnect(); // 尝试重连
};
}
reconnect() {
if (this.reconnectAttempts >= this.maxRetries) {
console.error(
'Max reconnect attempts reached. Please check your network.'
);
return; // 达到最大重连次数后停止重连
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); // 指数退避,最大 30 秒
this.reconnectAttempts++;
console.log(
`Reconnecting WebSocket in ${delay / 1000} seconds... (Attempt ${
this.reconnectAttempts
}/${this.maxRetries})`
);
setTimeout(() => {
this.init(); // 尝试重新初始化 WebSocket
}, delay);
}
// 发送消息
sendMessage(message) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
try {
this.socket.send(message);
console.log('Message sent:', message);
} catch (error) {
console.error('Error sending message:', error);
this.messageQueue.push(message);
}
} else {
console.warn('Connection lost, message queued:', message);
this.messageQueue.push(message); // 入队
}
}
// 清空队列
flushQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift(); // 从队列中取出消息
this.sendMessage(message); // 重发消息
}
}
}
// 使用示例
const wsManager = new WebSocketManager('wss://echo.websocket.org', 3);
// 模拟发送消息
wsManager.sendMessage('Hello, Server!');
wsManager.sendMessage('Another message');
// 模拟断线后重连,未发送的消息会在连接恢复后自动重发
setTimeout(async () => {
await wsManager.socket.close();
wsManager.sendMessage('after close');
}, 3000);
视频请求
class VideoManager {
constructor(videos) {
this.videos = videos; // 视频 URL 列表
this.currentIndex = 0; // 当前正在播放的视频索引
this.cache = {}; // 缓存 blob URL
}
async loadVideo(index) {
if (this.cache[index]) return this.cache[index];
const response = await fetch(this.videos[index]);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
this.cache[index] = url;
// 预加载下一条
if (index + 1 < this.videos.length) {
this.loadVideo(index + 1);
}
// 释放上一条
if (index - 1 >= 0 && this.cache[index - 1]) {
URL.revokeObjectURL(this.cache[index - 1]);
delete this.cache[index - 1];
}
return url;
}
async playVideo(videoElement, index) {
const url = await this.loadVideo(index);
videoElement.src = url;
videoElement.play();
this.currentIndex = index;
}
}
// 使用示例
const videoUrls = ['video1.mp4', 'video2.mp4', 'video3.mp4'];
const manager = new VideoManager(videoUrls);
const videoElement = document.querySelector('video');
document.querySelector('#next').addEventListener('click', () => {
manager.playVideo(videoElement, manager.currentIndex + 1);
});