1. 手写Object.create()
MDN解释:以一个现有对象作为原型,创建一个新对象(原链接)。
转换一下:把一个对象放在一个新对象的原型链上
MDN示例:
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
},
};
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // Inherited properties can be overwritten
me.printIntroduction();
// Expected output: "My name is Matthew. Am I human? true"
手写一个Object.create():
// 模拟的create方法,将obj放到新对象的原型链上
Object.prototype.myCreate = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
2. 手写instanceof
MDN解释:用于检测构造函数的
prototype属性是否出现在某个实例对象的原型链上。其返回值是一个布尔值。(原链接)。转换一下:检测某个构造函数是否存在于某个实例的原型链上
MDN示例:
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car("Honda", "Accord", 1998);
console.log(auto instanceof Car);
// 预期输出:true
console.log(auto instanceof Object);
// 预期输出:true
模拟实现instanceof
function myInstanceof(instance, constructor) {
// 获取实例的原型
// Object.getPrototypeOf() 静态方法返回指定对象的原型(即内部 [[Prototype]] 属性的值)。
let proto = Object.getPrototypeOf(instance);
// 获取构造函数的原型对象
let prototype = constructor.prototype;
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 循环原型链
proto = Object.getPrototypeOf(proto);
}
}
3. 手写new
new都干了啥事:
- 创建一个空对象
- 让整个空对象的原型指向构造函数的原型对象,目的是访问构造函数身上的方法和属性
- 让构造函数的
this指向这个新对象,目的是为了能给这个对象添加新的属性和方法- 返回这个新对象
function myNew(constructor, ...args) {
// 第一步和第二步
const newObj = Object.create(constructor.prototype);
// 第三步并执行这个函数
const result = constructor.apply(newObj, args);
// 如果执行了之后返回的是个新对象,就返回新对象,否则返回newObj
return result && (typeof result === 'object' || typeof result === 'function')
? result
: newObj;
}
4. 防抖
对于用户高频操作,在一定时间内只执行一次,如果在该时间内多次触发,这个时间将会被重置
基础版
function debounce(fn, delay) {
let timer = null;
// 核心防抖函数
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
// 由于这个地方是箭头函数,因此this指向的是外层作用域
fn.apply(this, args);
}, delay);
};
return _debounce;
}
带取消函数和立即执行函数版
function debounce(fn, delay) {
let timer = null;
// 保存上次执行的参数
let lastArgs = null;
// 核心防抖函数
const _debounce = function (...args) {
lastArgs = args;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
// 由于这个地方是箭头函数,因此this指向的是外层作用域
fn.apply(this, args);
}, delay);
};
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
};
// 调用后将会立即执行一次
_debounce.flush = function () {
fn.apply(this, lastArgs);
};
return _debounce;
}
5. 节流
在未来的一定时间内,无论触发多少次操作都只执行一次
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
6. 类型判断函数
几种判断类型的函数:
typeof:判断基础类型,无法区分null,对象,数组instanceof:判断实例是否属于某个构造函数,在继承的情况下也能判断Object.prototype.toString.call():判断实例的类型,可以区分null和undefined,返回的是字符串[object Type]
function getType(value) {
if (typeof value === 'object') {
const typeStr = Object.prototype.toString.call(value);
return typeStr?.slice(8, -1)?.toLowerCase();
}
return typeof value;
}
7. 函数柯里化
将一个多参数函数转化为一系列单参数函数的过程
function curry(fn) {
return function curried(...args) {
// 如果参数收集够了就直接执行函数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 否则就返回一个函数,继续收集参数
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
// 测试
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6
右函数柯里化:函数参数从右往左执行
function rightCurry(fn) {
return function curried(...args) {
// 如果参数收集够了就直接执行函数
if (args.length >= fn.length) {
return fn.apply(this, args.reverse());
}
// 否则就返回一个函数,继续收集参数
return (...nextArgs) => curried(...nextArgs, ...args);
};
}
8. 使用setTimeout实现setInterval
为什么不直接使用
setInterval(来自AI)
- 无法保证时间间隔:实际间隔 = 设定间隔 - 回调执行时间
- 任务堆积: 如果回调执行时间超过间隔时间,可能导致任务堆积
- 时间累积误差: 如果某次执行时间过长,会导致下次执行被延迟
基础版本
function mySetIntervalSimple(fn, delay) {
let timer = null;
function run() {
fn();
timer = setTimeout(run, delay);
}
// 启动
timer = setTimeout(run, delay);
// 返回一个清除函数
return () => clearTimeout(timer);
}
携带参数版本
function mySetIntervalWithArgs(fn, delay, ...args) {
let timer = null;
function run() {
fn(...args);
timer = setTimeout(run, delay);
}
timer = setTimeout(run, delay);
return () => clearTimeout(timer);
}
立即执行一次
function mySetIntervalImmediate(fn, delay, immediate = false) {
let timer = null;
function run() {
fn();
timer = setTimeout(run, delay);
}
// 通过timer是否为null来判断是否是第一次执行
if (immediate && timer === null) {
fn();
}
timer = setTimeout(run, delay);
return () => clearTimeout(timer);
}
解决时间偏移问题
function mySetInterval(fn, delay) {
let timer = null;
let startTime = Date.now();
// 用于计算下次执行时间
let count = 0;
function run() {
count++;
fn();
const nextTime = startTime + count * delay;
const interval = nextTime - Date.now();
// 确保延时不为负数
timer = setTimeout(run, Math.max(0, interval));
}
timer = setTimeout(run, delay);
return () => clearTimeout(timer);
}
9. 数组扁平化
完全扁平化
function flattenArray(arr) {
return arr.reduce((acc, item) => {
return Array.isArray(item)
? [...acc, ...flattenArray(item)]
: [...acc, item];
}, []);
}
指定深度扁平化
function flattenArrayWithDepth(arr, depth) {
if (!Array.isArray(arr) || depth <= 0) return arr;
return arr.reduce((acc, item) => {
return Array.isArray(item)
? [...acc, ...flattenArrayWithDepth(item, depth - 1)]
: [...acc, item];
}, []);
}
10. 解析url参数
function parseUrlParams(url) {
const urlObj = new URL(url);
const params = urlObj.searchParams;
const result = {};
params.forEach((value, key) => {
if (result.hasOwnProperty(key)) {
// 兼容处理数组的情况
result[key] = [
...(Array.isArray(result[key]) ? result[key] : [result[key]]),
value,
];
} else {
result[key] = value;
}
});
return result;
}
11. 循环引用判断
function existCircular(obj, visited = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return false;
if (visited.has(obj)) return true;
visited.add(obj);
const values = Object.values(obj);
for (const value of values) {
if (existCircular(value, visited)) return true;
}
visited.delete(obj);
return false;
}
骚操作:让编译器帮忙判断(面试可不能这么写,哈哈哈)
function compileCheckCircular(obj) {
try {
JSON.stringify(obj);
return false;
} catch (error) {
return error.message.includes('circular');
}
}
12. 手写sleep函数
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async function test() {
console.log('start', new Date());
await sleep(1000);
console.log('end', new Date());
}
13. 手写深比较函数isEqual
function isEqual(obj1, obj2, visited = new WeakMap()) {
// 处理基本类型和相同引用
if (obj1 === obj2) {
return true;
}
// 处理NaN的情况
if (Number.isNaN(obj1) && Number.isNaN(obj2)) {
return true;
}
// 类型检查
if (typeof obj1 !== typeof obj2) {
return false;
}
if (obj1 === null || obj2 === null) {
return false; // 到这里说明不全都为null,所以返回false
}
if (typeof obj1 !== 'object') {
return false; // 到这里说明不全都为对象,所以返回false
}
// 循环饮用判断
if (visited.has(obj1)) {
return visited.get(obj1) === obj2;
}
visited.set(obj1, obj2);
// 进行数组类型的判断
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// 如果两个对象的属性长度不一样则一定不相等
if (keys1.length !== keys2.length) {
return false;
}
// 如果两个对象的属性长度一样,则比较每个属性的值
return keys1.every(
(key) => obj2.hasOwnProperty(key) && isEqual(obj1[key], obj2[key], visited),
);
}
// 测试所有问题场景
console.log('=== 测试 null ===');
console.log(isEqual(null, null)); // true ✓
console.log(isEqual(null, {})); // false ✓
console.log(isEqual({}, null)); // false ✓
console.log('\n=== 测试数组 vs 对象 ===');
console.log(isEqual([1, 2, 3], { 0: 1, 1: 2, 2: 3 })); // false ✓
console.log('\n=== 测试 NaN ===');
console.log(isEqual(NaN, NaN)); // true ✓
console.log(isEqual(NaN, 0)); // false ✓
console.log('\n=== 测试循环引用 ===');
const circular1 = { a: 1 };
circular1.self = circular1;
const circular2 = { a: 1 };
circular2.self = circular2;
console.log(isEqual(circular1, circular2)); // true ✓
console.log('\n=== 测试嵌套对象 ===');
console.log(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })); // true ✓
console.log('\n=== 测试数组 ===');
console.log(isEqual([1, 2, 3], [1, 2, 3])); // true ✓
console.log(isEqual([1, [2, 3]], [1, [2, 3]])); // true ✓
console.log(isEqual([1, 2], [1, 2, 3])); // false ✓
14. 手写字符串repeat函数
function repeat(str, n) {
return Array(n).fill(str).join('');
}
15. 异步加载图片
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
});
}
16. 对象键名小驼峰和下划线命名方式互转
/**
* 将键名转换为驼峰命名
* @param {string} key 键名
* @returns {string} 驼峰命名
*/
function keyToCamel(key) {
return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
* 将键名转换为下划线命名
* @param {string} key 键名
* @returns {string} 下划线命名
*/
function keyToSnake(key) {
return key.replace(/([A-Z])/g, '_$1').toLowerCase();
}
/**
* 将对象的键名转换为驼峰命名
* @param {object} obj 对象
* @param {function} transformFn 转换函数
* @returns {object} 转换后的对象
*/
function keyTransform(obj, transformFn) {
// 先进行是否为对象类型的判断
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 对于数组内的元素进行递归处理
if (Array.isArray(obj)) {
return obj.map(keyTransform);
}
const result = {};
Object.keys(obj).forEach((key) => {
const camelKey = transformFn(key);
result[camelKey] = keyTransform(obj[key]);
});
return result;
}
17. 数组交并差集
/**
* 求两个数组的交集:两个数组中都有的元素
* @param {Array} arr1 数组1
* @param {Array} arr2 数组2
* @returns {Array} 交集
*/
function intersection(arr1, arr2) {
return arr1.filter((item) => arr2.includes(item));
}
/**
* 求两个数组的并集:两个数组中所有的元素
* @param {Array} arr1 数组1
* @param {Array} arr2 数组2
* @returns {Array} 并集
*/
function union(arr1, arr2) {
return [...new Set([...arr1, ...arr2])];
}
/**
* 求两个数组的差集:存在于arr1中,但不存在于arr2中的元素
* @param {Array} arr1 数组1
* @param {Array} arr2 数组2
* @returns {Array} 差集
*/
function difference(arr1, arr2) {
return arr1.filter((item) => !arr2.includes(item));
}
18. 树形结构与数组的互相转化
示例数据:
// 数据结构:复杂的树形结构示例
const tree = [
{
id: 1,
text: '根节点1',
parentId: 0,
level: 1,
type: 'folder',
children: [
{
id: 2,
text: '子节点1-1',
parentId: 1,
level: 2,
type: 'folder',
children: [
{
id: 5,
text: '孙节点1-1-1',
parentId: 2,
level: 3,
type: 'file',
},
{
id: 6,
text: '孙节点1-1-2',
parentId: 2,
level: 3,
type: 'file',
},
],
},
{
id: 3,
text: '子节点1-2',
parentId: 1,
level: 2,
type: 'folder',
children: [
{
id: 7,
text: '孙节点1-2-1',
parentId: 3,
level: 3,
type: 'folder',
children: [
{
id: 10,
text: '曾孙节点1-2-1-1',
parentId: 7,
level: 4,
type: 'file',
},
],
},
],
},
{
id: 4,
text: '子节点1-3',
parentId: 1,
level: 2,
type: 'file',
},
],
},
{
id: 8,
text: '根节点2',
parentId: 0,
level: 1,
type: 'folder',
children: [
{
id: 9,
text: '子节点2-1',
parentId: 8,
level: 2,
type: 'file',
},
{
id: 11,
text: '子节点2-2',
parentId: 8,
level: 2,
type: 'folder',
children: [
{
id: 12,
text: '孙节点2-2-1',
parentId: 11,
level: 3,
type: 'file',
},
],
},
],
},
{
id: 13,
text: '根节点3',
parentId: 0,
level: 1,
type: 'file',
},
];
// 扁平数组结构(对应上面的树形结构)
const objArr = [
// 根节点
{ id: 1, text: '根节点1', parentId: 0, level: 1, type: 'folder' },
{ id: 8, text: '根节点2', parentId: 0, level: 1, type: 'folder' },
{ id: 13, text: '根节点3', parentId: 0, level: 1, type: 'file' },
// 根节点1的子节点
{ id: 2, text: '子节点1-1', parentId: 1, level: 2, type: 'folder' },
{ id: 3, text: '子节点1-2', parentId: 1, level: 2, type: 'folder' },
{ id: 4, text: '子节点1-3', parentId: 1, level: 2, type: 'file' },
// 根节点2的子节点
{ id: 9, text: '子节点2-1', parentId: 8, level: 2, type: 'file' },
{ id: 11, text: '子节点2-2', parentId: 8, level: 2, type: 'folder' },
// 子节点1-1的孙节点
{ id: 5, text: '孙节点1-1-1', parentId: 2, level: 3, type: 'file' },
{ id: 6, text: '孙节点1-1-2', parentId: 2, level: 3, type: 'file' },
// 子节点1-2的孙节点
{ id: 7, text: '孙节点1-2-1', parentId: 3, level: 3, type: 'folder' },
// 子节点2-2的孙节点
{ id: 12, text: '孙节点2-2-1', parentId: 11, level: 3, type: 'file' },
// 孙节点1-2-1的曾孙节点
{ id: 10, text: '曾孙节点1-2-1-1', parentId: 7, level: 4, type: 'file' },
];
数组转为树形结构
function arrayToTree(arr) {
const map = {};
const result = [];
// 创建映射并初始化 children
arr.forEach((item) => {
map[item.id] = { ...item, children: [] };
});
// 建立关系
arr.forEach((item) => {
if (item.parentId === 0) {
result.push(map[item.id]);
} else if (map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
return result;
}
树形结构展开
function treeToArray(tree) {
const result = [];
const transform = (tmpTree) => {
tmpTree.forEach((node) => {
if (node?.children?.length) {
transform(node.children);
delete node.children;
}
result.push(node);
});
};
transform(tree);
return result;
}