JavaScript 面试题库

收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。

91个问题 初级 高级
全部难度 初级 中级 高级
逐题查看 显示全部答案
报告错误

语言核心

48个问题
01 ===、Object.is 与 SameValueZero 有何区别,选择哪一种会在什么场景产生影响? 中级 常见 查看答案 ▾ 收起 ▴

普通严格比较使用 ===:它不做类型强制转换,NaN 与自身不相等,而 0 与 -0 相等。Object.is 只改变这两个数值边界:NaN 与自身相等,0 与 -0 不同。SameValueZero 既认为 NaN 与自身相等,也认为两个零相等;Array.prototype.includes 以及 Map、Set 的键匹配都采用它。对数值去重、状态变化检测或查找 NaN 时,这个选择会影响结果。例如 includes 能找到 NaN,而使用严格相等的 indexOf 不能。

讲清楚了吗?
02 JavaScript 如何通过原型链查找属性,又该怎样安全地判断属性归属? 中级 常见 查看答案 ▾ 收起 ▴

读取属性时,运行时先检查对象自身,再沿内部原型链接逐层查找,直到找到该键或到达 null。自有属性可以遮蔽继承属性,包括访问器。in 运算符会同时报告自有和继承属性,因此不能用来判断归属。应使用 Object.hasOwn(object, key),尤其当对象可能具有 null 原型或覆盖了 hasOwnProperty 时。在输入边界还要注意,赋值可能触发继承的 setter。若接口接受任意键,应使用明确允许列表,或使用 null 原型字典。

讲清楚了吗?
03 为什么 JavaScript 私有字段不同于以下划线开头的普通属性? 高级 偶尔 查看答案 ▾ 收起 ▴

#name 是词法声明的私有元素,不是以字符串为键的属性。声明类之外的代码无法书写该名称,反射与 JSON 序列化不会枚举它,访问时还会检查接收者的私有标记。子类不会自动获得父类私有名称的直接访问权,Proxy 包装器也不会自动拥有目标对象的标记。相比之下,_name 只是命名约定,是否可读、可写、可枚举仍由普通属性描述符决定。应通过公开方法暴露行为,同时不要把语言级私有性误当成授权或加密。

讲清楚了吗?
04 与修改型数组方法相比,复制型数组方法提供哪些保证? 初级 常见 查看答案 ▾ 收起 ▴

sort、reverse、splice 等修改型方法会改变接收者;对应的 toSorted、toReversed、toSpliced 会返回不同的外层数组,并保持接收者不变。但这个保证只是浅层的:元素对象仍被共享,因此修改嵌套对象会从两个数组中同时可见。不同修改型方法的返回值也不一致,不能由修改行为推断。例如 sort 返回接收者,splice 返回被删除的元素。在状态代码中,判断更新是否不可变时必须同时追踪外层数组标识和嵌套对象标识。

延伸阅读 数组方法
讲清楚了吗?
05 ES 模块导入是“实时绑定”意味着什么,为什么循环依赖仍可能失败? 高级 偶尔 查看答案 ▾ 收起 ▴

ES 模块导入是导出方绑定的只读视图,而不是复制出的快照。导出模块之后更新 let 绑定时,导入方会看到新值,但不能通过导入名称赋值。执行前,加载器会先链接模块图,因此许多循环依赖可以建立。若顶层执行在某个模块初始化词法导出之前就读取它,循环仍会因暂时性死区而报错,或产生依赖顺序的行为。应打破循环,把读取延迟到函数调用时,或把共享契约移到更低层模块。

延伸阅读 模块
讲清楚了吗?
06 对象满足哪些契约才可迭代,循环提前结束时如何执行清理? 中级 偶尔 查看答案 ▾ 收起 ▴

可迭代对象提供 Symbol.iterator,并由它返回迭代器。每次调用 next 都返回含有 done、以及必要时含有 value 的对象。可迭代对象与迭代器可以是同一对象,也可以不是。for…of 因 break、return 或异常完成而提前退出时,会执行迭代器关闭:若存在 return 方法就调用它。生成器可借此在 finally 中清理资源。但这不表示手动消费的所有迭代器都会自行清理;直接调用 next 的代码仍必须定义并遵守所有权与关闭策略。

讲清楚了吗?
39 为什么 forEach 不会等待 async 回调,哪些写法能明确表达完成语义? 初级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,forEach() 会同步调用每个回调,并忽略其返回值,因此 async 回调返回的 Promise 既不会被收集,也不会被等待。外层函数可能已结束,工作和拒绝却仍无人处理。允许全部并发且需等待所有结果时,使用 await Promise.all(items.map(worker));需按顺序或限制容量时,使用带 awaitfor...of。大量输入应采用有界 worker pool,因为无界 Promise.all() 依然可能压垮下游。

讲清楚了吗?
40 数组方法如何处理空位,为什么展开稀疏数组会改变后续行为? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,map()filter()forEach() 等许多迭代方法会跳过没有对应属性的索引,而 for...of 与数组迭代器会在这些位置产生 undefinedmap() 会在结果中保留空位;展开数组则会把它们具体化为 undefined 元素,之后的回调就会访问它们。这会影响计数、校验和序列化。不要用空位表示业务状态;应显式归一化,或在必须区分空位与存储的 undefined 时使用 index in array

延伸阅读 数组方法
讲清楚了吗?
51 const 真正使什么不可变,又该如何单独审查对象所有权? 初级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,const 只禁止一个词法绑定被重新赋值,不会冻结该绑定引用的对象。属性与嵌套对象仍可变,其他别名也会观察到同一次修改。Object.freeze() 只限制目标对象自身这一层,嵌套引用和访问器副作用仍可能改变状态。审查时应分开判断绑定稳定性与数据所有权。若调用方需要快照,应只在标识必须分离的层级构造新对象,并测试嵌套标识。盲目深度冻结可能错误处理环、类实例、Proxy 和资源,同样需要明确契约。

讲清楚了吗?
52 为什么真值检查不等于存在性或有效性检查? 初级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,条件会把值转成 Boolean,因此 0-00nNaNfalse、空字符串、nullundefined 都是假值。空数组、空对象以及 "false" 这样的字符串却是真值。所以 if (value) 无法说明字段是否存在、数组是否有元素,或文本是否表示布尔值。只有 nullundefined 表示缺失时使用 ??;关心属性是否存在时使用 Object.hasOwn();类型、语法与范围则要按业务契约校验。用 || 提供默认值会静默覆盖有效的零、false 或空字符串。

讲清楚了吗?
53 let、const 与 var 在声明语句执行前有何不同? 初级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,进入作用域时,letconst 已经创建了块级绑定,但在声明执行前,它们一直位于暂时性死区且尚未初始化。提前读取任意一个都会抛出 ReferenceErrorconst 还必须在声明时初始化,之后不能重新赋值。var 属于函数或全局作用域,并在作用域建立时初始化为 undefined,因此提前读取会成功,却可能遮蔽顺序错误。“提升”不表示源码被移动。应优先使用词法声明并明确安排初始化顺序;如果已声明的导出仍未初始化,还要检查模块循环。

延伸阅读 JavaScript 基础
讲清楚了吗?
54 哪些日期字符串适合安全接收,又该如何检测无效 Date? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,规范定义的日期时间格式是可移植基线:Z 表示 UTC,显式偏移量标识一个时刻,仅日期形式按 UTC 解释,不带偏移量的日期时间则按本地时间解释。其他人类习惯格式可能由实现决定,看似合理的组件也可能被归一化而非报错。边界必须定义唯一语法与时区策略,再用 Number.isNaN(date.getTime()) 检查;无效 Date 对象仍是真值。校验前不要调用会抛出 RangeErrortoISOString(),也不要仅靠正则证明日期真实存在。

