七小AIGC很全面的前端面试题——手写题(上)
手写题是指面试官给出一个功能描述,要求面试者不使用现成的API或库,而是通过自己编写代码来实现这个功能。JavaScript基础知识掌握程度代码实现能力和逻辑思维对底层原理的理解编码规范和最佳实践手写题是前端面试中非常重要的一部分,它直接反映了候选人的基础能力和编程思维。通过本文介绍的手写题,包括数组操作、函数实现、异步编程和DOM操作等方面,希望能够帮助大家更好地准备前端面试。在下一篇文章中,我
很全面的前端面试题——手写题(上)
引言
在前端面试中,手写代码题是考察候选人基础能力和编程思维的重要环节。这类题目通常要求面试者在不依赖外部库的情况下,手动实现一些常见的功能或算法。对于前端开发者来说,掌握这些手写题不仅能帮助你在面试中脱颖而出,更能加深你对JavaScript核心概念的理解。
什么是手写题?
手写题是指面试官给出一个功能描述,要求面试者不使用现成的API或库,而是通过自己编写代码来实现这个功能。这类题目主要考察候选人的:
- JavaScript基础知识掌握程度
- 代码实现能力和逻辑思维
- 对底层原理的理解
- 编码规范和最佳实践
常见手写题及实现
1. 数组相关手写题
1.1 数组去重
// 方法一:使用Set
function uniqueArray1(arr) {
return [...new Set(arr)];
}
// 方法二:使用对象/Map记录
function uniqueArray2(arr) {
const result = [];
const obj = {};
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
result.push(arr[i]);
obj[arr[i]] = true;
}
}
return result;
}
// 方法三:使用filter和indexOf
function uniqueArray3(arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
解析:
- 方法一利用了Set数据结构自动去重的特性
- 方法二通过对象记录已出现过的元素
- 方法三利用indexOf查找元素第一次出现的位置
1.2 数组扁平化
// 方法一:使用递归
function flatten1(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten1(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
// 方法二:使用reduce
function flatten2(arr) {
return arr.reduce((prev, curr) => {
return prev.concat(Array.isArray(curr) ? flatten2(curr) : curr);
}, []);
}
// 方法三:使用ES6的flat方法(虽然不是手写,但作为了解)
function flatten3(arr, depth = 1) {
return arr.flat(depth);
}
解析:
- 方法一使用递归遍历数组,遇到子数组继续递归
- 方法二使用reduce函数简化递归逻辑
- 方法三是ES6内置方法,了解即可
1.3 实现数组的reduce方法
Array.prototype.myReduce = function(callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const arr = this;
let accumulator = initialValue;
let startIndex = 0;
if (initialValue === undefined) {
accumulator = arr[0];
startIndex = 1;
}
for (let i = startIndex; i < arr.length; i++) {
accumulator = callback(accumulator, arr[i], i, arr);
}
return accumulator;
};
// 使用示例
const sum = [1, 2, 3, 4].myReduce((acc, curr) => acc + curr, 0);
console.log(sum); // 10
解析:
- 实现了reduce的核心功能:遍历数组,将上一次回调函数的返回值作为下一次的初始值
- 处理了initialValue可选参数的情况
- 保持了与原生reduce相同的参数顺序和功能
2. 函数相关手写题
2.1 实现call方法
Function.prototype.myCall = function(context, ...args) {
// 如果context为null或undefined,则指向全局对象
context = context || window;
// 将当前函数作为context的一个属性
const fn = Symbol('fn'); // 使用Symbol避免属性名冲突
context[fn] = this;
// 调用函数并获取结果
const result = context[fn](...args);
// 删除添加的属性
delete context[fn];
return result;
};
// 使用示例
const obj = { value: 1 };
function fn(name, age) {
console.log(this.value);
console.log(name, age);
}
fn.myCall(obj, '张三', 18); // 输出: 1, 张三, 18
解析:
- 将当前函数作为context的一个属性调用
- 使用Symbol确保属性名不会与已有属性冲突
- 处理了context为null或undefined的情况
- 调用完成后删除添加的属性
2.2 实现apply方法
Function.prototype.myApply = function(context, args) {
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
// 处理args为null或undefined的情况
let result;
if (args) {
result = context[fn](...args);
} else {
result = context[fn]();
}
delete context[fn];
return result;
};
// 使用示例
const obj = { value: 1 };
function fn(name, age) {
console.log(this.value);
console.log(name, age);
}
fn.myApply(obj, ['李四', 20]); // 输出: 1, 李四, 20
解析:
- 与call类似,但参数以数组形式传入
- 处理了args为null或undefined的情况
- 其他逻辑与call基本相同
2.3 实现bind方法
Function.prototype.myBind = function(context, ...args1) {
const fn = this;
// 返回一个新函数
return function(...args2) {
// 判断是否作为构造函数使用
if (this instanceof fn) {
return new fn(...args1, ...args2);
} else {
return fn.apply(context, [...args1, ...args2]);
}
};
};
// 使用示例
const obj = { value: 1 };
function fn(name, age) {
console.log(this.value);
console.log(name, age);
}
const boundFn = fn.myBind(obj, '王五');
boundFn(25); // 输出: 1, 王五, 25
// 作为构造函数使用
const newObj = new boundFn(30);
console.log(newObj.value); // 输出: undefined
解析:
- 返回一个新函数,该函数可以传入参数
- 处理了作为构造函数使用的情况
- 合并了bind时传入的参数和调用时传入的参数
3. 异步编程相关手写题
3.1 实现Promise
class MyPromise {
constructor(executor) {
this.state = 'pending'; // pending, fulfilled, rejected
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
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; };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// 处理Promise解析
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called = false;
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
// 使用示例
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
}, 1000);
});
promise
.then(value => {
console.log(value);
return '第二次成功';
})
.then(value => {
console.log(value);
})
.catch(error => {
console.error(error);
});
解析:
- 实现了Promise的基本状态管理
- 实现了then方法,支持链式调用
- 处理了Promise解析过程(resolvePromise函数)
- 实现了catch方法
- 处理了循环引用的情况
3.2 实现防抖函数
function debounce(func, wait, immediate) {
let timeout;
return function(...args) {
const context = this;
// 清除之前的定时器
clearTimeout(timeout);
// 如果需要立即执行
if (immediate) {
// 如果没有定时器,则立即执行
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) {
func.apply(context, args);
}
} else {
// 延迟执行
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
};
}
// 使用示例
const handleScroll = debounce(() => {
console.log('滚动事件触发');
}, 300);
window.addEventListener('scroll', handleScroll);
解析:
- 使用setTimeout实现延迟执行
- 每次触发事件都会清除之前的定时器,重新计时
- 支持immediate参数,决定是否立即执行
- 保留了this和参数
3.3 实现节流函数
function throttle(func, limit) {
let inThrottle;
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!inThrottle) {
// 第一次立即执行
func.apply(context, args);
lastRan = Date.now();
inThrottle = true;
} else {
// 清除之前的定时器
clearTimeout(lastFunc);
// 设置新的定时器
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 使用示例
const handleResize = throttle(() => {
console.log('窗口大小改变');
}, 300);
window.addEventListener('resize', handleResize);
解析:
- 使用setTimeout和Date.now()控制执行频率
- 每次触发事件都会重置定时器
- 确保函数在指定的时间间隔内至少执行一次
- 保留了this和参数
4. DOM操作相关手写题
4.1 实现深拷贝
// 方法一:使用JSON.parse和JSON.stringify(有局限性)
function deepClone1(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 方法二:递归实现
function deepClone2(obj, hash = new WeakMap()) {
// 处理null或非对象
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 result = Array.isArray(obj) ? [] : {};
hash.set(obj, result);
// 递归拷贝所有属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone2(obj[key], hash);
}
}
return result;
}
// 使用示例
const obj = {
a: 1,
b: {
c: 2,
d: [3, 4, { e: 5 }]
},
f: new Date(),
g: /abc/g
};
const clonedObj = deepClone2(obj);
console.log(clonedObj);
解析:
- 方法一简单但有局限性(无法处理函数、循环引用等)
- 方法二使用递归和WeakMap处理各种数据类型和循环引用
- 处理了特殊对象类型(Date、RegExp)
- 使用WeakMap避免内存泄漏
4.2 实现一个简单的DOM选择器
function $(selector, context = document) {
// 去除前后空格
selector = selector.trim();
// 处理ID选择器
if (selector.startsWith('#')) {
return context.getElementById(selector.substring(1));
}
// 处理类选择器
if (selector.startsWith('.')) {
return context.getElementsByClassName(selector.substring(1));
}
// 处理标签选择器
return context.getElementsByTagName(selector);
}
// 使用示例
const element = $('#myElement');
console.log(element);
解析:
- 简单实现了ID、类和标签选择器
- 支持指定上下文元素
- 返回的是原生DOM元素或集合
手写题的解题技巧和注意事项
-
理解题目要求:在开始编码前,确保完全理解题目要求的功能和边界条件。
-
考虑各种情况:思考正常情况、边界情况和异常情况,确保代码的健壮性。
-
保持代码简洁:尽量使用简洁明了的代码实现功能,避免过度复杂化。
-
添加注释:在关键步骤添加注释,解释代码逻辑,展示你的思考过程。
-
注意性能:考虑算法的时间复杂度和空间复杂度,选择最优解。
-
遵循最佳实践:注意变量命名、代码格式、错误处理等。
-
测试代码:编写测试用例验证代码的正确性。
总结
手写题是前端面试中非常重要的一部分,它直接反映了候选人的基础能力和编程思维。通过本文介绍的手写题,包括数组操作、函数实现、异步编程和DOM操作等方面,希望能够帮助大家更好地准备前端面试。
在下一篇文章中,我们将继续探讨更多类型的手写题,包括数据结构、算法设计等方面的内容。敬请期待!
*注:本文中的代码示例仅供参考,实际面试中可能需要根据具体要求进行调整和优化。*喜欢的点个关注,每天为大家分享前端全栈、AIGC副业信息差等,更多知识分享尽在【程序员七小AIGC网站
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)