ES6 这块我按”能解释为什么”来记:let的词法环境、Map 和 Object 的取舍、??与||的边界,比报菜名有用。
Q1: let/const/var 的区别?const 的值真的不能改吗?
1
ES5 时代: var 的设计缺陷
var 是函数作用域,变量提升后初始化为 undefined,容易掩盖 Bug。没有块级作用域,for 循环中的变量会泄漏到外部。2
致命痛点: 变量提升导致隐蔽 Bug
console.log(a); var a = 1; 不报错,输出 undefined。你在不知情的情况下使用了未初始化的变量。3
ES6 引入 let/const
let 和 const 是块级作用域,有暂时性死区(TDZ),不能重复声明。声明前访问会报错,比 var 的静默失败更安全。4
核心机制: V8 的双环境设计
ES6 后 V8 将执行上下文拆分为”词法环境”(存放 let/const)和”变量环境”(存放 var)。
let 受控于词法环境栈的推入与弹出,完美隔绝块级作用域。5
关键洞察: const 保证的是引用不变
const 只保证引用不变,不保证值不变。const obj = {}; obj.name = 'Tom' 是合法的。let/const 用块级作用域和 TDZ 解决了 var 的设计缺陷,const 只保证引用不变。
var 的半吊子提升
var 声明会被提升,并且在创建阶段就被强行初始化为 undefined。let/const 与 TDZ
let 和 const 同样会被提升!但在执行到声明行之前,它们处于未初始化状态(TDZ)。此时访问会直接报错。Q2: 箭头函数和普通函数有什么区别?
基础梳理
专家视角深度剖析
箭头函数和普通函数的核心区别是this 的来源不同。以下是经典输出题的图解分析:
箭头函数输出题图解
V8 中箭头函数的 this 实现
箭头函数在 V8 中被编译时,其[[ThisMode]] 被标记为 kLexical。当引擎准备为它解析 this 时,一旦发现是 kLexical,会直接跳过运行时动态绑定阶段,强行去读取闭包上下文 Context 中的 this。这就是为什么 call/apply 也无法扭转箭头函数的 this。
箭头函数不能作为构造函数
箭头函数没有prototype 属性,也没有内部的 [[Construct]] 方法。使用 new 调用箭头函数会直接抛出 TypeError。
箭头函数的实际应用场景
箭头函数最适合用于回调函数中需要继承外层this 的场景,最典型的例子是数组方法和定时器回调。
Q3: 解构赋值的原理?有什么坑?
解构赋值的本质
解构赋值的本质是模式匹配——等号左边是一个”模式”,右边是数据源。引擎按照模式从数据源中提取对应的值并赋给变量。解构的常见坑与最佳实践
Q4: Map 和 Object 的区别?什么时候用 Map?
Map 的内部实现原理
Map 内部使用哈希表实现(V8 中是 OrderedHashMap),同时维护一条插入顺序链,因此size 是 O(1),迭代严格按插入顺序。
Map vs Object 的选择策略
WeakMap 与 Map 的区别
WeakMap 的 key 必须是对象,且是弱引用——当 key 对象没有其他引用时会被垃圾回收,适合存储 DOM 节点关联数据。Map 深度展开:内部实现与性能
Map 的内部实现原理(哈希表): Map 内部使用哈希表实现。V8 使用 OrderedHashMap:哈希桶负责定位,另有一条插入顺序链保证迭代顺序稳定。WeakMap 的弱引用原理与使用场景
弱引用的底层机制: WeakMap 的弱引用是语言层面的语义保证,不是手动引用计数。V8 的垃圾回收器(GC)使用标记-清除算法:当一个对象从 GC Root 不可达时,WeakMap 中的对应条目会在下次 GC 时被自动清除。Map 用哈希表实现 O(1) 增删查改,key 可以是任意类型且保持插入顺序;WeakMap 用弱引用关联对象,当 key 对象无其他引用时自动被 GC 清除,适合存储 DOM 元素关联数据和实现私有变量。
Set 深度展开:内部实现与去重机制
Set 的内部实现原理: Set 内部同样使用哈希表实现,但只存储 key,不存储 value。V8 对 Set 的优化策略与 Map 一致:少量元素用线性存储,大量元素切换到哈希表。=== 几乎一致,唯一的区别是 NaN === NaN 为 false,而 Set 认为 NaN 等于 NaN。
WeakSet 的使用场景
WeakSet 与 Set 的关系,等同于 WeakMap 与 Map 的关系:key 必须是对象,且是弱引用。Q5: 可选链 (?.) 和空值合并 (??) 的区别?
可选链与空值合并都是 ES2020 引入的语法:?? 和 || 的本质区别
?? 比 || 更精准:|| 对所有 falsy 值生效(0、”、false、null、undefined、NaN),?? 只对 null 和 undefined 生效。
可选链的短路特性
可选链?. 在遇到 null 或 undefined 时会立即短路返回 undefined,不会再执行后续的属性访问。
最佳实践与注意事项
Q6: Symbol 有什么用?
Symbol 的唯一性保证
每个 Symbol 值都是唯一的,即使描述(description)相同。Symbol.for() 是例外——它会在全局注册表中查找或创建 Symbol。
内置 Symbol 的应用
Q7: 数组方法的三大流派?为什么 React 里不能直接 sort?
三大流派分类
问到”你常用哪些数组方法”时,如果你只是报菜名,那只能拿 60 分。真正拉开差距的回答是:按副作用行为把数组方法分成三个流派,每个流派的适用场景和坑完全不同。副作用派:直接修改原数组
副作用派方法会原地修改数组本身。在纯 JavaScript 脚本里没问题,但一旦进入 React/Vue 的响应式世界,就可能变成隐形炸弹。纯函数派:返回新数组,原数组不动
纯函数派方法不会触碰原数组,而是返回一个全新的数组。这是 React/Vue 等框架最喜欢的模式——因为框架需要通过引用比较(===)来判断数据是否变化。
折叠派:reduce 不只是求和
大部分人对reduce 的理解停留在”求数组之和”。但 reduce 真正的威力是把数组”降维”成任意数据结构——Map、Object、嵌套树,什么都行。
结合框架规范:为什么 React 里不能直接 sort
React 和 Vue 的响应式系统都依赖引用比较来检测数据变化。如果你直接修改原数组,引用没变,框架就”看不见”变化,界面不会更新。ES2023 不可变方法:toSorted、toSpliced、toReversed
ES2023 终于给数组加上了纯函数版本的变异方法,解决了困扰开发者多年的痛点:V8 引擎底层:数组方法的性能陷阱
unshift 的 O(n) 开销:unshift 在数组头部插入元素,V8 需要把所有现有元素往后移一位——这是连续内存的移位操作,时间复杂度 O(n)。数据量大时性能断崖式下降。
delete 删除数组元素会在数组中留下”空洞”(empty slot),V8 会把这种数组从高效的 Packed Elements 模式降级为低效的 Holey Elements 模式,甚至进一步降级到 Dictionary Elements(字典模式),性能断崖式下降。
数组方法按副作用分为三派:副作用派(sort/splice/push)直接改原数组,纯函数派(map/filter/slice/toSorted)返回新数组,reduce 是折叠派能折叠成任意结构。React/Vue 中应优先用纯函数派;V8 底层对 Packed/Holey/Dictionary 三种数组模式有巨大的性能差异,避免
delete 和稀疏赋值。底层源码:V8 如何处理 Generator 和 async/await
Generator 在 V8 中是怎么实现暂停和恢复的?
Generator 被 V8 编译为状态机(详见 V8 源码:src/interpreter/bytecode-generator.cc):
await 相当于挂起等待 Promise settle,settle 后再恢复执行。需要区分的是,这是转译层面的等价模型——Babel 会把 async 函数编译成 Generator,而 V8 原生实现走的是内部挂起/恢复内建函数,不会真的创建一个 JS Generator 对象。
十、及格回答 vs 满分回答
问题:“let/const 和 var 的区别?”
及格回答(60-70分):“var 是函数作用域,let/const 是块级作用域。let/const 有暂时性死区,不能重复声明。const 不能重新赋值但可以修改对象属性。”问题出在:知道规则但不理解底层。 满分回答(90-100分):
“区别不仅在于作用域范围(函数作用域 vs 块级作用域),更在 V8 的字节码生成机制。var声明在编译阶段绑定到函数的 VariableProxy,生成LdaGlobal字节码。而let/const绑定到块级作用域的 Declaration Scope,生成LdaContextSlot。[1]”
“TDZ 的本质是 V8 在进入块级作用域时,将let/const变量初始化为特殊的the_hole(空洞)值。在遇到赋值语句前访问它,V8 检测到the_hole即抛出 ReferenceError,这是保证代码严谨性的机制。[2]”
“Generator 在 V8 中被编译为状态机。每次yield暂停时,V8 保存当前的执行状态到 Generator 对象的context中。next()恢复时,从 context 中恢复状态继续执行。async/await 可等价理解为 Generator + 自动执行器:await挂起等待 Promise settle 后恢复;Babel 正是这样转译的,V8 原生则用 resumable function 内建机制实现。[3]“
问题:“说说你 ES6 ES7 都用了什么 API”
及格回答(60-70分):“我用过 let/const、箭头函数、解构赋值、Promise、async/await、Map/Set、可选链、空值合并……”问题出在:报菜名,没有深度。 满分回答(90-100分):
“ES6+ 的演进有三条主线。第一条是异步编程的演进——从回调地狱到 Promise 再到 async/await。Promise 解决了回调嵌套问题,async/await 让异步代码看起来像同步代码。语义上 async/await 等价于 Generator + 自动执行器,Babel 的 regenerator 就是按这个模型转译的;V8 原生用 resumable function 内建机制完成挂起与恢复。[1]”
“第二条是数据结构的完善。ES5 时代只有 Object 和 Array,但 Object 的 key 只能是字符串,频繁增删性能差,遍历顺序不稳定。ES6 引入 Map/Set/WeakMap 解决了这些问题。Map 的 key 可以是任意类型,内部用哈希表实现,增删查通常是 O(1)(不考虑哈希冲突的极端情况)。WeakMap 的 key 是弱引用,当 key 对象没有其他引用时会被垃圾回收,适合存储 DOM 节点关联数据——用 Map 存 DOM 元素的元数据,节点移除后 Map 仍持有强引用,导致内存泄漏。[2]”
“第三条是防御性编程糖。ES2020 引入的可选链?.和空值合并??解决了深层属性访问和默认值设置的痛点。??比||更精准——||对所有 falsy 值生效(0、”、false),??只对 null 和 undefined 生效。实际项目中用户传timeout: 0时,用||会被覆盖为默认值,用??则正确保留 0。[3]”
“实际工程中还有兼容性问题需要关注。语法层(let/const、箭头函数、解构)靠 Babel 转译,内建对象(Promise、Map/Set、Symbol)靠 Polyfill,而 Proxy/Reflect 这类元编程 API 无法 Polyfill。配置useBuiltIns: 'usage'可以按需引入 polyfill,只打包用到的 API。物流 PDA 设备用 Chromium 49 内核,Promise.allSettled没被 polyfill 直接白屏——这些踩坑经验才是真正体现工程能力的地方。“
Babel + Polyfill 深度展开:ES6+ 的工程化兼容方案
ES6+ 特性的三层分类: ES6+ 的新特性在兼容性层面可以分为三类,处理方式完全不同:@babel/preset-env 是 Babel 的智能预设,它根据目标浏览器环境,自动决定需要哪些转换插件和 Polyfill。
useBuiltIns 决定了 core-js Polyfill 的注入方式,'usage' 是最推荐的模式。
语法层靠 Babel 转译(AST 解析 -> 代码生成),内建对象靠 core-js Polyfill,
@babel/preset-env + useBuiltIns: 'usage' + .browserslistrc 三者配合实现按需兼容。Proxy 等元编程 API 无法 Polyfill,只能换方案。常见追问
Generator 和 async/await 的关系?
Generator 和 async/await 的关系?
答题要点:
说出 async/await 是 Generator + Promise 的语法糖。高分回答:
“async/await 在语义上是 Generator + Promise 的语法糖。Generator 提供暂停/恢复能力(
yield),async/await 在此基础上自动执行并处理 Promise。Babel/regenerator 的转译正是把 async 编译成 Generator:遇到 await 就 yield 一个 Promise,Promise settle 后由自动执行器调用 gen.next(value) 继续;错误通过 gen.throw(err) 注入,所以 await 的错误能被 try/catch 捕获。V8 原生实现则用 resumable function 内建机制,效果等价。”Map 和 Object 的性能差异在哪?
Map 和 Object 的性能差异在哪?
答题要点:
从 V8 的 Inline Cache 角度分析。高分回答:
“Object 的属性查找依赖 V8 的 Inline Cache,如果对象形状稳定(monomorphic),性能极高——直接根据缓存的内存偏移量取值,O(1) 复杂度。但如果频繁增删属性导致形状变化(polymorphic/megamorphic),Inline Cache 失效,性能会下降。Map 的内部实现是哈希表,增删操作始终是 O(1),不受形状变化影响。所以频繁增删场景用 Map 更好,稳定结构的属性访问用 Object 更快。”
for...of 和 for...in 的区别?
for...of 和 for...in 的区别?
答题要点:
从遍历目标和遍历内容两个维度对比。高分回答:
“
for...in 遍历对象的可枚举属性(包括原型链上的),key 是字符串。for...of 遍历可迭代对象(Array、Map、Set、String 等),value 是值。数组遍历推荐用 for...of,不要用 for...in——for...in 会遍历原型链上的属性,且顺序不保证。”Object.is() 和 === 有什么区别?
Object.is() 和 === 有什么区别?
答题要点:
说出两个特殊 case。高分回答:
“
Object.is() 比 === 更严格,有两个关键区别:Object.is(NaN, NaN)返回true,而NaN === NaN返回falseObject.is(+0, -0)返回false,而+0 === -0返回true
Object.is 的本质是比较两个值的 SameValue 算法,而 === 用的是 Strict Equality 算法。”ES6 模块和 CommonJS 的区别?
ES6 模块和 CommonJS 的区别?
答题要点:
从加载方式、值的引用关系、循环依赖处理三个维度对比。高分回答:
“核心区别有三个:
- 加载方式:CommonJS 是运行时同步加载(
require),ES6 模块是编译时静态分析(import/export) - 值的关系:CommonJS 导出的是值的拷贝,ES6 模块导出的是值的引用——模块内部变量变化,导入方也能感知到
- 循环依赖:CommonJS 遇到循环依赖会返回已执行部分的值(可能不完整),ES6 模块因为是引用关系,能正确处理循环依赖”
Set 和数组的区别? 去重用哪个?
Set 和数组的区别? 去重用哪个?
答题要点:
从数据结构特性和去重场景两个角度分析。高分回答:
“Set 和数组的核心区别是 Set 的元素唯一(基于 SameValueZero 算法)。Set 去重的时间复杂度是 O(n),而
Array.prototype.filter + indexOf 是 O(n^2)。大数据量场景优先用 Set。”Promise.all / Promise.race / Promise.allSettled 的区别?
Promise.all / Promise.race / Promise.allSettled 的区别?
答题要点:
从行为差异和适用场景两个维度对比。高分回答:
“三者的区别:实际项目建议:用
- Promise.all:所有 Promise 都成功才返回成功,有一个失败就立即失败(fail-fast)
- Promise.race:返回第一个完成的结果(不管成功还是失败)
- Promise.allSettled:等所有 Promise 都完成(不管成功还是失败),返回每个结果的状态数组
Promise.allSettled 替代 Promise.all + try/catch,更优雅地处理部分失败。”如何实现一个符合 Promise A+ 规范的 Promise?
如何实现一个符合 Promise A+ 规范的 Promise?
答题要点:
说出状态机、回调队列、异步调度、thenable 展平四个核心。高分回答:
“手写 Promise 的关键不是背代码,而是理解四件事:
- 状态机:pending → fulfilled/rejected,状态只能改变一次(不可逆)
- 回调队列:pending 状态时把 then 的回调存起来,状态改变后批量执行
- 异步调度:then 的回调必须用
queueMicrotask异步执行,防 Zalgo 现象 - thenable 展平:
resolvePromise递归处理嵌套的 Promise 和 thenable 对象
延伸阅读与引用参考
- V8 Blog: Understanding V8’s Bytecode:V8 官方文档,解释 Ignition 解释器如何处理
let/const的LdaGlobal/LdaContextSlot字节码生成、变量提升与作用域绑定机制。[1] - Axel Rauschmayer: Variables and Shadowing (ES6):Axel Rauschmayer 在 Exploring ES6 中对 TDZ(暂时性死区)、
the_hole值和块级作用域的权威剖析,是理解 ES6 变量声明底层机制的最佳参考。[2] - V8 Source Code: bytecode-generator.cc:V8 字节码生成器的源码,包含箭头函数
kLexicalthis 模式、Generator 状态机以及 async 函数的挂起/恢复实现。[3] - MDN Web Docs: ES6:MDN 对 ES6+ 特性的完整文档,包括 Map/Set/WeakMap、Symbol、解构赋值、可选链、空值合并等所有 API 的权威说明。
- Exploring JavaScript (ES2015):Axel Rauschmayer 的 JavaScript 系列书籍网站,覆盖 ES2015 到最新标准的全面深度解析,包括箭头函数、解构赋值、Symbol 等专题。
