面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 21:35

Zookeeper 版本演进有哪些关键节点?升级怎么做?

核心答案ZooKeeper 从 2008 年开源至今,真正值得关注的版本节点只有几个:3.3.x 引入 Observer 角色,横向扩展读能力不拖慢写吞吐;3.4.x 剔除 UDP 选举,只保留 TCP 版 FastLeaderElection,部署最广的稳定版;3.5.x 新增容器节点(无子节点自动删除)、TTL 节点、动态 reconfig(不停机增减节点),从这版起支持在线扩缩容;3.6.x 支持 7 种节点类型,TLS 加密传输,内存和日志预分配优化;3.8.x 默认开启安全配置,增强容器化/K8s 适配。版本选择:新项目直接 3.8.x;老集群跑 3.4.x 且稳定、无动态扩容需求就不必急着升。升级怎么做滚动升级是标准做法——逐台停 follower、换包、重启,最后 leader 切换后再升原 leader。存活节点过半集群就可用。跨大版本需分步走:3.4 → 3.5 → 3.6 → 目标版本,不能跳级。升级前备份 dataDir 和事务日志,3.4.x 老集群可能只有日志没快照,首次启动新版会全量回放,耗时较长。追问Observer 和 Follower 的区别? Observer 不参与投票和写请求过半确认,只异步同步数据并处理读请求,适合跨机房或读多写少场景。3.5 的动态 reconfig 有什么坑? reconfig 是原子操作,但新旧节点网络不通或 myid 冲突时集群会卡在选举状态。先在测试环境验证,生产操作时保留回滚配置。KRaft 会取代 ZK 吗? Kafka KRaft 已移除 ZK 依赖,但 ZK 仍被 Dubbo、HBase 等广泛使用。短期不会淘汰,新项目可考虑 KRaft 版 Kafka 减少运维组件。
前端阅读 05月27日 21:35

XSS 和 CSRF 有什么区别?

核心区别XSS 是往页面里注入恶意脚本,让脚本在用户浏览器里执行;CSRF 是伪造用户身份,让浏览器替攻击者发请求。一个攻的是"用户对网站的信任",一个攻的是"网站对浏览器的信任"。| | XSS | CSRF ||---|---|---|| 原理 | 注入脚本在浏览器执行 | 伪造请求冒充用户 || 能力 | 可读写 DOM、窃取 Cookie、发任意请求 | 只能发请求,无法读响应 || 依赖登录 | 不需要 | 需要用户已登录 || 防护核心 | 输入过滤 + 输出编码 | Token + SameSite Cookie |XSS 防护要点三种类型:存储型(脚本入库,持久危害最大)、反射型(URL 参数带脚本)、DOM 型(前端 JS 拼接执行)。防护三板斧:输入侧:白名单校验,过滤特殊字符输出侧:HTML/JS/URL 分上下文编码,textContent 替代 innerHTML纵深防御:CSP 禁内联脚本,Cookie 设 HttpOnly// 输出编码示例function escapeHtml(str) { return str.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """);}CSRF 防护要点攻击者构造隐藏表单或图片标签,浏览器自动携带 Cookie 发请求,服务器以为是用户本人操作。防护四招:CSRF Token:每次请求携带服务端签发的随机 Token,攻击者拿不到就无法伪造SameSite Cookie:设为 Strict 或 Lax,阻止跨站携带校验 Referer/Origin:拒绝非本站来源的敏感请求自定义 Header:前端加 X-Requested-With,跨域限制让攻击者无法伪造<!-- 典型 CSRF 攻击 --><img src="https://bank.com/transfer?to=hacker&amount=10000" style="display:none">追问:XSS 能绕过 CSRF 防护吗?能。XSS 可以直接在页面内读取 CSRF Token 再发请求,等于 CSRF 的所有防线全部失效。所以防护 XSS 优先级更高——XSS 搞定了,CSRF 的 Token 机制才真正有效。
前端阅读 05月27日 21:34

whistle 如何解决跨域问题,有哪些常见的跨域场景?

答案Whistle 通过 resCors 协议一行规则即可解决跨域,无需手写脚本或配置 JSON 文件。最常用的三种写法:# 允许所有源(开发环境推荐)api.example.com resCors://*# 仅允许指定源api.example.com resCors://https://www.myapp.com# 启用 CORS 并自动回显请求中的 Originapi.example.com resCors://enableresCors://enable 会把响应头 Access-Control-Allow-Origin 设为请求携带的 Origin 值,适合需要携带 Cookie 的场景。resCors://* 直接设置 Access-Control-Allow-Origin: *,浏览器不会发送凭证但足够覆盖大多数调试需求。如果需要额外控制允许的方法或头部,用 resHeaders 补充:api.example.com resCors://* resHeaders://{cors-extra.json}另一种思路是代理到同源:通过 whistle 将前端和 API 映射到同一域名下,从根源消除跨域:my.app.com/api api.example.commy.app.com 127.0.0.1:3000常见跨域场景本地调试: 前端跑在 localhost:3000,后端在 api.example.com,加一条 resCors://* 即可。多子域名: www.example.com 访问 api.example.com,用 resCors://https://www.example.com 限定来源。携带凭证: 需要带 Cookie 时必须指定具体源,用 resCors://enable 或 resCors://https://www.myapp.com,不能使用 *。线上排查: 结合 SwitchyOmega 将流量导到 whistle,用 resCors://enable 临时放开,抓完即关。追问resCors 和 resHeaders 手动加 CORS 头有什么区别? resCors 是 whistle 内置协议,会同时处理预检请求(OPTIONS)的响应;手动加头容易遗漏 Access-Control-Max-Age 等字段,导致频繁预检。生产环境能用 whistle 解决跨域吗? 不能。whistle 是开发调试工具,生产环境应由 Nginx 或后端服务配置 CORS 策略。Cookie 跨域为什么不能用 *? 浏览器规范要求 Access-Control-Allow-Credentials: true 时 Origin 不能为 *,否则请求直接失败。
前端阅读 05月27日 21:33