延伸阅读 Date 对象
讲清楚了吗?
55 为什么增加 86,400,000 毫秒不总等于增加一个日历日? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,增加 86_400_000 会让一个时刻精确前进 24 个经过小时。本地日历日在夏令时转换处可能只有 23 或 25 小时,因此“明天同一本地时间”是另一个需求。Date 只保存时刻,不保存命名时区或重复规则。过期时长应使用经过时间算术;日历重复则应保留墙上时间字段和 IANA 时区,并定义空缺与重叠的处理规则。本地 setter 使用主机时区且会修改接收者,所以应先复制,并测试相关转换的两侧。

延伸阅读 Date 对象
讲清楚了吗?
56 Date 值穿过 JSON 边界时会发生什么,无效 Date 又会怎样? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,JSON 没有日期标量。JSON.stringify() 会调用有效 Date 对象的 toJSON(),得到 UTC ISO 字符串;之后解析该文本只会返回字符串,不会自动恢复 Date。对无效日期,toJSON() 返回 null,而直接调用 toISOString() 会抛出 RangeError。这种静默的类型变化可能遮蔽错误输入。序列化前要校验 getTime(),并明确 wire 字段是规范时刻字符串还是 epoch 毫秒。只能复原 schema 指定的字段;广泛转换所有日期形状的字符串,会损坏标识符和纯日期值。

讲清楚了吗?
57 JSON.parse 成功只能证明什么,值被信任前还必须做什么? 初级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,JSON.parse() 成功只证明完整输入符合 JSON 语法;它仍可能返回 null、原始值、数组,或缺少必填字段的对象。解析不能证明 schema 有效、业务不变量成立,也不代表已授权。捕获语法错误后,还要校验顶层类别、属性归属、每个嵌套类型、范围与允许值,再从允许列表构造领域对象。不要捕获所有失败并返回 {},否则非法输入会伪装成合法空数据。语法、校验与权限失败应保持区分,让调用方和日志保留真正失败的边界。

延伸阅读 JSON
讲清楚了吗?
58 JSON 契约应如何传输超出 Number 精确表示范围的整数? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,JSON 数字语法可以包含超出 JavaScript Number 精确范围的整数,而解析结果已经丢失精度。对这个已舍入的值执行 BigInt(value) 不能恢复原始数字,JSON.stringify() 遇到原始 BigInt 还会抛错。可移植契约应使用经验证的十进制字符串,明确字段含义,并在语法和范围检查后再转换为 BigInt。Node 24 还为原始值提供 reviver 源文本上下文,schema 已知字段可读取 context.source,但这属于运行时特定边界。应测试最大值与跨语言生产者,不要用通用 stringify 结果冒充等价性。

延伸阅读 JSON
讲清楚了吗?
59 解构模式中不同层级的默认值分别如何生效? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,只有当前模式位置取出的值精确为 undefined 时,解构默认值才会执行。在 function read({ profile: { name = 'guest' } = {} } = {}) 中,参数默认值保护被省略的外层参数,profile 默认值保护值为 undefined 的属性,叶子默认值保护值为 undefinedname。它们都不会替换显式 null。对不可信形状,应在复杂模式前校验,或在契约允许时用 ?? 归一化外层空值。常见错误是认为叶子默认值能保护缺失的中间对象;实际上求值在到达叶子前就会失败。

讲清楚了吗?
60 为什么对象 rest 不能作为安全脱敏边界,它究竟复制了什么? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,对象 rest 会创建新的普通对象,并复制剩余的自有、可枚举字符串与 Symbol 属性。它排除继承属性和非可枚举属性,读取时会调用 getter,把访问器转成数据值,并共享嵌套对象引用。{ password, ...publicUser } 这样的代码是拒绝列表:新增 token 或密钥会自动泄露。日志与响应应从公开字段允许列表构造,再逐个决定嵌套值需要复制到哪一层。只有未知字段应被保留时,对象 rest 才合适;它不适合保护未知字段。

讲清楚了吗?
61 数组解构如何消费并关闭迭代器? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,数组模式使用可迭代协议,不要求右值真的是 Array。每个位置都会请求一次 next(),留空位也会消费并丢弃一个迭代结果。如果模式先于迭代器结束,迭代器关闭会在存在 return() 时调用它。rest 元素会急切地把所有剩余值收集到新数组,因此面对无界源时可能耗尽内存或永不结束。测试自定义 iterable 时应统计 next()return() 调用。若每次读取昂贵或需要确认,显式迭代比紧凑解构更清楚,也能让清理责任可见。

讲清楚了吗?
62 为什么能编译现代 JavaScript 语法,不能证明部署后的模块一定能运行? 中级 常见 查看答案 ▾ 收起 ▴

以 Node 24 为基线时,兼容性包含相互独立的解析器、转换、运行时与主机层。编译器可以改写箭头函数或可选链等语法,却可能保留目标运行时缺少的内建 API,也不能让浏览器 DOM API 在 Node 中出现。模块指定符解析也由主机决定;bundler 别名或省略扩展名的 import 在原生执行时可能失败。应先定义部署运行时与产物,再在 Node 24 中运行该产物。只为必需 API 添加 polyfill,并验证转换保留迭代器关闭等语义;源码构建成功不等于端到端兼容。

讲清楚了吗?
63 生成器的 yield、next、return 与 throw 之间会传递哪些值? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,调用生成器函数只会创建暂停的迭代器,不会立即执行函数体。第一次 next(value) 会启动执行,但由于尚无等待接收的 yield,其参数会被忽略。之后的 next(value) 会让上一个暂停的 yield 表达式求值为该值。iterator.return(value) 请求完成并允许 finally 执行,iterator.throw(error) 则在暂停点注入异常。生成器最终返回值的 donetrue,不会被 for...of 或展开收集。双向生成器很紧凑,但容易误用;若消息顺序属于业务协议,应优先使用命名方法。

讲清楚了吗?
64 应用应如何用动态 import 安全地选择模块? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,import() 会按主机解析规则处理指定符,并返回解析为模块 namespace 的 Promise。让外部输入拼接指定符会扩大可解析范围,还可能让 bundler 无法发现目标。应把公开名称映射到固定 loader 函数,拒绝未知名称,并要求所有目标暴露同一份已记录的形状。在 await import() 边界捕获解析与求值拒绝,并与导出业务调用的失败区分。不要用时间戳 query 做缓存破坏;不同已解析 URL 会创建不同模块标识,重复顶层副作用,并可能使模块状态持续增长。

延伸阅读 模块
讲清楚了吗?
65 为什么 try...catch 可能捕获不到 async 函数调用的拒绝? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,调用 async 函数会立即返回 Promise。如果在 try 中发起调用却没有 await,后续拒绝属于该 Promise,发生时同步控制流已经离开该代码块,因此外层 catch 不会收到它。应在处理策略边界内等待操作,把 Promise 返回给有所有权的调用方,或为有意后台任务附加终结拒绝处理器。async 函数中即使在第一个 await 前抛错,也会拒绝其 Promise。核心陷阱是发起工作时没有为拒绝、取消、关停与可观测性分配所有权。

延伸阅读 错误处理
讲清楚了吗?
66 finally 如何可能意外覆盖返回值或错误? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,无论 trycatch 是返回还是抛错,finally 都会在控制流离开前执行。如果 finally 自己执行了 return 或 throw,这个新完成会替换正在传播的完成,有效结果或原始失败可能因此消失。应让 finally 只负责幂等清理,并正常结束。若清理也可能失败,必须明确规定是主错误、清理错误还是聚合错误跨越边界,不能依赖偶然优先级。还要记住 finally 不是进程关停保证,持久恢复不能只依赖 JavaScript 清理一定执行。

