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

显示模式

登录
ARCHIVE DOCUMENTJS

ES6 完全使用手册(更新版)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/16-ES6 完全使用手册
本文目录23 个章节
  1. 前言:先分清版本和提案
  2. 1. let 和 const
  3. 2. 模板字符串
  4. 3. 箭头函数
  5. 4. Symbol
  6. 5. Set 和 Map
  7. 6. for...of
  8. 7. Promise
  9. 8. async/await
  10. 9. class
  11. 10. Decorator(装饰器)
  12. 11. 函数参数默认值
  13. 12. Rest 和 Spread(剩余参数与展开语法)
  14. 13. 双冒号绑定运算符 ::(历史提案)
  15. 14. 解构赋值
  16. 15. 增强的对象字面量
  17. 16. 数组的扩展方法
  18. 17. Optional chaining(可选链,ES2020)
  19. 18. Logical assignment operators(逻辑赋值,ES2021)
  20. 19. Nullish coalescing(空值合并,ES2020)
  21. 20. Pipeline operator(管道操作符,提案)
  22. 总结
  23. 参考资料

ES6 完全使用手册(更新版)

Category(分类): JavaScript Status: 已更新

原文作者:冴羽 历史文章:ES6 完全使用手册

原文中的“ES6”主要指 ES2015 的新语法,同时混入了 ES2016、ES2018、ES2020 以及当时尚未定案的提案。本文尽量保留原来的章节和使用场景,并把每项特性标记为正式标准、较新的标准或仍在提案阶段。代码示例也修复了抓取造成的注释粘连、拼写错误、变量未声明和运行时语义错误。

前言:先分清版本和提案

严格来说:

  • ECMAScript 2015(ES2015,常被称为 ES6)let/const、模板字符串、箭头函数、类、模块、解构、迭代器/生成器、PromiseMap/SetSymbol 等;
  • ECMAScript 2016(ES7)Array.prototype.includes()、指数运算符;
  • ECMAScript 2017(ES8)async/awaitObject.values()Object.entries()、字符串填充、尾逗号、Object.getOwnPropertyDescriptors()
  • 对象展开是 ES2018;可选链是 ES2020;逻辑赋值是 ES2021
  • 装饰器和管道操作符不是“ES6 语法”。它们的标准化进度和工具链支持需要单独说明,不能看到 Babel 插件就认为浏览器原生支持。

ECMAScript 每年发布一个版本。实际项目应以目标运行时、构建工具和兼容性需求为准,而不是简单地把所有新语法都称为 ES6。

1. letconst

letconst 都是块级作用域声明,var 是函数作用域声明。现代代码通常默认使用 const,只有变量本身需要重新赋值时才使用 let

// 历史写法
var foo = 'bar'

// 需要重新赋值时使用 let
let count = 0
count += 1

// 引用不需要重新赋值时使用 const
const name = 'Alice'

const 只保护变量绑定,不会冻结对象或数组:

const user = { name: 'Alice' }
user.name = 'Bob' // 可以修改对象内部属性

// user = {} // TypeError,不能让 user 指向新对象

如果需要禁止对象扩展、修改或删除属性,应根据需求使用 Object.preventExtensions()Object.seal()Object.freeze();它们也只提供浅层保护,深层对象仍需单独处理。

暂时性死区

let/const 声明会被提升到块作用域,但在声明执行前处于暂时性死区(TDZ):

console.log(typeof value) // ReferenceError,而不是 'undefined'
let value = 1

这也是 let/constvar 的重要区别。不要依赖变量提升来组织复杂初始化逻辑。

2. 模板字符串

模板字符串使用反引号,可以插入表达式,也可以跨行:

const first = 'Alice'
const last = 'Wang'
const message = `Your name is ${first} ${last}.`

${...} 中可以放任意表达式,但不会自动转义 HTML:

const userInput = '<img src=x onerror=alert(1)>'
const html = `<p>${userInput}</p>` // 直接插入 DOM 时可能产生 XSS

