本文最后更新于170 天前,其中的信息可能已经过时,如有错误请发送邮件到1986413837@qq.com
面试官问的很细节 有些问题还是学习初期遇到的 现在已经遗忘了 还是要及时补充基础知识
问题一:事件流
面试官问题:如何给一个元素绑定事件,并让它在事件的捕获阶段触发,而不是默认的冒泡阶段触发?
你的回答:
- 提到了事件冒泡和
preventDefault(这个方法是阻止默认行为,而非控制事件流阶段),但混淆了概念。 - 意识到自己的问题不是阻止冒泡,而是希望在捕获阶段触发,但未能给出正确方法。
标准回答:
DOM事件流包括三个阶段:捕获阶段、目标阶段、冒泡阶段。addEventListener方法的第三个参数可以控制监听器在哪个阶段被触发。
- 默认情况下(或传入
false),监听器在冒泡阶段触发。 - 当第三个参数传入
true或一个{capture: true}的对象时,监听器会在捕获阶段触发。
// 在冒泡阶段触发(默认)
element.addEventListener('click', handleClick);
// 或显式设置为 false
element.addEventListener('click', handleClick, false);
// 在捕获阶段触发
element.addEventListener('click', handleClick, true);
// 或使用选项对象
element.addEventListener('click', handleClick, { capture: true });
问题二:事件处理
面试官问题:如何阻止事件的默认行为?以及,如果第三方库给一个元素绑定了事件,你无法修改其代码,如何阻止这个事件的回调函数执行?
你的回答:
- 对于阻止默认行为,提到了
preventDefault,这是正确的。 - 对于阻止第三方库的事件,思路是“移除事件”或“用变量控制”,但意识到无法控制第三方库的逻辑,并表示不清楚。
标准回答:
- 阻止默认行为:在事件处理函数中调用
event.preventDefault()方法。 - 阻止第三方库的事件回调:这是一个更棘手的问题。由于无法修改第三方库的代码,直接“移除”或“控制”其回调是困难的。更合理的思路是:
- 事件代理:在更早的捕获阶段(如果第三方库在冒泡阶段监听)或在父元素上监听事件,然后调用
event.stopImmediatePropagation()。这个方法能阻止同一元素上后续绑定的事件监听器被触发。但前提是你的监听器必须比第三方库的先执行(可以通过提高监听器优先级或也设置在捕获阶段来实现)。 - 重写方法(不推荐):极端情况下,可以重写
addEventListener等方法进行拦截,但这会破坏代码的稳定性,不推荐在生产环境使用。 - 最佳实践:优先考虑与第三方库的整合方式,或者选择另一个不冲突的库。
- 事件代理:在更早的捕获阶段(如果第三方库在冒泡阶段监听)或在父元素上监听事件,然后调用
问题三:JavaScript运行机制
面试官问题:宏任务与微任务的区别是什么?setTimeout和 Promise的递归调用,哪种会导致浏览器卡死?
你的回答:
- 知道微任务优先,但顺序描述不准确。
- 认为
Promise递归会导致卡死,但原因说不清。
标准回答:
- 运行机制:JavaScript是单线程的,使用事件循环模型。任务分为宏任务(如
setTimeout,setInterval, I/O)和微任务(如Promise.then,process.nextTick)。- 一次事件循环中,先执行一个宏任务,然后执行所有已产生的微任务,接着进行渲染,再开始下一个宏任务。
- 递归调用对比:
setTimeout递归:setTimeout是宏任务。每次递归都会创建一个新的宏任务排到队列末尾,不会阻塞事件循环,浏览器仍有机会进行渲染和响应其他操作,因此不会导致卡死,但会造成持续的CPU占用。Promise递归:Promise的回调是微任务。在一个宏任务中,递归地创建微任务会导致微任务队列永远清不完,因为执行一个微任务又会产生新的微任务。事件循环将无法进入下一个阶段(包括渲染),从而导致页面卡死,无响应。
问题四:TypeScript
面试官问题:interface和 type的区别?声明两个同名的 interface会怎样?
你的回答:
- 知道
interface可扩展(继承),type比较固定。 - 认为同名
interface会“灵活推断”,但描述不准确。
标准回答:
interface与type区别:interface:主要用于定义对象的形状,支持声明合并(见下一点),更适合面向对象编程,通过extends实现继承。type:别名,可以定义任何类型(原始值、联合类型、元组等),通常通过&实现交叉类型。它不能声明合并。
- 同名
interface:TypeScript支持声明合并。如果你声明了两个同名的interface,它们的成员会被合并到一个接口中。如果存在同名成员且类型不同,则会报错。
interface User { name: string; }
interface User { age: number; }
// 合并为:interface User { name: string; age: number; }
const user: User = { name: 'Alice', age: 30 }; // 正确
问题五:React
面试官问题:
useMemo和useCallback的区别?能用useMemo替代useCallback吗?useRef的用途?如何更新useRef的值?- 如何在组件销毁时执行一些操作(生命周期)?
你的回答:
- 知道
useMemo缓存数据,useCallback缓存函数,认为可以替代。 - 知道
useRef可用于获取DOM和存储可变值,但忘记如何更新。 - 知道有生命周期函数,但记不起是
useEffect的清理函数。
标准回答:
useMemovsuseCallback:useMemo:缓存一个计算结果(值)。const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);useCallback:缓存一个函数本身。const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);useCallback(fn, deps)等价于useMemo(() => fn, deps)。所以可以用useMemo来模拟useCallback,但语义上不推荐,直接用useCallback更清晰。
useRef:- 用途:① 访问DOM元素。② 保存一个在整个组件生命周期内可变但不会触发重新渲染的值(类似于类的实例属性)。
- 更新:通过修改
ref.current属性即可更新。ref.current = newValue。注意,修改.current不会引发组件重新渲染。
- 组件销毁时机:在函数组件中,使用
useEffect的清理函数(返回的函数)来模拟componentWillUnmount。
useEffect(() => {
// 组件挂载/更新后执行的操作
console.log('Component did mount/update');
return () => {
// 清理函数:组件卸载前执行
console.log('Component will unmount');
};
}, []); // 依赖数组为空,表示仅在挂载和卸载时执行
问题六:Git
面试官问题:不小心提交了一个commit到远程仓库,如何撤销/回滚它?
你的回答:
- 知道用
git log找到commit id。 - 但忘记了具体的回滚命令。
标准回答:
有两种主要方式:
1.git revert(推荐,安全):创建一个新的commit来抵消掉不想要的commit的更改。这会保留历史记录,适合团队协作。
git log # 找到要撤销的commit的哈希值(如 abc123)
git revert abc123
git push origin main
- 2.
git reset(危险,会重写历史):将分支指针直接回退到之前的某个commit。如果commit已推送到远程,不要轻易使用git reset,除非你确定只有你一人在这个分支上工作。
# 本地操作(--hard 会丢弃所有更改,慎用)
git reset --hard abc122 # 回退到abc123的前一个commit
# 强制推送到远程(危险!)
git push --force-with-lease origin main