延伸阅读 错误处理
讲清楚了吗?
67 如何区分 Map 中缺失的键与值为 undefined 的已存在键? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,键不存在和键显式存储 undefined 时,map.get(key) 都会返回 undefined。当“是否存在”本身有意义时,要先用 map.has(key),再读值。get(key) || fallback 的覆盖范围更广:它还会替换已存储的 0false 和空字符串。?? 能保留这些假值,但仍无法区分缺失与已存储的 undefined。应先定义值域,再选检查方式。对不允许 undefined 的计数器,(counts.get(key) ?? 0) + 1 很清楚;对三态数据,则应显式按 has() 分支。

延伸阅读 Map 与 Set
讲清楚了吗?
68 为什么 Set 不会去重字段相等的对象,应该改用什么? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,Set 成员和 Map 键都使用 SameValueZero。原始值的边界行为包括只保留一个 NaN 和一个零,但对象按标识比较,因此两个独立的 { id: 7 } 仍是两个成员。如果业务标识是 id,应以该稳定值作为 Map 键,并决定保留第一条还是最后一条记录。除非规范化规则完整处理属性顺序、不支持的值和冲突,序列化对象不是安全的通用键。显式归一化需要额外工作,但它使去重语义可测试,而不是偶然依赖引用复用。

延伸阅读 Map 与 Set
讲清楚了吗?
69 floor、ceil、trunc 与 round 对负数的处理有何不同? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,Math.floor() 向负无穷方向取整,ceil() 向正无穷方向取整,trunc() 向零方向截断。Math.round() 选择最近整数,精确中点向正无穷方向处理,因此 Math.round(-2.5)-2Math.round(-0.5) 还可以产生负零。这些规则不是金融舍入或银行家舍入。选择 API 前要先命名所需方向或中点策略,并测试正负小数与 -0。不要用 value | 0~~value,它们会转换为 32 位有符号整数,对大值或非有限值可能回绕。

延伸阅读 Math 对象
讲清楚了吗?
70 为什么 Number.EPSILON 不是浮点数比较的通用容差? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,Number.EPSILON1 与下一个更大可表示 Number 之间的距离。它不描述任意量级上的测量噪声或舍入误差。固定 epsilon 对大值可能过于严格,在零附近也可能不合适。应从领域误差预算推导绝对和相对容差,常见判定是 |a-b| 小于绝对下限,或小于较大量级的某个相对比例。离散计数和标识符仍应使用精确相等。常见陷阱是把所有比较都改成近似比较,从而模糊真实业务阈值。

延伸阅读 Math 对象
讲清楚了吗?
71 哪些情况下 Math.random 是错误的随机源? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,Math.random() 返回 [0, 1) 内近似均匀的 Number,没有标准种子接口,也不提供密码学强度保证。它适合视觉变化和普通非安全抽样,不适合重置 token、验证码、会话标识符或可重现模拟。安全用途应使用 Web Crypto 或经审查的高层 API;把安全随机字节映射到任意区间时,仍要避免取模偏差。测试与模拟应注入可设种子的伪随机源并记录种子。映射值之前还要定义空区间和端点是否包含,否则索引公式可能有偏或产生非法值。

延伸阅读 Math 对象
讲清楚了吗?
72 WeakMap 何时有助于内存所有权,为什么它不能替代清理? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,WeakMap 不会仅因键存在于集合中,就让对象或非注册 Symbol 键保持可达。它适合生命期应跟随外部已拥有对象的元数据。它不会弱化 listener、timer、数组、闭包或其他缓存中的引用,也永远不会关闭 socket 或取消 handler 订阅。资源必须使用显式、最好幂等的 close()、unsubscribe 函数或 abort signal。其取舍之一是可观测性:WeakMap 没有迭代、size 或确定性淘汰信号,需要指标或枚举时应使用带有界策略的 Map。

讲清楚了吗?
73 为什么 WeakMap 和 WeakSet 不提供 size 与迭代? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,弱键变得不可达后,对应条目可能消失,但垃圾回收时机是有意不作规定的。如果暴露 size、键列表或迭代,应用行为就能观察内存压力与回收调度,枚举本身还可能使键重新可达。因此弱集合只支持通过调用方已持有的键进行按标识查询和删除。它们不适合 dashboard、序列化、容量上限或确定性过期。这些场景应使用带显式淘汰策略的普通 Map 或 Set。绝不要为了枚举 WeakMap 而维护并行强键列表,因为该列表会抵消弱生命期的优势。

讲清楚了吗?
74 Node 24 中哪些值可以作为弱键,它们又如何匹配? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,WeakMap 的键与 WeakSet 的成员必须是可回收对象或非注册 Symbol。字符串、数字以及 Symbol.for() 返回的注册值不符合要求,会导致 TypeError;把字符串装箱只会创建具有错误查询标识的临时对象。匹配使用对象或 Symbol 标识,而不是字段或描述,因此重建 { id } 不会命中先前对象下的条目。若调用方通过稳定字符串或数字标识数据,应使用 Map。只有调用方已经携带规范对象,且元数据应与它共享生命期时,才选择弱集合。

讲清楚了吗?
75 Object.defineProperty 的默认标志与普通赋值有何不同? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,对象字面量属性或普通赋值通常会创建可写、可枚举、可配置的数据属性。Object.defineProperty(target, key, { value }) 会把所有省略的布尔描述符标志默认为 false,从而产生不可写、不可枚举、不可配置的属性。之后的写入可能在严格模式下抛错,枚举或删除也会像是忽略了该属性。应写明契约依赖的每个标志,并用 Object.getOwnPropertyDescriptor() 检查。把属性设为不可配置基本是单向决定,必须先确认后续初始化、测试或迁移不需要重新定义。

讲清楚了吗?
76 Object.assign 复制对象时会丢失什么,又会执行什么? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,Object.assign() 会从左到右读取每个源对象的可枚举自有字符串与 Symbol 键,再把当前值写入目标。读取会执行源 getter,写入可能执行目标 setter,后面的源会覆盖前面的键,而目标本身会被修改。该操作不保留源描述符或原型,嵌套对象也只复制引用。必须保留描述符行为和原型时,可使用 Object.getOwnPropertyDescriptors() 配合 Object.create()。对不可信输入,不要把任意键 assign 到高权限普通对象;应构造允许列表记录或有意设计的 null 原型字典。

讲清楚了吗?
77 为什么空 handler 的 Proxy 也可能破坏访问私有字段的方法? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,proxy 是不同于 target 的接收者,不会继承 target 的私有标记或内建内部槽。调用 proxy.method() 时可以找到 target 的方法,但调用会把 proxy 作为 this,于是 this.#field 抛出 TypeError。Map 方法等需要内部标记的内建 API 也有同类问题。因此空 handler 并不对所有对象透明。应优先编写显式 adapter。把选定方法绑定到 target 可能有效,但会改变函数标识、链式返回行为,以及 target 是否泄露,因此必须测试每个公开方法、getter、setter 与回调路径。

讲清楚了吗?
78 包含私有字段的类应如何定义复制与序列化? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,私有元素不是普通属性,因此对象展开、Object.assign()、反射、structuredClone() 与默认 JSON 序列化都不会复制类的私有标记和状态。冻结实例也不会阻止类方法修改私有字段。应定义只包含预期字段的公开数据投影或 toJSON(),在边界校验,并提供能恢复不变量的构造器或静态工厂。若 getter 直接返回私有可变数组,仍会泄露同一引用;应返回契约所需的投影或复制深度,不能把 # 名称当成完整封装。