whistle 常用的命令行操作有哪些?

核心命令Whistle 的命令行操作都通过 w2 命令完成,常用命令如下:w2 start — 启动服务,默认监听 8899 端口w2 start -p 8080 — 指定端口启动w2 start -S storageName — 指定存储目录,用于多实例管理w2 start -n user -w pass — 设置管理界面的登录用户名和密码w2 stop — 停止服务w2 restart — 重启服务w2 status — 查看运行状态w2 proxy — 设置系统代理w2 proxy 0 — 关闭系统代理w2 ca — 安装 HTTPS 根证书(抓 HTTPS 请求必须)w2 -h — 查看帮助安装只需 npm i -g whistle,Mac 用户也可用 brew install whistle。多实例管理需要同时运行多个 whistle 实例时,每个实例必须指定独立的端口和存储目录:w2 start -p 8010 -S 8010w2 start -p 8020 -S 8020建议端口和存储目录使用相同编号,便于管理。启动进阶参数--httpsPort 8001 — 启用 HTTPS 代理端口--socksPort 1080 — 启用 SOCKSv5 代理端口-P 8889 — 单独设置管理界面端口--inspect — 开启 Node.js 调试(默认 9229 端口)--config /path/to/config.json — 从配置文件加载参数追问:如何在命令行快速切换代理环境?通过 -S 指定不同存储目录,每个目录维护独立的规则集。脚本化切换:#!/bin/bashw2 stopw2 start -p 8899 -S $1执行 ./switch.sh dev 即可切换到 dev 环境的规则,无需手动编辑配置。
服务端阅读 05月27日 21:31

Zookeeper 是什么?它有哪些核心特性和应用场景?

Zookeeper 是什么?它有哪些核心特性和应用场景?\n\nZookeeper 是 Apache 维护的开源分布式协调服务,通过层次化命名空间和 Watcher 机制,为分布式应用提供配置管理、服务发现、分布式锁等协调能力。它本质上是一个高可用的分布式小文件存储系统,读多写少,适合做"共识"而非"存储"。\n\n## 核心特性\n\n一致性保证:所有客户端看到的数据视图一致,事务请求按顺序严格执行,要么全部成功要么全部失败。\n\n高可用:集群部署(通常3/5/7节点),过半节点存活即可服务。Leader 处理写请求并通过 ZAB 协议同步到 Follower,Follower 处理读请求,Observer 扩展读性能但不参与选举。\n\nWatcher 机制:客户端可对节点注册监听,数据变更时服务端主动推送通知。这是实现配置动态更新、服务上下线感知的基础。注意 Watcher 是一次性的,触发后需重新注册。\n\n## 数据模型\n\nZookeeper 采用树形结构,每个节点称为 ZNode。四类节点:\n\n- 持久节点:创建后永久存在,除非显式删除\n- 临时节点:绑定会话,断连自动删除——这是服务注册和分布式锁的实现基础\n- 持久顺序节点:自动追加递增序号,用于分布式队列\n- 临时顺序节点:临时+顺序,分布式锁的公平实现方式\n\n每个 ZNode 维护 dataVersion(数据版本)、cversion(子节点版本)、aversion(ACL版本),实现乐观锁。\n\n## 典型应用场景\n\n- 配置中心:节点存配置,Watcher 监听变更,实现动态推送\n- 服务注册与发现:服务启动时写临时节点,宕机自动摘除\n- 分布式锁:临时顺序节点实现公平锁,避免羊群效应\n- Leader 选举:利用临时顺序节点,序号最小的节点成为 Leader\n\n## 追问\n\nZAB 协议和 Paxos 的区别? ZAB 专为 Zookeeper 设计,支持崩溃恢复和消息广播两种模式,强调主备切换时的数据一致性;Paxos 是通用一致性算法,ZAB 可视为其简化变体,在 Leader 选举效率上更优。\n\n临时节点在什么情况下不会立即删除? 会话超时而非连接断开时触发删除。网络抖动期间客户端可能在其他 Server 上重建会话,此时临时节点迁移而非删除。
前端阅读 05月27日 21:30

如何使用 whistle 拦截 HTTPS 请求,证书如何配置?

