类型与作用域机制

共 31 题
📑 题目列表 31 题
#
★★★

1. var、let、const 在提升与暂时性死区的差异

var、let、const 在变量提升(hoisting)与暂时性死区(TDZ)方面有何差异?这些差异如何影响实际编码?

  • var 的提升与初始化为 undefined
  • let/const 的提升但未初始化,TDZ 期间访问报 ReferenceError
  • const 的只读绑定与对象内容可变

var 声明的变量会被提升到函数作用域顶部并初始化为 undefined,因此在声明之前访问不会报错;let 与 const 同样会被提升,但进入"未初始化"状态,从作用域入口到声明语句执行之间形成暂时性死区(TDZ),此期间访问会抛 ReferenceError,typeof 也无法豁免。const 除 TDZ 外还要求声明时初始化,且绑定不可重新赋值,但对象或数组的成员仍可修改。for 循环中 let 每次迭代创建独立绑定,闭包捕获正确;var 则共享同一绑定,配合异步回调会产生经典的"循环变量共享"问题。

本题考察声明机制的本质:三者都会被提升,差异在于初始化时机与绑定可变性。回答时抓住"TDZ 是提升后未初始化造成的访问窗口"这一核心,并结合 for 循环闭包、const 引用可变等工程场景说明差异的实际影响,比单纯背结论更有说服力。

#
★★★

2. 执行上下文、词法环境与作用域链的协作

执行上下文、词法环境(Lexical Environment)与作用域链是如何协作实现变量解析的?

  • 执行上下文的组成:词法环境、变量环境、this 绑定
  • 词法环境的外部引用(outer)如何构成作用域链
  • 嵌套函数与闭包对作用域链的影响

每次函数调用都会创建新的执行上下文,压入执行栈;上下文由词法环境(保存 let/const 声明)、变量环境(保存 var 声明)与 this 绑定组成。词法环境内部维护环境记录(环境记录项)与外部词法环境引用 outer,解析变量时先从当前环境记录查找,未命中则沿 outer 链逐级上溯直到全局环境,这条链就是作用域链。作用域链在函数定义时静态确定(词法作用域),与调用位置无关,因此嵌套函数可以访问外层变量,这也是闭包能捕获外层词法环境的机制基础。

本题考察 JS 引擎执行模型的核心概念。应从"上下文 = 环境 + this"的组成讲起,再说明作用域链由词法环境的 outer 链构成、按声明位置静态决定;能把"作用域链是词法环境链"与"闭包 = 函数 + 词法环境引用"联系起来,即证明理解到位。

#
★★★

3. 原始类型、引用语义与隐式转换(ToPrimitive/ToNumber/ToString)

JS 的原始类型与引用类型在语义上有何区别?隐式类型转换中的 ToPrimitive、ToNumber、ToString 抽象操作是如何工作的?

  • 7 种原始类型与对象在存储、赋值、比较上的语义差异
  • ToPrimitive 的 hint 机制(default/number/string)与 valueOf/toString 调用顺序
  • 常见隐式转换(+、==、比较运算)的规则链

原始类型(string、number、boolean、null、undefined、symbol、bigint)按值传递,不可变;对象按引用传递,赋值与比较的是引用地址,内容相同但不同引用视为不相等。隐式转换由引擎调用抽象操作完成:ToPrimitive 按 hint 决定优先调用 valueOf 还是 toString(对象转原始值),ToNumber/ToString 再把原始值转为目标类型。例如 []+[] 中数组先 valueOf 返回自身、再 toString 得到空串;null==undefined 为 true 而 null+1 得到 1,来自 ToNumber(null)=0 与 == 的特殊规则。== 会优先触发数值转换,而 === 不做任何转换。

本题考察类型系统的底层规则。建议分层回答:先讲值语义与引用语义,再讲 ToPrimitive 的 hint 与 valueOf/toString 顺序,最后用经典表达式([]+[]、{}+[]、null==undefined)验证规则链;能预测表达式结果即证明掌握了抽象操作。

console.log([] + []);        // ""(两个空串相加)
console.log({} + []);        // "[object Object]"
console.log(null == undefined); // true(== 的特例)
console.log(+[]);            // 0(ToNumber("") = 0)
#
★★★

4. Array.prototype 的 PACKED/HOLEY 双形状过渡与 array 性能取舍

V8 中数组的 PACKED/HOLEY 元素形状(element kind)是如何过渡的?对数组性能有何影响?

  • PACKED/HOLEY 与元素类型(SMI/DOUBLE/ELEMENTS)的组合
  • 空洞、类型变更导致形状降级的路径
  • 避免形状降级的编码实践

V8 依据元素种类(element kind)为数组选择存储表示:最先是 PACKED_SMI_ELEMENTS(全是小整数),随着插入双精度数变为 PACKED_DOUBLE_ELEMENTS,再插入对象变为 PACKED_ELEMENTS;若在数组中间留下空洞(如越界赋值、delete)则降级为 HOLEY 系列。形状只能从精细向粗放单向降级,无法恢复,每次降级都带来额外的类型检查开销,HOLEY 数组在访问时还要做空洞探测。工程上应避免:使用 Array(length) 或 new Array(n) 预创建空洞数组再填充、数组内混用多种类型、delete 元素;用 push 追加、保持单一元素类型、用 filter 清理空洞可维持高性能形状。