向页面插入用户输入时,应使用 textContent 或经过可靠的 HTML 清洗,而不是把模板字符串当作安全模板。

标签模板

标签模板会把字符串片段和插值作为参数传给函数:

function inspect(strings, ...values) {
  return {
    strings: [...strings],
    values
  }
}

const name = 'Alice'
console.log(inspect`Hello ${name}!`)
// { strings: ['Hello ', '!'], values: ['Alice'] }

原文提到的 oneLine 是一个自定义标签函数,不是 JavaScript 内置 API。标签函数的实现必须自己处理空白、转义和插值;不能因为使用了反引号就认为字符串已被 HTML、SQL 或 URL 安全转义。

3. 箭头函数

箭头函数的基本形式如下:

const add = (a, b) => a + b
const square = value => value * value
const log = value => {
  console.log(value)
}

箭头函数没有自己的 thisargumentsprototype,也不能用 new 调用。它适合回调和需要捕获外层 this 的场景,但不是普通函数的完全替代品。

不要用箭头函数定义需要动态 this 的对象方法

const object = {
  value: 1,
  getValue() {
    return this.value
  },
  wrong: () => this.value
}

console.log(object.getValue()) // 1
// object.wrong() 的 this 来自定义箭头函数的外层环境,不是 object

对象字面量本身不会形成新的 this 词法环境。需要动态 this 时使用方法简写或普通函数。

原型方法也不要写成共享箭头属性

function Foo(value) {
  this.value = value
}

Foo.prototype.getValue = function () {
  return this.value
}

如果写成 Foo.prototype.getValue = () => this.value,调用时不会把 Foo 实例绑定给箭头函数。另一方面,现代类字段中的箭头函数会为每个实例创建一份函数,适合自动绑定,但不等同于原型方法。

事件回调要看你需要哪个 this

button.addEventListener('click', function () {
  // 浏览器事件监听器中的 this 是 button
  this.textContent = 'Clicked'
})

class View {
  handleClick = () => {
    // 类字段箭头函数捕获实例 this
    this.update()
  }

  update() {}
}

原文中的 React componentWillMount 已废弃;React 或其他框架的生命周期应使用当前版本文档推荐的 API。

4. Symbol

1. 唯一的属性键

每次调用 Symbol() 都会创建一个不同的符号:

const first = Symbol('id')
const second = Symbol('id')
console.log(first === second) // false

const user = {
  [first]: 100
}

符号键不会出现在 for...inObject.keys() 或 JSON 序列化中,但它们不是绝对私有的:

console.log(Object.getOwnPropertySymbols(user)) // [first]
console.log(Reflect.ownKeys(user)) // [first]

Symbol.for() 使用全局符号注册表,语义与 Symbol() 不同:

const a = Symbol.for('shared')
const b = Symbol.for('shared')
console.log(a === b) // true
console.log(Symbol.keyFor(a)) // shared

2. 避免属性名冲突

历史代码可能用很长的字符串作为“私有”属性名:

const isMoving = Symbol('isMoving')

if (element[isMoving]) {
  smoothAnimations(element)
}
element[isMoving] = true

这可以降低普通字符串属性名冲突的概率,但不能阻止拥有对象引用的代码通过反射找到符号。真正需要封装时,应考虑闭包、模块私有绑定或 #privateField

3. 消除魔术字符串

原文把资源类型从字符串改成 Symbol,这种做法可避免不同库使用相同字符串时的冲突:

const TYPE_AUDIO = Symbol('audio')
const TYPE_VIDEO = Symbol('video')
const TYPE_IMAGE = Symbol('image')

function handleFileResource(resource) {
  switch (resource.type) {
    case TYPE_AUDIO:
      return playAudio(resource)
    case TYPE_VIDEO:
      return playVideo(resource)
    case TYPE_IMAGE:
      return previewImage(resource)
    default:
      throw new Error('Unknown type of resource')
  }
}

