今日学习内容
昨天在看直播的时候,看到一个挺有意思的问题,和 解构赋值 以及 自定义 Hook / 组件设计 有关,于是顺着这个点认真想了一下,记录下来。
平时我们非常习惯使用对象或数组的解构来获取数据:
const obj = { a: 1, b: 2, c: 3 };
const { a, b } = obj;
a; // 1
b; // 2
const arr = [1, 2, 3];
const [x, y] = arr;
x; // 1
y; // 2
在封装组件或者自定义 Hooks 的时候,我们同样需要向外暴露多个值,这时通常会有两种选择:
- 返回一个 对象
- 返回一个 数组
那么问题就来了:返回对象和返回数组,有什么本质区别吗?
一个真实的使用场景
假设我们有这样一个 Hook:
function useUser() {
const name = "张三";
const age = 18;
return { name, age };
}
正常使用时,我们会这样写:
const { name, age } = useUser();
但如果在同一个作用域中,多次调用 useUser,就会遇到一个很现实的问题 —— 命名冲突:
const { name: name1, age: age1 } = useUser();
const { name: name2, age: age2 } = useUser();
或者只能退而求其次:
const user1 = useUser();
const user2 = useUser();
user1.name;
user2.name;
而如果 useUser 返回的是数组,情况就会简单很多:
function useUser() {
const name = "张三";
const age = 18;
return [name, age];
}
const [name1, age1] = useUser();
const [name2, age2] = useUser();
因为 数组解构是基于位置的,变量名完全由使用方决定。
在这种「需要多次使用同一个 Hook」的场景下,数组解构确实更灵活一些。
有没有可能:同时支持对象解构和数组解构?
理想情况下,我希望一个 Hook:
- 只使用一次时,可以用对象解构,语义清晰
- 多次使用时,可以用数组解构,避免命名冲突
但直接这样写是行不通的:
function useUser() {
const name = "张三";
const age = 18;
return { name, age };
}
const [a, b] = useUser();
运行时会直接报错:
TypeError: return value is not iterable
原因在于:普通对象默认是不可迭代的。
可迭代对象指的是可以被
for...of遍历的对象,比如Array、String、Map、Set等。
解决方案一:数组也是对象(在数组上挂属性)
已知:数组本质上也是一种对象,因此我们可以在数组上添加自定义属性。
const arr: any[] = [];
arr.foo = "bar";
console.log(arr.foo); // 'bar'
基于这一点,可以这样实现 useUser:
function useUser() {
const name = "张三";
const age = 18;
const result: any = [name, age];
result.name = name;
result.age = age;
return result;
}
此时返回值既可以被当作数组:
const [a, b] = useUser();
console.log(a, b); // 张三 18
也可以被当作对象:
const { name, age } = useUser();
console.log(name, age); // 张三 18
解决方案二:使用 Object.assign
上一个方案的本质是「在数组对象上挂属性」。
既然如此,也可以使用 Object.assign,把对象的属性直接拷贝到数组上:
function useUser() {
const name = "张三";
const age = 18;
const result: any = [name, age];
Object.assign(result, { name, age });
return result;
}
使用方式与方案一完全一致:
const { name, age } = useUser();
const [a, b] = useUser();
这种写法在语义上会更直观一些。
解决方案三:让对象本身变成可迭代对象
既然对象默认不可迭代,那有没有可能 让对象本身支持迭代 呢?
答案是可以的 —— 通过实现 Symbol.iterator。
官方定义如下:
为了实现可迭代,对象必须实现
[Symbol.iterator]()方法。
示例实现:
function useUser() {
const name = "张三";
const age = 18;
const result: any = { name, age };
result[Symbol.iterator] = function* () {
yield this.name;
yield this.age;
};
return result;
}
此时:
const { name, age } = useUser();
console.log(name, age); // 张三 18
const [a, b] = useUser();
console.log(a, b); // 张三 18
对象解构读取属性,数组解构走自定义迭代器,两者互不冲突。
小结
从 JavaScript 语言层面来看,上述三种方案都是 合法且可行的。
但从工程实践的角度:
- 同时支持数组解构和对象解构,会增加理解和维护成本
- API 的“清晰性”往往比“灵活性”更重要
在真实项目中更常见的选择是:
- 业务型 Hook:直接返回对象,语义最清晰
- 类似
useState的 Hook:返回数组,更符合社区约定
这次记录与其说是在找一个“最佳写法”,不如说是在探索 JavaScript 语言能力与工程设计之间的边界。