本题考察对 V8 数组内部表示的认识。核心是"元素形状单向降级":类型混用与空洞是两大降级诱因,降级不可逆并牺牲性能。回答给出可操作的规避建议(避免 new Array(n)、保持类型一致、勿用 delete),体现从引擎实现到工程实践的贯通。

#
★★★

5. V8 垃圾回收的 Orinoco(Major GC)增量标记、并行、并发三阶段工程价值

V8 的 Orinoco 垃圾回收器如何通过增量标记、并行与并发协作控制停顿?对前端工程有何价值?

  • 标记-清除/整理的基本流程与全停顿问题
  • 增量标记(incremental)切分标记工作
  • 并行(parallel)与并发(concurrent)的线程模型差异

V8 使用 Orinoco 垃圾回收器:标记阶段可拆分为增量标记,把标记工作切成小片穿插在应用执行间隙,配合写屏障追踪增量期间的引用变化,避免长时间全停顿;并行标记由多个辅助线程与主线程同时执行互不依赖的任务,进一步缩短停顿;并发标记/清扫把工作完全交给后台线程,主线程几乎无感知。工程价值:长页面在 GC 停顿期间无法响应输入与渲染,Orinoco 把停顿从"一次性大冻结"变为"多个微小停顿",显著降低卡顿感知;监控上可通过 performance.measureUserAgentSpecificMemory() 观察堆大小变化,辅助定位内存增长问题。

本题考察 GC 停顿治理的演进路径。把握三个词的含义:增量(时间切片+写屏障)、并行(主线程+辅助线程同时干)、并发(后台线程独立干);再落到工程价值——停顿控制让前端长任务与动画更平滑,是性能调优的基础认知。

#
★★★

6. V8 TurboFan 优化条件(单态/多态/巨态)与 Megamorphic 的工程规避

V8 TurboFan 中的单态(monomorphic)、多态(polymorphic)、巨态(megamorphic)是什么?如何工程规避巨态导致的优化失效?

  • 内联缓存(IC)按调用点记录接收者形状
  • 单态/多态/巨态的缓存槽位数与优化策略差异
  • 保持形状一致的编码实践

TurboFan 依赖内联缓存(IC)按调用点记录函数调用时对象的形状(隐藏类),同一调用点只出现一种形状即为单态,可内联并激进优化;2-4 种形状为多态,优化受限;超过阈值或形状持续变化成为巨态,该调用点失去类型反馈,退化为通用路径且难以被优化。工程规避的核心是"保持形状稳定":构造对象时固定属性顺序(同一工厂函数、同一字面量结构)、避免动态增删属性、复用固定类而非散装对象、警惕可选链与解构在不同结构间切换。对热点路径(渲染循环、数据结构遍历)尤其重要。

本题考察 V8 优化的前提条件。先讲清 IC 如何按形状统计、单态→多态→巨态的退化逻辑,再给工程建议:形状稳定性是一切优化的地基。能举出"动态添加属性导致形状漂移"的反例,说明对引擎机制理解透彻。

#
★★★

7. V8 隐藏类(Hidden Classes / Maps)与内联缓存在对象属性访问的性能价值

V8 的隐藏类(Hidden Classes / Maps)与内联缓存(Inline Caches)如何在对象属性访问中发挥作用?

  • 隐藏类描述对象布局,相同结构共享隐藏类
  • 属性访问先查隐藏类偏移再读内存
  • IC 缓存(隐藏类, 偏移)键值对加速重复访问

V8 为每个对象关联一个隐藏类(Map),记录属性名到内存偏移的映射;用相同字面量或构造函数创建的对象共享同一隐藏类,属性访问先在隐藏类中查偏移、再直接读写对应槽位,避免字典查找。内联缓存在此基础上缓存"隐藏类→偏移"的键值对:同一属性访问点第一次命中后,后续相同形状的访问直接走缓存,接近字段访问速度。工程价值:保持对象形状一致(固定属性顺序与数量、避免动态增删)可让对象共享隐藏类,最大化 IC 命中率;反之形状漂移使每个对象独享隐藏类并让 IC 失效,性能显著下降。

本题考察 V8 对象模型的两大机制及其配合:隐藏类负责"布局描述",IC 负责"访问记忆"。回答要点明二者关系——IC 缓存的是隐藏类与偏移的映射,并给出保持形状稳定的工程结论,与"单态/巨态"知识点形成呼应。

#
★★★

8. with 语句在严格模式下的禁用与替代方案

with 语句为何在严格模式被禁用?有哪些安全的替代方案?

  • with 对作用域链的运行时扩展及其性能代价
  • 与变量声明、严格模式的冲突
  • 替代:解构赋值、对象成员访问、IIFE 传参

with 在运行时把对象并入作用域链头部,属性名可能遮蔽外部变量,导致变量解析无法静态完成:引擎无法预知标识符绑定目标,隐藏类与内联缓存全部失效,且同一行代码在不同调用下解析结果不同,故严格模式直接禁用以保证可静态分析与安全。工程替代:局部解构(const { a, b } = obj)明确提取变量;或直接用 obj.a 成员访问,可读性与性能都更好;模块/函数内多次引用时先用局部变量缓存引用。任何"把对象属性当变量用"的需求都应避免 with,必要时用 Proxy 包装提供显式访问语义。

