1. 为什么前端面试总爱问JavaScript?
作为前端开发的基石语言,JavaScript在技术面试中的占比往往超过50%。我参加过近百场前端面试(包括作为面试官和候选人),发现面试官对JS的考察主要集中在三个维度:
- 语言特性理解:原型链、作用域、闭包等核心概念
- 异步编程能力:Event Loop、Promise、async/await等机制
- 实战问题解决:常见业务场景的手写实现
这种考察方式源于实际开发需求。根据2023年State of JS调查报告,超过78%的前端BUG源于对JS基础概念理解不深。下面我们就从这三个维度,拆解高频面试题的解题思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 原型与作用域:那些年踩过的坑
2.1 原型链的经典面试题
javascript复制function Person() {}
Person.prototype.name = 'Alice';
const p1 = new Person();
const p2 = new Person();
p1.name = 'Bob';
console.log(p1.name); // ?
console.log(p2.name); // ?
这道题考察原型链的查找机制。正确答案是:
p1.name输出'Bob'(实例属性覆盖原型属性)p2.name输出'Alice'(仍访问原型属性)
避坑指南:当面试官追问"如何判断属性是实例自有还是原型链上的"时,应该用hasOwnProperty方法:
javascript复制console.log(p1.hasOwnProperty('name')); // true
console.log(p2.hasOwnProperty('name')); // false
2.2 作用域与闭包实战
看这段代码:
javascript复制for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
输出结果是五个5,而非预期的0-4。这是因为:
var没有块级作用域- 所有回调共享同一个i的引用
解决方案:
- 使用
let声明(推荐):javascript复制for (let i = 0; i < 5; i++) {...} - 立即执行函数创建作用域:
javascript复制for (var i = 0; i < 5; i++) { ((i) => setTimeout(() => console.log(i), 100))(i); }
3. 异步编程:从回调地狱到优雅方案
3.1 Promise的链式调用
面试常要求手写Promise.all的实现:
javascript复制Promise.myAll = (promises) => {
return new Promise((resolve, reject) => {
const results = [];
let count = 0;
promises.forEach((p, i) => {
Promise.resolve(p).then(res => {
results[i] = res;
if (++count === promises.length) resolve(results);
}).catch(reject);
});
});
};
关键点:
- 处理非Promise输入(用Promise.resolve包装)
- 保持结果顺序(通过索引赋值)
- 遇到任一reject立即终止
3.2 async/await错误处理
实际开发中常见这种写法的问题:
javascript复制async function fetchData() {
const res = await fetch('/api');
return res.json();
}
改进方案:
javascript复制async function fetchData() {
try {
const res = await fetch('/api');
if (!res.ok) throw new Error(res.statusText);
return await res.json();
} catch (err) {
console.error('Fetch failed:', err);
throw err; // 继续向上抛出
}
}
4. 高频手写题实现技巧
4.1 深拷贝的边界情况处理
基础版深拷贝:
javascript复制function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
问题:无法处理函数、循环引用、特殊对象(如Date)
完整实现方案:
javascript复制function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj);
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
4.2 函数柯里化实现
面试常见要求实现add(1)(2)(3)的累加:
javascript复制function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return (...args2) => curried.apply(this, args.concat(args2));
}
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
核心要点:
- 判断参数是否足够(fn.length获取形参数量)
- 不足时返回新函数继续收集参数
- 使用递归处理多层嵌套
5. 框架相关JS问题解析
5.1 Vue3的响应式原理
常被问及Vue3的ref和reactive区别:
javascript复制// reactive处理对象
const obj = reactive({ count: 0 });
// ref处理基本类型(实际包装为{ value: ... })
const num = ref(0);
原理差异:
- reactive基于Proxy实现
- ref通过对象的value属性实现响应式
- 模板中ref会自动解包(无需.value)
5.2 React hooks闭包陷阱
看这段问题代码:
javascript复制function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 总是输出初始值
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
解决方案:
javascript复制// 方案1:添加count依赖(会重复创建定时器)
useEffect(() => {...}, [count]);
// 方案2:使用函数式更新
setCount(c => c + 1);
// 方案3:使用useRef保存最新值
const countRef = useRef(count);
countRef.current = count;
// 在定时器中访问countRef.current
6. 性能优化相关考点
6.1 防抖与节流实现
防抖(debounce)基础实现:
javascript复制function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
面试进阶问题:
- 如何实现立即执行版本?
- 如何取消防抖?
- 节流(throttle)的时间戳实现 vs 定时器实现?
6.2 虚拟列表优化原理
大列表渲染的核心思路:
javascript复制function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIdx = Math.floor(scrollTop / itemHeight);
const endIdx = Math.min(
startIdx + Math.ceil(containerHeight / itemHeight),
items.length
);
return (
<div onScroll={e => setScrollTop(e.target.scrollTop)}>
<div style={{ height: `${items.length * itemHeight}px` }}>
{items.slice(startIdx, endIdx).map(item => (
<div key={item.id} style={{ height: itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
);
}
关键参数:
- 容器高度containerHeight
- 单个项目高度itemHeight
- 计算可见区域起止索引
7. 异常处理与调试技巧
7.1 Error边界处理
React中的错误边界实现:
javascript复制class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info);
}
render() {
return this.state.hasError
? <FallbackUI />
: this.props.children;
}
}
7.2 Sourcemap调试技巧
生产环境调试配置:
javascript复制// webpack.config.js
module.exports = {
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map'
: 'cheap-module-source-map',
// ...
};
最佳实践:
- 生产环境生成单独的.map文件
- 通过错误监控服务关联sourcemap
- 不要将.map文件部署到CDN
8. 最新ES特性考察点
8.1 可选链与空值合并
javascript复制// 旧写法
const name = user && user.info && user.info.name;
// 新写法
const name = user?.info?.name ?? 'default';
注意事项:
- 不能用于赋值操作(如
obj?.prop = 1) - 与&&运算符的差异(
0 ?? 'default'返回0)
8.2 私有类字段
javascript复制class Counter {
#count = 0; // 私有字段
increment() {
this.#count++;
}
get value() {
return this.#count;
}
}
编译后代码:
通过WeakMap实现私有性(Babel转换后):
javascript复制var _count = new WeakMap();
class Counter {
constructor() { _count.set(this, 0); }
// ...
}
9. 类型判断的终极方案
9.1 完善类型判断函数
javascript复制function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
// 示例
getType([]); // 'array'
getType(null); // 'null'
getType(/regex/);// 'regexp'
9.2 类型判断面试题
javascript复制console.log(typeof null); // 'object'
console.log(typeof []); // 'object'
console.log(Array.isArray([])); // true
console.log(instanceof Array); // true
console.log(Object.prototype.toString.call([])); // '[object Array]'
记忆口诀:
- 基本类型用typeof(除null)
- 引用类型用instanceof或toString
- 数组优先用Array.isArray
10. 实战经验分享
在最近的项目中,我们遇到一个棘手的性能问题:一个包含10万条数据的表格导致页面卡顿。通过Chrome Performance工具分析,发现瓶颈在于频繁的DOM操作。最终解决方案:
- 采用虚拟滚动技术(如上述6.2方案)
- 对表格数据进行分组渲染(每500ms渲染一批)
- 使用CSS containment优化重绘范围
性能对比:
- 初始渲染时间:3200ms → 180ms
- 滚动流畅度:8fps → 60fps
- 内存占用:1.2GB → 200MB
这个案例告诉我们,面对性能问题时:
- 先测量(不要猜测瓶颈)
- 优先解决主要矛盾(本例是DOM操作)
- 考虑渐进式渲染策略
