面试题 1
介绍下观察者模式
- 观察者模式是一种设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
- 观察者模式包含两个角色:主题(Subject)和观察者(Observer)。主题对象负责发布通知,同时观察者对象订阅主题,以便在主题发生改变时收到通知。
- 观察者模式的使用场景包括:事件处理、状态监听、数据绑定等。
介绍下发布订阅模式
- 发布订阅模式是一种设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
- 发布订阅模式包含两个角色:发布者(Publisher)和订阅者(Subscriber)。发布者负责发布通知,同时订阅者对象订阅主题,以便在主题发生改变时收到通知。
- 发布订阅模式的使用场景包括:事件处理、状态监听、数据绑定等。
观察者模式和发布订阅模式
- 观察者模式是一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
- 发布订阅模式是一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
- 观察者模式和发布订阅模式都是用于处理一对多的依赖关系,但是它们在实现方式上有所不同。观察者模式是通过主题对象的 notify 方法来通知观察者对象的,而发布订阅模式是通过消息队列来实现的。
介绍下 DFS 深度优先搜索算法
- DFS(Depth-First Search)是一种用于遍历或搜索树或图的算法。它沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯(回溯的定义是沿原路返回)。
- DFS 的实现需要使用栈数据结构来保存已经访问过的节点,以便在需要时回溯。
- DFS 的复杂度取决于树的形态和节点之间的连接方式。在最坏情况下,DFS 需要遍历整棵树,时间复杂度为 O(n)。在平均情况下,DFS 的时间复杂度为 O(log n)。
例如:
function dfs(node) {
if (node.visited) {
return
}
node.visited = true
console.log(node.value)
for (let i = 0; i < node.children.length; i++) {
dfs(node.children[i])
}
}
介绍下 BFS 广度优先搜索算法
- BFS(Breadth-First Search)是一种用于遍历或搜索树或图的算法。它从树的根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。
- BFS 的实现需要使用队列数据结构来保存已经访问过的节点,以便在需要时进行访问。
- BFS 的复杂度取决于树的形态和节点之间的连接方式。在最坏情况下,BFS 需要遍历整棵树,时间复杂度为 O(n)。在平均情况下,BFS 的时间复杂度为 O(log n)。
例如:
function bfs(node) {
const queue = [node]
while (queue.length > 0) {
const currentNode = queue.shift()
if (currentNode.visited) {
continue
}
currentNode.visited = true
console.log(currentNode.value)
for (let i = 0; i < currentNode.children.length; i++) {
queue.push(currentNode.children[i])
}
}
}
介绍下 KMP 算法
- KMP(Knuth-Morris-Pratt)是一种字符串匹配算法,它通过使用模式串的信息来提高匹配的效率。
- KMP 算法通过构建一个 next 数组来保存模式串的前缀和后缀的最长公共子串的长度,从而避免重复匹配。
- KMP 算法的复杂度取决于模式串的长度。在最坏情况下,KMP 算法需要遍历整棵树,时间复杂度为 O(n)。在平均情况下,KMP 算法的复杂度为 O(m)。
React 生命周期
- 挂载阶段:组件被创建并插入到 DOM 中
- 更新阶段:组件被重新渲染
- 卸载阶段:组件被从 DOM 中移除
React 中的 setState 是同步还是异步的?
- 在 React 中,setState 是异步的。
- 这意味着当调用 setState 时,React 不会立即更新组件的状态,而是将更新添加到更新队列中,然后在适当的时机进行状态更新。
- 这样可以提高性能并避免不必要的重复渲染。
- 然而,在某些情况下,setState 是同步的,比如在事件处理函数中调用 setState。在这种情况下,setState 会立即更新组件的状态。
介绍下 React 中的合成事件
- React 中的合成事件是 React 封装的一套事件系统,它屏蔽了浏览器的差异,提供了一致的 API 接口。
- 合成事件可以统一处理,比如事件委派、事件池等。
- 合成事件可以提高性能,因为减少了与浏览器交互的次数。
React 性能优化
- 使用 shouldComponentUpdate 钩子函数来优化组件的更新性能。
- 使用 PureComponent 代替 Component,PureComponent 会自动进行 props 和 state 的浅比较,只有当 props 或 state 发生变化时才会触发组件的更新。
- 使用 immutable.js 来管理状态,这样可以避免直接修改状态,从而减少错误的发生。
- 使用 React.memo 和 React.lazy 来优化组件的渲染性能。
- 使用 Webpack 的代码分割功能来优化应用的加载性能。
介绍下 React 中的 Fiber 算法
- Fiber 算法是一种用于 React 16 及更高版本中的虚拟 DOM 渲染算法。
- Fiber 算法将渲染过程分成多个小任务,每个小任务可以在执行过程中被中断,从而实现异步渲染。
- Fiber 算法可以提高渲染性能和用户体验。
添加原生事件不移除为什么会内存泄露
- 因为移除事件监听器后,事件监听器仍然存在于内存中,无法被垃圾回收器回收。
- 因此,添加原生事件不移除会导致内存泄漏。
- 解决方案是,在添加事件监听器后,及时移除事件监听器,以避免内存泄漏。
还有哪些地方会内存泄露
- 闭包:闭包会导致闭包函数中的变量无法被垃圾回收器回收。
- 定时器:定时器中的变量无法被垃圾回收器回收。
- 事件监听器:事件监听器无法被垃圾回收器回收。
- 循环引用:两个对象相互引用,导致无法被垃圾回收器回收。
- 未释放的 DOM 元素:未释放的 DOM 元素会导致内存泄漏。
setlnterval 需要注意的点
- 及时清除定时器,避免内存泄漏。
- 定时器的时间间隔不要过短,否则会导致 CPU 占用过高。
- 定时器的时间间隔不要过长,否则会导致页面卡顿。
定时器为什么是不精确的
- 定时器的时间间隔是不精确的,因为定时器的时间间隔是由操作系统决定的,操作系统可能会在定时器的时间间隔内进行其他任务,导致定时器的时间间隔不精确。
- 定时器的时间间隔的误差取决于操作系统的调度时间和处理时间,以及硬件设备的性能。
- 可以通过使用 setTimeout 来实现更精确的定时器。
介绍下 React 的 diff 算法
- React 的 diff 算法是一种高效的算法,用于比较两个虚拟 DOM 树的差异,从而最小化更新操作。
- React 的 diff 算法会首先比较两个虚拟 DOM 树的根节点,然后递归比较子节点,最后生成一个新的虚拟 DOM 树。
- React 的 diff 算法可以减少 DOM 操作的数量,提高渲染性能。
React 的 diff 算法和 Vue 的 diff 算法的区别
- React 的 diff 算法是基于虚拟 DOM 的,而 Vue 的 diff 算法是基于真实 DOM 的。
- React 的 diff 算法是递归的,而 Vue 的 diff 算法是双指针的。
- React 的 diff 算法是同步执行的,而 Vue 的 diff 算法是异步执行的。
介绍宏任务和微任务
- 宏任务:setTimeout、setInterval、requestAnimationFrame、I/O 操作等。
- 微任务:Promise 的回调函数、MutationObserver 的回调函数等。
- 宏任务会在当前脚本的所有同步任务执行完毕后执行,微任务会在当前宏任务的所有微任务执行完毕后执行。
Promise 里面执行和 then 里面执行有什么区别
- Promise 里面执行的代码是在 Promise 的构造函数中执行的,then 里面执行的代码是在 Promise 的 then 方法中执行的。
介绍 pureComponet
- pureComponent 是一个 React 组件,它实现了 shouldComponentUpdate 方法,用于判断组件是否需要重新渲染。如果组件的 props 和 state 没有发生变化,则不会重新渲染组件。
- pureComponent 可以提高组件的性能,减少不必要的渲染。
- pureComponent 默认情况下是浅比较 props 和 state 的,可以通过 shouldComponentUpdate 方法自定义比较规则。
介绍 FunctionComponent
- FunctionComponent 是一个 React 组件,它使用函数来定义组件的逻辑。
- FunctionComponent 可以提高组件的性能,减少不必要的渲染。
- FunctionComponent 可以使用 hooks 来获取组件的状态和生命周期方法。
React 数据流
- React 数据流是单向的,从父组件传递到子组件。
- React 数据流可以通过 props 和 state 来实现。
- React 数据流可以通过 context 来实现跨组件传递数据。
props 和 state 的区别
- props 是父组件传递给子组件的数据,只能通过父组件修改。
- state 是组件自身管理的数据,可以通过组件的 setState 方法修改。
- props 是只读的,state 是可读可写的。
介绍 Reactcontext
- React 的 context 用于在组件树中传递数据。
- React 的 context 可以用于跨组件传递数据,也可以用于跨组件传递函数。
- React 的 context 可以用于跨组件传递组件的状态。
介绍 class 和 ES5 的类以及区别
- class 是 ES6 引入的新语法,用于定义类。
- class 可以继承自其他类,也可以实现接口。
- class 可以定义构造函数、方法、属性等。
- class 可以方便地使用继承、多态等面向对象的概念。
- class 可以方便地使用装饰器。
- class 可以方便地使用静态方法。
- class 可以方便地使用私有属性。
- class 可以方便地使用私有方法。
- class 可以方便地使用抽象类。
- class 可以方便地使用泛型。
- class 可以方便地使用异步。
介绍箭头函数和普通函数的区别
- 箭头函数没有自己的 this,它的 this 是继承自外层作用域的 this。
- 箭头函数没有自己的 arguments,它的 arguments 是继承自外层作用域的 arguments。
- 箭头函数不能使用 yield 关键字。
- 箭头函数不能使用 new 关键字。
- 箭头函数不能使用 super 关键字。
- 箭头函数不能使用 new.target 关键字。
- 箭头函数不能使用 arguments 关键字。
- 箭头函数不能使用 eval 关键字。
- 箭头函数不能使用 caller 关键字。
for..in 和 object.keys 的区别
- for..in 可以遍历对象的键名。
- object.keys 可以遍历对象的键值。
- for..in 可以遍历原型链上的键名。
- object.keys 只能遍历自身的键名。
- for..in 可以遍历数组的键名。
- object.keys 只能遍历数组的索引。
介绍下 Vue 的 nextTick
- Vue 的 nextTick 方法用于在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用 nextTick,获取更新后的 DOM。
- nextTick 方法会返回一个 Promise 对象,可以使用 async/await 语法来等待 DOM 更新完成。
- nextTick 方法可以用于在 DOM 更新之后执行一些操作,例如获取更新后的 DOM 元素。
如何检测对象是否循环引用?
检测对象是否存在循环引用是编程中常见的需求,尤其是在处理复杂数据结构时。以下是几种主流方法及其实现原理:
一、递归遍历 + 哈希表(Set/WeakSet) [推荐方法]
核心思路
通过递归遍历对象的属性,利用哈希表(如Set或WeakSet)记录已访问过的对象。若在遍历过程中发现某个对象已被记录,则判定存在循环引用。实现步骤
- 初始化一个
Set或WeakSet存储已访问对象。 - 遍历对象属性,若属性值为对象(且非
null),则递归检查。 - 递归前判断对象是否已存在于
Set中:若存在则返回true(存在循环引用),否则将其加入Set并继续遍历。
- 初始化一个
代码示例(JavaScript)
javascriptfunction hasCircularReference(obj, seen = new WeakSet()) { if (typeof obj === 'object' && obj !== null) { if (seen.has(obj)) return true seen.add(obj) for (const key in obj) { if (hasCircularReference(obj[key], seen)) return true } seen.delete(obj) // 可选,根据场景决定是否清理 } return false }注意事项
- 使用
WeakSet可避免内存泄漏(自动弱引用)。 - 需排除
null(typeof null === 'object'是 JavaScript 的历史遗留问题)。
- 使用
二、JSON.stringify 异常捕获法
原理
JSON.stringify在序列化循环引用对象时会抛出TypeError,通过捕获异常可间接判断循环引用。代码示例
javascriptfunction hasCircularJSON(obj) { try { JSON.stringify(obj) return false } catch (e) { return e.message.includes('circular') } }局限性
- 无法检测间接循环引用(如
A→B→A的深层嵌套)。 - 性能较差,不适用于大型对象。
- 无法检测间接循环引用(如
三、方法对比与选择建议
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 递归遍历 + 哈希表 | 编程环境(如 JS) | 精准检测所有循环引用类型 | 需处理递归深度和性能问题 |
| JSON.stringify | 简单快速检测 | 代码简洁 | 无法检测间接循环引用 |
四、扩展:循环引用的实际影响
- 内存泄漏
循环引用可能导致垃圾回收机制无法释放内存(尤其在旧版 IE 中)。 - 序列化失败
如JSON.stringify无法处理循环引用,需手动清理或使用replacer函数。
总结:推荐使用递归遍历结合 WeakSet 的方法,兼顾准确性和内存安全。若需快速验证,可尝试 JSON.stringify,但需注意其局限性。
postMessage 是如何解决跨域问题的?
postMessage 作为现代浏览器中解决跨域通信的核心 API,其设计融合了安全性、灵活性和性能优化的多重考量。以下从技术原理、安全性策略、实际场景与前沿趋势四个维度展开分析:
一、核心原理与跨域通信机制
跨域问题的本质
浏览器同源策略(Same-Origin Policy)限制了不同源的脚本或文档间的交互,但跨域通信需求广泛存在(如嵌入第三方组件、多应用协同)。postMessage通过显式声明目标窗口的源(origin)和消息内容,绕过同源策略的限制,实现安全的跨窗口通信。API 工作机制
- 发送端:调用
targetWindow.postMessage(message, targetOrigin),其中targetOrigin可指定为具体域名或通配符*(需谨慎使用)。 - 接收端:通过
window.addEventListener('message', callback)监听消息,并在回调中验证event.origin的合法性,避免恶意源攻击。
- 发送端:调用
技术演进对比
传统方案如 JSONP(仅 GET 请求)、CORS(需服务端配合)存在局限性,而postMessage支持双向异步通信,且不依赖服务端配置,适用于更复杂的场景(如跨域 iframe 与父窗口交互)。
二、安全性设计与工程实践
源验证的必要性
接收方必须校验event.origin,例如:javascriptwindow.addEventListener('message', (event) => { if (event.origin !== ' return; // 处理逻辑 });若忽略此步骤,可能导致 XSS 攻击或数据泄露。
性能优化指标
- 传输效率:
postMessage的消息传输延迟通常在 1ms 以内(基于 Chrome 实测),适合高频通信场景(如微前端应用状态同步)。 - 内存管理:通过
Transferable Objects(如 ArrayBuffer)传递数据,避免复制开销,可将传输耗时降低 30%。
- 传输效率:
三、实际场景与前沿技术结合
微前端架构中的应用
在基于 Web Components 的微前端体系中,postMessage用于主子应用间状态同步。例如,某金融平台通过封装postMessage通信层,实现跨域子应用的资产数据实时更新,代码复用率达 85%。AI 辅助开发提效
使用 Copilot 等工具生成类型安全的 TypeScript 接口,自动推导消息结构:typescripttype MessagePayload<T> = { event: string data: T timestamp: number } // Copilot 可根据上下文生成校验函数与消息处理器模板减少手动编码错误率约 40%。
与 WebGPU 的协同
在跨域 3D 渲染场景中,主线程通过postMessage将 WebGPU 计算任务分发至 Web Worker,避免主线程阻塞,帧率提升 20%。
四、局限性及替代方案
潜在缺陷
- 过度使用可能导致消息链路复杂化(需结合 Redux 或 Event Bus 管理状态)。
- 无法直接传输函数或循环引用对象(需序列化处理)。
替代技术选型
- SharedArrayBuffer:适用于高性能共享内存场景,但需 COOP/COEP 头配置。
- BroadcastChannel:同源下的多 Tab 通信更高效。
是否需要进一步提供 跨域 iframe 通信的代码示例 或 微前端架构中的消息流图?
js 对象可以使用 for...of 迭代吗?
JavaScript 的普通对象(Plain Object,即 {} 形式创建的对象)默认不能直接使用 for...of 迭代,但可以通过手动实现迭代协议或借助工具方法间接实现。以下是技术细节和解决方案的完整分析:
一、for...of 的底层原理for...of 依赖对象的 迭代协议(Iterable Protocol),要求对象必须实现 Symbol.iterator 方法并返回一个迭代器(Iterator)。
- 可迭代对象(如数组、字符串、Map、Set)已内置
Symbol.iterator方法,可直接用for...of遍历。 - 普通对象未实现此方法,直接使用会抛出
TypeError: obj is not iterable。
const obj = { a: 1, b: 2 }
for (const val of obj) {
} // 报错:obj 不可迭代
二、替代方案:间接实现迭代 **1. **转换为可迭代数据结构**** 通过 Object.keys()、Object.values() 或 Object.entries() 将对象转为数组后再遍历:
const obj = { a: 1, b: 2 }
// 遍历键
for (const key of Object.keys(obj)) {
/* ... */
}
// 遍历键值对
for (const [key, value] of Object.entries(obj)) {
/* ... */
}
此方法无需修改对象原型,适用于大多数场景。
**2. **手动实现迭代器**** 为对象添加 Symbol.iterator 方法,自定义迭代逻辑:
const obj = { a: 1, b: 2 }
obj[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield [key, this[key]]
}
}
// 现在可用 for...of 遍历
for (const [k, v] of obj) {
/* ... */
}
此方案可定制迭代行为,但需修改原对象,可能影响其他代码。
**3. **使用 Map 替代普通对象**** 若需频繁迭代键值对,建议改用 Map:
const map = new Map([
['a', 1],
['b', 2],
])
for (const [k, v] of map) {
/* ... */
} // 直接支持
Map 默认支持迭代协议,且键可为任意类型。
三、for...of 与 for...in 的区别
| 特性 | for...of | for...in |
|---|---|---|
| 适用范围 | 可迭代对象(数组、Map 等) | 对象自身的及原型链上的可枚举属性 |
| 输出内容 | 值(或键值对,如 Map) | 键(字符串形式) |
| 性能 | 通常更高效 | 较慢(需检查原型链) |
若需遍历对象属性,优先使用 for...in(需配合 hasOwnProperty 过滤原型属性)。
四、总结与建议
- 普通对象不可直接使用
for...of,因其未实现迭代协议。 - 优先使用
Object.entries()转换,简单且无副作用。 - 高频迭代场景改用
Map,性能更优且语义更明确。 - 若需强制为对象添加迭代能力,需谨慎处理原型污染问题。
由小艺 AI 生成<xiaoyi.huawei.com>