本题考察"语言特性为何被弃用"的分析能力。抓住两个层面:一是性能(破坏静态作用域分析与优化),二是语义(运行时遮蔽造成歧义),再给出解构等替代方案;能说明 with 与严格模式设计目标的冲突即回答完整。

#
★★★

9. typeof 与 instanceof 的边界场景(跨 Realm、Symbol.toStringTag)

typeof 与 instanceof 分别有哪些边界场景?跨 Realm 与 Symbol.toStringTag 如何影响判断结果?

  • typeof 的历史遗留(typeof null === "object")与未声明变量
  • instanceof 依赖原型链与右侧构造函数
  • 跨 iframe/Worker 的构造函数不共享、Symbol.toStringTag 可伪造

typeof 返回类型字符串,但有已知坑点:typeof null 为 "object"(历史遗留)、typeof 未声明变量返回 "undefined" 不报错、函数返回 "function";它基于内部类型判定,不受跨 Realm 影响。instanceof 检查右侧构造函数的 prototype 是否在左侧对象的原型链上,因此跨 Realm(iframe、Worker、node_modules 双实例)时右侧构造函数不同,判断会失败;Symbol.toStringTag 可修改 Object.prototype.toString.call 的结果(如 [Symbol.toStringTag] = "Foo" 使结果为 "[object Foo]"),也影响依赖它的判断。工程上跨环境判定优先用 Object.prototype.toString 或专用 API(如 Array.isArray、Error.isError),避免直接 instanceof。

本题考察类型判断 API 的可靠性边界。要点是区分"基于内部类型"(typeof、toStringTag 可参与)与"基于原型链"(instanceof)两类机制,并给出跨 Realm 场景的工程对策;能解释 instanceof 在 iframe 间失效的原因即抓住本质。

#
★★★

10. Object.is() 与 === 在 NaN/+0/-0 的关键差异

Object.is() 与 === 在 NaN、+0、-0 的判断上有何关键差异?各自适用什么场景?

  • Object.is 对 NaN 与 +0/-0 的特殊处理
  • === 遵循 IEEE 754 语义
  • 工程中的选择依据

二者在绝大多数比较上等价,仅有两处差异:Object.is(NaN, NaN) 为 true,而 NaN === NaN 为 false;Object.is(+0, -0) 为 false,而 +0 === -0 为 true。=== 遵循 IEEE 754 数值语义,NaN 不等于自身、正负零相等;Object.is 采用 SameValue 语义,把 NaN 视为同一值、正负零视为不同值。工程场景:判断结果是否为 NaN 可用 Number.isNaN(比 Object.is(x, NaN) 更直白),需要区分 +0/-0(如符号方向、累加器状态)时用 Object.is 或 1/x 技巧;框架内部(如 React 的 Object.is 比较)用 SameValue 语义保证 NaN 状态更新可被识别。

本题考察相等性语义的三个层次(==、===、Object.is)。核心是记住两处差异:NaN 与正负零;再结合场景给出选择建议,能说出 React 等框架用 Object.is 比较 props/state 的原因即为加分项。

#
★★★

11. JS Number 的 IEEE 754 双精度浮点数精度问题与 Number.EPSILON 边界

JS Number 基于 IEEE 754 双精度浮点数,存在哪些精度问题?Number.EPSILON 在比较中如何使用?

  • 双精度 53 位有效尾数与二进制表示误差
  • 经典问题(0.1+0.2 !== 0.3)的成因
  • Number.EPSILON 与误差容差比较、整数安全范围

JS Number 是 IEEE 754 双精度,只有 53 位有效尾数,十进制小数多数无法精确二进制表示,因此 0.1+0.2 得到 0.30000000000000004 而非 0.3;同时超过 2^53 的整数不再精确,Number.MAX_SAFE_INTEGER 为 9007199254740991。Number.EPSILON 是 1 与大于 1 的最小可表示数的差值(约 2.22e-16),用于浮点比较的误差容差:判断相等用 Math.abs(a-b) < Number.EPSILON。工程上:金额等精确计算改用整数分/厘或 decimal 库(或 BigInt);大数用 BigInt;比较时给相对/绝对容差而非直接 ===。

本题考察浮点数的表示原理与工程对策。先讲清"53 位尾数→小数近似→误差累积"的链条,再引出 EPSILON 的容差比较法与整数安全范围,最后落到金额、ID 等场景的选型;能解释 EPSILON 为何是"相对机器精度"即到位。

#
★★★

12. 原型链、proto、prototype、constructor 的关系

原型链、proto、prototype 与 constructor 之间是什么关系?如何梳理这条对象关系链?

  • 实例的 proto 指向构造函数的 prototype
  • 构造函数的 prototype.constructor 回指构造函数
  • 原型链查找与 Object.prototype/Function.prototype 的顶端结构

