前端笔试题
1. 事件循环
先执行同步代码,当前任务结束后清空微任务,再进入下一个任务。
js
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve(3).then(console.log);
console.log(4);
});
console.log(5);
// 1、5、2、4、3js
async function async1() {
console.log(1);
await async2();
console.log(2);
}
async function async2() { console.log(3); }
console.log(4);
setTimeout(() => console.log(5));
async1();
Promise.resolve().then(() => console.log(7));
console.log(8);
// 4、1、3、8、2、7、5await 之前同步执行,await 后面的部分进入微任务队列。
2. 实现防抖
防抖的意思是短时间内反复触发,只执行最后一次。搜索输入、窗口 resize 常用。
js
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}apply 保留调用时的 this 和参数。生产版本还可以增加立即执行、取消和 flush 能力。
3. 递归遍历树并生成路径
js
function getPaths(node, parentPath = '') {
const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name;
const result = [currentPath];
for (const child of node.children ?? []) {
result.push(...getPaths(child, currentPath));
}
return result;
}思路是先保存当前节点路径,再把当前路径传给每个子节点。时间复杂度 O(n)。树很深时可改成栈迭代,避免调用栈溢出。
4. 找数组中出现次数最多的值
js
function mostFrequent(list) {
if (!list.length) return undefined;
const counts = new Map();
let result = list[0];
let max = 0;
for (const item of list) {
const count = (counts.get(item) ?? 0) + 1;
counts.set(item, count);
if (count > max) {
max = count;
result = item;
}
}
return { value: result, count: max };
}一次遍历,时间 O(n),空间 O(n)。并列时这版保留最早达到最高次数的值,真实需求要先约定并列规则。
5. 原型链和 this
js
const parent = {
name: 'Parent',
getName() { return this.name; },
};
const child = Object.create(parent);
child.name = 'Child';
child.getName(); // Child
parent.getName(); // Parent
Object.getPrototypeOf(child) === parent; // truechild 自己没有 getName,会沿原型链找到它;但调用者仍是 child,所以 this.name 是 Child。实际代码优先用 Object.getPrototypeOf,不要依赖 __proto__。
6. 两数之和
js
function twoSum(nums, target) {
const indexes = new Map();
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (indexes.has(need)) return [indexes.get(need), i];
indexes.set(nums[i], i);
}
return [];
}遍历当前值时,检查之前是否见过它的补数。时间 O(n),空间 O(n)。要用 has,不能用真假判断,否则索引 0 会被误判。
7. 实现 Promise.all
js
function promiseAll(iterable) {
const items = Array.from(iterable);
return new Promise((resolve, reject) => {
if (items.length === 0) return resolve([]);
const results = new Array(items.length);
let completed = 0;
items.forEach((item, index) => {
Promise.resolve(item).then(value => {
results[index] = value;
completed += 1;
if (completed === items.length) resolve(results);
}, reject);
});
});
}重点是保持输入顺序、兼容普通值、空数组立即完成,并在任意一个 Promise 拒绝时让整体拒绝。
8. 对象转 URL 查询参数
js
function toQuery(params) {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value == null) continue;
if (Array.isArray(value)) value.forEach(item => search.append(key, String(item)));
else search.set(key, String(value));
}
return search.toString();
}URLSearchParams 会负责转义。嵌套对象如何编码没有统一答案,需要和后端约定。
9. 数字每三位加逗号
业务代码优先用国际化 API:
js
new Intl.NumberFormat('en-US').format(10000); // 10,000只考正则时可以写:
js
String(10000).replace(/\B(?=(\d{3})+(?!\d))/g, ',');10. 实现深拷贝
现代浏览器优先 structuredClone(value),它支持循环引用、Map、Set、Date 等,但不能克隆函数和 DOM。
js
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value);
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
const result = Array.isArray(value)
? []
: Object.create(Object.getPrototypeOf(value));
seen.set(value, result);
for (const key of Reflect.ownKeys(value)) {
result[key] = deepClone(value[key], seen);
}
return result;
}这只是常见面试版。完整实现还要考虑属性描述符、Map/Set、TypedArray 等,不建议在生产里重复造轮子。
11. 下划线转驼峰
js
function toCamelCase(text) {
return text.replace(/_([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
}
toCamelCase('user_name_id'); // userNameId12. 数组排序
sort 默认按字符串排序,数字必须传比较函数,并且它会修改原数组。
js
const asc = [...numbers].sort((a, b) => a - b);
const desc = [...numbers].sort((a, b) => b - a);