答案whistle 作为本地代理,对 HTTPS 请求执行中间人解密:收到客户端 CONNECT 后,用自签 RootCA 为目标域名动态签发证书,再以该证书与客户端建立 TLS 连接,明文拿到请求内容后转发给真实服务器。要让客户端信任这些动态证书,必须把 whistle 的 RootCA 装进系统或浏览器的信任链。证书配置启动并打开管理界面 npm i -g whistle && w2 start浏览器访问 http://127.0.0.1:8899。下载根证书管理界面点击 HTTPS 标签,下载 RootCA。安装到系统信任链Mac:钥匙串访问 → 导入 → 双击设为"始终信任"Windows:双击证书 → 安装到"本地计算机" → 放入"受信任的根证书颁发机构"Linux:sudo cp rootCA.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates开启拦截勾选 HTTPS 面板的 "Capture HTTPS CONNECTs";只想拦截特定域名用规则 example.com filter://intercept。移动端手机连同一 Wi-Fi,浏览器访问 http://rootca.pro 下载证书,系统设置中信任该证书,再将手机代理指向电脑 IP:8899。踩坑点Android 7+:应用默认不信任用户证书,需 root 或配置 networksecurityconfigiOS 10.3+:安装后还得到 设置 → 通用 → 关于本机 → 证书信任设置 手动开启Firefox:独立证书库,需在 about:preferences 单独导入 RootCA证书锁定:部分 App 校验服务器证书指纹,代理无法拦截追问whistle 拦截 HTTPS 的原理?——中间人代理 + 动态签发证书如何只拦截特定域名?——filter://intercept 规则Android 高版本为什么抓不到包?——用户证书信任域变更
前端阅读 05月27日 21:29

RxJS 中如何处理背压(Backpressure)问题?

什么是背压RxJS 中 Observable 默认是推模型——生产者决定发射节奏。当数据产生速度超过消费者处理速度时,未处理的值会堆积在内存中,这就是背压问题。典型场景包括:高频 DOM 事件、WebSocket 消息流、快速轮询的传感器数据。RxJS 不像 RxJava 有 Flowable 这种原生背压类型,它的策略是"丢、缓、换"三种思路。丢:节流与采样不需要每一条数据时,主动丢弃多余值:throttleTime:时间窗口内只取第一个,适合 scroll/resize 等高频事件debounceTime:等输入静止后再发射,适合搜索框联想sampleTime:周期性取最新值,中间值全部丢弃auditTime:周期性取窗口末尾值,与 sample 语义不同// 滚动事件:200ms 内只处理一次fromEvent(window, 'scroll').pipe( throttleTime(200))// 搜索框:停止输入 300ms 后请求fromEvent(input, 'input').pipe( debounceTime(300), switchMap(e => search(e.target.value)))关键区别:throttle 保证有规律地采样,debounce 保证只在"安静"后触发,面试中经常要求区分二者。缓:批量打包需要全部数据但可以延迟处理时,把值攒起来一起发:bufferTime / bufferCount:按时间或数量打包成数组windowTime / windowCount:类似 buffer,但输出内层 Observable 而非数组// 每 500ms 打包一次传感器数据sensor$.pipe(bufferTime(500)).subscribe(batch => { processBatch(batch); // 一次处理多条});buffer 的风险是缓冲区无限增长,生产速度持续超过消费速度时仍然会内存溢出。实际项目中建议配合 take 或设置上限。换:切换并发模型改变数据消费方式来匹配生产速度:concatMap:完全串行,一个结束再发下一个,最安全但最慢mergeMap(n):限制并发数为 n,兼顾吞吐和资源switchMap:只保留最新请求,自动取消前一个// 并发限制为 3 的批量请求from(ids).pipe( mergeMap(id => fetchUser(id), 3))// 搜索场景:自动取消旧请求input$.pipe( debounceTime(300), switchMap(query => searchAPI(query)))面试高频追问:mergeMap、concatMap、switchMap、exhaustMap 四者的区别。核心区分点在于——新值到来时,是排队(concat)、并行(merge)、取消旧的(switch)还是忽略新的(exhaust)。实际选型思路面试中更看重你能否根据场景选对策略,而不是背诵 API:| 场景 | 推荐策略 | 理由 ||---|---|---|| 滚动/resize 事件 | throttleTime | 只需采样,丢数据无害 || 搜索输入联想 | debounceTime + switchMap | 避免无效请求,自动取消旧请求 || 批量 API 请求 | mergeMap(n) | 控制并发,兼顾效率 || 有序上传文件 | concatMap | 顺序保证,避免竞争 || 高频传感器数据 | bufferTime | 批量处理更高效 |RxJS 没有真正的拉取式背压,本质上是"有损控制"。如果业务要求零丢数据,需要考虑在架构层面引入队列或换用支持 Flowable 的方案。
前端阅读 05月27日 21:29

输入验证和输出编码有什么区别?如何正确使用它们来防止 XSS 攻击?