延伸阅读 私有字段
讲清楚了吗?
79 哪些 Proxy 不变量违反常在 target 被冻结或禁止扩展后才出现? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,Proxy trap 不能报告与 target 受保护事实矛盾的对象形状。ownKeys 不能返回重复键,不能省略不可配置的自有键,也不能为不可扩展 target 添加键。get trap 不能为不可写且不可配置的数据属性伪造不同值,写入相关 trap 也有对应限制。即使宽松测试在可扩展 target 上通过,违反规则仍会抛出 TypeError。应从匹配的 Reflect 结果开始,只做最小转换。测试必须覆盖不可配置属性、只读访问器与 Object.preventExtensions();把这类异常当成调用方错误,会遮蔽已损坏的 handler 契约。

延伸阅读 Proxy 与 Reflect
讲清楚了吗?
80 为什么单个 Proxy trap 很少足以执行完整的对象策略? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,基本对象操作有各自独立的 trap。set 中的校验不会拦截 Object.defineProperty(),而在 get 中隐藏某个键,也不会自动将它从 inReflect.ownKeys()、描述符查询或删除中隐藏。因此只由一个 trap 实现的策略可能被绕过,或给出互相矛盾的观察。应根据真实调用方能力建立操作矩阵,决定哪些转发、哪些拒绝,并使用匹配的 Reflect 方法和原始 receiver。如果调用方仍持有 target,或能执行未覆盖操作,Proxy 也不是可靠安全边界;授权应优先使用窄而明确的 API。

延伸阅读 Proxy 与 Reflect
讲清楚了吗?
81 Symbol()、Symbol.for() 与知名 Symbol 在标识和所有权上有何不同? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,每次调用 Symbol(description) 都会创建新的非注册标识;描述相同并不会让 Symbol 相等。Symbol.for(key) 会复用运行时级注册表中的标识,Symbol.keyFor() 也只能报告已注册键。Symbol.iterator 等知名值则是语言定义的固定协议键。若扩展键由某个 package 拥有,应导出一个普通 Symbol;只有需要有意的、带 namespace 的运行时约定时,才使用注册表。注册标识不会自动穿过 JSON、进程或持久化,且任何知道键的代码都能取回它,所以它既不是保密存储,也不是持久标识符。

延伸阅读 Symbol
讲清楚了吗?
82 哪些对象操作会包含 Symbol 键,它们为什么并不私有? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,Object.keys()for...in 排除 Symbol 键,而 Object.getOwnPropertySymbols()Reflect.ownKeys() 会显示它们。对象展开和 Object.assign() 会复制可枚举的自有 Symbol 键,默认 JSON 序列化却忽略它们。可枚举性与键类型是两个独立维度,所以在某项操作中不显眼不等于访问控制。持有该 Symbol 的调用方可直接读取属性,反射也能发现未知 Symbol。Symbol 适合避免扩展键冲突;语言级封装应使用私有字段或闭包状态。跨边界时要定义准确的键选择矩阵,不能假设复制与校验会看到同一组属性。

讲清楚了吗?
83 定义 Symbol.iterator 时,向 JavaScript 消费者承诺了什么? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,for...of、数组展开和 Array.from() 等语法会查找知名 Symbol.iterator 方法,并期望它返回一个通过 next() 产生结果对象的迭代器。直接返回数组并不足够;返回对象必须实现迭代器契约。若遍历应可重复,每次方法调用都应创建独立游标状态。消费者提前退出时,可能调用迭代器的 return() 执行清理。知名 Symbol 能避免命名冲突,却不会验证行为。测试应覆盖两次并发遍历、提前 break、消费者抛错和实体化上限,尤其是资源支撑或无限序列。

讲清楚了吗?
84 字符串 length 与正则匹配何时无法代表用户看到的字符? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,字符串 length 和方法索引计算 UTF-16 代码单元。带 uv 的正则可以让许多操作感知代码点,Unicode 属性转义也能对代码点分类,但一个可见字形仍可能由组合标记或零宽连接符连接多个代码点。因此 slice(0, 1) 可能拆分代理对,而 /^.$/u 仍可能拒绝一个可见 emoji 序列。必须说明长度限制的单位是字节、代码单元、代码点还是字形簇。界面字符应使用 Intl.Segmenter,并测试组合标记和连接 emoji;wire format 则遵循协议定义的单位。

讲清楚了吗?
85 如何通过 replace 或 replaceAll 按字面量插入不可信的替换文本? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,字符串形式的 replacement 有自己的语法:$& 表示完整匹配,$1$<name> 选择捕获组,$$ 生成美元符号。因此直接把用户数据当成该字符串传入,可能插值匹配文本,而不是按字面量插入提供的内容。应传入 text.replace(pattern, () => replacement) 这样的 replacer 函数;其返回值不会再解释美元 token。还要独立选择只替换第一处还是全部,以及搜索固定文本还是正则。只有动态搜索文本应按字面量解释时,才用 RegExp.escape() 转义,并测试 $&$1 与连续美元符号。

讲清楚了吗?
86 为什么对同一个全局或黏连 RegExp 反复调用 test 会产生交替结果? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,对带 gy 的 RegExp 调用 test()exec() 时,会从可变的 lastIndex 开始。匹配成功会把它推进到匹配结尾,失败则重置为零。因此复用一个模块级 matcher,会让布尔测试依赖先前调用或其他异步操作。只关心是否存在时应移除 g;独立操作应创建新表达式;或让一个 scanner 显式拥有状态并初始化 lastIndex。黏连 y 还要求匹配精确从该索引开始。测试应对同一输入重复调用,并交错处理两个输入,以暴露意外共享状态。

延伸阅读 正则表达式
讲清楚了吗?
87 比较用户可见文本时,什么情况下把两边都转成小写是错的? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,toLowerCase() 会执行 Unicode 大小写转换,但不能表达某个 locale 的完整搜索或排序规则、规范等价性,也不是标识符安全策略。对用户可见文本,应用目标 locale 和 sensitivity 配置一个 Intl.Collator,并只把 compare() 结果解释为负、零或正;具体数值大小没有保证。协议标识符可能要求 ASCII 大小写折叠或精确代码单元相等。不要把展示 locale 的排序规则用于授权键。应测试目标语言与规范化形式,因为良好的 UI 比较可能有意把安全边界必须区分的字符串视为相等。

延伸阅读 字符串方法
讲清楚了吗?
88 哪些类型化数组操作会共享字节,哪些会创建独立存储? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,view.subarray() 会在同一 backing buffer 上创建另一个窗口,view.slice() 则分配空间并复制选定元素。以另一个 view 构造类型化数组会转换并复制元素值;以 view.buffer、offset 和 length 构造则会重新解释共享字节。这些形式看起来可替换,却定义了不同所有权。如果被调方会在调用后保留数据,应按生命周期复制或转移;借用的共享 view 会让调用方后续写入改变结果。应双向测试修改传播,并保留 byteOffsetbyteLength,尤其是处理池化 Node Buffer 窗口时。

讲清楚了吗?
89 为什么二进制解析器应使用显式字节序和精确窗口的 DataView? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,多字节类型化数组使用运行时的本机字节序,而 DataView 通过显式 littleEndian 参数读写每个字段;省略或传 false 表示大端。接收 Uint8Array 或 Buffer 的解析器还必须用 new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) 保留其局部窗口。只使用 bytes.buffer 可能暴露池中无关字节。每次读取字段前都要检查剩余长度,然后校验有无符号与业务范围。来自协议的固定字节 fixture 比 encoder-decoder 往返测试更能发现端序错误,因为后者两端可能共享同一个错误假设。

