JavaScript

ES6+ 新特性系统整理(拟稿)

字数:13478    阅读时间:68min
阅读量:6

本文目标:系统整理 ES6 及后续版本的核心特性,面向面试复习,兼顾知识体系完整性。
风格:原理讲解 + 代码示例 + 面试题标注。
定位:ES6 知识的“索引 + 系统整理”中枢,与已有文章交叉链接,避免重复内容。
标注约定:

  • 面试题: 表示高频面试问题
  • 相关阅读: 表示可跳转旧文深入
  • 面试频率: 表示复习优先级

第一部分:ES6 概述与版本演进

1.1 ES6 与 ES2015、ES2016+ 的关系

ES6 全称 ECMAScript 6,是 ECMAScript 标准的第 6 版。2015 年正式发布后,标准委员会决定改为按年份命名,因此:

  • ES6 === ES2015
  • ES2016 是 ES7
  • ES2017 是 ES8
  • 以此类推

但日常交流中,很多人说的“ES6+”并不是严格指 ES2015 之后的每一个版本,而是泛指:

ES2015 及之后所有现代 JavaScript 新特性的统称。

1.2 各版本新增特性一览表(ES2015 → ES2022)

版本代表特性
ES2015 / ES6let / const、箭头函数、模板字符串、解构、默认参数、rest、扩展运算符、Class、模块化、Promise、Symbol、Set、Map、Iterator、Generator、Proxy、Reflect
ES2016指数运算符 **、Array.prototype.includes
ES2017async/await、Object.values/entries、padStart/padEnd、Object.getOwnPropertyDescriptors
ES2018Promise.prototype.finally、对象扩展运算符 {...obj}、异步迭代
ES2019flat/flatMap、Object.fromEntries、trimStart/trimEnd
ES2020可选链 ?.、空值合并 ??、BigInt、Promise.allSettled、globalThis、String.prototype.matchAll
ES2021逻辑赋值运算符 ??= / &&= / ||=、数字分隔符 1_000_000
ES2022Array.prototype.at、类私有字段 #field、顶层 await、静态块

面试频率:⭐⭐
快速带过即可,重点是知道版本关系与特性归属。


第二部分:变量声明——let、const 与 var 的全面对比

面试频率:⭐⭐⭐⭐⭐ 重点写

2.1 三种声明方式的核心差异

对比项varletconst
作用域全局 / 函数作用域块级作用域块级作用域
变量提升提升,初始化为 undefined提升,但存在 TDZ提升,但存在 TDZ
暂时性死区无有有
重复声明允许同一作用域不允许同一作用域不允许
必须初始化否否是
是否挂到全局对象浏览器全局 var 会挂到 window不会不会
可否修改可可绑定不可重新赋值

2.2 变量提升的本质:从执行上下文说起

很多资料只说“var 会提升,let/const 不会”,这个说法并不准确。更准确的说法是:

所有声明都会被提升,区别在于初始化时机和能否访问。

当 JavaScript 引擎进入一个执行上下文(全局或函数)时,会经历两个阶段:

  1. 创建阶段:扫描代码,把变量声明、函数声明登记到环境中
  2. 执行阶段:逐行执行代码

在创建阶段:

  • var 声明的变量被登记后立即初始化为 undefined
  • let/const 声明的变量被登记后不初始化,处于“未初始化”状态
  • 函数声明被完整提升,包括函数体

js

js

let/const 的变量其实也在环境里“存在”,只是不能访问,这个“存在但不可访问”的区间就叫 暂时性死区(TDZ)。

面试题:为什么要有 TDZ?

TDZ 是为了修复 var 的历史遗留问题。var 允许在声明前访问,得到 undefined,这会导致 bug 很难被发现:js

TDZ 让这种代码直接抛出 ReferenceError,提前暴露问题。

2.3 块级作用域的真实价值

块级作用域不只是“{} 内声明外面访问不到”,它解决了两个经典问题。

问题一:循环变量的闭包共享

js

var 是函数作用域,整个循环只有 一个 i,三个回调共享它,循环结束后 i 变成 3。js

