
面试前翻 ES6,最容易踩的坑不是「不会写」,而是记混了年份和归属。有人把 Object.values() 说成 ES6 新增,有人以为 includes() 是数组和字符串一起加的,还有人把 async/await 归到 ES2015 里去。这些细节平时不影响写代码,被追问一句「这个是哪一版进的标准」就容易卡壳。
这篇是我按年份重新捋的一份复习笔记,从 ES2015 一路排到 ES2019,外加当年还在提案阶段的一批语法。每一节不只列 API 名字,还会补上它在解决什么问题、什么场景真用得上、边界条件在哪。读完你至少能把「ES6 到底包含哪些东西」这件事讲清楚,面试被追问细节时心里有底。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- ES6 和 ES2015 到底是什么关系,为什么不该再叫 ES7、ES8
- ES2015 的全量特性,声明、解构、字符串、数值、对象、数组、函数、正则、Symbol、Set/Map、Proxy/Reflect、Class、Module、Iterator、Promise、Generator
- ES2016 到 ES2019 这四年每年加了什么,为什么后面几年的更新越来越小
- 当年还在提案里的
?.、??、BigInt、私有属性、顶层 await,现在都跑到哪一步了 - 每个特性的重点难点和踩坑清单,可以直接当面试自查表用
# 一、前言
ES6是ECMA为JavaScript制定的第6个标准版本- 标准委员会最终决定,标准在每年6月正式发布并作为当年的正式版本,接下来的时间里就在此版本的基础上进行改动,直到下一年6月草案就自然变成新一年的版本,这样一来就无需以前的版本号,只要用年份标记即可。
ECMAscript 2015是在2015年6月发布ES6的第一个版本。以此类推,ECMAscript2016是ES6的第二个版本、 ECMAscript 2017是ES6的第三个版本。ES6既是一个历史名词也是一个泛指,含义是5.1版本以后的JavaScript下一代标准,目前涵盖了ES2015、ES2016、ES2017、ES2018、ES2019
所以有些文章上提到的ES7(实质上是ES2016)、ES8(实质上是ES2017)、ES9(实质上是ES2018)、ES10(实质上是ES2019),实质上都是一些不规范的概念。从ES1到ES6,每个标准都是花了好几年甚至十多年才制定下来,你一个ES6到ES7,ES7到ES8,才用了一年,按照这样的定义下去,那不是很快就ES20了。用正确的概念来说ES6目前涵盖了ES2015、ES2016、ES2017、ES2018、ES2019
这里补一句我自己的理解。年份命名不只是换个叫法,它背后是 TC39 把「攒大版本」改成了「小步快跑」。一个提案只要走完 Stage 0 到 Stage 4 五个阶段,就直接搭上最近一班年度快车,不用等整个大版本谈拢。所以你会看到 ES2015 一口气塞了几十个特性,后面几年每年只有三五条,这不是标准停滞了,恰恰是流程顺了。
下面这张图是版本演进的整体脉络,可以先扫一眼建立坐标系,后面每一节都能对回这张图上。

顺着上面聊,日常交流里说「ES6」通常泛指 ES5.1 之后的一整套新语法,说「ES2015」才是精确指某一年的标准。写简历和答面试题的时候建议用年份,不容易被挑刺。
# 二、ES2015
ES2015 是这套标准里最厚的一版,也是绝大多数人口中「ES6」真正指的那一版。它一次性补齐了块级作用域、模块系统、类语法、迭代协议、异步原语这几块 JavaScript 缺了十几年的地基。后面 ES2016 到 ES2019 加的东西,基本都是在这批地基上打补丁。
所以这一节篇幅最长,建议按图里的分类逐块过。

