同构解构语法

317 阅读5分钟

| 在阅读本篇文章前,希望你已经对解构 (...) 语法足够熟悉。解构是在 ES6 中引入的 JavaScript 语法

解构在很多场景下都非常有用,例如:

  1. 值的交换 [a, b] = [b, a]
  2. 重新命名参数 { a: b } = { a: 'should named b' }
  3. 对象的浅合并 merged = {...a, ...b}
  4. 数组切片等

今天我想分享一些我对一些 Web 框架中的对解构的不成熟的想法💡

我做为一个 Vue 的爱好者,在平时的代码中经常使用解构语法。我也曾经很不情愿的在公司写了阵子的 React。随着 Vue3 的发布,其 Composition API 提供了非常相似(react hook)的抽象能力。受 react-use 的启发,我在今年年初编写了一个名为VueUse的hook工具库。

与 React hook 相同的是,Vue 的 Composition 语法也会传入一些参数,并且返回一些数据和函数。Javascript 作为一个类 C 的语言,它只允许返回一个值。所以作为返回多个值的一种解决方法,我们通常会使用数组或是对象包装我们想要返回的值,然后通过解构的方式拿到它们。正如你所学的那样,解构函数可以操作数组或对象,但是它们的表现是不同的。

解构数组 / 元组 (tuple)

在 React hook 中,使用数组解构是一种常见的做法,例如一些内置的 hook

const [count, setCount] = useState(0)

React hook的库自然会依循相似的理念来封装,例如 React-use:

const [on, toggle] = useToggle(true)
const [value, setValue, remove] = useLocalStorage('my-key', 'foo')

数组解构的好处在于,你可以自由地定义变量名,代码看起来很干净

解构对象

相比于 React 的 useState hook 返回的 gettersetter,在 Vue3 中会在单个对象中结合 gettersetter 创建一个 ref,命名更简单,不再需要解构

// React
const [counter, setCounter] = useState(0)
console.log(counter) // get
setCounter(counter + 1) // set

// Vue 3
const counter = ref(0)
console.log(counter.value) // get
counter.value++ // set

正因为在 Vue 中不需要像 React 一样为 gettersetter 重命名两次,所以在 VueUse 中,作者实现了大量返回对象的函数,例如:

const { x, y } = useMouse()

使用对象也带来了许多灵活性,例如

// 不需要解构,直接在命名空间上操作
const mouse = useMouse()

mouse.x
// 只使用部分值
const { y } = useMouse()
// 重命名
const { x: mouseX, y: mouseY } = useMouse()

虽然它可以满足不同偏好,并且命名属性可以表达更多的信息,但是其重命名的语法可能比数组解构更冗长。

同时支持

如果我们全都要?充分利用两边的优势,让用户自己来决定使用哪种风格来更好地满足自己的需求。

我全都要.png

作者曾经见过有一个库有过这种用法,但是不知道是哪一个了。不过,这个念头从此埋在心里。现在就来实现一下。

作者假设这个方法能返回一个同时具有数组和对象行为的对象。实线路径很明确,要么制作像对象行为的数组,要么制作像数组行的对象。

让对象的行为像数组一样

我想到的第一个可能的解决方案是让一个对象表现地像一个数组,你可能知道,数组实际上就是一个具有数组索引和一些原型的对象。所以代码会是这样的:

const data = {
  foo: 'foo',
  bar: 'bar',
  0: 'foo',
  1: 'bar',
}

let { foo, bar } = data
let [ foo, bar ] = data // error

当我们想像数组那样解构它时,它寄了:

Uncaught TypeError: data is not iterable

在我们研究如何使一个对象可迭代之前,让我们试试另一个方向

让数组的行为像对象一样

因为数组也是对象,我们可以直接拓展它,例如

const data = ['foo', 'bar']
data.foo = 'foo'
data.bar = 'bar'

let [ foo, bar ] = data
let { foo, bar } = data

它可以运行起来,今天这个b班就上到这里了!然而,如果你是一个完美主义者,你会发现有个边缘case没有被完美覆盖。如果我们使用 rest 模式来检索剩余部分,数字索引将意外地包含在 rest 对象中

let { foo, ...rest } = data

rest 会变成

{
  bar: 'bar',
  0: 'foo',
  1: 'bar'
}

可迭代对象

最后我们只能回到上一步,来探索如何让对象变得可以迭代。幸运地是,Symbol.iterator 正是用于解决这个问题的!(😱)es6文档 中说明了它的用法,我们稍作修改即可

const data = {
  foo: 'foo',
  bar: 'bar',
  *[Symbol.iterator]() {
    yield 'foo'
    yield 'bar'
  }
}

let { foo, bar } = data
let [ foo, bar ] = data

它能成功运行,但是 Symbol.iterator 还是会在 rest 模式下出现

let { foo, ...rest } = data

// rest
{
  bar: 'bar',
  Symbol(Symbol.iterator): ƒ*
}

因为我们是对对象的属性进行的操作,只需要把这个属性设置为不可枚举即可解决

const data = {
  foo: 'foo',
  bar: 'bar',
}

Object.defineProperty(data, Symbol.iterator, {
  enumerable: false,
  value: function*() {
    yield 'foo'
    yield 'bar'
  },
})

现在我们成功地把多余的属性藏起来了!

let { foo, ...rest } = data

// rest
{
  bar: 'bar',
}

不使用 Generator

可能你会对 function* 、 yield 这些新引入的语法有疑问。如果你没有使用过 Generator 函数,我们可以使用纯函数来实现它 参考这篇文章:

Object.defineProperty(clone, Symbol.iterator, {
  enumerable: false,
  value() {
    let index = 0
    let arr = [foo, bar]
    return {
      next: () => ({
        value: arr[index++],
        done: index > arr.length,
      })
    }
  }
})

TypeScript 支持

对我来说,如果没有 TypeScript 支持那上面的努力都是白费功夫的。令人惊讶地是,TypeScript 几乎开箱即用地只需要使用 & 运算符来插入对象和数据的类型。解构语法将正确推断两种用法中的类型

type Magic = { foo: string, bar: string } & [ string, string ]

封装(拿走不送)

最后,我把它封装成一个通用函数来合并数组和对象,使其可以同构解构。你可以直接复制下面的 TypeScript 片段来使用它,感谢你的阅读!

| ⚠️ 可能不支持 IE11. 详见 caniuse 或者使用polyfill

function createIsomorphicDestructure<
  T extends Record<string, unknown>,
  A extends readonly any[]
>(obj: T, arr: A): T & A {
  const clone = { ...obj }

  Object.defineProperty(clone, Symbol.iterator, {
    enumerable: false,
    value() {
      let index = 0
      return {
        next: () => ({
          value: arr[index++],
          done: index > arr.length
        })
      }
    }
  })
  
  return clone as T & A
}

使用方式

const foo = { name: 'foo' }
const bar: number = 1024

const obj = createIsomorphicDestructure(
  { foo, bar } as const,
  [ foo, bar ] as const
)

let { foo, bar } = obj
let [ foo, bar ] = obj

playground:

原文: antfu.me/posts/destr…