讲清楚了吗?
90 ArrayBuffer 被 resize 或 transfer 后,类型化数组 view 会发生什么? 高级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,使用 maxByteLength 创建的 ArrayBuffer 可以 resize。length-tracking view 会跟随可用存储;固定长度 view 在缩小导致其声明范围无法完整容纳时,会整体越界。它可能报告长度为零,再次扩容后恢复为界内,但已丢弃的字节不会恢复。transfer() 会创建目标存储并分离源 buffer,使所有旧 view 失效。resize 或 transfer 应有一个明确 owner,旧引用应视为已消费。变量仍在作用域中不代表字节仍可访问,所以要测试异步交接、缩小边界以及失效后使用的每个方法。

讲清楚了吗?

函数与作用域

17个问题
07 普通 JavaScript 函数的 this 是如何确定的? 初级 常见 查看答案 ▾ 收起 ▴

对普通函数而言,this 主要由调用形式决定,而不是由函数定义位置决定。new 会创建接收者,并具有最强的相关绑定;call、apply 或绑定函数提供显式接收者;object.method() 使用点号左侧的对象;严格模式下的普通调用得到 undefined。因此 const f = object.method 会丢失隐式接收者。应在边界处通过对象调用、使用包装函数,或只绑定一次。bind 会创建新的函数标识;若 API 之后要移除同一回调,就必须保存该标识。

延伸阅读 this 绑定
讲清楚了吗?
08 哪些情况下箭头函数不能替代普通函数? 中级 常见 查看答案 ▾ 收起 ▴

当 API 需要动态接收者、需要通过 new 构造、需要函数自身的 arguments 对象或生成器行为时,箭头函数并不适合。箭头函数从外层词法捕获 this、arguments、super 和 new.target;call、apply、bind 都不能替换捕获的 this。因此,它适合需要保留外层方法接收者的回调,却不适合作为必须操作点号左侧对象的原型方法。还要区分表达式体与块体:隐式返回对象字面量时必须加括号,否则花括号会被解析为块。

延伸阅读 箭头函数
讲清楚了吗?
09 闭包保留的是什么,它为什么既能保存状态,也可能让内存继续被占用? 中级 常见 查看答案 ▾ 收起 ▴

闭包保留对函数创建位置词法环境中绑定的访问,而不是冻结这些绑定当前值的副本。重新赋值被捕获的 let 后,稍后的调用会看到新值;不同工厂调用通常创建不同环境。内存取决于可达性:若长期监听器仍持有闭包,被捕获的对象也可能继续可达。只要整个引用环都不可达,环本身不等于泄漏。应保存准确的监听器标识,提供 unsubscribe 或 dispose,并只捕获回调真正需要的状态。

讲清楚了吗?
10 call、apply、bind 有何区别,包括绑定函数被用于构造时的行为? 高级 偶尔 查看答案 ▾ 收起 ▴

call 立即调用函数,参数逐个列出;apply 也立即调用,但从类数组值提供参数;bind 不会立即调用,而是返回一个固定接收者并可预置前置参数的新函数。当输入可迭代时,展开语法通常比 apply 更清楚。绑定函数若与 new 一起使用,仍会构造原目标:新实例成为 this,因此绑定的 thisArg 被忽略,但预置参数仍然有效。对箭头函数 bind 也不能改变其词法 this。不要在每次注册时重新 bind,因为每次都会产生不同回调标识。

讲清楚了吗?
11 在实际代码中,柯里化、偏函数应用、函数组合与 pipe 有何区别? 中级 偶尔 查看答案 ▾ 收起 ▴

柯里化把概念上接收多个参数的函数转换为分阶段接收参数的调用链,通常每阶段接收一个参数组。偏函数应用只是固定部分参数并返回接收其余参数的函数,不要求形成一元链。函数组合把前一函数输出连接到后一函数输入;compose 通常从右向左读,pipe 从左向右读。可复用辅助函数必须明确元数、占位符、接收者、同步或异步以及错误行为。只依赖 Function.length 很脆弱,因为默认参数和剩余参数不能完整表达语义输入契约。

讲清楚了吗?
12 防抖与节流有何区别,生产级包装器应暴露哪些生命周期控制? 中级 常见 查看答案 ▾ 收起 ▴

防抖会等待一段安静时间并把一串调用合并,适合用户停止输入后再搜索。节流把执行频率限制在设定上限内,适合连续滚动或指针更新。必须明确 leading 与 trailing 行为,因为它决定哪次调用提供参数,以及最后一次更新是否执行。生产级包装器应保留预期接收者,有意保存最新参数,并暴露 cancel;flush 也常有价值。组件卸载时要取消计时器,并中止已被替代的异步请求,因为不再调用回调并不会取消已经启动的工作。

延伸阅读
讲清楚了吗?
41 把函数声明改成 const 箭头函数时,哪些语义会改变? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,函数声明会在作用域实例化时完成初始化,因此同一作用域内位于声明之前的调用可以成功。const 箭头函数的绑定在初始化器执行前一直位于暂时性死区。箭头函数还没有自己的 thisargumentssupernew.target,也不能被 new 构造。只有调用方不依赖这些能力时,两种形式才能互换。自动重构前要搜索提前调用、构造用法、依赖接收者的调用和 arguments,避免将外观改写变成启动故障或错误捕获。

讲清楚了吗?
42 类字段中的箭头函数与原型方法有何不同? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,普通类方法是原型上的一个非可枚举函数,this 由每次调用决定。箭头函数字段则会在每个实例上初始化为自有属性,并闭包捕获该实例的 this,所以作为回调传递时不会丢失接收者。代价是每个实例都有独立函数标识,继承、spy 和原型 patch 的行为也会改变:自有字段可能遮蔽原型覆写。共享的多态行为应优先使用原型方法;只有契约需要可脱离调用的稳定标识时,才使用箭头字段。

讲清楚了吗?
43 严格模式与非严格模式函数如何处理传给 call 或 apply 的 thisArg? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,call()apply() 会把指定的 thisArg 原样传给严格模式的普通函数。非严格模式函数会用全局 this 值替换 nullundefined,并对数字等原始值接收者进行装箱。ES module 和类体自动使用严格模式,但最终由被调函数自身的严格性决定接收者是否归一化。箭头函数使用词法 this,会忽略指定接收者。不应把非严格装箱或全局替换当成 API 特性;应使用严格代码并校验显式接收者。

讲清楚了吗?
44 apply 何时能接受展开语法会拒绝的参数,两种写法又共享什么限制? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,fn.apply(receiver, args) 接受具有索引属性和 length 的类数组对象,它不必实现迭代协议。fn(...args) 则要求可迭代对象,因此普通的 {0: value, length: 1} 可以传给 apply,却不能直接展开。对数组和其他可迭代对象,展开通常更清楚;apply 在类数组边界仍有用。两者都会把集合转成独立调用参数,因而受引擎参数数量上限约束。不要把无界集合展开到 Math.max() 等调用中;应逐项聚合或分块处理。

讲清楚了吗?
45 super.method() 如何选择实现同时保留 this,脱离实例调用又会破坏什么? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,super.method() 会从父类原型开始查找属性,但仍使用当前 this 调用查到的函数;父类原型不会变成接收者。因此基类实现可以操作派生实例状态,并参与覆写链。若派生方法本身被取出后以普通形式调用,类的严格模式会使 this === undefined,字段访问将失败。应保留属性调用、使用包装函数,或只绑定一次。也不要在基类构造器中调用可覆写方法,因为派生字段要等 super() 返回后才初始化。