每个对象都有内部槽 [[Prototype]](访问器是 proto,标准 API 是 Object.getPrototypeOf),指向其原型对象;函数对象有 prototype 属性,作为它构造的实例的原型。关系链:new F() 创建实例 obj,obj.proto === F.prototype;F.prototype.constructor === F。属性访问沿 [[Prototype]] 链逐级查找直到 Object.prototype(其原型为 null)。类继承则通过 F.prototype.proto === Parent.prototype 形成两级链。工程上应使用 Object.getPrototypeOf/setPrototypeOf 或 Object.create 而非直接改 proto;理解这条链是掌握 instanceof、继承与属性遮蔽的基础。

本题考察 JS 对象模型的核心关系网。建议画三角关系(构造函数 ↔ prototype ↔ 实例)作答:实例 proto 指 prototype,prototype.constructor 回指函数;再补充链顶端与属性查找规则;能解释"函数也是对象,其 proto 指向 Function.prototype"即展示完整认知。

#
★★★

13. this 绑定规则(默认、隐式、显式、new、箭头)的优先级

JS 中 this 的绑定规则有哪些?它们的优先级如何排序?箭头函数有何特殊之处?

  • 默认绑定(非严格模式为全局对象、严格模式为 undefined)
  • 隐式绑定(调用者上下文)与显式绑定(call/apply/bind)
  • new 绑定最高优先级、箭头函数词法捕获

this 的绑定在调用时确定,按优先级从低到高:默认绑定(独立调用,严格模式 undefined,非严格全局对象)< 隐式绑定(obj.method() 中 this 为 obj)< 显式绑定(call/apply 立即指定,bind 返回新函数)< new 绑定(构造调用,this 为新实例,且优先级最高)。箭头函数没有自己的 this,按定义位置的词法作用域捕获外层 this,call/apply/bind 无法改变,也没有 prototype 不能作为构造函数。工程经验:先判断是否箭头函数,再按"new > 显式 > 隐式 > 默认"排查;丢失绑定(把方法拆出单独调用)是常见坑,用 bind 或箭头函数修复。

本题考察 this 规则的完整体系。按优先级排序是答题骨架,每个规则配一个判断条件;箭头函数单独强调"词法捕获、不可修改、不可 new"。能现场推导嵌套场景下 this 的值,说明真正掌握而非背诵。

#
★★★

14. 闭包的词法环境捕获与 Retainer Path 排查

闭包是如何捕获词法环境的?内存泄漏时如何通过 Retainer Path 排查闭包相关引用?

  • 闭包 = 函数 + 捕获的词法环境引用
  • 大对象被闭包意外持有的场景
  • DevTools Memory 面板 Retainer Path 的用法

闭包在函数定义时捕获当前词法环境的引用,函数对象存活期间该环境及其中的变量都不会被回收,即使外层函数早已返回。这带来便利也带来风险:回调、事件监听器、定时器中的闭包若捕获了大对象或 DOM 节点,会意外延长其生命周期形成泄漏。排查时在 DevTools Memory 面板抓取堆快照,对疑似泄漏对象查看 Retainer Path(保留者路径):从 GC 根到对象的引用链,闭包场景常见链为 "Closure → 上下文 → 大对象",路径上的上下文(context)显示闭包所在函数与捕获变量,据此定位注册监听/回调的位置并解除引用。

本题考察闭包的内存语义与调试方法。先解释闭包捕获的是词法环境引用而非快照,再说明泄漏机制(回调链持有环境),最后给出 Retainer Path 的标准排查流程;能说明"闭包泄漏的本质是引用链未被切断"即抓住要点。

#
★★

15. V8 TurboFan Maglev/Sparkplug 在性能优化与 GC 压力的取舍

V8 的 Sparkplug 与 Maglev 编译器在性能优化与 GC 压力之间如何取舍?

  • Sparkplug 快速非优化编译、Maglev 中档优化编译器
  • 字节码→机器码的层级演进与预热
  • 优化带来的对象分配与 GC 压力权衡

V8 采用多层编译架构:Ignition 解释执行字节码,函数变热后由 Sparkplug 做快速非优化编译(编译快、无类型假设),更热时升级到 Maglev(中等优化、面向多态场景的快速优化编译),长期热代码才交给 TurboFan 激进优化。取舍在于:优化层越高单次执行越快,但编译耗时、内存占用(机器码、内联缓存、反馈向量)与运行时检查越多;激进优化还可能内联产生更多临时对象,间接增加 GC 压力。工程启示:热点函数保持形状稳定以稳定获得优化,避免"解释执行/重新优化"来回抖动(deopt 颠簸),在性能与内存之间取得平衡。

本题考察多层级编译器的设计动机。回答框架是"层级越高优化越强、代价越大",并点出 Maglev 填补了 Sparkplug 与 TurboFan 之间的空白;最后落到工程启示(避免 deopt 抖动、控制内联膨胀),体现对"优化也有成本"的平衡观。

#
★★

16. == 隐式类型转换的完整规则链与经典判断([]==![] 为 true、null==undefined 为 true、{}+[] 的解析歧义)

== 的隐式类型转换规则链是怎样的?为什么 []==![] 为 true、null==undefined 为 true,而 {}+[] 存在解析歧义?

  • == 两侧类型不同时的转换优先级(对象→原始值、数值化)
  • null/undefined 的特殊相等规则
  • 语句上下文导致的解析歧义