如果值需要持久化、显示给用户或与后端交换,字符串或枚举对象往往更合适;不要为了“避免魔术字符串”无条件使用符号。

5. SetMap

1. 数组去重

const array = [1, 2, 2, 3, NaN, NaN]
const unique = [...new Set(array)]
console.log(unique) // [1, 2, 3, NaN]

Set 使用 SameValueZero 判断相等,因此可以把 NaN 视为相等;它不负责深度去重,两个结构相同但引用不同的对象仍是两个元素。

2. Map 与对象映射

原文使用对象或 Map 根据颜色查找水果:

const fruitByColor = {
  red: ['apple', 'strawberry'],
  yellow: ['banana', 'pineapple'],
  purple: ['grape', 'plum']
}

function fruitsByObject(color) {
  return Object.hasOwn(fruitByColor, color)
    ? fruitByColor[color]
    : []
}

const fruitByColorMap = new Map([
  ['red', ['apple', 'strawberry']],
  ['yellow', ['banana', 'pineapple']],
  ['purple', ['grape', 'plum']]
])

function fruitsByMap(color) {
  return fruitByColorMap.get(color) ?? []
}

对象的键主要是字符串和 Symbol,并且会受到原型、枚举规则和特殊键名影响;Map 可以使用任意值作为键,并明确提供 hasgetsetsize 等操作。小型固定配置用对象通常更简单,不要为了追求“更现代”而盲目替换。

6. for...of

for...of 遍历的是可迭代对象的值,而不是对象的所有属性。常见可迭代对象包括:

  • 数组、字符串;
  • SetMap
  • 生成器对象;
  • TypedArray;
  • 支持迭代器协议的现代 DOM 集合,例如多数浏览器中的 NodeList
  • 自定义实现了 [Symbol.iterator]() 的对象。
for (const value of ['a', 'b', 'c']) {
  console.log(value)
}

for (const [index, value] of ['a', 'b', 'c'].entries()) {
  console.log(index, value)
}

它的一个优势是可以使用 breakcontinue,并且在退出时有机会触发迭代器的 return() 清理逻辑:

for (const value of [1, 2, 3, 4]) {
  if (value === 3) break
  console.log(value)
}

遍历 Map

const map = new Map([
  ['name', 'Alice'],
  ['age', 18]
])

for (const key of map.keys()) console.log(key)
for (const value of map.values()) console.log(value)
for (const [key, value] of map) console.log(key, value)

for...of 不能直接遍历普通对象:

const object = { a: 1, b: 2 }
for (const [key, value] of Object.entries(object)) {
  console.log(key, value)
}

7. Promise

Promise 是 ECMAScript 标准内置的异步结果抽象。它解决的是回调结果的组合和错误传播,不会让耗时的 CPU 计算自动移到后台线程,也没有通用的内置取消方法。

1. 基本链式调用

原文用嵌套回调表达连续异步操作;现代代码可以写成 Promise 链:

request(url, (error, body) => {
  if (error) return handleError(error)
  writeFileAsync('1.txt', body, error => {
    if (error) return handleError(error)
    request(url2, (error, result) => {
      if (error) return handleError(error)
      console.log(result)
    })
  })
})

如果 API 返回 Promise:

requestAsync(url)
  .then(body => writeFileAsync('1.txt', body))
  .then(() => requestAsync(url2))
  .then(console.log)
  .catch(handleError)

then() 总是返回新的 Promise;回调中返回的值会成为下一个 then 的输入,返回 Promise 则会等待它结算。不要启动异步操作却不返回它,否则会产生 floating promise:

requestAsync(url)
  .then(body => writeFileAsync('1.txt', body))
  .then(() => console.log('文件已经写完'))

2. finally

Promise.prototype.finally() 在 ES2018 标准化,用于无论成功还是失败都执行清理:

fetch('/data.json')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return response.json()
  })
  .catch(error => console.error(error))
  .finally(() => console.log('finished'))