延伸阅读 this 绑定
讲清楚了吗?
46 默认参数究竟何时执行,又应如何处理 null? 初级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,只有参数被省略或精确为 undefined 时,默认初始化器才会在调用期间执行。显式传入的 null0false 或空字符串都会保留。初始化器在每次调用中从左到右执行,所以后面的默认值可以引用前面的参数,其中创建的对象也会在每次适用时重新创建。必须在边界决定 null 是缺失还是非法:前者可用 ?? 归一化,后者应拒绝。解构参数还需外层默认值,才能安全处理缺失的 undefined

延伸阅读 函数
讲清楚了吗?
47 为什么 let 循环中创建的闭包会看到不同值,而 var 闭包通常不会? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,带词法 let 声明的 for 循环会为每次迭代创建新绑定,因此每个闭包都会解析到对应迭代的变量。var 是函数作用域声明,只提供一个共享绑定;稍后执行的回调通常都会看到最终值。闭包保留的是绑定,而不是冻结的值副本,所以在循环外重新赋值被捕获的 let 也可能出现同类问题。循环索引应优先用 let,或把所需值传入小型工厂。还要判断捕获对象是共享还是每次迭代独有;let 不会克隆外部可变对象。

延伸阅读 闭包
讲清楚了吗?
48 如何证明某个闭包在无意中持续占用内存? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,对象能否回收取决于可达性,而不是引用是否成环,也不是某个局部变量是否被赋为 null。应在稳定输入下反复执行同一生命周期,拍摄可比的堆快照,并检查从根到增长对象的保留路径。常见路径是长寿命 emitter 持有已注册回调,回调再通过闭包环境持有组件状态。保存准确的回调标识并在销毁时移除,或使用由 owner 管理的 abort signal。修复后还要确认保留数量不再增长;一次堆增长可能只是预热、延迟回收或有意缓存。

讲清楚了吗?
49 为什么 Function.length 不适合作为通用 curry 辅助函数的完成规则? 中级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,Function.length 只计算第一个带默认值的参数之前的参数,不计剩余参数,一个解构模式也只算一个参数。它是描述性元数据,不是业务元数。因此 curry 辅助函数可能只收到 base 就执行 (base, options = {}, request),而仅有剩余参数的目标会报告零。对可选或可变签名应显式传入元数,或改用命名清晰的配置工厂。测试要覆盖支持的参数分组、额外与空调用、占位符,以及业务函数真正执行的时点。

讲清楚了吗?
50 异步 pipe 必须定义哪些同步组合辅助函数没有的契约? 高级 常见 查看答案 ▾ 收起 ▴

在 Node 24 中,同步 pipe 会把每个返回值直接传给下一阶段;如果某一阶段返回 Promise,下一阶段会把它当成普通对象。异步 pipe 应采用统一的等待规则,常见做法是从 Promise.resolve(input) 开始链式执行,使普通值与 Promise 都被等待。它还必须定义拒绝如何传播、谁拥有取消权,以及阶段是否只能串行。输入输出形状变化的阶段应命名。辅助函数内的全捕获 fallback 很危险,它可能把拒绝转成远离责任边界的假成功。

讲清楚了吗?
91 为什么在 Node.js 中不应从顶层 this 获取应用上下文? 中级 偶尔 查看答案 ▾ 收起 ▴

在 Node 24 中,顶层 this 取决于模块格式:在 ES module 中它是 undefined,CommonJS 文件则运行在包装函数内,顶层 this 指向 module.exports,而不是 globalThis。在这里创建的箭头函数会捕获该环境,无法通过 callapplybind 重新绑定。移动文件或修改最近 package 的 type 可能因此破坏原有假设。应显式传递依赖,或在拥有接收者的方法中创建回调。不要把顶层 this 当成可移植全局存储;应使用明确模块导出,只在确实需要进程级所有权时才使用 globalThis

延伸阅读 this 绑定
讲清楚了吗?

异步

8个问题
13 任务与微任务如何交互,微任务又怎样阻塞浏览器工作? 中级 常见 查看答案 ▾ 收起 ▴

一个任务中的 JavaScript 会运行到完成。随后进入微任务检查点,宿主会清空已排队的 Promise reaction、queueMicrotask 回调及其他微任务,包括它们新加入的微任务,然后才处理下一个任务,并通常在此后渲染。这解释了为什么同一轮中已兑现 Promise 的回调会先于零延迟定时器执行。边界也很重要:不断自我追加的微任务链会推迟定时器、输入处理和绘制。大量工作应拆成有上限的块,并通过任务级调度主动让出机会。

延伸阅读 事件循环
讲清楚了吗?
14 怎样判断多个异步操作应串行执行还是并发执行? 初级 常见 查看答案 ▾ 收起 ▴

首先建模数据依赖和副作用依赖。若操作 B 需要 A 的结果,就应等待 A 后再启动 B;若操作彼此独立且允许重叠,应先创建它们的 Promise,再统一等待,常见方式是 Promise.all。连续写 await A(); await B(); 会强制串行,而先创建两个 Promise 才能重叠执行。但并发不等于无限并行:大量输入通常需要 worker 池、速率限制或背压。还应保留结果对应的输入标识,定义失败策略,并传递取消信号以协作停止废弃工作。

延伸阅读
讲清楚了吗?
15 主要 Promise 组合器分别表达什么失败策略,又有哪些事情它们不会做? 中级 常见 查看答案 ▾ 收起 ▴

Promise.all 要求所有输入兑现,并在某项拒绝时拒绝。allSettled 等待每项结束,返回带状态标签的记录。any 在首项兑现时兑现,只有全部输入都拒绝时才以 AggregateError 拒绝。race 则采用首个结束项,无论兑现还是拒绝。只要组合器返回结果数组,其顺序按输入顺序,而不是完成顺序。关键是,组合结果提前结束不会取消其他操作。应先依据业务成功条件选择组合器,再加入 AbortSignal 等协作式取消,并等待底层操作所需的清理。

延伸阅读
讲清楚了吗?
16 为什么异步迭代器适合分页或流式数据,它们又应如何清理? 中级 偶尔 查看答案 ▾ 收起 ▴

异步可迭代对象暴露 Symbol.asyncIterator,每次 next 调用都返回迭代结果的 Promise。for await…of 会逐项请求值,因此生成器可以随着消费进度才获取下一页,而不是先缓冲整个数据集。这形成自然的拉取式节奏,但本身并不保证全局内存上限。自身拥有的资源应在 finally 中清理。循环提前退出时,迭代器关闭会在可用时调用 return,使生成器能够释放 reader、cursor 或连接。若底层 I/O 也需要取消,还应继续传递 AbortSignal。

延伸阅读
讲清楚了吗?
17 Streams API 如何表达背压、所有权与取消? 高级 偶尔 查看答案 ▾ 收起 ▴

ReadableStream 维护内部队列,并根据排队策略计算 desired size。行为良好的底层数据源只在消费者有容量或收到 pull 请求时入队,而不会无限生产。获取 reader 会把流锁定给该 reader;releaseLock 只是释放锁,并不会取消数据源。cancel 表示消费者不再需要数据,pipeTo 则协调读取、写入、背压、关闭与错误传播。必须明确由谁拥有取消责任。对于大数据流,除非两个分支能以相近速度消费,否则应谨慎使用 tee,以免慢分支造成额外缓冲。

延伸阅读
讲清楚了吗?
18 switchMap、mergeMap、concatMap 与 exhaustMap 分别编码了怎样的并发策略? 高级 少见 查看答案 ▾ 收起 ▴