let 在每次迭代时创建一个新的绑定,可以理解为:js

问题二:内层变量覆盖外层变量

js

因为 var name 提升到函数顶部,遮蔽了外层的 name。如果用 let:js

2.4 const 的“只读”本质

const 保证的是:

变量绑定指向的内存地址不可变,而不是值本身不可变。

js

从内存角度看:js

面试题:如何让对象真正不可变?

需要 Object.freeze(),但它是浅冻结:js

深冻结需要递归:js

2.5 全局 var 与全局 let/const 的区别

js

  • 全局 var 会挂到 window 对象上
  • 全局 let/const 存在于全局词法环境,不挂到 window

这在实际开发中有影响,比如:js

2.6 面试高频问题

面试题:for 循环中 let 到底创建了几个变量?

取决于循环的迭代次数。每次迭代都会创建一个新的块级绑定,但循环头部的声明只执行一次。

面试题:let 和 const 哪个性能更好?

在 V8 等现代引擎中,两者性能几乎相同。const 在语义上更明确,推荐优先使用,能避免意外重新赋值。

面试题:const 声明后可以修改属性吗?可以删除属性吗?

可以修改属性,也可以删除属性,只要不改变 const 变量本身的指向。js


第三部分:解构赋值

面试频率:⭐⭐⭐⭐

3.1 数组解构

js

面试题:为什么 [a, b] = [b, a] 能交换变量?

数组解构会先求值右侧,构造一个新数组 [b, a],然后依次赋值给左侧。等价于:js

3.2 对象解构

js

