- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第33期,链接:【若川视野 x 源码共读】第33期 | arrify 转数组。
arrify 介绍
-
库的作用
- 将传入的值转换为数组
读源码整体流程
从单元测试入手,一步一步跟着断点进行调试来阅读源码,在读的过程中产生的问题,如果不是阻塞性的问题都将其记下来,先将整体流程完成,然后再去解决之前产生的问题。
单元测试
该项目的单元测试是 index.test-d.ts 文件
/* eslint-disable @typescript-eslint/ban-types */
import {expectType, expectError, expectAssignable} from 'tsd';
import arrify from './index.js';
expectType<[]>(arrify(null)); // []
expectType<[]>(arrify(undefined)); // []
expectType<[string]>(arrify('🦄')); // ['🦄']
expectType<string[]>(arrify(['🦄'])); // ['🦄']
expectAssignable<[boolean]>(arrify(true)); // [true]
expectType<[number]>(arrify(1)); [1]
expectAssignable<[Record<string, unknown>]>(arrify({})); // [{}]
...
每一条测试通过期望得到的值与最终结果是否相等来判断用例是否通过。
同时也可以看到 arrify 函数的实现是在 index.js 中
这里产生了一些疑问,放到后面一起解决:
由这段代码产生
expectAssignable<[Record<string, unknown>]>(arrify({}))的问题
-
TS中的unknown是什么意思 -
TS中的Record是什么意思 -
arrify({})中没有提到string, unknown为什么在 TS 中要这样声明由这段代码产生
expectError(arrify(['🦄'] as const).push(''))的问题 -
TS中const是什么意思
将这些疑问滞后先去看源码,因为这些问题不阻塞阅读源码
源码
export default function arrify(value) {
if (value === null || value === undefined) {
return [];
}
if (Array.isArray(value)) {
return value;
}
if (typeof value === 'string') {
return [value];
}
if (typeof value[Symbol.iterator] === 'function') {
return [...value];
}
return [value];
}
前三个 if 代码就比较简单了
null或者undefined则返回空数组- 数组的话直接将其返回
- 字符串则将其转为数组返回
第四个 if 的代码需要看一下,在这里产生了较多的疑问
Symbol.iterator是什么...的作用
到这里源码就看完了,很简单的有一段代码,接下来解决前边的哪些问题
疑问
测试用例中的问题
TS 中的 unknown 是什么意思
unknown类型不能赋值给除了unknown或any的其他任何类型,使用前必需显式进行指定unknown以外的其他类型,或是在有条件判断情况下能够隐式地进行类型推断的情况。- 可以把任何值赋值给
unknown。 - 如果联合类型中有
unknown,那么最终得到的都是unknown类型;但如果联合类型中有any则联合类型会相当于any。 - 在交叉类型中(取多个类型的交集),任何类型都可以覆盖
unknown类型。这意味着将任何类型与unknown相交不会改变结果类型。 - 对于
unknown只可以使用==、!=、===、!==四个运算符,因为如果我们不知道我们正在使用的值的类型,大多数运算符不太可能产生有意义的结果。
使用场景
获取一个 JSON 字符串,这个时候不知道 JSON 是一个什么类型,可以将其定义为 unknown,这样在后续使用的时候必须对值进行类型检查才可使用。
unknown 和 any 的区别
unknown 是一种安全的 any,any 是什么类型都接受,而 unknown 是什么类型都不接受,在使用类型为 unknown 的值之前必须进行类型检查。
TS 中的 Record 是什么意思
Record<K,T> 构造具有给定类型 T 的一组属性 K 的类型,在将一个类型的属性映射到另一个类型的属性时,Record 非常方便。
interface InfoType {
id: number
name: string
}
let result: Record<number, InfoType> = {
0: { id: 1, name: "张三" },
1: { id: 2, name: "李四" },
2: { id: 3, name: "王二麻" },
}
// 最终结果
// 0: { id: 1, name: "张三" },
// 1: { id: 2, name: "李四" },
// 2: { id: 3, name: "王二麻" }
arrify({}) 中没有提到 string, unknown 为什么在 TS 中要这样声明
expectAssignable<[Record<string, unknown>]>(arrify({}))
-
Record<string, unknown>中为什么要声明string类型这里其实
string/number/symbol都可以(下边解答),给string猜测是因为object的key默认是string的。 -
Record<string, unknown>中为什么要声明 unknown 类型unknown的作用就是在不知道目标对象是一个什么类型的时候用的 -
1 中为什么说
string/number/symbol都可以看一下
Record的源码type Record<K extends keyof any, T> = { [P in K]: T; }K是通过extends keyof any得到的类型,keyof any得到的类型就是string/number/symbol三个类型 -
keyof的作用- 将某个类型中所有的
key作为一个联合类型返回 - 如果当前这个类型中有符串或数字索引签名(key 是动态的: [x: number]),keyof 将返回这些类型,(返回 x 的类型 number)
- 将某个类型中所有的
TS 中 const 是什么意思
- 对于基本类型的值,是将这个值定义为类型,例如这个值是
a,那么a as const就是将a定义为一个类型,在传入的时候只能是a这个类型,而不能传入'a'。 - 对于引用类型是将这个对象转换为一个只读对象,里边的
key和value就被锁定了,就变成了一个只读的元组类型。
源码中的问题
Symbol.iterator 是什么?什么类型的值会有这个值
-
这个值指向了默认迭代器,
Array、TypedArray、String、Map、Set这些类型会默认拥有这个属性。TypedArray 的解释: 只是一个概念,指的是所有描述二进制 buffer 的类数组(
Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array) -
那这里为什么使用拥有默认迭代器的来判断,而不是通过是否可迭代,也就是说这俩有什么区别?(知道这个答案之后觉得自己挺搞笑的^_^)
- 一个值只有拥有迭代器才是可迭代的值
解决 Symbol.iterator 是什么问题时引出的问题
-
{}没有默认迭代器,为什么采用for...in可以遍历- 因为后者是采用获取对象的可枚举属性来实现的遍历,而不是判断自身是否拥有默认迭代器。
-
在写代码的过程中不建议使用
for...in引用贺老知乎的回答
- 一直以来
for...in的行为就受到诟病,在许多场合我们只希望拿 own properties,因此不得不在for...in里用hasOwnProperty/propertyIsEnumerable之类的方法过滤。而这是一个低效又麻烦的方式。所以在ES5之后,我们一般不再使用for...in,而是使用Object.keys(obj).forEach(...)来遍历所有 own properties。
- 一直以来
-
为什么对象没有默认迭代器(为什么要这样设计)
对象之所以没有默认部署
Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。(个人理解,对象 key 的类型不同,在遍历时顺序就会不一样)本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5 没有Map结构,而 ES6 原生提供了。
... 的作用
-
可以在函数调用/数组构造时,将数组表达式或者
string在语法层面展开;还可以在构造字面量对象时,将对象表达式按key-value的方式展开。字面量一般指 [1, 2, 3] 或者 {name: "mdn"}