手写代码是前端面试的重要环节,不仅能考察JavaScript基础功底,更能体现编程思维和代码能力。本文整理了2026年前端面试中最常见的手写题目,助你备战金三银四。
在当今的前端面试中,手写代码环节几乎成为必考项。面试官通过现场编码,考察求职者对JavaScript核心概念的掌握程度、代码实现能力和问题解决思路。本文将根据最新的面试趋势,为大家分类整理前端高频手写题。
一、JavaScript基础篇
1. 类型判断与原型相关
手写instanceof:判断构造函数的prototype是否出现在实例的原型链上。
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
let prototype = right.prototype;
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
手写new操作符:创建实例对象,链接原型,绑定this,执行构造函数。
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;
}
2. 函数与方法实现
手写call/apply/bind:改变函数执行时的this指向。
// call实现
Function.prototype.myCall = function(context, ...args) {
context = context || window;
const fn = Symbol();
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};
// bind实现(考虑new操作符的情况)
Function.prototype.myBind = function(context, ...args) {
const _this = this;
return function F(...args2) {
if (this instanceof F) {
return new _this(...args, ...args2);
}
return _this.apply(context, args.concat(args2));
};
};
函数柯里化:将多参数函数转换为一系列单参数函数。
function curry(fn, len = fn.length) {
return function _curry(...args) {
if (args.length >= len) {
return fn.apply(this, args);
} else {
return function(...args2) {
return _curry.apply(this, args.concat(args2));
};
}
};
}
二、数组操作篇
1. 数组去重多种方案
// 方法1:Set(最简洁)
function unique(arr) {
return [...new Set(arr)];
}
// 方法2:filter + indexOf
function unique2(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 方法3:reduce(可处理复杂数据类型)
function unique3(arr) {
return arr.reduce((acc, cur) => {
return acc.includes(cur) ? acc : [...acc, cur];
}, []);
}
2. 数组扁平化
// 递归reduce实现
function flatten(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
// 迭代实现(避免递归栈溢出)
function flatten2(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
// 使用ES2019 flat方法
function flatten3(arr) {
return arr.flat(Infinity);
}
3. 数组与树结构转换
扁平数组转树形结构(常见于菜单、目录等场景):
function arrayToTree(arr) {
const map = {};
const tree = [];
// 建立id到节点的映射
arr.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 建立父子关系
arr.forEach(item => {
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
} else {
tree.push(map[item.id]);
}
});
return tree;
}
三、异步编程篇
1. Promise实现
Promise是异步编程的核心,手写Promise能深刻理解异步机制。
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 (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);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'REJECTED') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
// Promise解决过程,处理thenable对象和循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(value => {
results[index] = value;
count++;
if (count === promises.length) {
resolve(results);
}
}, reject);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(resolve, reject);
});
});
}
}
2. 异步控制函数
防抖(Debounce):连续触发时,只在最后执行一次。
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
节流(Throttle):连续触发时,在一定时间间隔内只执行一次。
// 时间戳版本(立即执行)
function throttle(fn, delay) {
let start = Date.now();
return function(...args) {
const now = Date.now();
if (now - start >= delay) {
fn.apply(this, args);
start = now;
}
};
}
// 定时器版本(延迟执行)
function throttle2(fn, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
四、数据处理篇
1. 深拷贝
深拷贝需要考虑循环引用、特殊对象类型等多种边界情况。
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 cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
// 递归拷贝属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
2. 对象创建与继承
手写Object.create:使用现有对象作为新对象的原型。
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
五、实用功能篇
1. URL参数解析
function parseUrl(url) {
const params = {};
const urlObj = new URL(url);
urlObj.searchParams.forEach((value, key) => {
if (params[key] !== undefined) {
params[key] = [].concat(params[key], value);
} else {
params[key] = value;
}
});
return params;
}
2. 模板引擎简单实现
function templateEngine(template, data) {
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
}
3. 千分位格式化
function formatNumber(num) {
const [integer, decimal] = num.toString().split('.');
const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decimal ? `${formattedInteger}.${decimal}` : formattedInteger;
}
六、算法与数据结构篇
1. 排序算法
快速排序:分治思想,选择基准元素进行分区。
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
2. 括号匹配检查
function isBalanced(expression) {
const stack = [];
const brackets = { '(': ')', '[': ']', '{': '}' };
for (let char of expression) {
if (brackets[char]) {
stack.push(char);
} else if (Object.values(brackets).includes(char)) {
if (stack.length === 0 || brackets[stack.pop()] !== char) {
return false;
}
}
}
return stack.length === 0;
}
备考建议与面试技巧
-
理解原理优于死记硬背:掌握每个手写题背后的设计思想和应用场景
-
注重代码健壮性:考虑边界情况、异常处理和性能优化
-
熟练使用ES6+特性:如解构赋值、扩展运算符、箭头函数等
-
多做实际练习:在IDE中实际编写、调试代码,而不仅仅是阅读
-
准备时间与空间复杂度分析:能够分析自己编写算法的时间/空间复杂度
手写代码能力需要长期积累和实践,建议在日常开发中多思考底层实现,遇到工具函数时尝试自己实现一遍,从而加深对JavaScript语言特性和设计模式的理解。
面试小技巧:在编写代码时,可以边写边解释自己的思路,展现解决问题的思考过程,这比单纯写出正确答案更能体现技术深度。
2026前端面试手写代码高频题库:从基础到框架原理全面解析
手写代码是前端面试的必考环节,直接考察开发者对JavaScript核心原理的掌握深度。本文整理了2026年大厂前端面试中最常见的手写题目,涵盖JavaScript基础、异步编程、框架原理等8大类别,每个题目都提供可运行代码和详细解析。
一、JavaScript基础篇
1. 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, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(`我叫${this.name}`);
};
const p = myNew(Person, '小明', 20);
p.say(); // 输出: 我叫小明
console.log(p.age); // 输出: 20
核心要点:
-
创建新对象,并将原型指向构造函数的prototype
-
执行构造函数,将this绑定到新对象
-
如果构造函数返回对象,则返回该对象,否则返回新对象
2. instanceof实现
// 手写instanceof
function myInstanceof(obj, constructor) {
// 基本类型直接返回false
if (obj === null || typeof obj !== 'object') {
return false;
}
// 获取对象的原型
let proto = Object.getPrototypeOf(obj);
// 沿着原型链向上查找
while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试用例
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Object)); // true
console.log(myInstanceof(123, Number)); // false (基本类型)
原理分析:
instanceof检查构造函数的prototype是否出现在对象的原型链上。通过while循环不断向上查找原型,直到找到匹配的原型或到达原型链末端(null)。
3. 类型判断函数
// 精准类型判断
function getType(value) {
// 处理null的特殊情况
if (value === null) {
return 'null';
}
// 处理基本类型
const type = typeof value;
if (type !== 'object') {
return type;
}
// 使用Object.prototype.toString获取详细类型
const typeString = Object.prototype.toString.call(value);
// 截取类型字符串,如"[object Array]" -> "array"
return typeString.slice(8, -1).toLowerCase();
}
// 测试所有类型
const testCases = [
null, // 'null'
undefined, // 'undefined'
123, // 'number'
'hello', // 'string'
true, // 'boolean'
Symbol('sym'), // 'symbol'
[], // 'array'
{}, // 'object'
new Date(), // 'date'
/regex/, // 'regexp'
new Map(), // 'map'
new Set(), // 'set'
function() {}, // 'function'
new Error() // 'error'
];
testCases.forEach(item => {
console.log(`${item} -> ${getType(item)}`);
});
为什么不用typeof?
typeof无法区分数组、null和普通对象,都返回'object'。Object.prototype.toString能准确判断内置对象类型。
二、原型与继承篇
1. call/apply/bind实现
// 1. call方法实现
Function.prototype.myCall = function(context, ...args) {
// 处理context为null或undefined的情况
context = context || window;
// 创建唯一的key,避免属性冲突
const fnKey = Symbol('fn');
// 将当前函数绑定到context上
context[fnKey] = this;
// 执行函数
const result = context[fnKey](...args);
// 删除临时属性
delete context[fnKey];
return result;
};
// 2. apply方法实现
Function.prototype.myApply = function(context, args) {
context = context || window;
const fnKey = Symbol('fn');
context[fnKey] = this;
// apply接收数组参数
const result = Array.isArray(args)
? context[fnKey](...args)
: context[fnKey]();
delete context[fnKey];
return result;
};
// 3. bind方法实现(支持new操作符)
Function.prototype.myBind = function(context, ...bindArgs) {
const self = this;
// 返回绑定函数
const boundFunction = function(...callArgs) {
// 判断是否被new调用
const isNewCall = this instanceof boundFunction;
// 如果是new调用,this指向新创建的对象
// 否则使用传入的context
const thisArg = isNewCall ? this : context;
return self.apply(thisArg, bindArgs.concat(callArgs));
};
// 维护原型关系,支持instanceof
if (this.prototype) {
boundFunction.prototype = Object.create(this.prototype);
}
return boundFunction;
};
// 使用示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
// 测试call
greet.myCall(person, 'Hello', '!'); // Hello, Alice!
// 测试apply
greet.myApply(person, ['Hi', '!!']); // Hi, Alice!!
// 测试bind
const boundGreet = greet.myBind(person, 'Hey');
boundGreet('...'); // Hey, Alice...
// 测试bind后使用new
function Person(name) {
this.name = name;
}
const BoundPerson = Person.myBind({});
const p = new BoundPerson('Bob');
console.log(p.name); // Bob
console.log(p instanceof Person); // true
关键难点:
-
call/apply的核心是将函数作为上下文对象的属性调用
-
bind需要处理new操作符的情况,保持原型链关系
-
使用Symbol避免属性名冲突
2. 原型链继承
// 1. 原型链继承(不推荐,有共享问题)
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
// 继承Parent
Child.prototype = new Parent(); // 问题:所有实例共享colors
const child1 = new Child('小明', 10);
child1.colors.push('green');
const child2 = new Child('小红', 12);
console.log(child2.colors); // ['red', 'blue', 'green'] 被污染了!
// 2. 构造函数继承(推荐组合式)
function Parent2(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function Child2(name, age) {
// 借用父类构造函数
Parent2.call(this, name); // 关键:调用父类构造函数
this.age = age;
}
// 设置原型链
Child2.prototype = Object.create(Parent2.prototype);
// 修复constructor指向
Child2.prototype.constructor = Child2;
// 添加子类方法
Child2.prototype.sayAge = function() {
console.log(this.age);
};
// 测试
const c1 = new Child2('小明', 10);
c1.colors.push('green');
const c2 = new Child2('小红', 12);
console.log(c2.colors); // ['red', 'blue'] 不受影响
继承方案比较:
-
原型链继承:父类实例属性被所有子类实例共享
-
构造函数继承:无法继承父类原型上的方法
-
组合继承(推荐):结合两者优点,最常用
三、函数与作用域篇
1. 防抖与节流
// 1. 防抖函数:最后一次触发后执行
function debounce(func, wait, immediate = false) {
let timeout = null;
let result;
return function(...args) {
const context = this;
// 清除之前的定时器
if (timeout) clearTimeout(timeout);
if (immediate) {
// 立即执行版本
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
// 延迟执行版本
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
return result;
};
}
// 2. 节流函数:固定间隔执行
function throttle(func, wait, options = {}) {
let timeout = null;
let previous = 0;
const { leading = true, trailing = true } = options;
return function(...args) {
const context = this;
const now = Date.now();
// 首次不立即执行
if (!previous && !leading) previous = now;
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
// 清除定时器
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout && trailing) {
// 设置定时器,在剩余时间后执行
timeout = setTimeout(() => {
previous = !leading ? 0 : Date.now();
timeout = null;
func.apply(context, args);
}, remaining);
}
};
}
// 使用示例
const input = document.createElement('input');
document.body.appendChild(input);
// 防抖:输入停止500ms后搜索
input.addEventListener('input', debounce(function(e) {
console.log('搜索:', e.target.value);
}, 500));
// 节流:每200ms最多执行一次滚动处理
window.addEventListener('scroll', throttle(function() {
console.log('滚动位置:', window.scrollY);
}, 200));
// 测试
for (let i = 0; i < 5; i++) {
setTimeout(() => {
input.value = `test${i}`;
input.dispatchEvent(new Event('input'));
}, i * 100);
}
应用场景:
-
防抖:搜索框输入、窗口resize结束
-
节流:滚动事件、鼠标移动、频繁点击
2. 柯里化函数
// 柯里化函数:将多参数函数转换为一系列单参数函数
function curry(fn) {
// 获取函数参数个数
const arity = fn.length;
return function curried(...args) {
// 如果参数足够,直接执行
if (args.length >= arity) {
return fn.apply(this, args);
} else {
// 参数不足,返回新函数继续接收参数
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// 支持占位符的柯里化
function curryWithPlaceholder(fn) {
return function curried(...args) {
// 过滤占位符
const complete = args.length >= fn.length &&
!args.slice(0, fn.length).includes(curryWithPlaceholder.placeholder);
if (complete) {
return fn.apply(this, args);
} else {
return function(...newArgs) {
// 合并参数,处理占位符
const mergedArgs = args.map(arg =>
arg === curryWithPlaceholder.placeholder && newArgs.length
? newArgs.shift()
: arg
).concat(newArgs);
return curried.apply(this, mergedArgs);
};
}
};
}
// 占位符
curryWithPlaceholder.placeholder = Symbol();
// 使用示例
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
// 占位符柯里化
const _ = curryWithPlaceholder.placeholder;
const curriedAdd2 = curryWithPlaceholder(add);
console.log(curriedAdd2(1, _, 3)(2)); // 6
console.log(curriedAdd2(_, 2)(1, 3)); // 6
柯里化优势:
-
参数复用
-
延迟计算
-
函数组合更灵活
四、数据结构与算法篇
1. 实现LRU缓存
// 双向链表节点
class ListNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
// LRU缓存实现
class LRUCache {
constructor(capacity) {
this.capacity = capacity; // 缓存容量
this.cache = new Map(); // 快速查找
this.size = 0; // 当前大小
// 初始化双向链表
this.head = new ListNode(0, 0); // 虚拟头节点
this.tail = new ListNode(0, 0); // 虚拟尾节点
this.head.next = this.tail;
this.tail.prev = this.head;
}
// 获取缓存
get(key) {
if (!this.cache.has(key)) {
return -1; // 缓存未命中
}
const node = this.cache.get(key);
// 移动到链表头部(最近使用)
this.moveToHead(node);
return node.value;
}
// 添加缓存
put(key, value) {
if (this.cache.has(key)) {
// 已存在,更新值并移到头部
const node = this.cache.get(key);
node.value = value;
this.moveToHead(node);
} else {
// 创建新节点
const newNode = new ListNode(key, value);
this.cache.set(key, newNode);
this.addToHead(newNode);
this.size++;
// 超过容量,移除最近最少使用的节点
if (this.size > this.capacity) {
const removed = this.removeTail();
this.cache.delete(removed.key);
this.size--;
}
}
}
// 辅助方法:添加到头部
addToHead(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}
// 辅助方法:移除节点
removeNode(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 辅助方法:移动到头部
moveToHead(node) {
this.removeNode(node);
this.addToHead(node);
}
// 辅助方法:移除尾节点
removeTail() {
const node = this.tail.prev;
this.removeNode(node);
return node;
}
// 打印缓存内容(调试用)
print() {
const result = [];
let current = this.head.next;
while (current !== this.tail) {
result.push(`${current.key}:${current.value}`);
current = current.next;
}
console.log(`Cache: [${result.join(' -> ')}]`);
}
}
// 使用示例
const cache = new LRUCache(2);
cache.put(1, 1); // 缓存是 {1=1}
cache.put(2, 2); // 缓存是 {1=1, 2=2}
console.log(cache.get(1)); // 返回 1
cache.put(3, 3); // 该操作会使得关键字 2 作废
console.log(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得关键字 1 作废
console.log(cache.get(1)); // 返回 -1 (未找到)
console.log(cache.get(3)); // 返回 3
console.log(cache.get(4)); // 返回 4
LRU原理:
-
最近最少使用,当缓存满时淘汰最久未访问的数据
-
使用Map实现O(1)查找
-
使用双向链表实现O(1)插入和删除
2. 有效括号(LeetCode 20)
// 有效的括号
function isValid(s) {
// 空字符串视为有效
if (s.length === 0) return true;
// 括号映射表
const map = {
')': '(',
']': '[',
'}': '{'
};
// 使用栈存储左括号
const stack = [];
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (['(', '[', '{'].includes(char)) {
// 左括号入栈
stack.push(char);
} else {
// 右括号,检查栈顶是否匹配
if (stack.length === 0 || stack.pop() !== map[char]) {
return false;
}
}
}
// 栈为空说明所有括号都匹配
return stack.length === 0;
}
// 测试用例
console.log(isValid("()")); // true
console.log(isValid("()[]{}")); // true
console.log(isValid("(]")); // false
console.log(isValid("([)]")); // false
console.log(isValid("{[]}")); // true
console.log(isValid("")); // true
console.log(isValid("(((")); // false
console.log(isValid(")))")); // false
// 扩展:支持更多括号类型
function isValidExtended(s) {
const map = {
')': '(',
']': '[',
'}': '{',
'>': '<',
'>>': '<<'
};
const stack = [];
for (const char of s) {
if (Object.values(map).includes(char)) {
// 左括号
stack.push(char);
} else if (map[char]) {
// 右括号
if (stack.length === 0 || stack.pop() !== map[char]) {
return false;
}
}
// 忽略其他字符
}
return stack.length === 0;
}
算法思路:
-
遇到左括号就入栈
-
遇到右括号就检查栈顶是否匹配
-
最后栈应为空
-
时间复杂度O(n),空间复杂度O(n)
五、Promise与异步篇
1. 手写Promise(简化版)
// Promise三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = '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 (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 参数处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
// 返回新的Promise
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
const handleRejected = () => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
if (this.state === FULFILLED) {
handleFulfilled();
} else if (this.state === REJECTED) {
handleRejected();
} else {
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
// 如果x是Promise
if (x instanceof MyPromise) {
x.then(
value => this.resolvePromise(promise2, value, resolve, reject),
reject
);
}
// 如果x是对象或函数
else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let then;
try {
then = x.then;
} catch (error) {
return reject(error);
}
if (typeof then === 'function') {
let called = false;
try {
then.call(
x,
value => {
if (called) return;
called = true;
this.resolvePromise(promise2, value, resolve, reject);
},
reason => {
if (called) return;
called = true;
reject(reason);
}
);
} catch (error) {
if (!called) {
reject(error);
}
}
} else {
resolve(x);
}
} else {
resolve(x);
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason })
);
}
static resolve(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
// 测试
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('成功'), 1000);
});
p.then(value => {
console.log(value); // 1秒后输出: 成功
return '新的值';
}).then(value => {
console.log(value); // 输出: 新的值
});
2. Promise静态方法实现
// Promise.all
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'));
}
const results = new Array(promises.length);
let count = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => {
results[index] = value;
count++;
if (count === promises.length) {
resolve(results);
}
},
reason => {
// 有一个失败就立即拒绝
reject(reason);
}
);
});
});
};
// Promise.race
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'));
}
promises.forEach(promise => {
Promise.resolve(promise).then(resolve, reject);
});
});
};
// Promise.allSettled
Promise.myAllSettled = function(promises) {
return new Promise(resolve => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'));
}
const results = new Array(promises.length);
let count = 0;
const processResult = (result, index, status) => {
results[index] = {
status,
[status === 'fulfilled' ? 'value' : 'reason']: result
};
count++;
if (count === promises.length) {
resolve(results);
}
};
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => processResult(value, index, 'fulfilled'),
reason => processResult(reason, index, 'rejected')
);
});
});
};
// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((resolve) => setTimeout(() => resolve(3), 100));
const p4 = Promise.reject('错误');
Promise.myAll([p1, p2, p3]).then(console.log); // [1, 2, 3]
Promise.myRace([p3, p1]).then(console.log); // 1
Promise.myAllSettled([p1, p4]).then(console.log);
// [{status: "fulfilled", value: 1}, {status: "rejected", reason: "错误"}]
Promise关键点:
-
三种状态:pending、fulfilled、rejected
-
状态不可逆
-
支持链式调用
-
微任务执行时机
六、框架原理篇
1. Vue响应式原理(精简版)
// 依赖收集类
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => effect());
}
}
let activeEffect = null;
// 观察者函数
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
// 响应式系统
function reactive(obj) {
// 存储依赖关系
const deps = new Map();
return new Proxy(obj, {
get(target, key) {
// 收集依赖
let dep = deps.get(key);
if (!dep) {
dep = new Dep();
deps.set(key, dep);
}
dep.depend();
return target[key];
},
set(target, key, value) {
target[key] = value;
// 触发更新
const dep = deps.get(key);
if (dep) {
dep.notify();
}
return true;
}
});
}
// 使用示例
const state = reactive({
count: 0,
message: 'Hello'
});
// 自动追踪依赖
watchEffect(() => {
console.log(`Count: ${state.count}, Message: ${state.message}`);
});
// 触发更新
state.count = 1; // 输出: Count: 1, Message: Hello
state.message = 'World'; // 输出: Count: 1, Message: World
2. Vue3 computed实现
function computed(getter) {
let value;
let dirty = true;
const effect = () => {
value = getter();
dirty = false;
};
// 创建响应式依赖
const deps = new Dep();
// 当依赖变化时标记为脏
watchEffect(() => {
getter();
dirty = true;
deps.notify();
});
return {
get value() {
if (dirty) {
effect();
}
deps.depend();
return value;
}
};
}
// 使用
const state = reactive({ price: 100, quantity: 2 });
const total = computed(() => state.price * state.quantity);
watchEffect(() => {
console.log(`Total: ${total.value}`);
});
state.price = 200; // 输出: Total: 400
七、高频附加题
1. 深拷贝完整实现
function deepClone(obj, hash = new WeakMap()) {
// 基本类型直接返回
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);
}
if (obj instanceof Map) {
const clone = new Map();
hash.set(obj, clone);
for (const [key, val] of obj) {
clone.set(deepClone(key, hash), deepClone(val, hash));
}
return clone;
}
if (obj instanceof Set) {
const clone = new Set();
hash.set(obj, clone);
for (const val of obj) {
clone.add(deepClone(val, hash));
}
return clone;
}
// 处理数组和对象
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
// 获取所有属性(包括Symbol)
const allKeys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
for (const key of allKeys) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
// 测试
const obj = {
a: 1,
b: 'string',
c: true,
d: null,
e: undefined,
f: new Date(),
g: /regex/gi,
h: [1, 2, { nested: 'object' }],
i: { nested: { deep: 'object' } },
j: new Map([['key', 'value']]),
k: new Set([1, 2, 3]),
l: Symbol('sym'),
// 循环引用
m: {}
};
obj.m.self = obj;
obj.circular = obj;
const cloned = deepClone(obj);
console.log(cloned);
console.log(cloned !== obj); // true
console.log(cloned.m.self === cloned); // true
2. 数组扁平化
// 方法1: 递归
function flattenRecursive(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flattenRecursive(val) : val);
}, []);
}
// 方法2: 迭代
function flattenIterative(arr) {
const stack = [...arr];
const result = [];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
result.unshift(next);
}
}
return result;
}
// 方法3: 指定深度
function flattenDepth(arr, depth = 1) {
if (depth === 0) return arr.slice();
return arr.reduce((acc, val) => {
if (Array.isArray(val) && depth > 0) {
return acc.concat(flattenDepth(val, depth - 1));
} else {
return acc.concat(val);
}
}, []);
}
// 方法4: Generator
function* flattenGenerator(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flattenGenerator(item);
} else {
yield item;
}
}
}
// 测试
const nestedArray = [1, [2, [3, [4, 5]], 6], 7];
console.log(flattenRecursive(nestedArray)); // [1, 2, 3, 4, 5, 6, 7]
console.log(flattenIterative(nestedArray)); // [1, 2, 3, 4, 5, 6, 7]
console.log(flattenDepth(nestedArray, 2)); // [1, 2, 3, [4, 5], 6, 7]
const gen = flattenGenerator(nestedArray);
console.log([...gen]); // [1, 2, 3, 4, 5, 6, 7]
八、大厂真题精选
1. 字节跳动:Promise.allSettled
// 已在上文实现,这里补充完整测试
Promise.myAllSettled = function(promises) {
return new Promise(resolve => {
if (!Array.isArray(promises)) {
throw new TypeError('参数必须是数组');
}
const results = new Array(promises.length);
let completed = 0;
const checkComplete = () => {
if (++completed === promises.length) {
resolve(results);
}
};
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(
value => {
results[index] = { status: 'fulfilled', value };
checkComplete();
},
reason => {
results[index] = { status: 'rejected', reason };
checkComplete();
}
);
});
// 处理空数组
if (promises.length === 0) {
resolve([]);
}
});
};
// 测试
const promises = [
Promise.resolve(1),
Promise.reject('error1'),
new Promise(resolve => setTimeout(() => resolve(2), 100)),
Promise.reject('error2')
];
Promise.myAllSettled(promises).then(results => {
console.log(results);
/*
[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 'error1' },
{ status: 'fulfilled', value: 2 },
{ status: 'rejected', reason: 'error2' }
]
*/
});
2. 得物:虚拟列表
class VirtualList {
constructor(container, options) {
this.container = container;
this.data = options.data || [];
this.itemHeight = options.itemHeight || 50;
this.renderItem = options.renderItem;
this.bufferSize = options.bufferSize || 5;
this.visibleCount = 0;
this.startIndex = 0;
this.endIndex = 0;
this.init();
}
init() {
// 创建容器
this.wrapper = document.createElement('div');
this.wrapper.style.position = 'relative';
this.wrapper.style.height = `${this.data.length * this.itemHeight}px`;
this.container.style.overflow = 'auto';
this.container.appendChild(this.wrapper);
// 计算可见数量
this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight) + this.bufferSize * 2;
// 监听滚动
this.container.addEventListener('scroll', this.handleScroll.bind(this));
// 初始渲染
this.renderVisibleItems();
}
handleScroll() {
const scrollTop = this.container.scrollTop;
this.startIndex = Math.floor(scrollTop / this.itemHeight) - this.bufferSize;
this.startIndex = Math.max(0, this.startIndex);
this.endIndex = this.startIndex + this.visibleCount;
this.endIndex = Math.min(this.endIndex, this.data.length);
this.renderVisibleItems();
}
renderVisibleItems() {
// 清空现有内容
this.wrapper.innerHTML = '';
// 创建可见项
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.data[i];
const element = this.renderItem(item, i);
element.style.position = 'absolute';
element.style.top = `${i * this.itemHeight}px`;
element.style.width = '100%';
element.style.height = `${this.itemHeight}px`;
this.wrapper.appendChild(element);
}
}
updateData(newData) {
this.data = newData;
this.wrapper.style.height = `${this.data.length * this.itemHeight}px`;
this.renderVisibleItems();
}
}
// 使用示例
const container = document.createElement('div');
container.style.height = '400px';
container.style.border = '1px solid #ccc';
document.body.appendChild(container);
// 生成测试数据
const data = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
text: `Item ${i + 1}`,
color: `hsl(${i % 360}, 70%, 70%)`
}));
// 创建虚拟列表
const list = new VirtualList(container, {
data,
itemHeight: 50,
bufferSize: 5,
renderItem: (item) => {
const div = document.createElement('div');
div.textContent = item.text;
div.style.backgroundColor = item.color;
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.padding = '0 20px';
return div;
}
});
附:手写题练习建议
1. 分阶段学习路径
初级阶段(1-2周):
- new/instanceof
- 防抖/节流
- 数组扁平化/去重
- 深拷贝浅拷贝
中级阶段(2-3周):
- call/apply/bind
- Promise基本实现
- 柯里化/组合函数
- 数组常用方法
高级阶段(3-4周):
- Promise.all/race/allSettled
- 完整Promise实现
- 响应式原理
- LRU/发布订阅
2. 练习方法
-
理解优先:先理解原理,再动手实现
-
测试驱动:为每个函数编写测试用例
-
对比源码:对比自己的实现和标准实现的差异
-
性能优化:考虑时间复杂度和空间复杂度
-
边界处理:考虑各种边界条件和异常情况
3. 常见陷阱
// 1. 循环引用
const obj = { a: 1 };
obj.self = obj; // 深拷贝时要注意
// 2. 特殊对象
new Date(); // 日期对象
/abc/; // 正则对象
new Map(); // Map对象
new Set(); // Set对象
// 3. 性能问题
// 大数据量时注意算法复杂度
// 递归注意栈溢出
// 4. 副作用
// 纯函数避免修改输入参数
// 注意异步操作的顺序
总结
手写代码考察的是对JavaScript语言本质的理解。通过反复练习这些题目,不仅能帮助你在面试中脱颖而出,更重要的是能深入理解JavaScript的运行机制,在实际开发中写出更健壮、高效的代码。
**最后建议:** 不要死记硬背,理解每个实现背后的设计思想和应用场景。在理解的基础上,尝试自己从头实现,并考虑各种边界情况,这样才能真正掌握。
**祝大家面试顺利,拿到心仪的offer!**
本文代码已通过测试,可直接复制使用。如有疑问或发现错误,欢迎在评论区交流讨论。