switchMap 在新的外层值到来时取消订阅上一个内层 Observable,适合可被替代的搜索。mergeMap 同时保留多个内层订阅,结果可能交错。concatMap 把外层值排队并按顺序处理内层流。exhaustMap 在当前内层流活跃时忽略新的外层值,可用于保护不可重复提交。关键边界是取消订阅,但只有 Observable 的 teardown 真正中止底层工作时,它才等于取消操作。应根据顺序、并发和取消语义选择,并测试完成与错误行为,而不是凭习惯选操作符。

延伸阅读
讲清楚了吗?
37 Node.js 中的 process.nextTick、Promise reaction 与事件循环阶段如何交互? 高级 偶尔 查看答案 ▾ 收起 ▴

JavaScript 回调会完整运行,然后才处理两个延迟队列。Node 先清空 next-tick 队列,再清空 Promise 微任务队列;新加入的条目也会被清空,之后才推进 timers、poll、check 等 libuv 阶段。这个优先级使 process.nextTick 适合狭窄的兼容边界,却不适合作为通用调度器:无限链会让 I/O 饥饿。Promise reaction 与 queueMicrotask 更可移植,但同样可能使任务工作饥饿。还要说明执行上下文,因为 ES module 顶层执行本身位于微任务中,可能改变观察到的顺序。

延伸阅读 事件循环
讲清楚了吗?
38 为什么把 CPU 密集型 JavaScript 标成 async 仍不能避免阻塞事件循环? 中级 常见 查看答案 ▾ 收起 ▴

async 只改变函数报告完成的方式,不会把函数体移到另一个线程。函数体仍会同步运行,直到 await 使它暂停;等待一个已经完成的计算,也无法追回事件循环线程此前被占用的时间。中等规模工作可以拆成经过测量的块,并通过任务级原语让出,使定时器和 I/O 获得执行机会。持续计算应使用 worker thread,同时把消息传输成本纳入设计。还要在代表性负载下测量事件循环延迟和尾延迟,因为任意选择的工作块可能增加开销,也可能仍阻塞过久。

延伸阅读 事件循环
讲清楚了吗?

浏览器

6个问题
19 怎样实现事件委托,才能避免操作错误的元素? 中级 常见 查看答案 ▾ 收起 ▴

在稳定祖先元素上注册一个监听器,用 event.target 查找事件最初来自的节点;event.currentTarget 始终是拥有监听器的祖先。由于目标可能是嵌套图标或包含文本的子元素,应使用 closest 匹配预期选择器,并确认匹配结果确实位于委托根内部后再操作。事件委托依赖传播机制;不冒泡事件、stopPropagation 和 Shadow DOM 边界可能要求使用捕获阶段、更换事件,或在组件内部处理。选择器应表达语义,读取 DOM 数据后要验证,并在卸载时移除祖先监听器。

延伸阅读
讲清楚了吗?
20 健壮的 fetch 包装器除了等待 fetch 本身,还必须处理什么? 中级 常见 查看答案 ▾ 收起 ▴

fetch 通常只因网络级失败或中止而拒绝,不会仅因服务器返回 404 或 500 而拒绝,因此必须检查 response.ok 或明确接受的状态范围。响应体是流,通常只能消费一次;只有确实需要两个消费者时,才应在消费前 clone。JSON 解析可能独立于 HTTP 状态失败,错误中应保留响应上下文。用户取消、组件卸载和截止时间都应传入 AbortSignal,并区分中止与业务失败。重试还必须定义幂等性、有界次数、退避和服务器指示,否则可能重复副作用。

延伸阅读
讲清楚了吗?
21 JavaScript 如何意外触发重复布局,范围最小的修复是什么? 高级 常见 查看答案 ▾ 收起 ▴

样式修改可能使布局信息失效。若循环先写入影响布局的样式,又立即读取 getBoundingClientRect 或 offsetHeight 等几何信息,浏览器为了返回当前值,可能在每轮同步更新样式与布局。这种读写交替就是布局抖动。范围最小的修复是先批量读取几何信息,在内存中计算,再批量写入;视觉更新常可安排到 requestAnimationFrame。边界在于测量:并非所有 DOM 访问都会强制布局,合成友好的属性也可能避开布局,但提升图层并非免费,仍应使用性能工具验证。

延伸阅读
讲清楚了吗?
22 IntersectionObserver 会报告什么,又不应被用来保证什么? 中级 偶尔 查看答案 ▾ 收起 ▴

IntersectionObserver 会异步报告目标元素相对 root 穿越所配置交叉阈值的情况,rootMargin 会调整这个观察边界。每个 entry 提供几何信息、交叉比例和 isIntersecting。它避免持续手动测量滚动位置,但回调不是每次像素移动的精确记录,送达时几何状态也可能再次改变。它适合懒加载、粗粒度可见状态或哨兵式分页。不要把它当作广告持续可见某段时间的证明;应结合时间累计和页面可见性规则,并在清理时 unobserve 目标。

延伸阅读
讲清楚了吗?
23 为什么新安装的 service worker 可能无法控制已经打开的页面? 高级 偶尔 查看答案 ▾ 收起 ▴

Service worker 具有 install、waiting 和 activate 阶段。更新成功后,只要旧 worker 仍控制着打开的客户端,新 worker 通常会等待,因此完成安装并不等于立即接管当前页面。skipWaiting 与 clients.claim 可以加速接管,但可能让旧页面代码配上新缓存逻辑,必须把它设计成协调一致的版本迁移。应使用 event.waitUntil 延长安装或激活工作,明确设置缓存版本,并只删除应用自己拥有的缓存。缓存响应是需要失效策略的数据,不是所有请求的永久离线副本。

延伸阅读
讲清楚了吗?
24 Web Component 应如何跨越 Shadow DOM 边界定义公开状态与事件契约? 高级 少见 查看答案 ▾ 收起 ▴

丰富的 JavaScript 值应通过 property 暴露,可序列化的声明式配置适合 attribute;二者是否互相反射必须明确设计,不能假设自动同步。observedAttributes 与 attributeChangedCallback 应规范化输入,同时避免形成反射循环。Shadow DOM 能隔离内部节点和许多样式,但不是安全边界。需要离开 shadow tree 的事件应使用 CustomEvent,并按契约设置 bubbles 与 composed,同时让 detail 保持精简且有文档。生命周期回调可能多次执行,因此 connectedCallback 中的设置必须具备幂等性,或在 disconnectedCallback 中成对清理。

延伸阅读
讲清楚了吗?

Node 运行时

6个问题
25 为什么不能死记 Node.js 中 timer、immediate 与微任务的一种全局顺序? 高级 常见 查看答案 ▾ 收起 ▴

Node 会在事件循环的不同阶段执行回调,而 Promise reaction 与 process.nextTick 回调会在 JavaScript 回调周围的特定检查点清空。process.nextTick 有独立的高优先级队列,递归填充时可能饿死 I/O。setTimeout(…, 0) 与 setImmediate 的相对顺序取决于它们在何处被调度,以及外围是否处于 I/O 轮次,因此不存在脱离上下文的统一答案。回答时应先说明调度上下文,再追踪相关阶段和检查点。不要把任一 API 当作正确性锁;顺序应通过 await、队列或显式状态转换表达。

延伸阅读
讲清楚了吗?
26 Node 的并发在什么情况下仍会阻塞事件循环,什么时候 worker thread 才有帮助? 中级 常见 查看答案 ▾ 收起 ▴