finally 通常不会改变原来的结果;但如果 finally 回调抛错或返回拒绝的 Promise,链会变成拒绝。

8. async/await

async 函数总是返回 Promise。await 会暂停当前异步函数,等待值被兑现或拒绝;它不会阻塞整个 JavaScript 执行环境。即使等待的是普通值,异步函数也会在后续异步时机继续执行。

1. 让顺序逻辑更清晰

async function loadData() {
  await fetchData()
  return 'done'
}

等价的 Promise 写法是:

function loadDataWithPromise() {
  return fetchData().then(() => 'done')
}

多个有依赖关系的步骤可以这样写:

async function loadMoreData() {
  const data = await fetchData()
  if (data.moreData) {
    return fetchAnotherData(data)
  }
  return data
}

2. 错误处理

try/catch 可以捕获 await 的拒绝和函数中同步抛出的错误:

async function loadSafely() {
  try {
    const response = await fetch('/data.json')
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return await response.json()
  } catch (error) {
    console.error(error)
    throw error
  }
}

下面的 try/catch 不能捕获 Promise 异步回调中稍后发生的拒绝:

try {
  fetchData().then(handleData)
} catch (error) {
  // 只能捕获 fetchData() 同步抛出的错误,不能捕获后续 Promise 拒绝
}

如果使用 Promise 链,应在链尾 catch;如果使用 await,应在合适的边界使用 try/catch。不要为了“所有函数都包一层 catch”而吞掉错误。

3. 不要制造“async 地狱”

下面的变量名覆盖了同名函数,是错误示例:

// 错误:局部变量 getList 覆盖了要调用的函数
// const getList = await getList()

如果两个请求互不依赖,不要连续等待:

async function loadBoth() {
  const listPromise = getList()
  const anotherListPromise = getAnotherList()

  const [list, anotherList] = await Promise.all([
    listPromise,
    anotherListPromise
  ])

  return { list, anotherList }
}

也可以直接写成:

const [list, anotherList] = await Promise.all([
  getList(),
  getAnotherList()
])

Promise.all() 会并发启动输入操作,并在任意一个拒绝时拒绝结果 Promise;它不会取消已经启动的其他操作。如果业务需要全部结果,可以使用 Promise.allSettled();如果需要第一个成功结果,可以考虑 Promise.any()

9. class

ES2015 的 class 提供了更清晰的类和继承语法。它不是传统构造函数的简单文本替换:类方法不可枚举,类构造器必须使用 new 调用,类体默认处于严格模式。

class Foo {
  static bar() {
    return this.baz()
  }

  static baz() {
    return 'hello'
  }

  baz() {
    return 'world'
  }
}

console.log(Foo.bar()) // hello
console.log(new Foo().baz()) // world

原型方法不会复制到每个实例:

class Shape {
  constructor(width, height) {
    this.width = width
    this.height = height
  }

  get area() {
    return this.width * this.height
  }
}

const square = new Shape(10, 10)
console.log(square.area) // 100
console.log(Object.hasOwn(square, 'area')) // false,getter 在原型上

现代 JavaScript 还支持公共/私有类字段和私有方法:

class Counter {
  #value = 0

  increment() {
    this.#value += 1
    return this.#value
  }
}

#value 是真正的私有品牌检查,不等同于约定俗成的 _value,也不等同于 Symbol。

10. Decorator(装饰器)

原文把装饰器放在“ES6 使用手册”中,但装饰器不是 ES2015 特性。装饰器提案仍在 TC39 迭代,当前工具链中的旧版 Babel/TypeScript 装饰器和标准装饰器语义也不完全相同。

下面的代码应看作提案/工具链示意,不能默认在浏览器原生 JavaScript 中运行:

@log
class MathBox {
  add(a, b) {
    return a + b
  }
}

在不使用装饰器语法时,可以用普通高阶函数实现函数装饰模式:

function logCall(fn) {
  return function (...args) {
    const result = fn.apply(this, args)
    console.log({ args, result })
    return result
  }
}

