原文地址: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()
虽然这对于不同的偏好和命名属性来说可能是自解释的,但重命名可能比数组解构更冗长。
两者都支持 #
如果我们可以支持两者,如何呢?利用每一边的优势,并让用户决定哪种风格更适合他们的需求。
我曾经见过一个库支持这种用法,但我不记得是哪个了。不过,这个想法从那时起就一直在我心中。现在我打算尝试一下。
我的假设是,它返回一个同时具有array
和object
行为的对象。路径是明确的,要么创建一个类似于array
的object
,要么创建一个类似于 object
的array
。
让对象像数组一样工作 #
我想到的第一个可能的解决方案是让一个对象表现得像一个数组,正如你可能知道的,数组实际上是具有数字索引和一些原型的对象。所以代码会是这样的:
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.defineProperty
与enumerable: 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