核心区别输入验证在数据进入系统时检查合法性,输出编码在数据输出到页面时转义危险字符。两者不可互相替代——验证拦不住所有恶意输入,编码兜底保证即使漏网也不会执行。一句话:验证是门卫,编码是防弹衣。输入验证在接收用户输入的环节,对数据的格式、类型、长度做白名单校验,拒绝不符合预期的内容。// 白名单:只允许字母数字function validateUsername(name) { return /^[a-zA-Z0-9]+$/.test(name);}关键原则:用白名单,不用黑名单——黑名单永远补不完绕过方式服务端必须验证,客户端验证可被绕过验证的是"数据是否合法",不是"数据是否危险"输出编码在数据渲染到页面之前,根据输出上下文对特殊字符做转义,使浏览器将其视为文本而非代码。// HTML上下文:转义 < > " ' &function escapeHtml(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''');}不同上下文需要不同编码:HTML正文、HTML属性、JavaScript内联、URL参数、CSS——各用对应的转义函数或库(如 DOMPurify 处理 HTML,encodeURIComponent 处理 URL)。为什么两者缺一不可只做验证:攻击者总能找到绕过方式(编码变形、特殊字符组合),验证无法覆盖所有场景只做编码:恶意数据会进入数据库,污染日志、影响其他系统、触发二阶XSS实际流程:用户输入 → 输入验证 → 存储 → 输出编码 → 渲染,每一层各自兜底模板引擎默认编码(EJS <%=%>、Handlebars {{}})能挡住大部分HTML上下文XSS,但JavaScript上下文和URL上下文仍需手动处理。追问:CSP 能替代输出编码吗?不能。CSP(Content Security Policy)限制脚本加载来源,是纵深防御的一环,但无法防御同源内的内联脚本注入,且旧浏览器不支持。CSP 是保险绳,输出编码是安全带,都得系。
服务端阅读 05月27日 21:28

Zookeeper 的 Watcher、ACL 和事务操作怎么用?

Watcher、ACL 和事务操作是 ZooKeeper 的三大高级特性Watcher 是一次性的:触发后自动失效,必须在回调中重新注册,否则会丢事件。注册方式有三种——getData 监听数据变化、getChildren 监听子节点变化、exists 监听节点创建。常见坑:客户端串行执行回调,回调里不能做耗时操作,否则阻塞后续事件处理。ACL 控制"谁能对哪个节点做什么"。权限分 CREATE/READ/WRITE/DELETE/ADMIN 五种,方案有 world(开放)、auth(认证用户)、digest(用户名密码)、ip(地址段)。生产建议:关键路径用 digest 方案收紧权限,避免 world:anyone 的 OPENACLUNSAFE。事务操作通过 multi 接口实现,将多个操作打包原子提交。底层依赖 zxid 保证顺序一致性——zxid 高 32 位是 Leader 周期号,低 32 位递增计数。multi 全部成功或全部失败,不存在部分执行。典型场景:同时创建多个互相关联的节点。追问:Watcher 为什么设计成一次性? 服务端为每个节点维护 Watcher 集合,一次性触发后即清理,避免海量长连接下内存膨胀。代价是客户端必须重注册,Curator 的 Cache 机制封装了这个逻辑。追问:ACL 和 Linux 文件权限有什么区别? ACL 是三元组 scheme:id:perm,scheme 决定认证方式而非简单的用户组。同一个节点可以叠加多条 ACL,粒度到操作类型而非读写两位。追问:multi 操作中某一步失败会怎样? 服务端预校验所有操作,任一失败则整体回滚,已执行的操作也会撤销。这是 ZAB 协议两阶段提交在客户端侧的体现。
前端阅读 05月27日 21:26

RxJS 中如何创建自定义操作符?

核心答案RxJS 创建自定义操作符有三种方式,优先使用组合现有操作符,其次用 pipeable 函数,避免直接 new Observable:// 1. 组合现有操作符(推荐)function debounceSearch(ms = 300) { return pipe( debounceTime(ms), distinctUntilChanged() );}// 2. pipeable 函数function debug<T>(label: string): OperatorFunction<T, T> { return source$ => source$.pipe( tap(v => console.log(label, v)) );}三种方式的区别| 方式 | 适用场景 | 复杂度 ||------|---------|--------|| 组合现有操作符 | 逻辑由已有操作符拼装即可 | 低 || pipeable 函数(返回 source$ => new Observable) | 需要精细控制订阅和退订 | 中 || 直接 new Observable | 极端定制场景,需手动管理全部生命周期 | 高 |直接 new Observable 需要自己处理 next/error/complete 和 teardown,容易遗漏导致内存泄漏,非必要不使用。追问:操作符内如何保证资源释放?返回的 Observable 订阅时必须返回 teardown 函数:function withTimeout<T>(ms: number): OperatorFunction<T, T> { return source$ => new Observable(subscriber => { const timer = setTimeout(() => subscriber.error(new Error('timeout')), ms); const sub = source$.subscribe({ next: v => subscriber.next(v), error: e => subscriber.error(e), complete: () => subscriber.complete() }); // teardown:取消定时器 + 退订上游 return () => { clearTimeout(timer); sub.unsubscribe(); }; });}忘记返回 teardown 是自定义操作符最常见 bug,会导致定时器、子订阅等资源无法回收。追问:RxJS 7+ 写法有什么变化?retryWhen、tap(next, error, complete) 三参数重载等已在 RxJS 7 中弃用或移除。自定义操作符应使用 retry({ count, delay }) 等新 API,组合操作符时优先用 pipe() 而非链式调用。
前端阅读 05月27日 21:26

RxJS 中如何处理错误?有哪些错误处理操作符?