const add = logCall((a, b) => a + b)
console.log(add(1, 2)) // 3

如果项目确实使用装饰器,应明确:

  • 使用的是 TypeScript、Babel legacy decorators 还是当前标准提案实现;
  • 构建工具如何配置;
  • 输出目标是否需要转译;
  • 装饰器的初始化顺序、this、私有元素和类字段语义是否符合项目预期。

React 的 connect 也可以用普通函数包裹组件:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent)

@connect 只是另一种工具链语法,不能说明装饰器已经成为 ES6 原生能力。

11. 函数参数默认值

参数默认值只在实参为 undefined 时生效,不会因为传入 0、空字符串、falsenull 而生效:

function createUser(name = '匿名', age = 0) {
  return { name, age }
}

console.log(createUser(undefined, 0)) // { name: '匿名', age: 0 }
console.log(createUser(null, null)) // { name: null, age: null }

原来的 quantity || 1 会把 0 错误地替换成 1

function test(quantity) {
  const q = quantity ?? 1
  return q
}

解构参数和默认对象

function doSomething({
  foo = 'Hi',
  bar = 'Yo!',
  baz = 13
} = {}) {
  return { foo, bar, baz }
}

console.log(doSomething())
console.log(doSomething({ foo: '', baz: 0 }))

这里对象参数本身为 undefined 时使用 {};属性值为 null 时不会触发属性默认值。若要把 null 也视为空值,需要显式使用 ??

React/JSX 示例应避免把已经接收的变量写成不存在的 classname

const Button = ({ className = 'default-size' }) => (
  <span className={className}>Button</span>
)

12. Rest 和 Spread(剩余参数与展开语法)

... 在不同位置有不同名字和语义:

  • 函数参数位置:rest parameter,收集剩余参数;
  • 函数调用和数组字面量位置:array spread,展开可迭代对象;
  • 对象字面量位置:object spread,复制自身可枚举字符串/Symbol 属性。

1. 把 arguments 转成数组

function sortNumbers(...numbers) {
  return numbers.sort((a, b) => a - b)
}

console.log(sortNumbers(10, 2, 1)) // [1, 2, 10]

rest 参数是真正的数组;arguments 是另一个类数组对象,不能把两者混为一谈。

2. 调用参数

function sum(a, b, c) {
  return a + b + c
}

const numbers = [1, 2, 3]
console.log(sum(...numbers)) // 6
console.log(Math.max(...[14, 3, 77])) // 77

展开元素数量很大时要留意引擎的参数数量限制;需要处理任意长度数据时,使用循环或 reduce 通常更稳妥。

3. 构造和复制数组

const students = ['Jine', 'Tom']
const persons = ['Tony', ...students, 'Aaron']
const copy = [...persons]
const merged = [...[0, 1], ...[2, 3]]

数组展开是浅拷贝:

const source = [{ id: 1 }]
const copy = [...source]
copy[0].id = 2
console.log(source[0].id) // 2

4. 对象展开

对象展开在 ES2018 标准化:

const obj1 = { foo: 'bar', x: 42 }
const obj2 = { foo: 'baz', y: 13 }

const cloned = { ...obj1 }
const merged = { ...obj1, ...obj2 }

console.log(merged) // { foo: 'baz', x: 42, y: 13 }

后面的属性会覆盖前面的同名属性。它只复制自身可枚举属性,是浅拷贝,不会复制原型、不可枚举属性或访问器的完整描述符。

5. 有条件地构造对象

原文使用 name && { name },但这种写法会丢掉空字符串、0false。如果“是否传入”以 undefined 为判断标准,可以这样写:

function pick({ id, name, age }) {
  return {
    guid: id,
    ...(name !== undefined ? { name } : {}),
    ...(age !== undefined ? { age } : {})
  }
}

6. React 中传递 props

const params = { value: 1, title: 'Hello' }

<CustomComponent {...params} />

