ES6、ES7、ES8 特性一锅炖(更新版)
Category(分类): JavaScript Status: 已更新
原文作者:CrazyCodeBoy 历史文章:ES6、ES7、ES8 特性一锅炖
原文发布时,ES2015、ES2016、ES2017 经常被称作 ES6、ES7、ES8。本文尽量保留原文的版本路线和示例,但修复代码抓取造成的注释粘连、变量名错误、Promise 时序错误、
Object.values/entries对象名错误以及错误处理不完整等问题,并补充今天的标准状态。装饰器、管道操作符等实验性语法不会再冒充 ES6/ES8 正式特性。
一、ECMAScript 版本概览
ECMAScript 是由 Ecma International 标准化的脚本语言规范;JavaScript 是 ECMAScript 的一种实现和宿主生态。现在的规范按年份发布,不再把“最新版本”长期停留在 ES6、ES7 或 ES8。
| 版本 | 发布时间 | 代表性内容 |
|---|---|---|
| ECMAScript 5 | 2009 | 严格模式、JSON、数组方法、属性描述符等 |
| ECMAScript 2015(ES6) | 2015 | let/const、类、模块、箭头函数、模板字符串、解构、迭代器、生成器、Promise、Map/Set、Symbol |
| ECMAScript 2016(ES7) | 2016 | Array.prototype.includes()、指数运算符 ** |
| ECMAScript 2017(ES8) | 2017 | async/await、Object.values()、Object.entries()、padStart/padEnd、尾逗号、属性描述符 API |
| ECMAScript 2018 及以后 | 2018— | 对象展开、异步迭代、可选 catch 绑定、可选链、空值合并、逻辑赋值等按年份持续加入 |
“ES6”可以作为 ES2015 的历史简称,但不要把后续所有语法都统称为 ES6。能否使用某个特性,应查看目标浏览器/Node.js 版本和构建策略。
二、ES6(ECMAScript 2015)
ES2015 的特性非常多,下面保留原文常用的几类:
- 类(class)和继承;
- 模块(
export/import); - 箭头函数;
- 函数参数默认值;
- 模板字符串;
- 解构赋值;
- 展开语法;
- 对象属性简写;
- Promise;
let与const;- Symbol、Map、Set、Iterator、Generator。
1. 类(class)
class 提供了更清晰的构造器、原型方法和继承语法。它不是传统构造函数的纯文本替换:类体默认是严格模式,类方法不可枚举,类构造器必须使用 new 调用。
class Animal {
constructor(name, color) {
this.name = name
this.color = color
}
toString() {
return `name: ${this.name}, color: ${this.color}`
}
}
const animal = new Animal('dog', 'white')
console.log(animal.toString())
console.log(Object.hasOwn(animal, 'name')) // true
console.log(Object.hasOwn(animal, 'toString')) // false,方法在原型上
子类构造器必须先调用 super(),之后才能访问子类构造器中的 this:
class Cat extends Animal {
constructor(action) {
super('cat', 'white')
this.action = action
}
toString() {
return `${super.toString()}, action: ${this.action}`
}
}
const cat = new Cat('catch')
console.log(cat instanceof Cat) // true
console.log(cat instanceof Animal) // true
console.log(cat.toString())
如果子类没有写构造器,JavaScript 会提供一个默认构造器并转发参数;如果写了构造器,就必须在使用 this 前调用 super()。现代 JavaScript 还支持私有字段、公共类字段和静态字段:
class Counter {
#value = 0
static description = 'counter'
increment() {
this.#value += 1
return this.#value
}
}
2. 模块化(Module)
ES2015 引入原生模块。一个模块文件拥有自己的顶层作用域,通过 export 暴露接口,通过 import 引入依赖:
// test.js
export const name = 'Rainbow'
export const age = 24
export function greet() {
return `Hello ${name}`
}
// main.js
import { name, age, greet } from './test.js'
console.log(name, age, greet())
也可以使用默认导出:
// math.js
export default function add(a, b) {
return a + b
}
export const version = '1.0.0'
import add, { version as mathVersion } from './math.js'
浏览器中需要使用模块脚本:
<script type="module" src="main.js"></script>
模块脚本默认具有类似 defer 的加载行为,并且跨源模块请求需要满足 CORS。Node.js 则需要依据 package.json 的 type、.mjs/.cjs 扩展名和运行参数选择模块系统。
3. 箭头函数(Arrow function)
箭头函数可以省略 function 和部分括号:
const add = (a, b) => a + b
const identity = value => value
const run = () => {
console.log('run')
}
表达式函数体会隐式返回值;块函数体必须显式 return:
const getObject = id => ({ id, name: 'Temp' })
const getObjectWrong = id => {
id: id
}
console.log(getObjectWrong(1)) // undefined
箭头函数没有自己的 this、arguments 和 prototype,不能用于构造函数。它会捕获定义位置外层的 this,因此适合回调,但不适合需要动态 this 的对象方法。对象字面量本身不会创建新的 this 环境。
监听器解绑陷阱
bind 每调用一次都会返回新的函数引用,绑定和解绑必须保存同一个引用:
class PauseMenu {
constructor(appState) {
this.appState = appState
this.onAppPaused = this.onAppPaused.bind(this)
}
mount() {
this.appState.addEventListener('change', this.onAppPaused)
}
unmount() {
this.appState.removeEventListener('change', this.onAppPaused)
}
onAppPaused(event) {
console.log(event)
}
}
原文中的 componentWillMount 是旧版 React 生命周期,已经废弃;在 React Native 或其他框架中应使用当前版本的挂载/卸载 API。类字段箭头函数也是一种现代绑定方式,但会为每个实例创建函数:
class PauseMenu {
onAppPaused = event => {
console.log(event)
}
}
4. 函数参数默认值
默认参数只在实参为 undefined 时生效:
function foo(height = 50, color = 'red') {
return { height, color }
}
console.log(foo(0, '')) // { height: 0, color: '' }
旧式的 height || 50 会把 0 当成没有值:
function oldFoo(height, color) {
height = height || 50
color = color || 'red'
return { height, color }
}
console.log(oldFoo(0, '')) // { height: 50, color: 'red' }
如果默认条件是 null 或 undefined,可以使用空值合并:
function modernFoo(height, color) {
return {
height: height ?? 50,
color: color ?? 'red'
}
}
5. 模板字符串
const first = 'Your'
const last = 'name'
const message = `Your name is ${first} ${last}.`
模板字符串支持多行和表达式,但不会自动转义 HTML、SQL 或 URL 中的特殊字符。处理用户输入时要使用正确的上下文转义策略。
6. 解构赋值
数组解构按位置取值:
const values = ['one', 'two', 'three', 'four']
const [one, two, three] = values
const [first, , , last] = values
let a, b
;[a, b] = [1, 2]
;[a, b] = [b, a]
缺失值可以使用默认值:
const [x = 5, y = 7] = [1]
console.log(x, y) // 1 7
对象解构按属性名取值,也可以重命名:
const student = {
name: 'Ming',
age: 18,
city: 'Shanghai'
}
const { name, age, city } = student
const { name: studentName } = student
深度解构时要让默认值保持对象结构:
const data = { user: { profile: { name: 'Ming' } } }
const {
user: {
profile: { name = '匿名' } = {}
} = {}
} = data
7. 展开语法(Spread syntax)
在函数调用和数组构造中,展开语法需要可迭代对象:
function sum(x, y, z) {
return x + y + z
}
const numbers = [1, 2, 3]
console.log(sum(...numbers)) // 6
const students = ['Jine', 'Tom']
const people = ['Tony', ...students, 'Aaron']
数组展开是浅拷贝:
const source = [{ id: 1 }]
const copy = [...source]
copy[0].id = 2
console.log(source[0].id) // 2
对象展开直到 ES2018 才标准化:
const obj1 = { foo: 'bar', x: 42 }
const obj2 = { foo: 'baz', y: 13 }
const clonedObj = { ...obj1 }
const mergedObj = { ...obj1, ...obj2 }
console.log(mergedObj) // { foo: 'baz', x: 42, y: 13 }
对象展开复制自身可枚举属性,后面的属性覆盖前面的同名属性,不会深拷贝,也不会复制原型和不可枚举属性。
在 React JSX 中可以把对象属性展开为 props:
const params = { name: 'Jine', age: 21 }
<CustomComponent {...params} />
不要把含有不应暴露的内部字段或事件的对象无选择地全部透传给 DOM。
8. 对象属性简写
const name = 'Ming'
const age = 18
const city = 'Shanghai'
const student = { name, age, city }
对象还支持方法简写和计算属性名:
const key = 'name'
const object = {
[key]: 'Ming',
greet() {
return `Hello ${this[key]}`
}
}
9. Promise
Promise 通过状态和链式 API 组合异步操作:
const wait = ms => new Promise(resolve => {
setTimeout(resolve, ms)
})
wait(1000)
.then(() => {
console.log('Hello')
return wait(1000)
})
.then(() => {
console.log('Hi')
})
.catch(error => {
console.error(error)
})
原文重复使用同一个 waitSecond Promise,第二个 then 并不会再次等待一秒;Promise 只能结算一次。要串行等待两个阶段,应像上面一样在回调中返回新的 Promise。
Promise 的 then 回调总是异步执行;返回值会传给下一个 then,返回 Promise 会等待其结算。Promise 本身没有通用取消协议,网络请求通常要配合 AbortController。
10. let 与 const
var 是函数作用域:
{
var a = 10
}
console.log(a) // 10
let 和 const 是块级作用域,并且声明前处于暂时性死区:
{
let b = 10
const c = 20
}
// console.log(b) // ReferenceError
// console.log(c) // ReferenceError
三、ES7(ECMAScript 2016)
ES2016 的特性数量较少,主要包括 includes() 和指数运算符。
1. Array.prototype.includes()
如果只需要判断数组是否包含某个值,includes 比 indexOf(...) !== -1 更直接:
const frameworks = ['react', 'angular', 'vue']
console.log(frameworks.includes('react')) // true
console.log([NaN].includes(NaN)) // true
它与 indexOf 的区别是:indexOf 返回位置并且找不到 NaN,includes 返回布尔值并使用 SameValueZero。
2. 指数运算符 **
console.log(2 ** 10) // 1024
console.log(Math.pow(2, 10)) // 1024
指数运算符是右结合的:
console.log(2 ** 3 ** 2) // 512,相当于 2 ** (3 ** 2)
负数底数需要括号:
console.log((-2) ** 2) // 4
// -2 ** 2 // SyntaxError
四、ES8(ECMAScript 2017)
ES2017 的代表性内容包括:
async/await;Object.values();Object.entries();String.prototype.padStart()与padEnd();- 函数参数和调用参数允许尾逗号;
Object.getOwnPropertyDescriptors()。
1. async/await
async 函数总是返回 Promise,await 只暂停当前异步函数,不会阻塞整个 JavaScript 执行环境:
function login(userName) {
return new Promise(resolve => {
setTimeout(() => resolve({ id: '1001', userName }), 600)
})
}
function getData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === '1001') resolve('Success')
else reject(new Error('Unknown user'))
}, 600)
})
}
function doLogin(userName) {
return login(userName)
.then(user => getData(user.id))
}
async function doLoginWithAwait(userName) {
const user = await login(userName)
return getData(user.id)
}
doLogin('Alice').then(console.log)
doLoginWithAwait('Alice').then(console.log)
错误应使用 try/catch 或在调用处处理拒绝:
async function safeLogin(userName) {
try {
const user = await login(userName)
return await getData(user.id)
} catch (error) {
console.error(error)
throw error
}
}
并发场景
两个请求互不依赖时,连续写两个 await 会串行等待:
async function charCountAddSequential(data1, data2) {
const first = await charCount(data1)
const second = await charCount(data2)
return first + second
}
可以先启动两个请求,再等待它们:
async function charCountAdd(data1, data2) {
const [first, second] = await Promise.all([
charCount(data1),
charCount(data2)
])
return first + second
}
function charCount(data) {
return new Promise(resolve => {
setTimeout(() => resolve(data.length), 1000)
})
}
Promise.all 的“并发”是指多个异步操作在时间上同时开始,不是创建多个 JavaScript 主线程。它在一个输入拒绝时立即拒绝结果 Promise,但不会自动取消其他操作。
错误处理方式
捕获整个异步函数的错误:
charCountAdd('Hello', 'Hi')
.then(console.log)
.catch(console.error)
也可以在函数内部对不同步骤分别处理:
async function charCountAddWithFallback(data1, data2) {
let first = 0
let second = 0
try {
first = await charCount(data1)
} catch (error) {
console.error('first failed', error)
}
try {
second = await charCount(data2)
} catch (error) {
console.error('second failed', error)
}
return first + second
}
不要为了继续执行而无条件吞掉错误;对于必须同时成功的任务,应让错误继续向上冒泡,或使用 Promise.allSettled() 明确记录每项结果。
2. Object.values()
Object.values() 返回对象自身可枚举字符串键的值,不包含原型链属性:
const object = { a: 1, b: 2, c: 3 }
console.log(Object.values(object)) // [1, 2, 3]
原来的 Object.values(obj1) 是变量名错误,应使用已经定义的 object。
它只处理对象自身可枚举属性,Symbol 键不会出现在结果中。Object.values(null) 和 Object.values(undefined) 会抛出 TypeError。
3. Object.entries()
Object.entries() 返回自身可枚举字符串键值对数组:
for (const [key, value] of Object.entries(object)) {
console.log(`key: ${key}, value: ${value}`)
}
如果需要把键值对转换回普通对象,可以使用 Object.fromEntries()(ES2019):
const doubled = Object.fromEntries(
Object.entries(object).map(([key, value]) => [key, value * 2])
)
console.log(doubled) // { a: 2, b: 4, c: 6 }
4. 字符串填充
ES2017 增加 padStart 和 padEnd:
console.log('0.0'.padStart(4, '10')) // '10.0'
console.log('0.0'.padStart(8)) // ' 0.0'
console.log('0.0'.padEnd(4, '0')) // '0.00'
console.log('0.0'.padEnd(10, '0')) // '0.00000000'
targetLength 按 JavaScript 字符串长度计算;填充字符串过长时会被截断。它不会按显示宽度处理中文、emoji 或复杂 grapheme cluster。
5. 函数参数列表尾逗号
ES2017 允许函数参数列表和调用参数列表最后保留逗号,便于多人协作和添加参数:
function createUser(
name,
age,
) {
return { name, age }
}
const user = createUser(
'Alice',
18,
)
尾逗号只是语法便利,不会改变函数调用语义,也不代表函数多接收了一个参数。
6. Object.getOwnPropertyDescriptors()
属性描述符包含 value/writable 或 get/set,以及 enumerable、configurable:
const object = {
name: 'Jine',
get age() {
return 18
}
}
console.log(Object.getOwnPropertyDescriptors(object))
它常用于保留 getter、setter、可写性等元数据地复制对象:
const clone = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(object)
)
console.log(clone.age) // 18
这与 { ...object } 不同:对象展开会读取 getter 的结果并创建普通数据属性,而描述符复制可以保留 accessor。两种方式都只处理自身属性,不会自动复制原型。
五、原文之外的现代补充
1. 可选链和空值合并
const name = user?.profile?.name ?? '匿名'
可选链是 ES2020,空值合并也是 ES2020;它们不是 ES8。?? 只把 null/undefined 当作缺失值,不会把 0、false、'' 替换掉。
2. 逻辑赋值
config.timeout ??= 3000
options.debug ||= false
||=、&&=、??= 在 ES2021 标准化。选择哪一个取决于业务想判断所有 falsy 值,还是只判断 nullish 值。
3. 装饰器和管道操作符
原文时代常通过 Babel 插件尝试使用:
@log
class Service {}
value |> double |> increment
这两种语法都不应放进“ES6/ES8 已支持”的列表。装饰器和管道操作符仍需查看 TC39 当前提案阶段及项目工具链;不同插件的语法和语义可能不同,未配置转译器时不能直接运行。
六、总结
ES6/ES7/ES8 是理解现代 JavaScript 的重要历史节点,但不是今天唯一的版本概念。阅读旧文章时应特别留意:
- 把正确的旧知识保留为历史背景;
- 明确标注特性的 ECMAScript 年份;
- 区分正式标准、浏览器/Node.js 实现和 Babel/TypeScript 提案;
- 不要把 Promise 的“异步”误解为多线程;
- 不要把默认参数、
||、??和逻辑赋值的空值规则混为一谈; - 使用运行时兼容性数据和构建目标决定是否需要转译或 polyfill。
参考资料
- ECMAScript Language Specification
- ECMAScript 2015(第 6 版)
- ECMAScript 2016(第 7 版)
- ECMAScript 2017(第 8 版)
- MDN:JavaScript Guide
- MDN:Using promises
- MDN:Async function
- MDN:
Object.values() - MDN:
Object.entries() - MDN:
Object.getOwnPropertyDescriptors() - MDN:
Array.prototype.includes() - TC39 Decorators proposal
- TC39 Pipeline operator proposal