核心答案RxJS 中 Observable 一旦出错,整个流就会终止。常用的错误处理操作符有五个:catchError — 捕获错误,返回替代 Observable,流继续retry(n) — 出错后重新订阅,最多重试 n 次retryWhen — 自定义重试策略(延迟、指数退避等)finalize — 流结束(无论成功还是出错)时执行清理onErrorResumeNext — 出错后跳到下一个 Observable 继续其中 catchError 是面试最常考的,关键在于理解它的位置决定行为:放在 mergeMap 内部只捕获单条流错误,放在外部则整个流被替换。catchError 的位置陷阱// 错误写法:外层 catchError,一条失败整条流终止source$.pipe( mergeMap(id => fetchData(id)), catchError(() => of(fallback)) // 任一请求失败,后续全部跳过);// 正确写法:内层 catchError,单条失败不影响其他source$.pipe( mergeMap(id => fetchData(id).pipe( catchError(() => of(fallback)) // 只替换这一条 )));面试中经常追问这个区别:内层捕获让每条数据流独立容错,外层捕获则是兜底策略。retry 与 retryWhen 怎么选retry(3) 简单粗暴,立刻重试三次。实际项目中更常见带延迟的重试:source$.pipe( retryWhen(errors => errors.pipe( scan((count, err) => { if (count >= 3) throw err; return count + 1; }, 0), delayWhen(count => timer(Math.pow(2, count) * 1000)) ) ));指数退避重试是生产环境的标准做法,面试能答出这个基本过关。注意 RxJS 7 之后 retryWhen 已废弃,推荐用 retry({ delay: ... }) 替代。finalize 不是 finallyfinalize 无论成功、出错还是取消订阅都会执行,适合释放资源(关闭连接、清除定时器等)。它不接收参数,拿不到错误信息,别跟 Promise 的 finally 混淆。追问方向catchError 返回 throwError 会怎样?——错误继续向上传播retry 的重试是重新订阅还是重新执行?——重新订阅整个上游Observable 出错后订阅者还能收到值吗?——不能,流已终止
服务端阅读 05月27日 21:25

Zookeeper 有哪些典型的应用场景?如何实现分布式锁和服务注册发现?

核心场景与原理Zookeeper 的典型应用场景本质是对三大能力的组合:临时节点(会话结束自动删除)、顺序节点(自带递增编号)、Watcher 机制(变更实时通知)。分布式锁是面试追问最多的场景。实现方式:客户端在 /lock 下创建临时顺序节点,获取所有子节点后判断自己是否序号最小——最小则持锁,否则 Watch 前一个节点。释放时删除自身节点即可,会话断开临时节点也会自动清除,不会死锁。监听前序节点而非所有节点,是为了避免"羊群效应"。服务注册与发现依赖临时节点 + Watch。服务上线创建临时节点注册地址,消费者 Watch 该目录获取实例列表。提供者宕机后节点自动消失,消费者收到通知更新列表。Dubbo 早期即用此方式做服务治理。配置中心将配置写入持久节点,客户端 Watch 变更,改动实时推送,适合读多写少的强一致性场景。Master 选举同样借助顺序节点——参与者各创建临时顺序节点,序号最小者成为 Master,其余 Watch 前序节点,故障时自动重选。Kafka Controller 选举便用此方式。此外还有命名服务(顺序节点生成全局唯一 ID)、分布式队列(FIFO 与 Barrier 两种模型)、集群管理(临时节点感知成员上下线)。生产实践要点ZooKeeper 适合强一致性、读多写少、数据量小的协调场景,不适合海量存储或高并发写入。Kafka、HBase 仍依赖 ZK 做元数据管理和选举,但新系统更多选择 Nacos(注册+配置一体)、etcd(更轻量)等替代。ZK 分布式锁比 Redis 锁更可靠——CP 模型保证主从切换不丢锁;代价是性能更低。金融等对锁可靠性要求高的场景优先选 ZK。追问方向Watch 是一次性的,触发后须重新注册,通知有延迟,如何保证不丢事件?临时顺序节点创建和序号判断之间网络分区会发生什么?ZK 集群 Leader 选举过程中能否对外提供服务?
前端阅读 05月27日 21:25

RxJS 中的调度器(Scheduler)是什么?如何使用?

调度器是什么调度器决定 RxJS 中通知(next/error/complete)的执行时机和上下文。简单说:它控制一段 Observable 逻辑"在什么时候跑"——同步、微任务、宏任务还是动画帧。RxJS 内置 5 种调度器,核心区别如下:| 调度器 | 底层机制 | 典型场景 ||--------|----------|----------|| null(同步) | 直接递归调用 | 默认,少量数据 || queueScheduler | 同步蹦床调度 | 递归/迭代,防栈溢出 || asapScheduler | Promise.then(微任务) | 优先级高于宏任务的异步 || asyncScheduler | setInterval(宏任务) | 延迟、定时操作 || animationFrameScheduler | requestAnimationFrame | 浏览器动画 |怎么指定调度器三种方式:1. 创建时传入——作为操作符最后一个参数:of(1, 2, 3, asyncScheduler).subscribe(console.log);2. observeOn / subscribeOn:observeOn(asyncScheduler):下游通知切到异步调度subscribeOn(asyncScheduler):上游订阅动作切到异步调度二者区别常被追问:subscribeOn 影响的是"什么时候开始执行 Observable 逻辑",observeOn 影响的是"下游在哪个调度器上接收通知"。3. schedule 方法——手动调度:asyncScheduler.schedule(() => console.log('1s后'), 1000);面试高频追问asapScheduler 和 asyncScheduler 有什么区别?asapScheduler 走微任务队列(Promise.then),asyncScheduler 走宏任务队列(setTimeout/setInterval)。微任务优先级更高,会在当前宏任务结束后、下一个宏任务之前执行。需要尽快响应但不阻塞主线程时选 asap,需要真正延迟时选 async。queueScheduler 和同步调度器有什么区别?同步调度器直接递归调用,大量数据可能栈溢出。queueScheduler 用蹦床调度(trampoline),把递归展开为循环迭代,避免栈溢出同时保持同步语义。什么时候该手动指定调度器?大多数时候不需要——RxJS 按最小并发原则自动选择默认调度器。手动指定主要出现在三个场景:测试中用 TestScheduler 控制时间、动画中用 animationFrameScheduler 同步渲染帧、需要改变默认执行上下文时。如何在测试中控制调度器?使用 TestScheduler,它提供虚拟时钟,可以用 marble diagram 声明时序并同步断言结果,避免真实等待。选择建议不需要调度器就不加。需要延迟选 async,需要非阻塞优先级选 asap,递归防溢出选 queue,动画选 animationFrame。过度使用调度器只会增加复杂度和性能开销。
前端阅读 05月27日 21:24