# 声明
先看这块。var 最让人头疼的地方有两个,一是没有块级作用域,for 循环里声明的变量会漏到外面;二是变量提升,声明还没执行就能访问到 undefined。let 和 const 就是冲着这两个问题来的。
const命令:声明常量let命令:声明变量
作用
作用域
- 全局作用域
- 函数作用域:
function() {} - 块级作用域:
{}
作用范围
var命令在全局代码中执行const命令和let命令只能在代码块中执行
赋值使用
const命令声明常量后必须立马赋值let命令声明变量后可立马赋值或使用时赋值
声明方法:
var、const、let、function、class、import
重点难点
- 不允许重复声明
- 未定义就使用会报错:
const命令和let命令不存在变量提升 - 暂时性死区:在代码块内使用let命令声明变量之前,该变量都不可用
暂时性死区这块很多人记成了「let 没有提升」。严格说变量还是被提升到了块顶部,只是在执行到声明语句之前它处于未初始化状态,访问就抛 ReferenceError。这个区别在面试里会被追问,答「没有提升」容易被反问一句「那为什么 typeof 也报错」。
再补一个 const 的老误会。const 锁的是绑定不是值,const arr = [] 之后 arr.push(1) 完全合法,只有 arr = [] 才报错。想让对象内容也不可改,得配合 Object.freeze(),而且它只冻一层。
# 解构赋值
解构解决的是「从一坨数据里挑几个字段出来」这件事。以前得写 const name = res.data.user.name 一行一行摘,现在一行搞定,函数入参也能顺手带默认值。这套语法在处理接口返回和写工具函数时使用频率极高。
- 字符串解构:
const [a, b, c, d, e] = "hello" - 数值解构:
const { toString: s } = 123 - 布尔值解构:
const { toString: b } = true
对象解构
- 形式:
const { x, y } = { x: 1, y: 2 } - 默认:
const { x, y = 2 } = { x: 1 } - 改名:
const { x, y: z } = { x: 1, y: 2 }
数组解构
- 规则:数据结构具有
Iterator接口可采用数组形式的解构赋值 - 形式:
const [x, y] = [1, 2] - 默认:
const [x, y = 2] = [1]
函数参数解构
- 数组解构:
function Func([x = 0, y = 1]) {} - 对象解构:
function Func({ x = 0, y = 1 } = {}) {}
应用场景
- 交换变量值:
[x, y] = [y, x] - 返回函数多个值:
const [x, y, z] = Func() - 定义函数参数:
Func([1, 2]) - 提取JSON数据:
const { name, version } = packageJson - 定义函数参数默认值:
function Func({ x = 1, y = 2 } = {}) {} - 遍历Map结构:
for (let [k, v] of Map) {} - 输入模块指定属性和方法:
const { readFile, writeFile } = require("fs")
重点难点
- 匹配模式:只要等号两边的模式相同,左边的变量就会被赋予对应的值
- 解构赋值规则:只要等号右边的值不是对象或数组,就先将其转为对象
- 解构默认值生效条件:属性值严格等于undefined
- 解构遵循匹配模式
- 解构不成功时变量的值等于
undefined undefined和null无法转为对象,因此无法进行解构
第三条那个「严格等于 undefined」是真踩过的坑。接口返回 { count: null },你写 const { count = 0 } = res,拿到的还是 null 不是 0,因为默认值只在属性值严格等于 undefined 时才生效。后来 ES2020 补的 ?? 就是专门收拾 null 这种情况的,这个后面提案那一节还会说。
还有个写法值得单拎出来:function Func({ x = 1, y = 2 } = {}) 里那个外层 = {} 千万别省。省了之后调用 Func() 不传参,解构 undefined 直接报错。
# 字符串扩展
字符串这块的改动可以分两拨看。一拨是模板字符串和标签模板,解决拼接可读性;另一拨是一堆 Unicode 相关方法,解决 JavaScript 早年只按 16 位处理字符、遇到 emoji 和生僻字就算错长度的老问题。
Unicode表示法:大括号包含表示Unicode字符(\u{0xXX}或\u{0XXX})- 字符串遍历:可通过for-of遍历字符串
- 字符串模板:可单行可多行可插入变量的增强版字符串
- 标签模板:函数参数的特殊调用
String.raw():返回把字符串所有变量替换且对斜杠进行转义的结果String.fromCodePoint():返回码点对应字符codePointAt():返回字符对应码点(String.fromCodePoint()的逆操作)normalize():把字符的不同表示方法统一为同样形式,返回新字符串(Unicode正规化)repeat():把字符串重复n次,返回新字符串matchAll():返回正则表达式在字符串的所有匹配includes():是否存在指定字符串startsWith():是否存在字符串头部指定字符串endsWith():是否存在字符串尾部指定字符串
重点难点
以上扩展方法均可作用于由4个字节储存的
Unicode字符上
这句是整节的关键。"👍".length 返回 2 而不是 1,因为它由两个代理对码元组成;用 for-of 遍历或者 [..."👍"] 展开,拿到的才是一个完整字符。做输入框字数统计、做昵称截断的时候,这个差别直接决定了会不会把 emoji 劈成两半变乱码。这个我踩过,当时截出来的半个字符在 iOS 上显示成方块。
再纠正一处归属。上面列表里的 matchAll() 严格说不属于 ES2015,它是后来才进标准的方法,很多教材按主题把它并到「字符串扩展」一章里,容易让人以为是同一批。具体进标准的年份以 MDN 为准,面试答「后来补的」比乱报年份安全。
# 数值扩展
数值这块新增的东西多,但日常真正高频的就三四个。二进制和八进制字面量是给位运算和权限掩码用的;Number.isNaN() 和 Number.isInteger() 是为了绕开全局 isNaN() 会先做类型转换的坑;Number.EPSILON 则是浮点数比较的标准解法。