异步 I/O 让 Node 等待时不占用 JavaScript 线程,但 JavaScript 回调仍在事件循环线程上运行。长时间计算、超大解析或同步文件系统调用都会阻塞其他回调,与外围函数是否声明为 async 无关。libuv 线程池只服务于特定原生操作,不会自动把任意 JavaScript 计算移过去。大量 CPU 密集型 JavaScript 可以使用 worker thread,但必须值得承担消息传递和序列化开销。应先测量事件循环延迟、限制输入大小;若根因是无界数据处理,则优先考虑流式处理或更小批次。

延伸阅读
讲清楚了吗?
27 Express 中间件链应如何建立唯一且清晰的响应与错误边界? 中级 常见 查看答案 ▾ 收起 ▴

每个中间件都应在“结束响应”与“继续委托”之间选择其一,并在发送响应后停止控制流。响应后再调用 next,或下游运行后再次发送,都会造成重复 header 错误和混乱的所有权。应在路由边界附近验证输入,让处理器职责集中,并把失败交给唯一的终结错误中间件;它以四参数签名区别于普通中间件。异步拒绝如何转发取决于 Express 版本和包装方式,因此必须明确约定并测试。最终处理器应映射已知运行错误、隐藏内部细节,并在 header 已发送后避免再次响应。

延伸阅读 错误处理
讲清楚了吗?
28 应如何选择 NestJS provider 的作用域,隐藏的可变状态会造成什么问题? 高级 偶尔 查看答案 ▾ 收起 ▴

无状态服务以及连接池等可安全共享的资源应使用默认 singleton 作用域。request scope 为每个请求创建实例,并沿依赖 provider 传播,会增加分配和依赖图成本,因此只应用于真正绑定请求的状态。transient scope 为每个消费者创建实例,只有在这种所有权是有意设计时才合适。singleton 若保存“当前用户”或请求数据,会造成跨请求竞态与泄漏。应把请求值作为方法参数传递,或放入显式上下文抽象,并缩小模块 export 范围,让依赖所有权保持可见。

延伸阅读
讲清楚了吗?
29 Deno 权限控制如何支持最小权限,它的边界又在哪里? 中级 偶尔 查看答案 ▾ 收起 ▴

Deno 默认不会获得广泛的文件、网络、环境变量、子进程等访问权,除非显式授予权限。应只授权所需的具体主机或目录,而不是使用全权限标志。程序可以查询或请求权限,但部署策略必须决定是否允许交互式提示。权限限制的是运行时能力;它不能证明导入代码可信,不能替代应用授权,也无法在访问已经获准后继续保护数据。还应审查依赖图,按项目策略固定并验证依赖,必要时拆分进程职责,并测试缺少权限时是否安全失败。

延伸阅读
讲清楚了吗?
30 Electron 的 renderer 与 main process 之间,怎样的 IPC 边界才安全? 高级 少见 查看答案 ▾ 收起 ▴

renderer 发来的消息应视为不可信输入,尤其当页面可能显示远程或用户控制内容时。应启用 context isolation,不暴露原始 ipcRenderer 或 Node 能力,而由 preload 脚本通过 context bridge 发布小型允许列表 API。main process 在执行特权操作前,必须验证 channel、sender、参数形状、授权和目标资源。相比通用 execute channel,应优先使用具有明确结果与错误 schema 的请求响应 handler。导航、新窗口和外部 URL 打开还需要各自的允许列表。IPC 隔离能减少能力暴露,但不能替代常规输入验证。

延伸阅读
讲清楚了吗?

模式与工具链

6个问题
31 工厂模式何时能改善 JavaScript 设计,何时只是多余的间接层? 中级 偶尔 查看答案 ▾ 收起 ▴

当创建过程需要验证配置、选择不同实现、组装依赖,或在稳定接口后隐藏构造细节时,工厂很有价值。它为调用方提供单一策略边界,也便于测试注入替代实现,而无需了解具体构造函数。若工厂只是给一个简单类的 new 改名,没有新增约束或变化,就只是多余间接层。返回对象应具有明确的行为契约,不支持的类型要尽早失败,生命周期所有权必须清楚。若工厂创建 socket、worker 或订阅,返回抽象应暴露 close 或 dispose,而不是隐藏清理责任。

延伸阅读
讲清楚了吗?
32 观察者模式与发布订阅模式有何区别,它们又共享哪些运行风险? 高级 偶尔 查看答案 ▾ 收起 ▴

在直接观察者设计中,subject 知道自己的 observer 集合并逐一通知。发布订阅则把 broker 或事件 channel 放在 publisher 与 subscriber 之间,减少直接认知,但更依赖清晰的事件契约和追踪能力。两者都必须明确订阅所有权、unsubscribe 行为、重复监听策略,以及 handler 抛错时的规则。还要决定交付是同步、排队、有序还是并发,因为这些选择会影响重入和背压。不能让一个被遗忘的订阅使整个组件图保持可达;应返回取消句柄,并把卸载测试纳入契约。

延伸阅读
讲清楚了吗?
33 怎样判断状态应属于本地、全局、派生、持久化,还是服务器状态? 中级 常见 查看答案 ▾ 收起 ▴

首先判断所有权与权威来源。状态应留在需要修改它的最小子树中。能够从现有状态计算出的值应保持派生关系,不要再保存一份需要同步的副本。只有多个远距离消费者确实需要协调写入时,才提升为全局状态。服务器状态具有远程权威、过期、重试和缓存失效问题,应使用查询缓存,而不是把它伪装成普通客户端状态。持久化是另一个边界,不是默认选择:必须给存储形状加版本,读取时验证,处理迁移与配额失败,也不要为了方便把秘密放入浏览器存储。

延伸阅读
讲清楚了吗?
34 怎样让 Jest 异步测试因正确原因而稳定失败? 中级 常见 查看答案 ▾ 收起 ▴

测试必须 return 或 await 代表被测行为的 Promise,否则断言执行前测试就可能结束。预期拒绝时,应 await expect(promise).rejects,或在 try/catch 中配合断言次数,避免意外兑现静默通过。fake timer 能控制计划时间,但 Promise 微任务和外部 I/O 可能需要根据所选计时器模式另行推进或 mock。每个测试后都应恢复 timer、spy 和全局修改。优先测试可观察结果,而不是逐次调用的实现细节;每个测试还应拥有独立数据,避免顺序和并行执行影响结果。

延伸阅读
讲清楚了吗?
35 为什么 Playwright locator 与 web-first assertion 比固定时长 sleep 更可靠? 初级 常见 查看答案 ▾ 收起 ▴

locator 会在动作或断言执行时针对当前页面重新解析,Playwright 还会等待相关可操作条件,例如可见、稳定以及能够接收事件。web-first assertion 会重试,直到条件成功或超时。固定 sleep 只是猜测应用何时就绪:运行快时浪费时间,运行慢时仍会失败。应优先使用与用户可见契约关联的 role、label 或 test-id locator,保持 browser context 隔离,并等待具体可观察状态。自动等待无法修复含糊的 locator,也无法补救从未暴露就绪状态的应用。

延伸阅读
讲清楚了吗?
36 Alpine.js 中 x-show 与 x-if 有何区别,由此产生什么生命周期取舍? 初级 偶尔 查看答案 ▾ 收起 ▴

x-show 会保留元素在 DOM 中,只切换其 display 状态,因此隐藏期间本地 DOM 状态以及已挂接的组件工作仍然存在。x-if 通过 template 有条件地创建和移除 DOM,可避免保留大型非活动子树,但会付出创建与销毁成本,并重置元素本地状态。选择依据应是生命周期语义,而不只是“是否可见”。频繁切换的轻量 UI 适合 x-show,昂贵或语义上不存在的内容适合 x-if。若初始隐藏内容可能在 Alpine 初始化前闪现,应同时使用 x-cloak 和对应 CSS 规则。

延伸阅读
讲清楚了吗?