手写题(面试)
1.Promise
手写Promise,Promise.all,Promise.race,Promise.allSettled,Promise实现并发控制,最多请求三次
2.发布订阅模式
https://juejin.cn/post/7052637219084828680?searchId=20250812094909B7F1AD4D7354DC8DF37E#heading-2
3.new
function myNew(Constructor, ...args) {// 1. 创建空对象,原型指向构造函数的原型const obj = Object.create(Constructor.prototype);// 2. 执行构造函数,绑定 thisconst result = Constructor.apply(obj, args);// 3. 判断返回值:只有对象或函数才替换,其余一律返回 objif (result !== null && (typeof result === 'object' || typeof result === 'function')) {return result;}return obj;
}
4.深拷贝,浅拷贝
// 1. 修正后的浅拷贝
function shallow(obj){const newObj = {};for(const key in obj){newObj[key] = obj[key];}return newObj;
}// 2. 修正后的深拷贝
function deepCopy(obj){const newObj = Array.isArray(obj) ? [] : {};for(const key in obj){if(obj.hasOwnProperty(key)){if(obj[key] !== null && typeof obj[key] === 'object'){newObj[key] = deepCopy(obj[key]);}else{newObj[key] = obj[key];}}}return newObj;
}// 3. 测试用例:嵌套对象
const original = { a: 1, b: { c: 2 } };// 4. 分别拷贝
const s = shallow(original);
const d = deepCopy(original);// 5. 修改内部对象
original.b.c = 999;// 6. 打印结果
console.log('原始对象', original); // { a:1, b:{c:999} }
console.log('浅拷贝结果', s); // { a:1, b:{c:999} } ← 跟着变
console.log('深拷贝结果', d); // { a:1, b:{c:2} } ← 没变
5.防抖节流
function debounce(fn,delay){let timer = nullconst _dounce = function(...args){if(timer){clearTimeout(timer)}time = setTimeout(() => {fn.apply(this,args)}, delay);}return _dounce
}function throttle(fn,delay){let last = 0const _throttle =function(...args){const now = new Date().getTime()if((now-last)>=delay){fn.apply(this,args)last = now}}return _throttle
}// 打印函数
const log = (type, i) => console.log(type, i, Date.now());// 包装
const debouncedLog = debounce((i) => log('debounce', i), 300);
const throttledLog = throttle((i) => log('throttle', i), 300);// 连续调用 20 次,间隔 50ms
for (let i = 0; i < 20; i++) {setTimeout(() => {debouncedLog(i);throttledLog(i);}, i * 50);
}
6.instanceof
function myInstanceof(left, right) {// 1. 获取构造函数的 prototypeconst prototype = right.prototype;// 2. 获取对象的原型(__proto__)let proto = Object.getPrototypeOf(left);// 3. 沿着原型链向上查找while (proto !== null) {if (proto === prototype) {return true; // 找到了,返回 true}proto = Object.getPrototypeOf(proto); // 继续往上}return false; // 链结束也没找到,返回 false
}console.log(myInstanceOf([], Array)) // true
console.log(myInstanceOf([], Object)) // true(原型链:[] → Array.prototype → Object.prototype → null)
console.log(myInstanceOf(123, Number)) // false(原始值没有原型链)
7.数组与树🌲相互转化
// 数组转树
function arrayToTree(list, root = null) {const map = {}; // id -> nodeconst tree = [];// 先把所有节点放入 map,方便 O(1) 查找for (const node of list) {map[node.id] = { ...node, children: [] };}// 再挂到父节点上for (const node of list) {const treeNode = map[node.id];if (node.parentId == null) {tree.push(treeNode); // 根节点} else {const parent = map[node.parentId];parent && parent.children.push(treeNode);}}return tree;
}/* ====== 测试 ====== */
const arr = [{ id: 1, name: 'root', parentId: null },{ id: 2, name: 'child1', parentId: 1 },{ id: 3, name: 'child2', parentId: 1 },{ id: 4, name: 'grandson', parentId: 2 },
];
console.log(JSON.stringify(arrayToTree(arr), null, 2));// 树转数组
function treeToArray(tree) {const res = [];function dfs(nodes) {if (!Array.isArray(nodes)) return;for (const node of nodes) {const { children, ...item } = node; // 去掉 childrenres.push(item);dfs(children);}}dfs(tree);return res;
}/* ====== 测试 ====== */
const tree = [{id: 1,name: 'root',children: [{id: 2,name: 'child1',children: [{ id: 4, name: 'grandson' }],},{ id: 3, name: 'child2' },],},
];
console.log(treeToArray(tree));
8.数组、对象扁平化
// 数组扁平化
function flat(arr,depth = 1){let res = []for(let i = 0;i<arr.length;i++){if(Array.isArray(arr[i])&&depth){res = res.concat(flat(arr[i]),depth-1)}else{res.push(arr[i])}}return res
}// 对象扁平化function ObjcetFlat(obj = {}){const res = {}function flat(item,preKey = ''){Object.entries(item).forEach(([key,val])=>{const newKey = preKey?'${preKey}.${key}':'key'if(val&&typeof(val)=='object'){flat(val,newKey)}else{res[newKey] = val}})}flat(obj)return res}
9.Ajax、Axios、Fetch
// Ajax
const xhr = new XMLHttpRequest()
xhr.open('GET',url,true)
xhr.onreadystatechange = function(){if(this.readyState!==4){return}if(this.status==200){console.log(this.response)}else{throw new Error(xhr.statusText)}
}
xhr.send
// Fetch
fetch('https://jsonplaceholder.typicode.com/posts/1').then(response => response.json()) // 将响应转换为 JSON 格式.then(data => console.log(data)) // 处理成功的响应.catch(error => console.error('Error:', error)); // 处理错误fetch('https://jsonplaceholder.typicode.com/posts', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({title: 'foo',body: 'bar',userId: 1,}),
})
.then(response => response.json()) // 将响应转换为 JSON 格式
.then(data => console.log(data)) // 处理成功的响应
.catch(error => console.error('Error:', error)); // 处理错误
// Axios
axios.get('wwww.sadasda')
.then(response=>{console.log(response)
}).catch(err=>{console.log(err)
})axios.post('www.adsad',{id:'',name:''
}).then(response=>{}).catch(err=>{})
10.数组Api
Array.prototype.map = function(fn) {
const res = []
for(let i = 0; i < this.length; i++) {
res.push(fn(this[i], i, this))
}
return res
}Array.prototype.filter = function(fn) {
const res = []
for(let i = 0; i < this.length; i++) {
if(fn(this[i], i, this)) {
res.push(this[i])
}
}
return res
}Array.prototype.reduce = function(fn, initValue) {
let res, start = 0
if(arguments.length !== 1) {
res = initValue
} else {
res = this[0]
start = 1
}
for(let i = start; i < this.length; i++) {
res = fn(res, this[i], i, this)
}
return res
}
11.URL解析
const parseUrl = (url) => {
const tmpUrl = url.split("?")[1]
const resObj = {}
for(const str of tmpUrl.split("&")) {
let [key, value] = str.split("=")
value = decodeURIComponent(value)
if(resObj.hasOwnProperty(key)) {
resObj[key] = [].concat(resObj[key], value)
} else if(value == "undefined") { // !!!
resObj[key] = true
} else {
resObj[key] = value
}
}
return resObj
}
12.对象比较
13.虚拟dom转化为真实dom
function createRealDOM(vdom) {// 1. 处理文本节点:如果vdom是字符串或数字,直接创建文本节点if (typeof vdom === 'string' || typeof vdom === 'number') {return document.createTextNode(vdom);}// 2. 创建DOM元素const element = document.createElement(vdom.type);// 3. 处理属性(包括事件监听器)const props = vdom.props || {};Object.keys(props).forEach(propName => {// 跳过children属性(单独处理)if (propName === 'children') return;// 处理事件监听器(以'on'开头的属性)if (propName.startsWith('on')) {const eventType = propName.toLowerCase().substring(2);element.addEventListener(eventType, props[propName]);} // 处理特殊属性(如className、htmlFor)else if (propName === 'className') {element.setAttribute('class', props[propName]);} else if (propName === 'htmlFor') {element.setAttribute('for', props[propName]);}// 处理style对象else if (propName === 'style' && typeof props.style === 'object') {Object.assign(element.style, props.style);}// 处理普通HTML属性else {element.setAttribute(propName, props[propName]);}});// 4. 递归处理子节点const children = props.children || [];// 统一处理:将单个子节点转换为数组const childNodes = Array.isArray(children) ? children : [children];childNodes.forEach(child => {// 跳过空子节点(null/undefined/boolean)if (child != null && child !== false) {element.appendChild(createRealDOM(child));}});return element;
}// 示例使用
const virtualDOM = {type: 'div',props: {className: 'container',onClick: () => console.log('Clicked!'),style: { color: 'red', fontSize: '20px' },children: [{ type: 'h1', props: { children: 'Hello World' } },{ type: 'p', props: { className: 'text',children: ['This is a ',{ type: 'b', props: { children: 'demo' } }]} }]}
};const realDOM = createRealDOM(virtualDOM);
document.body.appendChild(realDOM);
14.call
Function.prototype.call = function (thisArg, ...args) {// 1. 拿到要执行的那个函数(即 fn = 调用 call 的函数)const fn = this;// 2. 把 thisArg 包装成对象// - 如果是原始值(1、"abc"、true 等),用 Object(...) 变成包装对象(Number、String、Boolean)// - 如果是 null / undefined,则默认指向全局对象(浏览器里是 window)thisArg = thisArg != null ? Object(thisArg) : window;// 3. 用唯一 Symbol 作为属性名,临时把 fn 挂到 thisArg 上const tag = Symbol('call');thisArg[tag] = fn;// 4. 通过对象方法调用的方式执行函数,从而把 this 绑定到 thisArgconst res = thisArg[tag](...args);// 5. 执行完后删掉临时属性,避免污染delete thisArg[tag];// 6. 把函数返回值返回return res;
};//把函数 fn 临时挂到 thisArg 上,再当成对象方法调用,就能让 fn 里的 this 指向 thisArg。用 Symbol 做键名保证不会冲突,并在最后清理
15.计数器
function App() {const [count, setCount] = React.useState(0)return (<div className="text-center"><button onClick ={()=>{setCount(count-1)}}>减少</button><p>当前计数:{count}</p><button onClick={() => setCount(count + 1)}>增加</button></div>)
}
优化点:加减提成函数,并且写成函数式更新const handleAdd = () => setCount(prev => prev + 1);
原因:当把「更新函数」传给异步逻辑或其他组件时,用 setState(prev => ...) 可以彻底摆脱闭包旧值陷阱;直接写 setState(count + 1) 则会把当时的 count 锁死
假设用户快速点击“增加”按钮 3 次:const [count, setCount] = useState(0);
const handleClick = () => {setCount(count + 1); // ❌ 问题就出在这里!setCount(count + 1);setCount(count + 1);
};你期望结果是:0 → 3
但实际结果可能是:0 → 1因为 count 是函数组件在某次渲染时的快照(闭包)。
即使状态已经更新,count 的值在本次事件处理函数中仍然是旧的。
// 在第一次渲染时,count = 0
// 即使你调用了 setCount,count 还是 0,直到组件重新渲染
16.倒计时
import React, { useState, useEffect } from 'react';function SimpleCountdown() {const [count, setCount] = useState(10); // 倒计时初始值const [isActive, setIsActive] = useState(false); // 是否正在运行useEffect(() => {let timer;if (isActive && count > 0) {timer = setTimeout(() => {setCount(count - 1);}, 1000);}return () => clearTimeout(timer); // 清除定时器}, [count, isActive]);// 开始const start = () => setIsActive(true);// 暂停const pause = () => setIsActive(false);// 重置const reset = () => {setIsActive(false);setCount(10);};return (<div style={{ textAlign: 'center', marginTop: '50px' }}><h1>倒计时: {count}</h1><div>{!isActive ? (<button onClick={start} disabled={count === 0}>开始</button>) : (<button onClick={pause}>暂停</button>)}<button onClick={reset} style={{ marginLeft: '10px' }}>重置</button></div></div>);
}export default SimpleCountdown;
17.TodoList
import React, { useState } from 'react';
function App(){const [todo,setTodo] = useState([]) // todo列表const [input,setInput] = useState('') // 输入框文字const [editText,setEditText] = useState('') // 修改的文字const [editIndex,setEditIndex] = useState(-1) //修改的是哪个// 增加任务function add(){if(input.trim()){setTodo([...todo,input])setInput('')}}// 删除任务function deleteTodo(index){setTodo(todo.filter((item,i)=>i!==index))}//开始编辑function startEdit(index){setEditIndex(index)setEditText(todo[index])}// 保存编辑function saveEdit(){const newTodos = [...todo]newTodos[editIndex] = editTextsetTodo(newTodos)setEditIndex(-1)}// 8. 取消编辑的函数const cancelEdit = () => {setEditIndex(-1); // 直接退出编辑模式,不保存};return (<div><div>Todo List</div><input value = {input}onChange = {(e)=>{setInput(e.target.value)}}placeholder = "请输入任务"></input><button onClick ={add}>增加任务</button><ul>{todo.map((item,index)=>(<li key ={index}>{editIndex === index?(<><inputvalue = {editText}onChange = {(e)=>{setEditText(e.target.value)}}></input><button onClick ={()=>{saveEdit()}}>保存</button><button onClick={cancelEdit}>取消</button></>):(<>{item}<button onClick={()=>{startEdit(index)}}>修改</button><button onClick={()=>{deleteTodo(index)}}>删除</button></>)}</li>))}</ul></div>)
}
export default App;
18.排序方法
// 1、冒泡排序--大的向后排小的向前排
function bubbleSortFn(arr) {for (let i = arr.length - 1 ; i >= 1; i--) {for(let j = 0; j < i; j++) {if (arr[j] > arr[j + 1]) {let temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}return arr;
}
console.log('bubbleSortFn:', bubbleSortFn([12, 33, 1, 3, 49, -1, 88]))
// 2、选择排序--选择出小的向前排
function selectSortFn(arr) {for(let i = 0; i < arr.length; i++) {// 默认第一次最小的值下标let min = i;// 向后比较for (let j = i + 1; j < arr.length; j++) {if (arr[j] < arr[min]) {// 如果有更小的min = j;}};// 把小的换前面来let temp = arr[i];arr[i] = arr[min];arr[min] = temp;}return arr;
}
console.log('selectSortFn:', selectSortFn([12, 33, 1, 3,2, 49, -1, 88]))
// 3、插入排序--局部有序
function insertSortFn(arr) {// 默认第一个是有序的for(let i = 1; i < arr.length; i++) {let temp = arr[i];let j = i;while(arr[j - 1] > temp && j > 0) {arr[j] = arr[j - 1];j--;}arr[j] = temp;}return arr;
}
console.log('insertSortFn:', insertSortFn([12, 33, 1, 3,2, 49, -1, 88]))
// 3、插入排序--局部有序
function insertSortFn(arr) {// 默认第一个是有序的for(let i = 1; i < arr.length; i++) {let temp = arr[i];let j = i;while(arr[j - 1] > temp && j > 0) {arr[j] = arr[j - 1];j--;}arr[j] = temp;}return arr;
}
console.log('insertSortFn:', insertSortFn([12, 33, 1, 3,2, 49, -1, 88]))
Array.prototype.quickSort = function () { const rec = (arr) => { // 预防数组是空的或者只有一个元素, 当所有元素都大于等于基准值就会产生空的数组if(arr.length === 1 || arr.length === 0) { return arr; }const left = [];const right = [];//以第一个元素作为基准值 const mid = arr[0];//小于基准值的放左边,大于基准值的放右边for(let i = 1; i < arr.length; ++i) { if(arr[i] < mid) { left.push(arr[i]);} else { right.push(arr[i]);}}//递归调用,最后放回数组 return [...rec(left),mid,...rec(right)];};const res = rec(this);res.forEach((n,i) => { this[i] = n; })
}const arr = [2,3,4,5,3,1];
arr.quickSort();
console.log(arr);
19.函数珂里化
function curry(fn) {return function curried(...args) {// 如果参数数量足够,执行原函数if (args.length >= fn.length) {return fn.apply(this, args);} else {// 参数不足,返回新函数继续接收参数return function(...nextArgs) {return curried.apply(this, args.concat(nextArgs));}}}
}// 使用示例
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