== 在类型相同时直接比较,类型不同时按规则转换:一方为 null/undefined 且另一方也是 null/undefined 时为 true(二者互相相等,但不等于其他值);一方为对象时先 ToPrimitive;随后尽量把两侧转为数值比较(boolean 转 number、string 转 number)。[]==![] 中 ![] 为 false 转 0,[] 经 ToPrimitive 为 "" 再转 0,故相等;null==undefined 走特例恒 true;{}+[] 在语句上下文(行首)时 {} 被解析为代码块而非对象字面量,实际计算 +[] 得 0,若整体作为表达式则得到 "[object Object]"。工程上推荐始终使用 === 或 Object.is 避免规则链带来的心智负担。

本题考察 == 规则的完整推导。答题应给出转换优先级骨架(null/undefined 特例→ToPrimitive→ToNumber),再用经典表达式逐步演算验证;{}+[] 的歧义属于解析器上下文问题,能区分"代码块 vs 对象字面量"即展示深度。

#
★★

17. Symbol.for() 与 Symbol() 的全局注册表与跨 Realm 行为差异

Symbol.for() 与 Symbol() 有何区别?全局注册表在跨 Realm 场景下的行为如何?

  • Symbol() 每次创建全新唯一符号
  • Symbol.for(key) 基于全局注册表按 key 复用
  • 跨 iframe/Worker 时全局注册表是否共享

Symbol() 每次调用创建全新的唯一符号,不进入任何注册表,即使描述相同也互不相等;Symbol.for(key) 则先在全局符号注册表中按字符串 key 查找,存在则返回既有符号,否则创建并登记,因此同一 key 在不同位置调用返回同一符号。注册表是全局的,跨 Realm(iframe、Worker)依然共享,Symbol.for 返回值相同,可用于跨上下文共享协议标识;而 Symbol() 创建的符号天然隔离。工程上:需要跨模块/跨 Realm 稳定标识(如协议 key)用 Symbol.for,仅需本地唯一键用 Symbol(),注意 Symbol.keyFor 只能查询注册表中的符号。

本题考察 Symbol 的两种创建形态。核心差异在"是否进入全局注册表"及其带来的复用/共享语义,跨 Realm 共享是常考延伸;能说出 Symbol.keyFor 与注册表的查询关系即回答完整。

#
★★

18. Intl.NumberFormat 在货币、千分位、单位格式化上的工程化应用

Intl.NumberFormat 在货币、千分位、单位格式化上有哪些工程化应用?使用时需注意什么?

  • 区域敏感的数字格式化 API
  • style: currency/decimal/unit 与 notation
  • 性能复用与最小整数位数等选项

Intl.NumberFormat 按区域设置格式化数字:style:"currency" 输出带货币符号(useGrouping、currencyDisplay 控制符号形态),style:"decimal" 处理千分位分组,style:"unit" 附加单位(如 "MB"、"km/h");notation 可切 scientific/compact(compact 输出 "1.2万" 等本地化缩写)。工程要点:实例可复用于大量数字,避免循环内重复构造;minFractionDigits/maxFractionDigits 控制小数位;formatToParts 可拆解格式化片段做高亮;需同时保证区域数据(locale data)在打包时完整引入,警惕多语言下数字方向与分组规则差异。

本题考察 ICU 数字格式化 API 的应用面。回答按"货币/千分位/单位"三类场景展开,并补充复用、精度选项与 formatToParts 等工程细节;能说明 currency 格式化包含货币符号与本地化规则即体现深度。

#
★★

19. Intl.Segmenter 在 CJK 文本分词与词边界检测的能力

Intl.Segmenter 在 CJK 文本分词与词边界检测上有何能力?工程中如何应用?

  • 按 grapheme/word/sentence 三种粒度切分
  • CJK 无空格分词的语言特性
  • 与正则 \b、string 索引切割的对比

Intl.Segmenter 基于 ICU 断字规则把文本切分为段:granularity 可选 "grapheme"(用户感知字符,含 emoji 组合)、"word"(词边界,CJK 按词典与上下文切分)、"sentence"。中文没有空格分隔,String.split 与正则 \b 无法正确分词,Segmenter 按 Unicode 分词规则给出词边界,是统计字数、搜索高亮、富文本光标移动、文本溢出截断的安全基础。工程注意:Segmenter 需区域数据支持且不可细粒度定制,生产环境(如长文本切词)常配合词频词典做扩展;遍历用 for...of 段迭代器按位置切分,避免 substring 按码元切割破坏代理对。

本题考察国际化分词能力。重点在于 CJK 场景:无空格语言分词必须依赖断字规则而非空白或 \b,并注意码元(UTF-16 单元)与用户感知字符的差别;能说明 emoji 组合字符需 grapheme 粒度即展示细节。

#
★★

20. Map/Set 与 WeakMap/WeakSet 的键类型、迭代顺序与 GC 行为

Map/Set 与 WeakMap/WeakSet 在键类型、迭代顺序与 GC 行为上有哪些差异?

  • 强引用集合的任意值键与插入序迭代
  • Weak 集合的弱引用键与不可迭代
  • 缓存、对象关联元数据的场景选择