JSX 的展开属性也只是把对象属性传入组件;不要把不必要的内部属性、事件或用户输入无选择地全部透传给 DOM。

13. 双冒号绑定运算符 ::(历史提案)

原文的下面语法来自已长期停滞/未标准化的 bind operator 提案:

foo::bar
foo::bar(...args)

它不是当前 ECMAScript 标准,不能直接用于普通浏览器或 Node.js。不要把它当作 ES6 语法。现代替代写法是:

const bound = bar.bind(foo)
const result = bar.call(foo, ...args)

如果只需要在回调中保留外层 this,通常使用箭头函数:

button.addEventListener('click', () => controller.handleClick())

14. 解构赋值

解构可以从数组或对象中提取值,并支持重命名、默认值和剩余属性。

1. 对象基本解构

const student = { name: 'Ming', age: 18, city: 'Shanghai' }
const { name, age, city } = student

const { name: studentName } = student
console.log(studentName) // Ming

函数参数也可以直接解构:

function renderUser({ name, active }) {
  return `${name}: ${active ? 'active' : 'inactive'}`
}

如果参数可能缺失,给整个参数提供默认对象:

function renderUser({ name = '匿名' } = {}) {
  return name
}

2. 数组解构和默认值

const values = ['one', 'two', 'three', 'four']
const [one, two, , four] = values

let a, b
;[a, b] = [1, 2]
;[a, b] = [b, a]

const [first = 'default', second = 'default'] = []

在没有声明变量的情况下进行赋值解构,开头的分号可以避免被上一行表达式的自动分号插入解析影响。更推荐把相关语句写在清晰的语句边界中。

3. 深度解构

const object = {
  a: {
    b: {
      c: 1
    }
  }
}

const {
  a: {
    b: { c = '' } = {}
  } = {}
} = object

默认值要与下一层结构类型匹配;用空字符串代替对象会让后续解构语义混乱。

4. 数组方法名称要准确

原文的 splite 是拼写错误,应为 split

const locale = 'zh-CN'
const [language, country] = locale.split('-')

5. 只获取部分属性

function getPosition() {
  return { left: 1, right: 2, top: 3, bottom: 4 }
}

const { left, right } = getPosition()
const [firstValue, , thirdValue] = [10, 20, 30]

15. 增强的对象字面量

1. 属性简写

const name = 'Ming'
const age = 18

const student = {
  name,
  age
}

2. 方法简写

const api = {
  getUser(id) {
    return { id }
  }
}

3. 计算属性名

const key = 'a_b'
const object = {
  [key]: 'value',
  [`${key}_2`]: 'another value'
}

console.log(object.a_b) // value

16. 数组的扩展方法

1. keys()entries()values()

const array = ['a', , 'c']

console.log(Object.keys(array)) // ['0', '2'],跳过空槽
console.log([...array.keys()]) // [0, 1, 2],按索引生成
console.log([...array.entries()]) // [[0, 'a'], [1, undefined], [2, 'c']]
console.log([...array.values()]) // ['a', undefined, 'c']

数组迭代器会访问索引范围,因此稀疏数组中的空槽表现与 Object.keys() 不同。

2. includes()

includes 返回布尔值,并使用 SameValueZero,因此可以找到 NaN

const fruits = ['apple', 'strawberry', 'cherry']
console.log(fruits.includes('apple')) // true
console.log([NaN].includes(NaN)) // true

它与 indexOf 不完全等价:indexOf 返回索引,找不到时返回 -1,并且不能找到 NaN。如果需要索引,继续使用 indexOf

3. find()findIndex()

const inventory = [
  { name: 'apples', quantity: 2 },
  { name: 'bananas', quantity: 0 },
  { name: 'cherries', quantity: 5 }
]

const cherries = inventory.find(item => item.name === 'cherries')
const cherryIndex = inventory.findIndex(item => item.name === 'cherries')

console.log(cherries) // { name: 'cherries', quantity: 5 }
console.log(cherryIndex) // 2