注意:对象解构赋值给已声明变量时,需要用括号包裹,否则 { 会被解析为块语句。js

面试题:解构赋值是浅拷贝还是深拷贝?

解构赋值对基本类型是值拷贝,对引用类型是引用拷贝,所以是浅拷贝。js

3.3 函数参数解构

js

面试题:为什么参数解构要写成 { a } = {}?

因为如果调用时不传参数,形参为 undefined,对 undefined 解构会报错:js

加上 = {} 后,不传参时形参默认是 {},解构成功,a 取默认值。

3.4 解构的常见陷阱

js


第四部分:函数扩展

面试频率:⭐⭐⭐⭐⭐ 重点写
相关阅读:箭头函数 this 指向可链接到《JS 中 this 指针问题》

4.1 箭头函数

js

箭头函数与普通函数的区别

对比项普通函数箭头函数
this调用时动态决定继承外层作用域
arguments有自己的 arguments没有,使用外层
构造函数可以 new不能 new
prototype有没有
yield可用于生成器不能作为生成器
super视上下文继承外层
new.target有无

this 的绑定规则回顾

普通函数的 this 有四条绑定规则,优先级从高到低:

  1. new 绑定:new Foo() 中 this 指向新对象
  2. 显式绑定:call / apply / bind
  3. 隐式绑定:obj.fn() 中 this 指向 obj
  4. 默认绑定:非严格模式指向 window,严格模式 undefined

箭头函数不适用这四条规则,它的 this 在定义时就确定了。js

面试题:箭头函数能否用 call/apply/bind 改变 this?

不能。call/apply/bind 对箭头函数的 this 无效。js

面试题:箭头函数为什么不能作为构造函数?

因为箭头函数没有 [[Construct]] 内部方法,也没有 prototype 属性。js

箭头函数不适用的场景

  1. 对象方法需要动态 this
  2. 构造函数
  3. 需要 arguments 的函数
  4. 需要 prototype 的函数
  5. Vue 选项式 API 的 methods(需要 this 指向组件实例)
  6. 需要 yield 的生成器函数

4.2 函数参数扩展

默认参数

js

默认参数是惰性求值的,每次调用都会重新计算。js

面试题:默认参数与 TDZ 的关系

默认参数会形成自己的作用域,参数之间可以相互引用,但后面的参数可以引用前面的参数,反之不行。js

js

rest 参数

js

rest 与 arguments 区别:

对比项argumentsrest
类型类数组真数组
能否使用数组方法不能直接使用可以
箭头函数中没有有,取决于外层
是否包含所有参数包含只包含未命名参数
是否可被解构不推荐可以

面试题:rest 参数和扩展运算符是同一个东西吗?

不是。它们语法相同(都是 ...),但使用场景相反:

  • rest 参数:用在函数定义中,把多个参数收集成数组
  • 扩展运算符:用在函数调用或数组/对象字面量中,把可迭代对象展开

js


第五部分:对象与数组扩展

面试频率:⭐⭐⭐⭐
相关阅读:数组扩展可链接到《数组去重的几种方法》扩展版

5.1 对象扩展

属性简写、方法简写、计算属性名

js

Object.assign()

js

特点:

  • 浅拷贝
  • 后者覆盖前者
  • 可拷贝 Symbol 属性
  • 不拷贝原型链属性
  • 不拷贝不可枚举属性

面试题:Object.assign 和扩展运算符 {...obj} 有什么区别?

两者都是浅拷贝,主要区别:

  1. Object.assign 会触发目标对象的 setter,扩展运算符不会
  2. Object.assign 可以合并多个源对象,扩展运算符通常用于字面量
  3. 扩展运算符对 null/undefined 更安全

js

Object.keys() / values() / entries()

js

注意:都不包含 Symbol 键,也不包含不可枚举属性。

Object.getOwnPropertyDescriptors()

js

常用于配合 Object.defineProperties 实现保留 getter/setter 的浅拷贝:js

5.2 数组扩展

Array.from() 与 Array.of()

js

面试题:Array.from 和扩展运算符 [...arr] 有什么区别?

  • Array.from 可以转换类数组对象(有 length 和索引)和可迭代对象
  • 扩展运算符只能用于可迭代对象

js

find() / findIndex()

js

与 filter 区别:find 返回第一个匹配元素,filter 返回所有匹配元素组成的数组。

includes() 与 indexOf 的区别

js

includes 使用 SameValueZero 比较,能识别 NaN。indexOf 使用严格相等,不能识别 NaN。

flat() / flatMap()

js

flatMap 等价于先 map 再 flat(1)。

面试题:手写一个 flat

js


第六部分:字符串与数值扩展

面试频率:⭐⭐⭐

6.1 字符串扩展

模板字符串

js

支持多行、变量嵌入、表达式嵌入。

标签模板

js

标签模板的典型应用:防止 XSS 攻击、国际化。js

常用方法

js

String.raw

js

6.2 数值扩展

js

面试题:0.1 + 0.2 为什么不等于 0.3?

浮点数在 IEEE 754 标准下无法精确表示 0.1 和 0.2,相加后产生误差。js


第七部分:新的数据结构——Symbol、Set、Map 及 Weak 变体

面试频率:⭐⭐⭐⭐⭐ 重点写

7.1 Symbol

Symbol 是原始数据类型,主要用途是解决属性名冲突。js

即使描述相同,每个 Symbol 也是唯一的。

Symbol 作为属性名

js

Symbol 属性不可枚举:js

面试题:Symbol 为什么不可枚举?

因为 Symbol 的设计目标就是作为“隐藏属性”或“元属性”,不希望被普通遍历逻辑意外访问,避免和业务属性冲突。

Symbol.for() 与全局注册表

js

Symbol.for 会在全局注册表中查找,没有就创建,有就返回同一个。

内置 Symbol

Symbol用途
Symbol.iterator定义默认迭代器
Symbol.asyncIterator定义异步迭代器
Symbol.toPrimitive定义对象转原始值行为
Symbol.toStringTag定义 Object.prototype.toString 返回值
Symbol.hasInstance定义 instanceof 行为

js

面试题:Symbol 能完全替代字符串作为属性名吗?

不能。Symbol 适合做“元属性”和“避免冲突的私有标记”,但序列化(JSON.stringify)、Object.keys、for...in 都不包含 Symbol,不适合做业务数据的普通键。

7.2 Set 与 WeakSet

js

Set 的去重原理

Set 内部使用 SameValueZero 算法比较值:

  • NaN 等于 NaN
  • +0 等于 -0
  • 对象比较引用

js

数组去重

js

面试题:Set 去重有什么局限?

  1. 不能对对象去重(引用不同)
  2. 不能按对象的某个属性去重,需要手动实现

js

WeakSet

  • 只能存对象引用
  • 弱引用,不影响垃圾回收
  • 不可遍历
  • 没有 size

js

典型应用:标记对象是否被处理过,而不阻止其被回收。

7.3 Map 与 WeakMap

js

Map 与普通对象对比

对比项ObjectMap
键类型字符串 / Symbol任意类型
顺序不完全可靠插入顺序
size手动计算map.size
频繁增删一般更优
默认原型有原型链干净
序列化直接支持 JSON需要手动转换

面试题:Map 的键为什么可以是对象?

Map 内部使用类似哈希表的结构,键的比较使用 SameValueZero,对象键通过引用比较。普通对象的键会被强制转成字符串,对象会变成 '[object Object]'。js

WeakMap

  • 键必须是对象
  • 弱引用
  • 不可遍历
  • 没有 size

js

面试题:WeakMap 的典型应用场景

  1. 私有数据存储:Class 外部无法访问

js

  1. DOM 节点关联数据:节点被删除后数据自动回收
  2. 缓存:避免缓存对象导致内存泄漏

7.4 四者对比总结表

数据结构是否可遍历键类型限制引用强度使用场景
Set是任意值强引用去重、集合运算
WeakSet否对象弱引用临时标记对象
Map是任意值强引用复杂键值对
WeakMap否对象弱引用私有数据、缓存

面试题:WeakMap 为什么不可遍历?

因为它是弱引用,键可能随时被垃圾回收,遍历结果不确定。如果允许遍历,可能导致不可预期的行为。


第八部分:迭代器与生成器

面试频率:⭐⭐⭐⭐

8.1 Iterator 迭代器

可迭代协议:对象必须实现 Symbol.iterator 方法,返回一个迭代器。js

手动实现一个可迭代对象

js

for...of 与 for...in 区别

对比项for...offor...in
遍历内容可迭代对象的值可枚举属性名
是否包含原型链否是
适合数组是否
适合对象默认不可迭代是
能否中断支持 break/continue支持 break/continue

面试题:为什么 for...in 不适合遍历数组?

  1. 会遍历原型链上的可枚举属性
  2. 遍历顺序不保证
  3. 索引是字符串类型,不是数字

js

8.2 Generator 生成器

js

yield 的暂停与恢复

Generator 函数执行到 yield 会暂停,调用 next() 恢复。js

面试题:第一次 next() 传参为什么无效?

因为第一次 next() 是启动生成器,执行到第一个 yield 暂停。此时还没有 yield 表达式在等待接收值,所以传参被忽略。

yield* 委托

js

Generator 与异步编程

Generator + Promise + 执行器是 async/await 的前身。js

典型应用场景

  • 状态管理
  • 惰性求值
  • 异步流程控制
  • 无限序列

js


第九部分:类(Class)与继承

面试频率:⭐⭐⭐⭐
相关阅读:ES5 继承对比链接到《JS 面向对象》

9.1 Class 基础语法

js

私有字段

js

面试题:Class 和 ES5 构造函数有什么区别?

对比项ES5 构造函数Class
调用方式可以不用 new必须用 new
严格模式非严格内部默认严格模式
方法可枚举是否
变量提升函数声明提升不提升到可调用
继承手动设置原型链extends / super

9.2 Class 继承

js

面试题:为什么子类 constructor 中必须先调用 super()?

因为子类的 this 是由父类构造函数创建的。在 super() 之前,子类没有自己的 this,访问 this 会报错。js

面试题:super 的本质是什么?

super 在构造函数中指向父类构造函数([[Prototype]] 的 constructor),在方法中指向父类的原型对象。js

静态方法的继承

js

因为 Dog.__proto__ === Animal,所以静态方法也被继承。

面试题:Class 继承的本质是什么?

仍然是原型链。extends 做了两件事:

  1. Dog.prototype.__proto__ = Animal.prototype(实例方法继承)
  2. Dog.__proto__ = Animal(静态方法继承)

第十部分:模块化

面试频率:⭐⭐⭐⭐
相关阅读:可与《跨域的几种方法》交叉链接;若内容过多可独立成篇

10.1 ES Module 基础

js

重命名:js

聚合导出:js

10.2 动态导入

js

条件加载、懒加载:js

并行加载:js

10.3 ES Module 与 CommonJS 的核心差异

对比项ES ModuleCommonJS
加载时机静态编译时运行时
导出值值引用 / 活绑定值拷贝
this顶层 undefinedmodule.exports
循环依赖活绑定,处理更好可能拿到不完整导出
使用场景浏览器 / 现代构建Node 传统模块

面试题:ESM 的值引用是什么意思?

js

CommonJS 中:js

面试题:ESM 的 import 变量能修改吗?

不能。导入绑定是只读的,但如果是对象,可以修改对象内部属性。js


第十一部分:异步编程

面试频率:⭐⭐⭐⭐⭐ 重点写

11.1 Promise 基础回顾

Promise 三种状态:

  • pending
  • fulfilled
  • rejected

状态一旦改变不可逆。js

链式调用本质:每次 then 返回一个新的 Promise。

面试题:then 中返回值和抛出错误的处理

js

  • then 中返回普通值,会被包装成 resolved Promise
  • then 中抛出错误,会进入下一个 catch
  • catch 后如果没有再抛错,后续 then 会继续执行

相关阅读:详细实现见《手写 Promise A+ 规范的 Promise 代码》,此处只做使用层面快速回顾并链接。

11.2 Promise 静态方法

方法行为成功条件失败条件
Promise.all全部成功才成功所有 fulfilled任意一个 rejected
Promise.race第一个 settled第一个 fulfilled第一个 rejected
Promise.allSettled等待所有完成永远 fulfilled不会 reject
Promise.any第一个成功任意一个 fulfilled全部 rejected,抛 AggregateError

js

面试题:Promise.all 中一个失败,其他 Promise 还会执行吗?

会。Promise.all 只是控制返回的 Promise 状态,不会取消其他 Promise。其他 Promise 仍然会继续执行,只是结果被忽略。

面试题:Promise.allSettled 的返回值格式

js

手写实现放《手写题合集》。

11.3 async / await

js

要点:

  • async 函数返回值始终是 Promise
  • await 后面跟非 Promise 值,会被包装成 Promise
  • 错误处理可用 try/catch 或 .catch()
  • 并发优化:Promise.all + await 替代串行 await

面试题:async 函数返回 Promise,那 return 值怎么处理?

js

等价于:js

如果 async 函数中抛出错误,返回的 Promise 会变成 rejected:js

面试题:await 后面跟非 Promise 值会怎样?

会被 Promise.resolve() 包装,但仍然会进入微任务队列。js

并发优化

串行:js

并发:js

11.4 async/await 的执行顺序

async/await 底层是 Promise 的语法糖,执行顺序详见《JS 事件循环机制》的扩展章节。

示例:js

执行过程:

  1. 同步执行 console.log('script start')
  2. 调用 async1(),同步执行到 await async2()
  3. 执行 async2(),输出 async2,返回 resolved Promise
  4. await 后面的代码 console.log('async1 end') 进入微任务队列
  5. 同步执行 console.log('script end')
  6. 微任务队列执行,输出 async1 end

面试题:await 后面的代码什么时候执行?

进入微任务队列,在当前同步代码执行完后、下一个宏任务之前执行。


第十二部分:Proxy 与 Reflect

面试频率:⭐⭐⭐

12.1 Proxy

js

常用陷阱:

  • get
  • set
  • has
  • deleteProperty
  • apply
  • construct
  • ownKeys

应用场景

  1. 数据校验

js

  1. 日志记录
  2. 响应式系统
  3. Vue 3 响应式原理

12.2 Reflect

Reflect 让 Object 操作变成函数行为。js

与 Proxy 配合

js

面试题:Proxy 和 Object.defineProperty 的区别?

对比项ProxyObject.defineProperty
代理范围整个对象单个属性
新增属性可拦截需要手动处理
数组可拦截需要重写方法
拦截操作13 种有限
兼容性ES6+ES5

Vue 2 用 defineProperty,Vue 3 用 Proxy。

面试题:Proxy 的 receiver 是什么?

receiver 是代理对象或继承代理对象的对象。在 Reflect.get 中传入 receiver 可以保证 getter 中的 this 指向正确。js


第十三部分:ES6+ 后续版本补充特性

面试频率:⭐⭐⭐⭐
可选链 ?. 和空值合并 ?? 建议单独展开讲与 || 的区别。

特性版本简要说明
** 指数运算符ES20162 ** 3 = 8
Array.prototype.includesES2016替代 indexOf 判断包含
Object.values() / entries()ES2017对象遍历新方法
padStart() / padEnd()ES2017字符串填充
Object.getOwnPropertyDescriptors()ES2017获取完整属性描述符
async/awaitES2017异步编程终极方案
Promise.prototype.finallyES2018无论成功失败都执行
对象扩展运算符 { ...obj }ES2018对象浅拷贝 / 合并
flat() / flatMap()ES2019数组扁平化
Object.fromEntries()ES2019键值对数组转对象
可选链 ?.ES2020安全访问嵌套属性
空值合并 ??ES2020仅对 null/undefined 取默认值
BigIntES2020大整数类型
Promise.allSettled()ES2020等待所有 Promise 完成
globalThisES2020统一全局对象引用
String.prototype.matchAllES2020全局匹配所有结果
逻辑赋值运算符 ??= / &&= / ||=ES2021逻辑运算与赋值结合
数字分隔符 1_000_000ES2021提高数值可读性
Array.prototype.at()ES2022支持负索引
类私有字段 #fieldES2022真正的私有属性
顶层 awaitES2022模块顶层直接使用 await

可选链 ?.

js

空值合并 ??

js

区别:

  • || 对 0、''、false、NaN 也会取默认值
  • ?? 只对 null 和 undefined 取默认值

面试题:?. 和 && 的区别?

js

?. 更精确,不会因为假值误判。

面试题:BigInt 有什么用途?

用于表示超过 Number.MAX_SAFE_INTEGER 的整数。js

面试题:globalThis 解决了什么问题?

不同环境全局对象不同:

  • 浏览器:window
  • Node:global
  • Web Worker:self

globalThis 统一了引用方式。js


复习优先级建议

🔴 第一批

第二、四、七、十一部分:

  • let/const + TDZ
  • 箭头函数 this
  • Map / Set / WeakMap
  • Promise 静态方法 + async/await 执行顺序

🟡 第二批

第三、五、八、九部分:

  • 解构
  • 对象 / 数组扩展
  • Generator
  • Class

🟢 第三批

第六、十、十二、十三部分:

  • 字符串 / 数值扩展
  • 模块化
  • Proxy / Reflect
  • 后续版本补充

与旧文衔接汇总

  • 第十一部分 Promise 基础:只做使用层面快速回顾,标注“手写实现见《手写 Promise A+ 规范的 Promise 代码》”
  • 第十一部分 async/await 执行顺序:标注“详见《JS 事件循环机制》的扩展章节”
  • 第九部分 ES5 继承对比:链接到《JS 面向对象》
  • 第五部分数组方法:链接到《数组去重的几种方法》的扩展版
  • 第四部分箭头函数 this 指向:链接到《JS 中 this 指针问题》
  • 第十部分模块化:可与《跨域的几种方法》交叉链接,若内容过多可独立成篇

结语

ES6+ 不是零散语法点的堆砌,而是一套现代 JavaScript 工程体系。面试复习时建议按“变量 → 函数 → 数据结构 → 异步 → 元编程 → 模块化”的主线串联,再用后续版本特性补充细节。本文可作为索引,遇到具体实现时再跳转到对应旧文深入。

野生小园猿
励志做一只遨游在知识海洋里的小白鲨
查看“野生小园猿”的所有文章 →

发表评论

邮箱地址不会被公开。

相关推荐