韩海Tempest

译文:解构 ` ... ` 是使用对象还是数组?

Nov 15 · 15min

原文地址:https://antfu.me/posts/destructuring-with-object-or-array

解构是 ES6 中引入的 JavaScript 语言特性,我假设在继续之前你已经熟悉了它。

我们在许多场景中发现它非常有用,例如值交换、命名参数、对象的浅合并、数组切片等。今天,我想分享一些我对某些 Web 框架中的“解构”的一些不成熟的想法。

我肯定是一个 Vue 的爱好者,我用它写了很多应用。我以前在公司不情愿地写过 React。随着最近 Vue 3.0 的发布,其令人兴奋的 Composition API 提供了类似的能力来抽象。受到 react-use 的启发,我今年早些时候写了一个名为 VueUse 的可组合实用工具库。

类似于 React 的 Hooks,Vue 的可组合函数将接受一些参数并返回一些数据和函数。JavaScript 就像其他 C-风格的编程语言一样,只允许一个返回值。因此,为了返回多个值,我们通常会将它们包装在数组或对象中,然后解构返回的数组/对象。正如你已经看到的,我们在这里有两个不同的哲学,使用数组或对象。

数组解构

在 React hooks 中,使用数组解构是一个常见的做法。例如,内置函数:

const [counter, setCounter] = useState(0)

React hooks 的库自然会选择类似的理念,例如 react-use:

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

数组解构的好处相当直接——你可以自由地为变量命名,同时保持代码的整洁外观。

解构对象

Vue 3 中创建了一个 ref ,将获取器和设置器合并到单个对象内部,而不是返回 React 中的useState获取器和设置器。命名更简单,不再需要解构。

// 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

由于我们不需要像 React 那样为 getter 和 setter 重命名相同的事物两次,所以在 VueUse 中,我用对象返回实现了大部分函数,例如:

const { x, y } = useMouse()

使用对象为用户提供了更多的灵活性,如

// no destructing, clear namespace
const mouse = useMouse()

mouse.x
// use only part of the value
const { y } = useMouse()
// rename things
const { x: mouseX, y: mouseY } = useMouse()

虽然这对于不同的偏好和命名属性来说可能是自解释的,但重命名可能比数组解构更冗长。

两者都支持

如果我们可以支持两者,如何呢?利用每一边的优势,并让用户决定哪种风格更适合他们的需求。

我曾经见过一个库支持这种用法,但我不记得是哪个了。不过,这个想法从那时起就一直在我心中。现在我打算尝试一下。

我的假设是,它返回一个同时具有arrayobject行为的对象。路径是明确的,要么创建一个类似于arrayobject,要么创建一个类似于 objectarray

让对象像数组一样工作

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

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

这有效,我们现在可以收工了!然而,如果你是个完美主义者,你会发现有一个边缘情况没有得到妥善处理。如果我们使用剩余模式来获取剩余的部分,数字索引会意外地包含在剩余对象中。

const { foo, ...rest } = data

rest 将会是:

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

可迭代对象

让我们回到我们的第一个方法,看看我们是否可以创建一个可迭代的对象。幸运的是,Symbol.iterator专门为此任务设计!文档显示了确切的用法,进行一些修改后,我们得到如下结果:

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

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

它工作得很好,但Symbol.iterator仍然会包含在其余模式中。

let { foo, ...rest } = data

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

既然我们正在处理对象,实现一些属性不可枚举就不应该困难。通过使用Object.definePropertyenumerable: false:

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

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

现在我们成功地隐藏了额外的属性!

const { foo, ...rest } = data

// rest
{
  bar: 'bar'
}

生成器

如果你不喜欢生成器的使用,我们可以通过纯粹的函数实现,遵循这篇文章

Object.defineProperty(clone, Symbol.iterator, {
  enumerable: false,
  value() {
    let index = 0
    const 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。更多信息:支持的浏览器

function createIsomorphicDestructurable<
  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 = createIsomorphicDestructurable(
  { foo, bar } as const,
  [foo, bar] as const
)

let { foo, bar } = obj
let [foo, bar] = obj
>
CC BY-NC-SA 4.0 2021-PRESENT © 韩海Tempest