find 返回第一个匹配元素,找不到时返回 undefinedfindIndex 返回索引,找不到时返回 -1。两者找到结果后会停止遍历。不要为了“性能”机械替换 filter:如果需要全部匹配元素,filter 才是正确表达。

17. Optional chaining(可选链,ES2020)

可选链在访问值为 nullundefined 的对象时短路:

const object = {
  foo: { bar: { baz: 42 } }
}

console.log(object?.foo?.bar?.baz) // 42
console.log(object?.missing?.value) // undefined

也可以调用可能不存在的方法:

const maybeCallback = undefined
maybeCallback?.('value') // 不调用,结果为 undefined

注意:可选链不能让未声明的变量变得安全,exists?.() 中如果 exists 根本没有声明,仍然会抛 ReferenceError。应先声明变量,或访问对象属性。

可选链不能作为赋值目标:

let user = null
// user?.name = 'Alice' // SyntaxError

常与空值合并运算符一起使用:

const displayName = user?.profile?.name ?? '匿名'

18. Logical assignment operators(逻辑赋值,ES2021)

逻辑赋值包括 ||=&&=??=

let count = 0
count ||= 10 // count 为 falsy,因此变成 10

let enabled = true
enabled &&= false // enabled 为 truthy,因此变成 false

let label = null
label ??= 'default' // 只有 null/undefined 才赋值

x ||= y 大致等价于 x || (x = y),但左侧表达式只求值一次,并且右侧在不需要时不会求值:

const config = {
  get value() {
    console.log('get')
    return 1
  },
  set value(value) {
    console.log('set', value)
  }
}

config.value ||= 2 // 只读取,不触发 setter

注意对象链可能为 null

// obj.a.b ||= value 仍会在 obj.a 为 null 时抛错
// 可选链目前也不能直接作为赋值左值

逻辑赋值不是简单的“更短写法”,要先确定业务需要的是 falsy 语义还是 nullish 语义。

19. Nullish coalescing(空值合并,ES2020)

?? 只有在左侧是 nullundefined 时才使用右侧:

const count = 0
const text = ''

console.log(count ?? 42) // 0
console.log(text ?? 'default') // ''
console.log(null ?? 'default') // default

|| 的区别:

console.log(0 || 42) // 42
console.log(0 ?? 42) // 0

?? 不能与 ||&& 无括号混用,否则是语法错误:

const value = (a || b) ?? c

?? 也支持短路求值,左侧不是空值时不会执行右侧表达式。

20. Pipeline operator(管道操作符,提案)

原文的代码:

5 |> double |> increment |> double

不是当前 ECMAScript 标准。管道操作符仍是 TC39 提案,语法方案和占位符形式也可能变化,不能在未配置转译器的浏览器或 Node.js 中直接运行。

现在可以用普通函数嵌套、方法链或临时变量表达同样逻辑:

const double = value => value * 2
const increment = value => value + 1

const result = double(increment(double(5)))
console.log(result) // 22

可读性更重要时,临时变量并不是“落后写法”:

const doubled = double(5)
const incremented = increment(doubled)
const result = double(incremented)

如果项目使用 Babel 的管道提案插件,应在构建配置和代码注释中明确这是实验性语法,并锁定插件版本。

总结

ES2015 之后的 JavaScript 已经形成按年发布的标准。掌握新语法时,除了记住简写形式,还应理解:

  • const 不等于深度不可变;
  • 箭头函数不等于普通函数的短写,尤其是 thisarguments 和构造能力不同;
  • for...of 依赖迭代器协议;
  • 对象/数组展开是浅拷贝;
  • async/await 建立在 Promise 之上,顺序等待和并发等待需要区分;
  • ||??、默认参数和逻辑赋值处理的“空值”范围不同;
  • 装饰器和管道操作符属于提案或工具链能力,不能与正式标准混写;
  • 是否能使用某个特性,要结合目标浏览器、Node.js 版本、构建工具和 polyfill 策略。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS