前端代码题
数组 Array
1. 数组去重
// 方法一:使用 Set
function uniqueArray(arr) {
return [...new Set(arr)];
}
// 方法二:使用 filter
function uniqueArray(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 方法三:使用 reduce
function uniqueArray(arr) {
return arr.reduce((acc, cur) => {
return acc.includes(cur) ? acc : [...acc, cur];
}, []);
}
2. 数组扁平化
// 方法一:使用 flat
function flatten(arr) {
return arr.flat(Infinity);
}
// 方法二:递归
function flatten(arr) {
return arr.reduce((acc, cur) => {
if (Array.isArray(cur)) {
acc.push(...flatten(cur));
} else {
acc.push(cur);
}
return acc;
}, []);
}
// 方法三:使用 toString
function flatten(arr) {
return arr
.toString()
.split(",")
.map((item) => Number(item));
}
3. 数组排序
// 冒泡排序
// 每次比较相邻的两个元素,如果前一个元素比后一个元素大,则交换它们的位置
// 每次循环结束后,最大的元素会被交换到最后一个位置
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
// 快速排序
// 选择一个基准元素,将数组分为两部分,一部分小于基准元素,一部分大于基准元素
// 然后对两部分分别进行快速排序
// 时间复杂度:O(nlogn)
// 空间复杂度:O(logn)
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[0];
// const left = arr.slice(1).filter((item) => item <= pivot);
// const right = arr.slice(1).filter((item) => item > pivot);
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
4. 数组乱序
function shuffle(arr) {
const newArr = [...arr];
for (let i = newArr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArr[i], newArr[j]] = [newArr[j], newArr[i]];
}
return newArr;
}
字符串 String
1. 字符串反转
function reverseString(str) {
return str.split("").reverse().join("");
}
// 或者使用递归
function reverseString(str) {
if (str.length <= 1) return str;
return reverseString(str.slice(1)) + str[0];
}
2. 判断回文字符串
function isPalindrome(str) {
const cleanStr = str.toLowerCase().replace(/[^a-z0-9]/g, "");
return cleanStr === cleanStr.split("").reverse().join("");
}
3. 字符串转驼峰
function toCamelCase(str) {
return str.replace(/[-_\s]+(.)?/g, (match, char) => {
return char ? char.toUpperCase() : "";
});
}
函数 Function
1. 防抖函数
频繁触发事件,只执行最后一次
function debounce(func, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
2. 节流函数
频繁触发事件,每隔一段时间执行一次
function throttle(func, limit) {
let inThrottle = null;
return function (...args) {
if (inThrottle) return;
func.apply(this, args);
inThrottle = setTimeout(() => {
inThrottle = null;
}, limit);
};
}
3. 柯里化函数
function curry(fn) {
const arity = fn.length; // 获取原始函数的参数个数
function curried(...args) {
if (args.length >= arity) {
return fn(...args); // 参数够了,直接调用原始函数
} else {
return (...nextArgs) => curried(...args, ...nextArgs); // 参数不够,继续返回一个函数收集参数
}
}
return curried;
}
function add(a, b, c) {
return a + b + c;
}
// 使用示例
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
console.log(curriedAdd(1, 2, 3)); // 输出 6
4. 实现 call
Function.prototype.myCall = function (context, ...args) {
/**
* 1. 确定 this 的指向
* globalThis 是全局对象,在浏览器中是 window,在 Node.js 中是 global
* 如果 context 为 null 或 undefined,则指向 globalThis
* 如果 context 为原始值,则需要使用 Object 包装为对象
*/
context = context === null || context === undefined ? globalThis : Object(context);
/**
* 2. 执行函数,函数就是 this,因为 this 会指向函数 myCall 的调用者
* 给 context 对象临时添加一个属性 fn,指向调用 myCall 的函数
* 使用 Symbol 确保属性名唯一,避免覆盖原有属性
* 使用 Object.defineProperty 确保属性不可枚举
*/
const fn = this;
const fnKey = Symbol("fn");
Object.defineProperty(context, fnKey, {
value: fn,
enumerable: false,
});
/**
* 3. 执行函数,并传入参数
* 使用 context[fnKey] 调用函数,并传入参数
* 使用 delete 删除 context 对象上的 fn 属性
* 返回函数执行结果
*/
const result = context[fnKey](...args);
delete context[fnKey];
return result;
};
function method(a, b) {
console.log(this.name, a, b);
}
const obj = {
name: "John",
};
method.myCall(obj, 1, 2);
5. 实现 apply
Function.prototype.myApply = function (context, args) {
context = context || window;
// 给 context 对象临时添加一个属性 fn,指向调用 myApply 的函数
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
6. 实现 bind
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function (...moreArgs) {
return fn.apply(context, args.concat(moreArgs));
};
};
对象 Object
1. 深拷贝
// 递归拷贝属性值,使用 WeakMap 解决循环引用问题
function deepClone(obj, hash = new WeakMap()) {
// 1. 递归出口
if (obj === null || typeof obj !== "object") return obj;
// 如果是 Date 或 RegExp 对象,则直接返回新的实例
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 2. 如果已经拷贝过,则直接返回拷贝后的结果
if (hash.has(obj)) return hash.get(obj);
// 3. 创建一个新的对象或数组
const cloneObj = Array.isArray(obj) ? [] : {};
// 将拷贝后的对象或数组添加到 WeakMap 中
hash.set(obj, cloneObj);
// 4. 遍历对象的属性,递归拷贝属性值
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
/**
* 也可以用 Object.keys(obj) 循环
* 它只会遍历 对象自身的可枚举属性,不会遍历原型链上的属性
* 所以不需要再额外写 hasOwnProperty 检查
*/
// 5.返回拷贝后的对象或数组
return cloneObj;
}
2. 实现 instanceof
// 用于判断一个值是否是某个构造函数的实例
function myInstanceof(left, right) {
// 获取 left 的原型(等价于 left.__proto__,但更安全)
let proto = Object.getPrototypeOf(left);
// 获取 right 的 prototype
const prototype = right.prototype;
// 循环判断,直到 proto 为 null 为止
while (true) {
// 如果 proto 为 null,则返回 false
if (!proto) return false;
// 如果 proto 等于 prototype,则返回 true
if (proto === prototype) return true;
// 否则,继续向上查找原型链
proto = Object.getPrototypeOf(proto);
}
}
// 使用示例
console.log(myInstanceof(1, Number)); // false
3. 实现 new
// 实现 new 操作符
function myNew(constructor, ...args) {
// 1. 创建一个空对象
const obj = Object.create(constructor.prototype);
// 2. 将空对象作为 this 调用构造函数
const result = constructor.apply(obj, args);
// 3. 如果构造函数返回的是对象,则返回该对象,否则返回新创建的对象
return result instanceof Object ? result : obj;
}
function myNew2(constructor, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 将空对象的原型指向构造函数的原型
obj.__proto__ = constructor.prototype;
// 3. 将空对象作为 this 调用构造函数
const result = constructor.apply(obj, args);
// 4. 如果构造函数返回的是对象,则返回该对象,否则返回新创建的对象
return result instanceof Object ? result : obj;
}
// 使用示例
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = myNew(Person, "John", 20);
console.log(person); // { name: "John", age: 20 }
4. 扁平数据转树形结构
const arr = [
{ id: 1, name: "Item 1", pid: 0 },
{ id: 2, name: "Item 2", pid: 1 },
{ id: 3, name: "Item 3", pid: 1 },
{ id: 4, name: "Item 4", pid: 3 },
{ id: 5, name: "Item 5", pid: 4 },
];
const tree = {
id: 1,
name: "Item 1",
pid: 0,
children: [
{
id: 2,
name: "Item 2",
pid: 1,
children: [],
},
{
id: 3,
name: "Item 3",
pid: 1,
children: [
{
id: 4,
name: "Item 4",
pid: 3,
children: [
{
id: 5,
name: "Item 5",
pid: 4,
children: [],
},
],
},
],
},
],
};
/** 1️⃣ 数组转对象 */
const arrayToTree = (arr) => {
// 1. 初始化根节点和查找表
let obj = {};
const lookup = {};
// 2. 创建查找表,每个元素以其 id 为键存储在 lookup 中,并初始化 children 属性为空数组
arr.forEach((item) => {
lookup[item.id] = { ...item, children: [] };
});
// 3. 构建树结构
arr.forEach((item) => {
// 获取当前元素在查找表中的引用,也就是一个树节点
const treeItem = lookup[item.id];
if (item.pid === null || item.pid === 0) {
// 1. 如果当前元素是根节点,直接将其赋值给根节点
obj = treeItem;
} else {
// 2. 如果当前元素有父节点,将其添加到父节点的 children 数组中
const parent = lookup[item.pid];
if (parent) {
parent.children.push(treeItem);
}
}
});
// 4. 返回根节点数组,这个数组包含所有的树结构
return obj;
};
const result = arrayToTree(arr);
console.dir(result, { depth: null });
console.log(JSON.stringify(result, null, 2));
/** 2️⃣ 对象转数组 */
const treeToArray = (tree) => {
const result = [];
// 递归函数
const traverse = (node) => {
// 移除 children 属性
const { children, ...item } = node;
result.push(item);
// 递归遍历 children
if (children?.length) {
children.forEach((child) => traverse(child));
}
};
traverse(tree);
return result;
};
异步 Promise
1. 实现 Promise
/** 定义 Promise 类*/
class MyPromise {
// 构造函数:接收一个执行器函数 executor
constructor(executor) {
// 初始化 Promise 的状态和值
this.status = "pending"; // 初始状态:等待中
this.value = undefined; // 成功值:当 Promise 被 resolve 时存储的值
this.reason = undefined; // 失败原因:当 Promise 被 reject 时存储的错误信息
this.onFulfilledCallbacks = []; // 成功回调队列:存储所有 then 方法中注册的成功回调函数
this.onRejectedCallbacks = []; // 失败回调队列:存储所有 then 方法中注册的失败回调函数
// 定义 resolve 方法:将 Promise 状态从 pending 变为 fulfilled
const resolve = (value) => {
// 如果状态不是 pending,则直接返回,确保 Promise 状态只能改变一次
if (this.status !== "pending") return;
// 使用 queueMicrotask 确保回调函数在微任务队列中执行,模拟 Promise 的异步特性
queueMicrotask(() => {
// 如果 resolve 的值是一个 Promise 实例,则需要等待该 Promise 完成
if (value instanceof MyPromise) {
value.then(resolve, reject);
} else {
// 将状态改为 fulfilled,并存储成功值
this.status = "fulfilled";
this.value = value;
// 执行所有注册的成功回调函数
this.onFulfilledCallbacks.forEach((fn) => fn(value));
}
});
};
// 定义 reject 方法:将 Promise 状态从 pending 变为 rejected
const reject = (reason) => {
// 如果状态不是 pending,则直接返回,确保 Promise 状态只能改变一次
if (this.status !== "pending") return;
// 使用 queueMicrotask 确保回调函数在微任务队列中执行
queueMicrotask(() => {
// 将状态改为 rejected,并存储失败原因
this.status = "rejected";
this.reason = reason;
// 执行所有注册的失败回调函数
this.onRejectedCallbacks.forEach((fn) => fn(reason));
});
};
// 执行 executor 函数,并传入 resolve 和 reject 方法
try {
executor(resolve, reject);
} catch (err) {
// 如果 executor 执行过程中抛出异常,则调用 reject 方法
reject(err);
}
}
/**
* then 方法:注册 Promise 状态改变后的回调函数
* 实例方法:通过 Promise 实例调用,例如 promise.then(onFulfilled, onRejected)
*
* @param {Function} onFulfilled - 成功时的回调函数,接收 Promise 的解决值
* @param {Function} onRejected - 失败时的回调函数,接收 Promise 的拒绝原因
* @returns {MyPromise} 返回一个新的 Promise,实现链式调用
*
* 使用示例:
* new MyPromise(resolve => resolve(42))
* .then(value => value * 2)
* .then(value => console.log(value)) // 输出: 84
*/
then(onFulfilled, onRejected) {
// 返回一个新的 Promise,实现链式调用
return new MyPromise((resolve, reject) => {
// 定义处理成功的回调函数
const handleFulfilled = (value) => {
try {
// 如果 onFulfilled 是一个函数,则执行它并处理其返回值
if (typeof onFulfilled === "function") {
const result = onFulfilled(value);
// 处理 then 方法返回的 Promise 或其他值
resolvePromise(result, resolve, reject);
} else {
// 如果 onFulfilled 不是函数,则直接将值传递给下一个 Promise
resolve(value);
}
} catch (err) {
// 如果回调函数执行过程中抛出异常,则调用 reject 方法
reject(err);
}
};
// 定义处理失败的回调函数
const handleRejected = (reason) => {
try {
// 如果 onRejected 是一个函数,则执行它并处理其返回值
if (typeof onRejected === "function") {
const result = onRejected(reason);
// 处理 then 方法返回的 Promise 或其他值
resolvePromise(result, resolve, reject);
} else {
// 如果 onRejected 不是函数,则直接将错误传递给下一个 Promise
reject(reason);
}
} catch (err) {
// 如果回调函数执行过程中抛出异常,则调用 reject 方法
reject(err);
}
};
// 根据当前 Promise 的状态执行不同的逻辑
if (this.status === "fulfilled") {
// 如果当前 Promise 已经是 fulfilled 状态,则异步执行成功回调
queueMicrotask(() => handleFulfilled(this.value));
} else if (this.status === "rejected") {
// 如果当前 Promise 已经是 rejected 状态,则异步执行失败回调
queueMicrotask(() => handleRejected(this.reason));
} else {
// 如果当前 Promise 是 pending 状态,则将回调函数添加到队列中
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
}
/**
* catch 方法:注册 Promise 失败时的回调函数
* 实例方法:通过 Promise 实例调用,例如 promise.catch(onRejected)
*
* @param {Function} onRejected - 失败时的回调函数,接收 Promise 的拒绝原因
* @returns {MyPromise} 返回一个新的 Promise,实现链式调用
*
* 使用示例:
* new MyPromise((_, reject) => reject(new Error('出错了')))
* .catch(err => console.error(err)) // 输出: Error: 出错了
*/
catch(onRejected) {
return this.then(undefined, onRejected);
}
/**
* finally 方法:无论 Promise 成功还是失败,都会执行的回调函数
* 实例方法:通过 Promise 实例调用,例如 promise.finally(callback)
*
* @param {Function} callback - 无论成功还是失败都会执行的回调函数
* @returns {MyPromise} 返回一个新的 Promise,实现链式调用
*
* 使用示例:
* new MyPromise(resolve => resolve(42))
* .then(value => console.log(value)) // 输出: 42
* .finally(() => console.log('完成')) // 输出: 完成
*/
finally(callback) {
return this.then(
// 成功时执行 callback 并返回原值
(value) => {
callback();
return value;
},
// 失败时执行 callback 并抛出原错误
(reason) => {
callback();
throw reason;
}
);
}
/**
* 静态方法:直接通过类名调用的方法,不需要创建类的实例
* 例如:MyPromise.resolve(42) 而不是 new MyPromise(...).resolve(42)
*
* 静态 resolve 方法:创建一个已解决的 Promise
* 使用场景:当需要将一个值转换为 Promise 时
* 示例:MyPromise.resolve(42).then(value => console.log(value)) // 输出: 42
*/
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
/**
* 静态 reject 方法:创建一个已拒绝的 Promise
* 使用场景:当需要创建一个表示错误的 Promise 时
* 示例:MyPromise.reject(new Error('出错了')).catch(err => console.error(err))
*/
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
/**
* 静态 all 方法:等待所有 Promise 完成,返回一个包含所有结果的数组
* 使用场景:当需要并行执行多个异步操作,并等待所有操作完成时
* 特点:如果任何一个 Promise 失败,整个 all 方法返回的 Promise 也会失败
* 示例:
* MyPromise.all([
* fetch('/api/users'),
* fetch('/api/posts')
* ]).then(([users, posts]) => console.log(users, posts))
*/
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = []; // 存储所有 Promise 的结果
let count = 0; // 计数器,记录已完成的 Promise 数量
// 遍历所有 Promise
promises.forEach((p, i) => {
// 确保每个项都是 Promise 实例
MyPromise.resolve(p).then((value) => {
// 将结果存储在对应位置,保持顺序
results[i] = value;
count++;
// 当所有 Promise 都完成时,返回结果数组
if (count === promises.length) {
resolve(results);
}
}, reject); // 如果任何一个 Promise 失败,则整个 all 方法失败
});
});
}
/**
* 静态 race 方法:返回第一个完成的 Promise 的结果
* 使用场景:当需要从多个异步操作中获取最快完成的结果时
* 特点:无论成功还是失败,只要有一个 Promise 完成,race 方法就会返回其结果
* 示例:
* MyPromise.race([
* fetch('/api/fast-server'),
* fetch('/api/slow-server')
* ]).then(result => console.log('最快返回的结果:', result))
*/
static race(promises) {
return new MyPromise((resolve, reject) => {
// 遍历所有 Promise
promises.forEach((p) => {
// 确保每个项都是 Promise 实例
MyPromise.resolve(p).then(resolve, reject); // 第一个完成的 Promise 将决定整个 race 的结果
});
});
}
}
/** 内部工具函数:处理 then 方法返回值为 Promise 的情况 */
function resolvePromise(result, resolve, reject) {
// 如果返回值是 Promise 实例,则等待其完成
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
// 如果返回值不是 Promise 实例,则直接传递给下一个 Promise
resolve(result);
}
}
2. 实现 Promise.all
const myPromiseAll = (promises) => {
// 检查传入的是否为一个可迭代对象
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError("Argument must be an iterable"));
}
// 返回一个新的 Promise
return new Promise((resolve, reject) => {
// 定义结果数组
const results = [];
// 定义变量存储完成的 Promise 数量
let completedPromises = 0;
// 如果是空数组
if (promises.length === 0) {
return resolve(results);
}
// 循环遍历 Promise 数组
promises.forEach((promise, index) => {
// 使用 Promise.resolve 确保每个项都是一个 Promise,因为数组中传入的可能不是 Promise,而是一个值
Promise.resolve(promise).then(
(value) => {
// 保证返回结果的数组顺序不变
results[index] = value;
completedPromises++;
// 如果所有的 Promise 都完成,则返回解决的 Promise
if (completedPromises === promises.length) {
resolve(results);
}
},
(reason) => {
// 如果有一个 Promise 被拒绝,则返回拒绝的 Promise
reject(reason);
}
);
});
});
};
DOM
1. 事件委托
function delegate(element, eventType, selector, handler) {
element.addEventListener(eventType, function (event) {
const target = event.target;
if (target.matches(selector)) {
handler.call(target, event);
}
});
}
2. 获取元素位置
function getElementPosition(element) {
const rect = element.getBoundingClientRect();
return {
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset,
width: rect.width,
height: rect.height,
};
}
算法题
1. 两数之和
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
return [];
}
2. 最长回文子串
function longestPalindrome(s) {
let start = 0,
maxLength = 1;
function expandAroundCenter(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}
return right - left - 1;
}
for (let i = 0; i < s.length; i++) {
const len1 = expandAroundCenter(i, i);
const len2 = expandAroundCenter(i, i + 1);
const len = Math.max(len1, len2);
if (len > maxLength) {
start = i - Math.floor((len - 1) / 2);
maxLength = len;
}
}
return s.substring(start, start + maxLength);
}
3. 无重复字符的最长子串
function lengthOfLongestSubstring(s) {
const map = new Map();
let maxLength = 0;
let start = 0;
for (let i = 0; i < s.length; i++) {
if (map.has(s[i])) {
start = Math.max(start, map.get(s[i]) + 1);
}
map.set(s[i], i);
maxLength = Math.max(maxLength, i - start + 1);
}
return maxLength;
}
4. 合并两个有序数组
function merge(nums1, m, nums2, n) {
let i = m - 1,
j = n - 1,
k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
5. 有效的括号
function isValid(s) {
const stack = [];
const map = {
")": "(",
"}": "{",
"]": "[",
};
for (let char of s) {
if (char === "(" || char === "{" || char === "[") {
stack.push(char);
} else {
if (stack.pop() !== map[char]) {
return false;
}
}
}
return stack.length === 0;
}
6. 爬楼梯
function climbStairs(n) {
if (n <= 2) return n;
let prev = 1,
curr = 2;
for (let i = 3; i <= n; i++) {
const temp = curr;
curr = prev + curr;
prev = temp;
}
return curr;
}
7. 二叉树遍历
// 前序遍历
function preorderTraversal(root) {
const result = [];
function traverse(node) {
if (!node) return;
result.push(node.val);
traverse(node.left);
traverse(node.right);
}
traverse(root);
return result;
}
// 中序遍历
function inorderTraversal(root) {
const result = [];
function traverse(node) {
if (!node) return;
traverse(node.left);
result.push(node.val);
traverse(node.right);
}
traverse(root);
return result;
}
// 后序遍历
function postorderTraversal(root) {
const result = [];
function traverse(node) {
if (!node) return;
traverse(node.left);
traverse(node.right);
result.push(node.val);
}
traverse(root);
return result;
}
概念题
3. 发布订阅模式
class EventEmitter {
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));
} else {
console.error(`事件 ${eventName} 不存在`);
}
}
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter((cb) => cb !== callback);
}
}
}
const eventEmitter = new EventEmitter();
eventEmitter.on("click", () => {
console.log("click");
});
eventEmitter.emit("click");
4. 观察者模式
/** 发布者 */
class Subject {
constructor() {
// 存储观察者列表
this.observerLists = [];
}
// 添加观察者
addObs(obs) {
// 判断观察者是否有和存在更新订阅的方法
if (obs && obs.update) {
// 添加到观察者列表中
this.observerLists.push(obs);
}
}
// 通知观察者
notify() {
this.observerLists.forEach((obs) => {
// 每个观察者收到通知后 会更新事件
obs.update();
});
}
// 清空观察者
empty() {
this.observerLists = [];
}
}
/** 观察者 */
class Observer {
// 定义观察者内容更新事件
update() {
// 在更新事件要处理的逻辑
console.log("目标更新了");
}
}
// 创建发布者
const sub = new Subject();
// 创建观察者
const obs1 = new Observer();
const obs2 = new Observer();
// 把观察者添加到列表中
sub.addObs(obs1);
sub.addObs(obs2);
// 发布者开启了通知 每个观察者者都会自己触发 update 更新事件
sub.notify();
5. 实现 useEffect
// useEffect(setup, dependencies?);
const createEffectManager = () => {
let cleanup;
let dependencies = [];
const useEffect = (setup, deps) => {
const isChanged = dependencies.some((dep, i) => dep !== deps[i]);
if (isChanged || !dependencies.length) {
if (typeof cleanup === "function") {
cleanup();
}
cleanup = setup();
dependencies = deps;
}
};
return { useEffect };
};
const effectManager = createEffectManager();
const MyComponent = () => {
effectManager.useEffect(() => {
console.log("Effect has been run");
return () => {
console.log("Cleanup previous effect");
};
}, []);
};
MyComponent(); // First render
MyComponent(); // Second render with same dependencies
6. 请求并发
// 现有 100 个请求需要发送,请设计一个算法,使用 Promise 来控制并发(并发数量最大为 10)
/** 模拟 100 个请求 */
const requestList = [];
for (let i = 1; i <= 100; i++) {
requestList.push(
() =>
new Promise((resolve, reject) => {
// 模拟随机错误
setTimeout(() => {
if (i === 92) {
reject(new Error("出错了,出错请求:" + i));
} else {
console.log("done", i);
resolve(i);
}
}, Math.random() * 5000);
// setTimeout(() => {
// console.log('done', i);
// resolve(i);
// }, Math.random() * 1000);
})
);
}
/** 方法一 Promise.all() */
// 如果有一个请求失败了,这一组的请求就都没有返回值了
const parallelRun = async (max) => {
const requestSliceList = [];
for (let i = 0; i < requestList.length; i += max) {
requestSliceList.push(requestList.slice(i, i + max));
}
for (let i = 0; i < requestSliceList.length; i++) {
const group = requestSliceList[i];
try {
const res = await Promise.all(group.map((fn) => fn()));
console.log("接口返回值为:", res);
} catch (error) {
console.error(error);
}
}
};
// parallelRun(10);
/** 方法二 Promise.allSettled() */
// 如果有一个请求很耗时,该组请求就会堵塞,导致后续请求很慢
const parallelRun2 = async (max) => {
const requestSliceList = [];
for (let i = 0; i < requestList.length; i += max) {
requestSliceList.push(requestList.slice(i, i + max));
}
for (let i = 0; i < requestSliceList.length; i++) {
const group = requestSliceList[i];
try {
// 使用 allSettled 替换 all
const res = await Promise.allSettled(group.map((fn) => fn()));
console.log("接口返回值为:", res);
} catch (error) {
console.error(error);
}
}
};
// parallelRun2(10);
/** 方法三 运行池和等待队列 */
const pool = new Set();
const waitQueue = [];
/**
* @description: 限制并发数量的请求
* @param {*} reqFn:请求方法
* @param {*} max:最大并发数
*/
const request = (reqFn, max) => {
return new Promise((resolve, reject) => {
// 判断运行池是否已满
const isFull = pool.size >= max;
// 包装的新请求,每个异步请求完成后,将该请求从运行池中删除,同时从等待队列中取一个新请求放入运行池中执行
const newReqFn = () => {
reqFn()
.then((res) => {
resolve(res);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
// 请求完成后,将该请求从运行池中删除
pool.delete(newReqFn);
// 从等待队列中取出一个新请求放入运行池执行
const next = waitQueue.shift();
if (next) {
pool.add(next);
next();
}
});
};
if (isFull) {
console.log("🚀🚀🚀 full: ");
// 如果运行池已满,则将新的请求放到等待队列中
waitQueue.push(newReqFn);
} else {
console.log("🚀🚀🚀 not full: ");
// 如果运行池未满,则向运行池中添加一个新请求并执行该请求
pool.add(newReqFn);
newReqFn();
}
});
};
requestList.forEach(async (item, index) => {
console.log("🚀🚀🚀 index: ", index);
const res = await request(item, 10);
console.log(res);
});
// 现成库 p-limit
7. 实现 computed 函数
const memory = (fn) => {
// 缓存对象,用于存储函数的计算结果
const cache = new Map();
// 返回一个新的函数
return function (...args) {
// 将参数转换为字符串,用作缓存的键
const key = JSON.stringify(args);
// 如果缓存中存在结果,则直接返回缓存结果
if (cache.has(key)) {
return cache.get(key);
}
// 否则,调用原函数计算结果
const result = fn(...args);
// 将结果存入缓存中
cache.set(key, result);
// 返回计算结果
return result;
};
};
// 示例用法
const complexCalculation = (num) => {
console.log("计算中...");
return num * num;
};
const memoizedCalculation = memory(complexCalculation);
console.log(memoizedCalculation(5)); // 计算中... 25
console.log(memoizedCalculation(5)); // 25(从缓存中读取,不会再次计算)
console.log(memoizedCalculation(6)); // 计算中... 36
console.log(memoizedCalculation(6)); // 36(从缓存中读取,不会再次计算)
8. 任务队列的中断和恢复
要求
- 依次顺序执行一系列任务
- 所有任务全部完成后可以得到每个任务的执行结果
- 需要返回两个方法,start 用于启动任务,pause 用于暂停任务
- 每个任务具有原子性,即不可中断,只能在两个任务之间中断
function processTasks(...tasks) {
const results = [];
let isRunning = false;
let currentIndex = 0;
return {
/** 开始执行任务 */
start() {
return new Promise((resolve) => {
// 如果任务正在执行,则返回一个 Promise,表示任务正在执行
if (isRunning) return;
isRunning = true;
// 执行任务的函数
const _executeTasks = async () => {
while (currentIndex < tasks.length) {
const task = tasks[currentIndex++];
const result = await task();
results.push(result);
if (!isRunning) return;
}
isRunning = false;
resolve(results);
};
// 执行任务
_executeTasks();
});
},
/** 暂停执行任务 */
pause() {
isRunning = false;
},
};
}
9. SEO 优化
页面结构优化
- 合理使用语义化标签
- Meta 标签优化
- URL 结构清晰
内容优化
- 关键字布局,关键字自然出现在标题、段落首尾和图片的 alt 等位置
- 内容丰富,多使用多媒体内容,图片使用 alt 属性
- 使用 a 标签实现内部链接优化串联,链接文本也可以包含关键字
性能优化
- 减少阻塞资源,link 使用 preload 或 prefetch 预加载
- 图片懒加载
- 减少 HTTP 请求,使用 CDN
- 设置合理的缓存策略,减少重复请求
10. 网站爬虫原理
1. 爬虫的基本流程
种子 URL
- 爬虫启动时,先拿到一批初始 URL(种子 URL)。
- 这些 URL 可能是网站首页或已知页面列表。
抓取页面
- 爬虫向 URL 发送 HTTP 请求(GET),获取 HTML、JSON 等内容。
- 会处理响应状态码:200 表示成功,404/500 表示失败。
解析页面
- 爬虫解析 HTML 或 API 返回的数据。
- 提取:页面文本、标题、关键词、图片、链接等。
- 常用技术:正则、DOM 解析库(如 cheerio)、XPath。
提取链接 & 入队列
- 页面中新的 URL 被加入待抓取队列(Queue)。
- 爬虫重复抓取新 URL,形成广度优先或深度优先的抓取策略。
数据存储
- 抓取和解析后的数据保存到数据库或索引系统(如 Elasticsearch)。
去重 & 避免循环
- 通过 URL 哈希、数据库或 Bloom Filter 避免重复抓取。
2. 技术细节
请求模拟
- 爬虫可能需要模拟浏览器行为:设置 User-Agent、Cookie、Referer。
- 对 AJAX 或动态渲染页面,可能需要用 Headless 浏览器(如 Puppeteer、Playwright)渲染 JS。
抓取策略
- 深度优先 DFS:先抓取当前页面的链接,再抓它的子页面。
- 广度优先 BFS:先抓取当前层级的所有链接,再抓下一层。
- 可设置优先级、延迟和并发控制,避免过载网站。
反爬机制应对
- IP 封禁 / 请求频率限制
- 验证码 / 登录限制
- 动态内容 / JS 渲染
- User-Agent 检测
- 爬虫通常需要轮换 IP、设置请求间隔、模拟浏览器。
3. 搜索引擎爬虫特性(SEO 相关)
搜索引擎的爬虫(如 Googlebot)有一些特殊优化:
优先抓取高价值页面
- 通过 PageRank 或内部链接权重,先抓重要页面。
增量抓取
- 只抓新内容或更新内容,节省资源。
抓取 JS 动态内容
- 现代搜索引擎可以执行 JS,但比 SSR 页面慢。
抓取策略控制
- 遵循 robots.txt(告诉爬虫哪些页面不抓取)。
- 支持 sitemap.xml,快速发现网站 URL。
✅ 总结
爬虫的核心就是:
请求网页 → 解析内容 → 提取 URL → 入队抓取 → 存储数据,
并配合策略、去重和反爬应对。
搜索引擎爬虫只是这个过程的专业版本,还会考虑抓取优先级、增量抓取和 JS 渲染。
11. 如何实现浏览器内多个标签页之间的通信?
- 使用 websocket 协议。因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。
- 监听 localStorage 的变化。因为 localStorage 的变化会触发 storage 事件,所以可以通过监听 storage 事件来实现标签页之间的通信。
- 使用新的 API,BroadcastChannel。