Map/Set 的键可以是任意值(含原始类型与对象),按插入顺序迭代,拥有 size 与完整遍历 API;WeakMap/WeakSet 的键只能是对象(WeakMap)或对象元素(WeakSet),对键持弱引用——键无其他引用时即被 GC 回收,因此不可迭代、无 size、无 clear,也不可枚举。GC 行为差异决定用途:缓存对象关联的元数据(DOM 节点关联状态、对象私有字段)用 WeakMap 避免泄漏;需要遍历、统计、按顺序输出时用 Map/Set。注意 WeakMap 的弱引用针对"键",值仍被强持有,值引用了键时会形成环但可被整体回收。

本题考察强弱引用集合的本质差异。答题框架:键类型、迭代能力、GC 语义三个维度对比,并落到"防止泄漏选 Weak、需要遍历选强引用"的工程决策;能说出"WeakMap 弱的是键不是值"即体现精准理解。

#
★★

21. BigInt 与普通数字的混合运算规则

BigInt 与普通 Number 的混合运算规则是什么?为何禁止直接混算?

  • BigInt 与 Number 是不同数值类型
  • 混合运算抛 TypeError 的语义考虑
  • 显式转换(BigInt()/Number())与精度风险

BigInt 与 Number 属于不同数值类型:任何把二者混在一起的算术运算(+、-、*、/、% 等)都会直接抛 TypeError,禁止隐式混算,因为隐式转换要么丢失 BigInt 的精度、要么破坏 Number 的语义,静默转换会造成难以察觉的错误。需要混算时必须显式转换:BigInt(x) 或 Number(x),转换时注意 Number(BigInt) 可能溢出精度、BigInt(小数) 会抛异常(要求整数)。比较运算符(==、<、>)允许跨类型比较(== 会数值化),而 === 永远为 false。工程上:金融与 ID 计算全程用 BigInt,边界处显式转换并校验精度。

本题考察 BigInt 的类型隔离设计。核心是"禁止隐式混算是语言层的精度保护",再讲清显式转换的时机与坑(Number 溢出、BigInt 不接受小数);能区分"算术抛错、比较允许、=== 恒 false"三类行为即完整。

#
★★

22. Symbol 类型与内置 Symbol(Symbol.iterator、Symbol.toPrimitive 等)

Symbol 类型有何特性?内置 Symbol(Symbol.iterator、Symbol.toPrimitive 等)在语言机制中扮演什么角色?

  • Symbol 的唯一性、不可枚举性、作为属性键
  • 内置 well-known symbols 定义协议
  • 自定义行为覆盖(迭代、转换、标记)

Symbol 是第 7 种原始类型:每次 Symbol() 创建的符号全局唯一,作为对象属性键时不可被 for...in/Object.keys 枚举(可经 Object.getOwnPropertySymbols 获取),用于避免命名冲突、实现隐藏元数据。语言本身用一批内置 Symbol(well-known symbols)定义协议:Symbol.iterator 让对象可被 for...of 与展开迭代,Symbol.asyncIterator 支持 for await...of,Symbol.toPrimitive 可覆盖对象转原始值的默认行为,Symbol.hasInstance 自定义 instanceof 判定,Symbol.toStringTag 影响 Object.prototype.toString 输出,Symbol.species 控制派生对象构造。工程上:自定义可迭代对象实现 [Symbol.iterator],库设计用 Symbol 键做协议扩展而不污染公共 API。

本题考察 Symbol 的协议化作用。前半讲唯一性与属性键特性,后半以内置 Symbol 为例说明"语言把行为协议挂到 Symbol 上"的设计,能列出三四个 well-known symbols 及各自挂钩的语法机制即展示广度。

#
★★

23. 严格模式对变量声明、this、静默错误的影响

严格模式对变量声明、this 绑定与静默错误分别有何影响?

  • 未声明赋值、delete 等静默错误改为抛错
  • 函数调用 this 不再装箱为全局对象
  • eval、arguments 的严格语义

严格模式通过 "use strict" 启用,主要影响:变量声明上,未声明即赋值(隐式全局)直接抛 ReferenceError,delete 不可删除的标识符或属性抛 TypeError,变量名与函数参数重名受限;this 绑定上,独立函数调用 this 为 undefined(而非装箱为全局对象),call/apply 传入的 null/undefined 不再被替换为全局对象;静默错误全面升级为异常,如对只读属性赋值、扩展不可扩展对象、重复属性名(对象字面量)等。此外 eval 拥有独立作用域、arguments 不再与参数联动(也不再可赋值)。工程上模块与 class 默认严格,遗留脚本显式开启可尽早暴露隐患。

本题考察严格模式的行为差异清单。建议按"声明/赋值、this、静默错误、eval/arguments"分块作答,重点是"把静默失败变成显式异常"的设计意图;能举例说明 call 传 null 的行为差异即说明理解到位。

#
★★

24. finalizationRegistry 与 WeakRef 在大对象延迟释放的工程价值

FinalizationRegistry 与 WeakRef 在延迟释放大对象上有何工程价值?使用上有哪些注意事项?

  • WeakRef 的弱引用语义与 deref 使用
  • FinalizationRegistry 的注册与清理回调
  • GC 非确定性、回调时机不可依赖