如何在TypeScript中处理枚举?

TypeScript中如何处理枚举?TypeScript枚举分为数字枚举、字符串枚举和常量枚举三种。面试中重点考察它们的区别、反向映射机制以及何时该用union type替代。数字枚举默认从0自增,支持反向映射(值→名):enum Status { Pending, Approved, Rejected }// Status.Pending === 0, Status[0] === "Pending"字符串枚举不支持反向映射,但可读性更好,调试时值有意义:enum Role { Admin = "ADMIN", User = "USER" }const enum在编译时内联,不生成JS对象,减小产物体积:const enum Color { Red, Green, Blue }let c = Color.Red; // 编译后: let c = 0关键区别与陷阱数字枚举有反向映射,字符串枚举没有异构枚举(数字+字符串混合)不推荐使用,TypeScript本身也会警告const enum在isolatedModules模式下可能出问题,跨模块引用需谨慎枚举是运行时对象,占用JS产物体积何时用union type替代当值少且不需要反向映射时,union type更轻量:type Direction = "up" | "down" | "left" | "right"union type零运行时开销、类型安全、支持tree-shaking,是现代TS项目的首选。枚举更适合值多、需要反向映射或运行时遍历的场景。追问const enum和普通enum编译产物有什么区别?为什么字符串枚举没有反向映射?keyof typeof Status能得到什么类型?
前端阅读 05月27日 21:22

Appium 的 Desired Capabilities 是什么?

Appium 的 Desired Capabilities 是一组键值对,用于告诉 Appium Server 如何配置自动化测试会话——包括在哪个平台、哪台设备、启动哪个应用、使用哪个自动化引擎。客户端以 JSON 格式发送这些参数,服务端据此创建对应的测试环境并返回 session ID。核心参数有哪些?必须掌握的几个关键参数:platformName:目标平台,值为 Android / iOS / Windows,必填deviceName:设备名称,真机或模拟器均可,必填automationName:自动化引擎,Android 用 UiAutomator2,iOS 用 XCUITestapp:待测应用的路径或远程 URL(.apk / .ipa){ "platformName": "Android", "deviceName": "Pixel 5", "automationName": "UiAutomator2", "app": "/path/to/app.apk"}Android 和 iOS 有什么区别?这是面试高频追问。两者差异主要体现在应用标识和引擎配置上:Android 专属:用 appPackage 指定包名,用 appActivity 指定启动 Activity。例如 appPackage: "com.example.app"、appActivity: ".MainActivity"。iOS 专属:用 bundleId 标识应用,真机测试需要配置 xcodeOrgId 和 xcodeSigningId 完成签名,模拟器则设置 udid: "auto" 即可。引擎选择也不同——Android 默认 UiAutomator2,iOS 默认 XCUITest,选错引擎会直接导致会话创建失败。noReset 和 fullReset 有什么区别?noReset: true — 会话结束后保留应用数据,下次启动不重装,适合调试阶段复用状态fullReset: true — 每次会话前卸载应用并重装,确保干净环境,适合持续集成场景两者互斥,不能同时为 true。面试中常追问"你项目中用的哪个,为什么",回答时要结合实际场景。常见踩坑点appActivity 前缀漏写点号 — 应写 .MainActivity 而非 MainActivity,否则 Appium 报找不到 ActivityautomationName 与平台不匹配 — iOS 用了 UiAutomator2 会直接报错,务必对齐平台真机 udid 填错 — 可通过 adb devices(Android)或 Xcode 设备列表(iOS)确认追问:如何在多平台项目中管理 Capabilities?将 Android 和 iOS 配置拆分为独立文件,运行时通过环境变量切换:const caps = process.env.PLATFORM === 'ios' ? require('./ios.config') : require('./android.config');这样既避免配置混乱,又便于 CI 流水线按平台并行执行。
前端阅读 05月27日 21:18

Appium 如何进行移动 Web 测试?

