以下是作为前端面试官常用的10道手撕代码题目,涵盖JavaScript核心、CSS、算法及框架原理,结合高频考点与实际开发场景设计:
1. 手写防抖(debounce)与节流(throttle)
要求:实现防抖函数(延迟执行)和节流函数(固定频率执行),并说明应用场景(如搜索框输入、窗口resize事件)。
1.防抖节流
2. 数组去重的多种实现
要求:用至少3种方法实现(如Set
、filter+indexOf
、reduce
),并分析时间复杂度的差异。
3. 深拷贝与浅拷贝的实现
要求:手写深拷贝函数,处理循环引用、函数、Symbol等特殊类型,对比JSON.parse(JSON.stringify())
的局限性。
前端中常用的数据类型判断方法各有其适用场景和局限性,以下是四种主要方式及其分析:
浅拷贝与深拷贝
深拷贝
1. typeof
作用与示例
• 原理:返回变量类型的字符串表示,适用于基本数据类型判断。
console.log(typeof 1); // 'number'
console.log(typeof 'str'); // 'string'
console.log(typeof undefined); // 'undefined'
• 局限性:
• 无法区分引用类型:数组、对象、null
均返回 'object'
。
• 函数类型特殊处理:typeof function(){}
返回 'function'
,但函数本质仍是对象。
2. instanceof
作用与示例
• 原理:通过原型链检查对象是否为某个构造函数的实例,适合判断引用类型。
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Number(1) instanceof Number); // true
• 局限性:
• 无法判断基本类型字面量:1 instanceof Number
返回 false
。
• 跨窗口失效:不同全局环境(如iframe)的实例可能误判。
• 原型链干扰:所有引用类型(如数组)的原型链最终指向 Object
,导致 [] instanceof Object
返回 true
。
3. constructor
作用与示例
• 原理:通过对象的 constructor
属性获取其构造函数。
console.log([].constructor === Array); // true
console.log('str'.constructor === String); // true
• 局限性:
• 易被修改:若重写对象的 prototype
,constructor
可能丢失或指向错误。
• 无法处理 null
和 undefined
:这两个值没有 constructor
属性。
4. Object.prototype.toString.call()
作用与示例
• 原理:调用对象内部的 [[Class]]
标识,返回格式如 [object Type]
的标准字符串。
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(null)); // '[object Null]'
• 优势:
• 全面性:支持所有数据类型,包括 null
、undefined
和 ES6 新增类型(如 Symbol
)。
• 局限性:
• 无法判断继承关系:如无法区分自定义类实例的具体父类。
总结与建议
- 基本类型:优先使用
typeof
,但需注意null
的误判问题。 - 引用类型:
• 若需判断具体类型(如数组),结合Array.isArray()
或Object.prototype.toString.call()
。
• 避免依赖instanceof
判断跨环境对象。 - 高精度场景:统一使用
Object.prototype.toString.call()
,通过字符串截取获取类型标识(如slice(8, -1)
)。
例如,实现一个通用类型判断函数:
function getType(val) {
if (val === null) return 'null';
const type = typeof val;
if (type !== 'object') return type; // 基本类型直接返回
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
console.log(getType([])); // 'array'
console.log(getType(Symbol())); // 'symbol'
4. Promise并发控制
要求:实现一个函数limitRequest(urls, max)
,限制同时最多处理max
个请求,按顺序返回结果(考察异步控制与队列管理)。
如何应对页面请求接口的大规模并发问题
手写Promise
class Promise {
static resolve(value) {
if (value instanceof Promise) {
return value;
}
return new Promise((resolve) => {
resolve(value);
});
}
static reject(reason) {
return new Promise((_, reject) => {
reject(reason);
});
}
static race(promises) {
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
Promise.resolve(promise).then(resolve, reject);
});
});
}
static all(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completedCount = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
(reason) => {
reject(reason);
}
);
});
});
}
constructor(executor) {
// 状态
this.status = "pending";
// 成功结果
this.value = undefined;
// 失败原因
this.reason = undefined;
// 成功回调函数队列
this.onResolvedCallbacks = [];
// 失败回调函数队列
this.onRejectedCallbacks = [];
let resolve = (value) => {
if (this.status === "pending") {
// 新的值需要是 Promise,则递归解析
if (value instanceof Promise) {
return value.then(resolve, reject);
}
this.status = "fulfilled";
this.value = value;
// 异步执行所有成功回调
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
let reject = (reason) => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
// 异步执行所有失败回调
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
// 执行器本身错误,直接 reject
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected = typeof onRejected === "function" ? onRejected : (reason) => {
throw reason;
};
let context = this;
return new Promise((resolve, reject) => {
function handleOnFulfilled() {
try {
const result = onFulfilled(context.value);
if (result instanceof Promise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
}
function handleOnRejected() {
try {
const result = onRejected(context.reason);
if (result instanceof Promise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
}
if (this.status === "fulfilled") {
setTimeout(() => {
handleOnFulfilled();
}, 0);
}
if (this.status === "rejected") {
setTimeout(() => {
handleOnRejected();
}, 0);
}
// 当状态为 pending 时
if (this.status === "pending") {
// onFulfilled 传入到成功数组
this.onResolvedCallbacks.push(handleOnFulfilled);
// onRejected 传入到失败数组
this.onRejectedCallbacks.push(handleOnRejected);
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(
(value) => Promise.resolve(onFinally()).then(() => value),
(reason) => Promise.resolve(onFinally()).then(() => {
throw reason;
})
);
}
}
// // 示例用法
// const promise1 = new Promise((resolve) => setTimeout(() => resolve('one'), 1000));
// const promise2 = new Promise((resolve) => setTimeout(() => resolve('two'), 2000));
//
// // 使用 Promise.race
// Promise.race([promise1, promise2]).then((value) => {
// console.log('Promise.race result:', value); // 输出: Promise.race result: one
// });
//
// // 使用 Promise.all
// Promise.all([promise1, promise2]).then((values) => {
// console.log('Promise.all result:', values); // 输出: Promise.all result: [ 'one', 'two' ]
// });
//
// // 生成多个测试 Promise
// function generatePromises(count) {
// const promises = [];
// for (let i = 0; i < count; i++) {
// const delay = Math.random() * 1000; // 随机延迟 0 - 1000ms
// promises.push(new Promise((resolve) => {
// setTimeout(() => {
// resolve(i);
// }, delay);
// }));
// }
// return promises;
// }
// // 测试 Promise.race
// function testPromiseRace(count) {
// const promises = generatePromises(count);
// const startTime = performance.now();
// Promise.race(promises).then(() => {
// const endTime = performance.now();
// console.log(`Promise.race with ${count} promises took ${endTime - startTime} ms`);
// });
// }
//
// // 测试 Promise.all
// function testPromiseAll(count) {
// const promises = generatePromises(count);
// const startTime = performance.now();
// Promise.all(promises).then(() => {
// const endTime = performance.now();
// console.log(`Promise.all with ${count} promises took ${endTime - startTime} ms`);
// });
// }
//
// // 测试不同数量的 Promise
// const testCounts = [10, 100, 1000];
// testCounts.forEach((count) => {
// testPromiseRace(count);
// testPromiseAll(count);
// });
5. CSS实现三角形与垂直居中
要求:
• 用CSS画一个三角形(利用border
透明属性);
• 用Flex布局实现元素垂直居中(至少两种方式)。
#triangle {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red;
border-top: 100px solid red;
/*border: 20px solid transparent;*/
/*border-top-color: red;*/
/*border-bottom-color: red;*/
}
6. 括号匹配算法
要求:编写函数判断字符串中的括号是否有效(如"({[]})"
有效,"([)]"
无效),使用栈结构实现。
7. 函数柯里化(Currying)
要求:实现一个通用柯里化函数,支持curry(add)(1)(2)(3)
调用形式,其中add
接收多个参数求和。
8. 数组扁平化(Flatten)
要求:将多维数组展开为一维数组(如[1, [2, [3]]]
转为[1,2,3]
),用递归、reduce
或ES6的flat
方法实现。
9. React/Vue的虚拟DOM对比
要求:手写一个简化版虚拟DOM的Diff算法,比较两棵树的不同并输出差异(考察节点复用与Key的作用)。
10. 实现一个简易的EventEmitter
要求:实现事件订阅(on
)、触发(emit
)、取消订阅(off
)功能,支持异步事件队列。
考察重点
• 代码健壮性:处理边界条件(如空值、异常输入);
• 性能优化:避免不必要的计算或内存泄漏;
• 原理理解:解释实现思路与底层机制(如闭包、原型链、事件循环)。
提示:以上题目可根据候选人经验调整难度(如增加异步流程、复杂数据结构处理)。建议要求候选人现场编码,并逐步追问优化方案(如防抖节流的时间参数动态化、深拷贝的循环引用检测等)。
以下是一些常见 JavaScript 代码实现的详解,结合应用场景和实现原理进行说明:
嗯,用户让我帮忙回答10个JavaScript的手写题目,每个题目都需要详细实现和解释。首先,我得逐一看看每个问题,然后结合提供的搜索结果来整理答案。
第一个题目是防抖和节流
。搜索结果里有很多关于防抖和节流的实现,比如非立即执行和立即执行两种防抖方式。防抖的核心是延迟执行,而节流是固定频率执行。应用场景方面,搜索框输入用防抖,窗口resize可能用节流或防抖,得看具体需求。我需要写出两者的代码,并说明应用场景,注意处理闭包中的变量。
第二个是数组去重
,至少三种方法。搜索结果里提到了Set、filter+indexOf、reduce等方法。Set最简单,时间复杂度是O(n);filter+indexOf因为每次都要检查索引,可能到O(n²);reduce同样可能接近O(n²)。需要比较它们的性能差异。
第三个是深拷贝
,处理循环引用和特殊类型。JSON方法的局限性是不能处理函数和循环引用,递归实现时需要用WeakMap来跟踪已拷贝的对象,处理Date、RegExp等类型。需要对比JSON方法的缺点,并展示如何处理这些情况。
第四个是Promise并发控制
,实现limitRequest函数。搜索结果里提到了使用队列和递归,或者async/await结合Promise.race。需要维护一个执行队列,控制同时进行的请求数量,按顺序返回结果。要注意异步管理和队列的执行顺序。
第五个是CSS画三角形和垂直居中
。三角形用border的透明属性,比如设置三个边透明,另一边显示颜色。垂直居中Flex布局的话,可以用align-items和justify-content,或者用margin:auto的方式。需要给出两种Flex实现方式。
第六个括号匹配
,用栈结构。遇到左括号入栈,右括号时检查栈顶是否匹配。最后栈应该为空。需要处理各种括号类型,确保顺序正确。
第七个函数柯里化
,支持多参数调用。柯里化函数需要递归或闭包保存参数,直到参数数量足够执行原函数。比如curry(add)(1)(2)(3)需要累积参数,当参数数目满足时调用add求和。
第八个数组扁平化
,可以用递归、reduce或flat方法。递归的话遍历数组,将元素展开;reduce方法累加结果;flat方法最简单但可能有兼容性问题。需要比较不同方法的实现。
第九个虚拟DOM的Diff算法
。简化版的话,可以比较节点的类型和属性,处理子节点的差异。Key的作用是复用节点,减少DOM操作。需要手写一个简单的Diff函数,输出差异。
第十个EventEmitter
,实现on、emit、off。使用对象存储事件和回调数组,emit时触发所有回调,支持异步可能需要用Promise或setTimeout。要注意取消订阅时从数组中移除回调,避免内存泄漏。
每个问题都需要结合搜索结果中的代码示例,并解释实现原理和注意事项。比如防抖中的闭包和定时器管理,深拷贝中的循环引用处理,Promise并发中的队列控制等。同时,要引用对应的搜索结果作为参考,确保答案的准确性。
1. 防抖(Debounce)与节流(Throttle)
防抖实现
核心:延迟执行,若重复触发则重置计时器。
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
节流实现
核心:固定时间间隔执行一次。
function throttle(func, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
func.apply(this, args);
lastTime = now;
}
};
}
应用场景
• 防抖:搜索框输入联想(延迟请求)、窗口 resize 停止后计算布局
• 节流:滚动事件触发加载、高频点击按钮防重复提交
2. 数组去重的多种实现
方法 1:Set(时间复杂度 O(n))
const unique = arr => [...new Set(arr)];
方法 2:filter + indexOf(时间复杂度 O(n²))
const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);
方法 3:reduce(时间复杂度 O(n²))
const unique = arr => arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);
性能对比
• Set 最快(哈希表特性),reduce 和 filter 需遍历检查重复项,适合小规模数据
3. 深拷贝与浅拷贝
深拷贝实现(处理循环引用、特殊类型)
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (hash.has(obj)) return hash.get(obj); // 解决循环引用
const clone = new obj.constructor();
hash.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}
JSON 方法的局限性
• 无法处理函数、Symbol、undefined
• 丢失原型链、无法处理循环引用
4. Promise 并发控制
实现 limitRequest(urls, max)
async function limitRequest(urls, max) {
const results = [];
const executing = [];
for (const url of urls) {
const p = fetch(url).then(res => {
executing.splice(executing.indexOf(p), 1);
return res;
});
results.push(p);
executing.push(p);
if (executing.length >= max) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
原理
• 维护执行队列,通过 Promise.race
动态控制并发数
5. CSS 实现三角形与垂直居中
三角形(利用 border)
.triangle {
width: 0;
height: 0;
border: 20px solid transparent;
border-top-color: red;
}
Flex 垂直居中(两种方式)
- 容器设置:
.container {
display: flex;
justify-content: center;
align-items: center;
}
- 子元素设置:
.child {
margin: auto;
}
在前端开发中,实现元素的水平垂直居中是常见的布局需求。以下是几种主流方法及其适用场景,综合多个技术文档整理而来:
一、Flexbox 弹性布局(现代推荐方案)
通过设置父容器为弹性布局,可快速实现子元素居中:
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
该方法无需子元素固定尺寸,支持响应式布局,适用于大多数现代浏览器。
二、Grid 网格布局(高效简洁方案)
利用网格布局的place-items
属性可一键实现居中:
.parent {
display: grid;
place-items: center; /* 同时定义行列对齐 */
}
Grid 布局特别适合复杂布局场景,代码简洁且维护性强。
三、绝对定位 + Transform(传统兼容方案)
适用于需要脱离文档流的元素:
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 自适应宽高 */
}
优势在于不依赖元素尺寸,但需要父容器设置定位上下文。
四、行内元素特殊处理
针对行内或文本元素:
- 单行文本:父元素设置相同的
line-height
与高度 - 多行内容:结合伪元素实现垂直居中:
.parent {
text-align: center;
}
.parent::before {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
}
.child {
display: inline-block;
vertical-align: middle;
}
这种方法适合表单控件、按钮等行内元素的对齐。
五、固定尺寸元素的经典方案
当元素尺寸已知时:
.child {
width: 200px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px; /* 高度的一半 */
margin-left: -100px; /* 宽度的一半 */
}
或使用calc()
动态计算:
.child {
top: calc(50% - 50px);
left: calc(50% - 100px);
}
适合传统项目或需要精确控制尺寸的场景。
扩展建议:
• 响应式布局优先选择 Flex/Grid 方案
• 动态内容推荐使用 Transform 方案
• IE兼容需注意 Grid 布局仅支持 IE10+,Flexbox 需添加-ms-
前缀
• 容器高度未知时,Flex/Grid 仍能保持居中效果
6. 括号匹配算法(栈结构)
实现
function isValidBrackets(str) {
const stack = [];
const map = { '(': ')', '[': ']', '{': '}' };
for (const char of str) {
if (map[char]) {
stack.push(char);
} else if (Object.values(map).includes(char)) {
if (stack.length === 0 || map[stack.pop()] !== char) return false;
}
}
return stack.length === 0;
}
原理
• 左括号入栈,右括号匹配栈顶元素
7. 函数柯里化
通用柯里化函数
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return (...args2) => curried.apply(this, args.concat(args2));
}
};
}
// 示例:const add = (...nums) => nums.reduce((a, b) => a + b, 0);
调用形式
curry(add)(1)(2)(3)()
输出 6
8. 数组扁平化
方法 1:递归
function flatten(arr) {
return arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flatten(cur) : cur), []);
}
方法 2:ES6 flat
const flatten = arr => arr.flat(Infinity);
方法 3:扩展运算符
function flatten(arr) {
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
return arr;
}
9. 虚拟 DOM Diff 算法(简化版)
实现差异对比
function diff(oldNode, newNode) {
const patches = [];
if (oldNode.type !== newNode.type) {
patches.push({ type: 'REPLACE', newNode });
} else if (oldNode.props !== newNode.props) {
patches.push({ type: 'UPDATE_PROPS', props: newNode.props });
}
// 递归对比子节点(需处理 Key 优化复用)
return patches;
}
Key 的作用
• 识别节点身份,减少不必要的重新渲染
10. 简易 EventEmitter
实现
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
(this.events[event] ||= []).push(callback);
}
emit(event, ...args) {
(this.events[event] || []).forEach(cb => cb(...args));
}
off(event, callback) {
this.events[event] = this.events[event]?.filter(cb => cb !== callback);
}
}
异步队列支持
• 在 emit
中使用 setTimeout
或 Promise.resolve()
包裹回调
代码健壮性建议
• 防抖/节流:处理 this
绑定和参数透传
• 深拷贝:使用 WeakMap
避免内存泄漏
• Promise 并发:处理请求失败和重试机制
原理总结
• 闭包:防抖/节流、柯里化通过闭包保存状态
• 原型链:EventEmitter 基于类实现事件管理
• 异步队列:Promise 并发控制依赖事件循环调度