WeakRef 持有对象的弱引用,不阻止 GC,通过 deref() 在对象仍存活时取回强引用;FinalizationRegistry 注册"对象+令牌",当对象被 GC 回收后异步触发清理回调,可用于释放关联的昂贵外部资源(如 WebAssembly 内存句柄、缓存条目)。工程价值:避免大对象被缓存、监听器、外部资源句柄意外强持有;如实现弱缓存(键强值弱)或按需释放图像位图。注意事项:清理回调在微任务时机之后不确定执行,可能晚于对象回收甚至根本不执行(进程退出前),因此不能依赖它做关键清理(文件关闭、持久化);deref 返回的对象必须立即使用并置空局部引用,防止其再次被强持有。

本题考察弱引用机制的边界。答题要点是"WeakRef 读取+FinalizationRegistry 清理"的配合,以及"GC 时机不确定,清理回调不可作为可靠机制"的工程警示;能说明为何弱引用缓存可能"明明还在却被回收"即抓住本质。

#
★★

25. globalThis 在多环境(浏览器/Worker/Node)的统一访问与 polyfill 治理

globalThis 如何统一浏览器、Worker 与 Node 中的全局对象访问?老环境如何 polyfill?

  • 各环境全局对象名差异(window/self/global)
  • globalThis 的标准统一入口
  • polyfill 实现与严格模式注意点

历史上浏览器全局对象是 window、Web Worker 是 self、Node.js 是 global,跨环境代码需自行探测;ES2020 引入 globalThis 作为标准的全局对象统一引用,各环境按规范将其绑定到自己的全局对象,可放心用于库与通用代码。polyfill 治理:老环境可用"对象引用链探测"方式实现——先取 Function('return this')() 或 globalThis(支持则直接用),再按环境探测 self/window/global,注意严格模式下 Function 构造器才能拿到全局对象。工程注意:在 ESM 模块顶层 this 为 undefined,取全局对象必须用 globalThis;多包共存时避免重复定义并做特性检测而非环境字符串判断。

本题考察跨环境全局访问的标准化演进。回答顺序:环境差异→globalThis 统一→polyfill 探测链,并点出 ESM 顶层 this 的坑;能说明"用特性检测而非 UA 判断"即体现工程成熟度。

#
★★

26. Date.parse 的实现差异与 Temporal 替代的工程意义

Date.parse 在不同浏览器/环境中有何实现差异?Temporal API 为何是更优的替代?

  • Date.parse 对非 ISO 格式解析不一致
  • 时区与夏令时的隐式本地化行为
  • Temporal 的强类型时间模型

Date.parse 依据实现定义的格式解析字符串:ISO 8601 格式被广泛支持且解析一致,但非标准格式(如 "March 5, 2024"、本地化格式)在不同引擎、不同时区下结果可能不同,且解析失败返回 NaN 而不抛错,容易静默出错;Date 本身把"日期时间"与"时区"耦合在单个毫秒数里,操作本地时区与夏令时极易踩坑。Temporal 将时间拆分为 Instant(绝对时刻)、ZonedDateTime(带时区日期时间)、PlainDate/PlainTime/PlainDateTime(无时区日历时间)、Duration 等强类型对象,解析与运算规则明确、不可变、可预测。工程意义:金融排程、跨时区协作等场景用 Temporal 消除解析歧义与时区隐式转换,是 ES2026 标准的一部分,目前可通过 polyfill 渐进采用。

本题考察日期时间 API 的演进动机。先列 Date.parse 的解析差异与 Date 模型的时区耦合缺陷,再对比 Temporal 的类型化时间模型,最后落到工程选型与 polyfill 路线;能举出夏令时导致的时间跳变案例即展示实战认知。

#
★★

27. Array.prototype.flat/flatMap 在稀疏数组与类数组对象的行为差异

Array.prototype.flat 与 flatMap 对稀疏数组和类数组对象有何行为差异?

  • flat 的深度参数与稀疏数组空洞跳过
  • flatMap 先 map 后 flat(1) 的等价语义
  • 对类数组对象不自动转换

flat(depth) 按深度展平嵌套数组,默认 depth=1;对稀疏数组,展平过程会跳过空洞(与 map 保留空洞不同,flat 会把空洞滤除)。flatMap 等价于先 map 再 flat(1):映射函数返回数组时会被展开一层,是"一对多映射"的常用写法(如展开每组子项),返回的数组若含稀疏项同样被压平滤除。行为差异:两者都是 Array.prototype 方法,通过 this 取值,但不像 Array.from 那样做类数组转换——对类数组对象(如 arguments)调用 flat/flatMap 会因 length 与索引读取失败而表现异常,需先 Array.from 或 [...it] 转真数组。工程上:展开树形数据用 flatMap 替代 map+concat,注意 depth 过深时的性能。

本题考察数组展平方法的细节语义。核心是"flat 过滤空洞、flatMap=map+flat(1)、方法只作用于真数组"三要点,能举例说明 flatMap 的一对多展开场景即体现应用能力。

#

28. Object.freeze/seal/preventExtensions 的浅冻结与递归冻结

Object.freeze、seal、preventExtensions 三者有何区别?为何都是浅层操作,递归冻结如何实现?

  • 三个 API 对属性增删改的限制梯度
  • 浅冻结只作用于对象自身
  • 递归冻结的深度优先遍历实现

