面试必知:最全手写JS题解析与实战指南
2025.09.19 12:47浏览量:0简介:本文汇总了面试中最常见的手写JS题,涵盖数据类型、函数、原型链、异步处理等核心知识点,通过详细解析和代码示例帮助开发者系统掌握手写JS题的解题思路,提升面试通过率。
面试中最全的手写JS题解析
在前端开发面试中,手写JavaScript代码题是考察候选人基础能力的核心环节。这类题目不仅能检验开发者对语言特性的理解,还能体现其编码规范和问题拆解能力。本文将系统梳理面试中最常出现的手写JS题,从基础到进阶逐层解析,帮助读者构建完整的知识体系。
一、数据类型与基础操作
1. 类型判断:实现typeof
的增强版
原生typeof
存在局限性(如无法区分Array
和Object
),面试常要求实现更精准的类型判断函数。
function getType(value) {
const typeStr = Object.prototype.toString.call(value);
return typeStr.match(/\[object (.*?)\]/)[1].toLowerCase();
}
// 测试用例
getType([]); // "array"
getType(null); // "null"
getType(/regex/); // "regexp"
关键点:
- 使用
Object.prototype.toString
获取标准类型标识 - 通过正则提取实际类型名称
- 覆盖所有原始类型和引用类型
2. 深拷贝实现
深拷贝是面试高频题,需处理循环引用、特殊对象(如Date、RegExp)等边界情况。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
let clone;
if (obj instanceof Date) clone = new Date(obj);
else if (obj instanceof RegExp) clone = new RegExp(obj);
else {
clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
}
return clone;
}
优化方向:
- 使用
WeakMap
避免内存泄漏 - 特殊对象单独处理
- 递归实现时注意栈溢出风险
二、函数与高阶编程
1. 柯里化(Currying)实现
柯里化要求将多参数函数转换为单参数函数的嵌套调用。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
}
// 使用示例
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3); // 6
应用场景:
- 参数复用
- 延迟执行
- 函数组合
2. 防抖与节流
这两个函数是性能优化的经典手段,需清晰区分实现差异。
// 防抖:事件触发后等待n秒再执行
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
}
}
// 节流:固定时间间隔执行一次
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
}
}
选择依据:
- 防抖适合输入框联想等场景
- 节流适合滚动事件、鼠标移动等高频事件
三、原型链与继承
1. 实现new
操作符
需理解构造函数执行过程:创建对象、关联原型、执行构造方法、返回对象。
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
// 测试
function Person(name) {
this.name = name;
}
const p = myNew(Person, 'Tom');
console.log(p.name); // "Tom"
执行步骤:
- 创建空对象并关联原型
- 调用构造函数,绑定
this
- 处理构造函数返回值
2. 多种继承方式实现
需掌握原型链继承、构造函数继承、组合继承等模式的差异。
// 组合继承示例
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承
Child.prototype.constructor = Child;
const child1 = new Child('Tom', 18);
child1.colors.push('blue');
console.log(child1.colors); // ["red", "blue"]
const child2 = new Child('Jerry', 20);
console.log(child2.colors); // ["red"]
模式对比:
| 继承方式 | 优点 | 缺点 |
|————————|—————————————|—————————————|
| 原型链继承 | 实现简单 | 共享引用属性 |
| 构造函数继承 | 可传参、不共享属性 | 无法继承原型方法 |
| 组合继承 | 结合两者优点 | 调用两次父构造函数 |
四、异步编程
1. 实现Promise
需完整实现then
的链式调用、状态变更、异步决议等特性。
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 : value => value;
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;
}
}
// 辅助函数:处理thenable对象和循环引用
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);
}
}
实现要点:
- 三种状态管理
- 异步执行回调
- 链式调用支持
- 循环引用检测
2. 实现Async/Await
基于Generator函数实现简易版Async/Await。
function asyncToGenerator(generatorFn) {
return function(...args) {
const gen = generatorFn.apply(this, args);
return new Promise((resolve, reject) => {
function step(key, arg) {
let info;
try {
info = gen[key](arg);
} catch (error) {
return reject(error);
}
const { value, done } = info;
if (done) {
return resolve(value);
}
Promise.resolve(value).then(
v => step('next', v),
e => step('throw', e)
);
}
step('next');
});
}
}
// 使用示例
const asyncFunc = asyncToGenerator(function* () {
const a = yield 1;
const b = yield a + 2;
return b + 3;
});
asyncFunc().then(console.log); // 6
工作原理:
- 将Generator函数包装为Promise
- 通过
step
函数控制执行流程 - 自动处理
yield
表达式的异步决议
五、算法与数据结构
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;
}
// 优化版(支持深度参数)
function flattenDeep(arr, depth = 1) {
return depth > 0
? arr.reduce((prev, cur) =>
prev.concat(Array.isArray(cur) ? flattenDeep(cur, depth - 1) : cur),
[])
: arr.slice();
}
应用场景:
- 处理API返回的嵌套数据
- 数据预处理
2. 实现LRU缓存
需结合Map数据结构和双向链表实现高效操作。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // 重新插入实现最近使用
return value;
}
return -1;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
性能分析:
get
和put
操作时间复杂度均为O(1)- 利用Map保持插入顺序的特性
六、备考建议
- 系统梳理知识体系:按数据类型、函数、异步、算法等维度分类整理
- 刻意练习:每天手写3-5道典型题,注重代码规范和边界处理
- 理解原理:不仅要会写,更要明白为什么这样实现
- 模拟面试:找同伴进行模拟面试,训练表达能力和临场反应
- 查看源码:研究Lodash、Axios等库的实现,学习最佳实践
通过系统准备和针对性练习,开发者可以显著提升手写JS题的解题能力,在面试中展现出扎实的基本功和良好的编码素养。记住,面试官考察的不仅是代码的正确性,更是解决问题的思路和工程化思维。
发表评论
登录后可评论,请前往 登录 或 注册