Appium 支持对移动浏览器中的 Web 应用进行自动化测试,核心思路是通过设置 browserName 能力让 Appium 驱动移动端 Chrome 或 Safari,然后用标准 WebDriver 协议操作页面元素。Android Chrome 与 iOS Safari 配置Android 端将 browserName 设为 Chrome,Appium 会自动调用 Chromedriver 驱动浏览器:const caps = { platformName: 'Android', browserName: 'Chrome', deviceName: 'Pixel 5'};iOS 端将 browserName 设为 Safari,Appium 通过 XCUITest 驱动 Safari:const caps = { platformName: 'iOS', browserName: 'Safari', deviceName: 'iPhone 14', automationName: 'XCUITest'};关键区别:Android 需要 Chromedriver 版本与设备 Chrome 版本匹配,版本不匹配时会报 SessionNotCreatedException,需通过 chromedriverExecutable 指定驱动路径或让 Appium 自动更新。移动 Web 与原生 App 测试的差异移动 Web 测试用 CSS 选择器和 XPath 定位 HTML 元素,而非 accessibility id 或 uiautomator。触摸交互需要用 W3C Actions API 模拟手势,比如滑动:await driver.actions([{ type: 'pointer', id: 'finger', parameters: { pointerType: 'touch' }, actions: [ { type: 'pointerMove', origin: 'viewport', x: 200, y: 800 }, { type: 'pointerDown' }, { type: 'pointerMove', origin: 'viewport', x: 200, y: 200 }, { type: 'pointerUp' } ]}]);另一个常见陷阱:移动浏览器的地址栏会自动收起,导致元素坐标偏移,建议用元素相对定位而非绝对坐标。响应式布局与横竖屏测试用 driver.setRect() 改变窗口尺寸验证响应式断点,用 driver.rotate() 切换横竖屏:await driver.setRect({ width: 375, height: 667 });// 验证移动端布局const menu = await driver.findElement(By.css('.mobile-menu'));assert(await menu.isDisplayed());await driver.rotate({ screen: 'LANDSCAPE' });// 验证桌面端布局常见问题与排查元素定位失败:移动浏览器渲染的 DOM 可能与桌面不同,用 driver.getPageSource() 检查实际 DOM 结构,注意浏览器可能注入了 meta viewport 或自定义样式。页面加载超时:移动网络延迟大,需要设置更长的隐式等待,或用显式等待:await driver.wait(until.elementLocated(By.id('content')), 15000);Chromedriver 版本冲突:Appium 2.0 默认不内置 Chromedriver,需安装 appium-chromedriver 扩展,或通过 --chromedriver-version 指定版本。追问方向混合应用中如何切换 WebView 上下文与原生上下文?用 driver.getContexts() 获取所有上下文后 driver.switchTo().context('WEBVIEW_xxx')。如何在真机上抓取移动浏览器网络请求?通过 Chromedriver 的 CDP 支持(cdpPort 能力)或代理工具如 mitmproxy。
计算机基础阅读 05月27日 21:17

ASCII 码中数字字符怎么和整数互相转换?

数字字符和 ASCII 码怎么互相转换?数字字符 '0'-'9' 的 ASCII 值是 48-57,连续排列。核心转换就一个公式:数字字符的 ASCII 值 = 数字值 + 48(即 ord('0'))。字符转整数: 用字符减去 '0' 的 ASCII 值即可。int num = ch - '0'; // C/Javaint num = ord(ch) - ord('0') # Python整数转字符: 数字加上 '0' 的 ASCII 值。char ch = num + '0'; // C/Javachar = chr(num + ord('0')) # Python判断是否为数字字符:// C/Javaif (ch >= '0' && ch <= '9')// Pythonif '0' <= ch <= '9'为什么是减 '0' 而不是减 48?语义清晰。ch - '0' 直接表达"求这个字符代表的数字",而 ch - 48 需要读者心算 48 是什么。另外,ASCII 并非唯一编码标准,用 '0' 做基准在 EBCDIC 等编码下逻辑不变(虽然值不同),代码可移植性更好。实战:手写字符串转整数不用 parseInt / int(),手动实现:def str_to_int(s): num = 0 for c in s: if not ('0' <= c <= '9'): break num = num * 10 + (ord(c) - ord('0')) return num每一轮把已有结果左移一位(乘 10),再加上新数字位。追问:负数怎么处理?跳过开头的 '-',按正数转换,最后取反即可。注意 "-0" 和溢出的边界情况,这是面试中常见的 follow-up。
计算机基础阅读 05月27日 21:17

如何判断字符串是否为纯 ASCII 字符串