preventExtensions 禁止新增属性;seal 在 preventExtensions 基础上把现有属性全部设为不可配置(禁止删除与改描述符);freeze 再进一步把所有属性设为只读(writable:false),三者限制力度递增。三者都是浅层操作:只作用于对象自身,嵌套对象仍可修改,因此"深冻结"需递归遍历属性值(对引用类型递归调用 freeze),同时处理循环引用(用 Set/WeakSet 记录已冻结对象)与原型链(通常只冻结自有属性)。工程上:freeze 用于不可变常量、配置对象与函数式状态防护,深冻结用于防篡改的全局配置;注意 frozen 对象在严格模式下修改会抛错,非严格模式静默失败。

本题考察对象限制 API 的梯度与浅层边界。先画限制力度对比表,再强调"浅层"语义并给出带循环引用处理的递归冻结实现思路;能说出严格模式下的抛错差异即完整。

#

29. 属性描述符(writable、enumerable、configurable、get/set)的控制能力

属性描述符中的 writable、enumerable、configurable 与 get/set 各自控制什么?工程上有哪些应用?

  • 数据描述符与访问器描述符的结构
  • 三个布尔标志的控制语义
  • defineProperty 在框架与防篡改中的应用

属性描述符分两类:数据描述符(value、writable、enumerable、configurable)与访问器描述符(get、set、enumerable、configurable)。writable 控制赋值是否生效;enumerable 控制 for...in、Object.keys、展开与 JSON.stringify 是否包含该属性;configurable 控制能否修改描述符、能否 delete,一旦为 false 则不可逆(除非 writable 变 false 后不能再改)。get/set 定义访问器,读取与赋值时调用函数,可做校验、日志、计算属性。工程应用:defineProperty 实现响应式数据劫持(Vue 2)、隐藏内部字段、只读常量(writable:false)、拦截 JSON 序列化的非枚举字段,以及 Object.freeze 的底层实现基础。

本题考察对象属性的细粒度控制。先分清两类描述符与三个标志各自语义,再给工程场景;能说明 configurable:false 的不可逆性与 writable:false 的特例即展示对规范细节的把握。

#

30. 词法作用域与动态作用域的区别,为何说 JS 的 this 是其唯一的动态作用域机制

词法作用域与动态作用域有何区别?为什么说 JS 的 this 是唯一的动态作用域机制?

  • 词法作用域按定义位置静态决定
  • 动态作用域按调用链决定
  • this 绑定在调用时按调用方式动态确定

词法作用域(静态作用域)在代码书写时由嵌套结构决定,变量解析与调用位置无关,JS 的普通变量查找、闭包捕获都遵循词法作用域;动态作用域则在运行时沿调用栈(调用者链)查找变量,Perl 与 Bash 的某些特性采用这种模型。JS 的 this 是例外:它的绑定不在定义时确定,而在调用时按调用方式(默认/隐式/显式/new/箭头)动态决定,行为上类似"沿调用链传递的动态绑定",因此常被称为 JS 中唯一的动态作用域机制——不过严格说它是"调用上下文绑定"而非变量查找链。理解这点有助于区分闭包捕获(词法)与 this 传递(动态)两套独立机制,避免混淆。

本题考察作用域模型的概念辨析。回答框架:两种作用域的定义对比→JS 变量是词法作用域→this 按调用方式动态绑定所以像动态作用域,并澄清 this 并非变量查找链;能说明闭包与 this 是两套正交机制即理解到位。

#

31. 手写 call/apply/bind 的关键点(Symbol 唯一键挂载、this 绑定、bind 返回函数被 new 的处理)

手写 call/apply/bind 的关键点有哪些?如何兼容 bind 返回函数被 new 调用的情况?

  • 用 Symbol 唯一键挂载方法避免属性名冲突
  • this 为 null/undefined 时的默认绑定
  • bind 返回函数被 new 时 this 指向新实例且保留原型链

手写 call/apply 的核心是把函数临时挂到目标对象上再调用:用 Symbol() 生成唯一键(避免覆盖对象已有属性且不可枚举),把 this 函数赋给 obj[key],用 objkey 调用后 delete,若 this 为 null/undefined 则按默认绑定指向全局对象(严格模式 undefined)。bind 则返回新函数:调用时用 apply 以缓存的目标 this 与合并参数执行;关键兼容是返回函数被 new 调用时,this 应指向 new 创建的新实例而非 bind 的目标,实现上可用 new.target 判断(ES6 中返回箭头函数不行,需普通函数并检查 new.target 或构造调用标志),同时返回函数应继承原函数的 prototype 以便 new 后 instanceof 成立。参数合并用展开符,注意 bind 的柯里化参数在前。

本题考察手写实现的核心边界。答题按"挂载调用→null/undefined 默认绑定→bind 柯里化→new 兼容→原型链"的层次展开,其中 new 兼容与原型链保留是最容易遗漏的点;能说明为何必须用普通函数而非箭头函数实现 bind 即展示细节掌握。

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this;
  function bound(...rest) {
    return fn.apply(new.target ? this : ctx, args.concat(rest));
  }
  bound.prototype = Object.create(fn.prototype);
  return bound;
};