面试必杀技:最全手写JS题精讲与实战指南
2025.09.19 12:47浏览量:0简介:本文整理了面试中高频出现的手写JS题目,涵盖基础语法、异步编程、原型链、闭包等核心知识点,提供详细解析与代码实现,助力开发者攻克技术面试难关。
面试中最全的手写JS题:从基础到进阶的实战指南
在前端技术面试中,手写JS代码是检验开发者基础功底的核心环节。无论是初级工程师还是资深开发者,都可能被要求现场实现某些经典算法或语言特性。本文将系统梳理面试中最常出现的手写JS题,涵盖基础语法、异步编程、原型链、闭包等核心知识点,并提供可运行的代码示例与深度解析。
一、基础语法与数据类型操作
1. 深拷贝实现
题目:实现一个函数,能够深度克隆任意JavaScript对象(包括循环引用)。
解析:深拷贝需要处理多种数据类型(Object、Array、Date、RegExp等),并解决循环引用导致的栈溢出问题。
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型和null/undefined
if (obj === null || typeof obj !== 'object') return obj;
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理特殊对象类型
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 创建对应类型的空对象
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
// 递归拷贝属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
// 处理Symbol属性
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
关键点:
- 使用WeakMap解决循环引用
- 区分基本类型与引用类型
- 特殊对象类型的单独处理
- Symbol属性的拷贝
2. 类型判断函数
题目:实现一个比typeof更准确的类型判断函数。
解析:typeof无法区分Array、Date等对象类型,需要结合Object.prototype.toString。
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
// 测试
console.log(getType([])); // "array"
console.log(getType(new Date())); // "date"
console.log(getType(/regex/)); // "regexp"
优势:
- 统一处理所有类型
- 避免instanceof的跨窗口问题
- 代码简洁高效
二、异步编程与事件循环
1. Promise实现
题目:手写一个简化版Promise,支持then方法链式调用。
解析:需要理解Promise的三种状态(pending/fulfilled/rejected)和微任务队列机制。
class MyPromise {
constructor(executor) {
this.state = 'pending';
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(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) return reject(new TypeError('Chaining cycle detected'));
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 (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
核心机制:
- 状态机管理
- 异步调度(setTimeout模拟微任务)
- 链式调用处理
- 值穿透与Promise解析
2. Async/Await原理模拟
题目:用Generator函数模拟async/await的实现。
解析:理解async函数本质是Generator函数的语法糖。
function asyncToGenerator(generatorFn) {
return function() {
const gen = generatorFn.apply(this, arguments);
return new Promise((resolve, reject) => {
function step(key, arg) {
let record;
try {
record = gen[key](arg);
} catch (err) {
return reject(err);
}
const { value, done } = record;
if (done) return resolve(value);
return Promise.resolve(value).then(
v => step('next', v),
e => step('throw', e)
);
}
step('next');
});
};
}
// 使用示例
const fetchData = asyncToGenerator(function*(){
const res1 = yield fetch('url1');
const res2 = yield fetch('url2');
return res2;
});
三、原型链与继承
1. 实现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 Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, ${this.name}`);
};
const p = myNew(Person, 'Tom');
p.sayHi(); // "Hi, Tom"
执行步骤:
- 创建空对象并设置proto指向构造函数prototype
- 调用构造函数,this指向新对象
- 返回新对象(若构造函数返回对象则返回该对象)
2. 继承实现
题目:实现ES5的类继承(组合寄生式继承)。
解析:最完善的继承方式,解决原型链污染和属性共享问题。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 继承属性
Parent.call(this, name);
this.age = age;
}
// 继承方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
// 测试
const child1 = new Child('Tom', 5);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "black"]
const child2 = new Child('Jerry', 6);
console.log(child2.colors); // ["red", "blue"]
优势:
- 属性独立(通过Parent.call)
- 方法共享(通过原型链)
- 避免直接修改Child.prototype导致的污染
四、性能优化与算法
1. 防抖与节流
题目:实现防抖(debounce)和节流(throttle)函数。
解析:高频事件处理的核心优化手段。
// 防抖:事件触发后等待n秒再执行,若期间再次触发则重新计时
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:固定时间间隔内最多执行一次
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 立即执行版节流
function throttleImmediate(fn, delay) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
应用场景:
- 防抖:搜索框输入、窗口resize
- 节流:滚动事件、鼠标移动
2. 数组扁平化
题目:实现一个函数将多维数组扁平化为一维数组。
解析:考察递归与数组方法的使用。
// 方法1:递归
function flatten(arr) {
let result = [];
for (let item of arr) {
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
}
return result;
}
// 方法2:reduce + 递归
function flatten(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flatten(val) : val);
}, []);
}
// 方法3:ES6展开运算符(仅限一层)
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
// 方法4:Generator函数实现
function* flattenGenerator(arr) {
for (let item of arr) {
if (Array.isArray(item)) {
yield* flattenGenerator(item);
} else {
yield item;
}
}
}
// 测试
const arr = [1, [2, [3, [4]], 5]];
console.log(flatten(arr)); // [1, 2, 3, 4, 5]
五、面试准备建议
- 系统梳理知识体系:按照语言特性、异步编程、设计模式等维度分类整理
- 代码规范训练:注意变量命名、缩进、注释等细节
- 边界条件考虑:始终检查null/undefined、循环引用等特殊情况
- 性能优化意识:在实现中体现对时间复杂度、空间复杂度的考虑
- 沟通表达能力:面试时清晰阐述实现思路和考虑因素
结语
手写JS题考察的是开发者对语言本质的理解程度。本文整理的题目覆盖了前端面试中最核心的知识点,建议读者结合实际项目经验深入理解每个实现背后的原理。在准备面试时,不仅要能够写出正确代码,更要能解释清楚为什么这样实现,以及可能的优化方向。掌握这些基础,将帮助你在技术面试中脱颖而出。
发表评论
登录后可评论,请前往 登录 或 注册