核心思路ASCII 字符的编码值范围是 0–127(0x00–0x7F),共 128 个字符。判断一个字符串是否为纯 ASCII,本质就是检查其中每个字符的编码值是否都小于 128。任何编码值 ≥ 128 的字符都是非 ASCII 字符(如中文、emoji、法文重音字母等)。最简实现Python 一行搞定:s.isascii() # Python 3.7+内部等价于 all(ord(c) < 128 for c in s),发现非 ASCII 字符立即返回 False,时间复杂度 O(n)。JavaScript 用正则:/^[\x00-\x7F]*$/.test(str)\x00-\x7F 就是 0–127 的十六进制表示,正则引擎会逐字符匹配,命中非 ASCII 即失败。Java 17+ 用内置方法:str.chars().allMatch(Character::isAscii)Go 逐 rune 判断:func isASCII(s string) bool { for _, r := range s { if r > 127 { return false } } return true}C 逐字节判断(注意必须用 unsigned char,否则高位字符会被当作负数):bool is_ascii(const char *s) { for (size_t i = 0; s[i]; i++) if ((unsigned char)s[i] > 127) return false; return true;}面试追问Q: 空字符串算 ASCII 吗?算。空集合不包含非 ASCII 字符,逻辑上为真,isascii() 和正则方式也返回 True。Q: UTF-8 编码下 ASCII 字符有什么特殊性?ASCII 字符在 UTF-8 中只占 1 字节,且编码值与 ASCII 完全一致。因此可以用 len(s) == len(s.encode("utf-8")) 间接判断:长度不等说明存在多字节字符。Q: 如何用 SIMD 加速?对长字符串,可以将每 16 字节加载到 SIMD 寄存器,与 127 做比较,一次判断 16 个字符是否都在 ASCII 范围内,比逐字符快一个数量级。Rust 的 bstr 库已内置此优化。边界注意控制字符(0–31)也是合法 ASCII,别误判C/C++ 中 char 是否有符号由实现定义,必须强转 unsigned charUnicode 组合字符(如 é = e + \u0301)各部分均在 ASCII 范围内,但视觉上是非 ASCII 外观,需根据业务场景决定是否额外处理
前端阅读 05月27日 21:17

Appium 如何进行元素定位?

Appium 提供了多种元素定位策略,面试中需重点掌握各策略的适用场景和优先级选择。核心答案Appium 支持 ID、Accessibility ID、XPath、Class Name、CSS Selector、UIAutomator(Android)、iOS Predicate、iOS Class Chain 共 8 种定位策略。优先级从高到低:ID / Accessibility ID > 平台专属定位器 > XPath。ID 定位最稳定,Android 用 resource-id,iOS 用 name,优先选用。Accessibility ID 跨平台统一,Android 对应 content-desc,iOS 对应 accessibilityIdentifier,是跨平台方案的首选。XPath 功能强但性能差,遍历 DOM 树开销大,且易因 UI 变动失效,应作为兜底方案。平台专属定位器性能优于 XPath:Android 的 UIAutomator 用 UiSelector 组合条件查询,iOS 的 Predicate String 类似 SQL WHERE 子句,Class Chain 比 Predicate 更简洁,三者均在原生引擎执行,速度快。// 优先:ID 定位driver.findElement(By.id('submit_button'));// 跨平台:Accessibility IDdriver.findElement(By.accessibilityId('submit_button'));// 兜底:XPath(避免绝对路径)driver.findElement(By.xpath('//android.widget.Button[@text="Submit"]'));追问:如何提升定位稳定性?三点原则:缩小搜索范围(先定位父容器再找子元素)、显式等待(driver.wait(until.elementLocated(...)) 替代硬编码等待)、缓存元素引用(避免重复定位同一元素)。追问:元素找不到怎么排查?依次检查:定位策略是否正确、元素是否尚未加载(加显式等待)、元素是否在其他上下文中(用 driver.getContexts() 检查,WebView 场景需切换上下文)。若定位到多个元素,改用更精确的属性组合或 findElements 加索引筛选。
前端阅读 05月27日 21:17

Appium 如何进行手势操作?

Appium 手势操作依赖 TouchAction(1.x)和 W3C Actions API(2.x 推荐)实现,核心是通过 press、moveTo、release 组合链模拟触摸行为,所有手势必须调用 perform() 才会执行。核心手势怎么写?点击:element.click() 最为直接;坐标点击用 driver.touchActions([{action:"tap", x:100, y:200}])。长按:W3C Actions 写法——driver.actions({async:true}).move({origin:el}).press().pause(2000).release().perform(),pause 控制按压时长。滑动:本质上就是 press → moveTo → release 的坐标链。上滑把 startY 设大、endY 设小即可,横向同理。拖拽:driver.actions({async:true}).dragAndDrop(src, target).perform(),底层与滑动一致,只是起止都是元素。多点触控怎么处理?缩放(pinch/spread)需要两根手指同时操作。Appium 1.x 用 MultiTouchAction 分别添加两个 TouchAction 再 perform;2.x 推荐用 W3C PointerInput 创建多个 pointer,各自执行 press/move/release 后一起 perform。关键点:两指必须真正"同时",不是串行执行。用 MultiTouchAction 时两个 action 是并行发送的。坐标怎么算才稳定?绝对坐标在不同设备上必定偏移。正确做法是通过 element.getRect() 取元素位置后算中心点,或用屏幕尺寸算百分比坐标(如 size.width * 0.8)。这样换设备不会崩。手势操作最常踩什么坑?元素不可见就操作——必须先 wait 直到 elementIsClickable动画没结束就操作——适当 sleep 或等动画元素消失Appium 2.x 还在用 TouchAction——已废弃,切换到 W3C Actions APIiOS 和 Android 手势 API 有差异——iOS 支持 mobile: 系列扩展命令(如 mobile: swipe),Android 部分手势需要 UiAutomator2 配合追问:Appium 1.x 的 TouchAction 和 2.x 的 W3C Actions 有什么区别?TouchAction 是 Appium 自定义 API,链式调用 press/wait/release,2.x 起已废弃。W3C Actions 是 WebDriver 标准协议,通过 PointerInput + Sequence 描述动作序列,跨浏览器/跨平台兼容性更好。迁移时核心变化是把 TouchAction 链替换为 actions().move().press().pause().release() 调用链。