技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

ES6、ES7、ES8 特性一锅炖(更新版)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/18-ES6、ES7、ES8特性一锅炖(ES6、ES7、ES8学习指南)
本文目录7 个章节
  1. 一、ECMAScript 版本概览
  2. 二、ES6(ECMAScript 2015)
  3. 三、ES7(ECMAScript 2016)
  4. 四、ES8(ECMAScript 2017)
  5. 五、原文之外的现代补充
  6. 六、总结
  7. 参考资料

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 52009严格模式、JSON、数组方法、属性描述符等
ECMAScript 2015(ES6)2015let/const、类、模块、箭头函数、模板字符串、解构、迭代器、生成器、Promise、Map/Set、Symbol
ECMAScript 2016(ES7)2016Array.prototype.includes()、指数运算符 **
ECMAScript 2017(ES8)2017async/awaitObject.values()Object.entries()padStart/padEnd、尾逗号、属性描述符 API
ECMAScript 2018 及以后2018—对象展开、异步迭代、可选 catch 绑定、可选链、空值合并、逻辑赋值等按年份持续加入

“ES6”可以作为 ES2015 的历史简称,但不要把后续所有语法都统称为 ES6。能否使用某个特性,应查看目标浏览器/Node.js 版本和构建策略。

二、ES6(ECMAScript 2015)

ES2015 的特性非常多,下面保留原文常用的几类:

  • 类(class)和继承;
  • 模块(export/import);
  • 箭头函数;
  • 函数参数默认值;
  • 模板字符串;
  • 解构赋值;
  • 展开语法;
  • 对象属性简写;
  • Promise;
  • letconst
  • 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.jsontype.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

箭头函数没有自己的 thisargumentsprototype,不能用于构造函数。它会捕获定义位置外层的 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' }

如果默认条件是 nullundefined,可以使用空值合并:

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. letconst

var 是函数作用域:

{
  var a = 10
}
console.log(a) // 10

letconst 是块级作用域,并且声明前处于暂时性死区:

{
  let b = 10
  const c = 20
}

// console.log(b) // ReferenceError
// console.log(c) // ReferenceError

三、ES7(ECMAScript 2016)

ES2016 的特性数量较少,主要包括 includes() 和指数运算符。

1. Array.prototype.includes()

如果只需要判断数组是否包含某个值,includesindexOf(...) !== -1 更直接:

const frameworks = ['react', 'angular', 'vue']

console.log(frameworks.includes('react')) // true
console.log([NaN].includes(NaN)) // true

它与 indexOf 的区别是:indexOf 返回位置并且找不到 NaNincludes 返回布尔值并使用 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 增加 padStartpadEnd

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/writableget/set,以及 enumerableconfigurable

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 当作缺失值,不会把 0false'' 替换掉。

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 的重要历史节点,但不是今天唯一的版本概念。阅读旧文章时应特别留意:

  1. 把正确的旧知识保留为历史背景;
  2. 明确标注特性的 ECMAScript 年份;
  3. 区分正式标准、浏览器/Node.js 实现和 Babel/TypeScript 提案;
  4. 不要把 Promise 的“异步”误解为多线程;
  5. 不要把默认参数、||?? 和逻辑赋值的空值规则混为一谈;
  6. 使用运行时兼容性数据和构建目标决定是否需要转译或 polyfill。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS