- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第33期,链接:第33期 | arrify 转数组
这篇笔记会更多涉及源代码和对应的知识点,坐稳了吗,Let's go!
首先,我们看下index.js里的内容
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];
}
这个库主要功能就是无论传入什么都给你返回一个数组,这样就来到我们今天的第一个笔记点:Symbol.iterator
Symbol.iterator
迭代器模式
迭代器Iterator是一种接口,为不同的数据结构提供统一的访问机制。
一般而言,迭代器Iterator的遍历过程如下:
- 创建一个指针对象,指向当前数据结构的起始位置,迭代器对象本质是一个指针对象
- 第一次调用指针对象的next方法,将指针指向了数据结构的第一个成员
- 第二次调用指针对象的next方法,将指针指向了数据结构的第二个成员
- 不断调用,以此类推,直到它指向了数据结构的结束位置
每次调用next方法一般至少会有两个信息,一个代表当前成员的值,另一个则是布尔值,表示遍历是否结束
上述我认为是对迭代器模式一个大概的理解,具体在不同语言中肯定会有不同的实现方式。
JS的默认Iterator接口
在JavaScript中,有的数据结构有默认的Iterator接口,也就是说他们可以通过上述方法进行遍历。而这个默认的Iterator接口就是放在数据结构的 Symbol.iterator 属性里。
原生具备Iterator接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的arguments对象
- NodeList对象
我们拿最熟悉的数组举例
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator](); // 调用这个接口返回一个迭代器对象
iter.next(); // { value: 'a', done: false }
iter.next(); // { value: 'b, done: false }
iter.next(); // { value: 'b, done: false }
iter.next(); // { value: undefined, done: true }
其他的数据结构,例如对象,Iterator接口需要自己部署在那个属性上,他们默认的 Symbol.iterator 属性是undefined。
萌新如我可能会问,那Map不是更强大的Objcet吗(从定义上来说),为什么Map可以而Object却不行呢?
这个问题可以试着打印一下它们,也许就会有更为直观的感受了~
扩展运算符在这里的作用
扩展运算符是其中一个会默认调用Iterator接口的场合。而且它提供了一种便利,可以将任何部署了Iterator接口的数据结构转为数组
const str = "hello";
[...str]; // ['h', 'e', 'l', 'l', 'o']
所以源码中想要表达的意思显而易见了呀~ 而且为什么要先对array和string做一个约束,你明白了吗~~
生成器 Generator
本来想“就事论事”的,但既然谈到了迭代器,而且若川给的参考链接也有生成器内容,不如就把他们一起消化了吧~
建立在刚刚迭代器模式的基础上,生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。
我的理解是:它是一个状态机,内部有多个状态。你可以像刚刚迭代器遍历一样逐个访问里面的状态。
它的典型特征是:1. function* 2. yield 让我们看个例子
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
// 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,
// 只有调用next方法才会执行函数里的代码
const hw = helloWorldGenerator();
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }
与Iterator接口的关系
由于Generator函数就是迭代器生成函数,可以把Generator函数赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口
function* objEntries() {
let keys = Reflect.ownKeys(this);
for (let key of keys) {
yield [key, this[key]]
}
}
const obj = { a:1, b:2 }
obj[Symbol.iterator] = objEntries;
[...obj]
如果上面的Reflect.ownKeys()换成Object.keys(),结果会变成这样
所以差别其实不大,看你的需求了,这两个方法的范围不一样,具体细节就不展开了,笔记越写越长了😂
其实我在写笔记的时候,一直在翻阮一峰老师写的ES6标准入门,这本书我前两年看过,还有圈画的痕迹,现在再次看的时候感觉当时没记住什么... (汗颜)
但我觉得这次写完,我下次不会再这么一头雾水了,不管是对的还是错的(🤦♀️),我对它们产生了比较具体的印象,输出的才是自己的~~
TypeScript知识点
接下去我们来读index.d.ts
export default function arrify<ValueType>(
value: ValueType
): ValueType extends (null | undefined)
? [] // eslint-disable-line @typescript-eslint/ban-types
: ValueType extends string
? [string]
: ValueType extends readonly unknown[]
? ValueType
: ValueType extends Iterable<infer T>
? T[]
: [ValueType];
泛型
泛型是什么?为什么要使用泛型?
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种。
举个例子:我们打算创建一个identity函数,这个函数会返回任何传入它的值
// 不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number { return arg; }
// 或者这样
function identity(arg: any): any { return arg; }
这样就丢失了一些信息:传入的类型与返回的类型应该是相同的
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T { return arg; }
这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
定义好之后,有两种使用方法
- 传入所有的参数,包括类型参数T
- 只传入参数,然后编译器会根据参数帮我们确定T的类型
// 以下两种方法output的类型都会为string
// 方法一
let output = identity<string>("myString");
// 方法二
let output = identity("myString");
所以,我们回到源代码,valueType 就是我们上面所说的类型参数T,我们传入某个类型的值,它的返回值类型则是后面这一长串所代表的意思,我们继续往下看。
extends表示的条件类型
条件类型有点像JS里的三元表达式
SomeType extends OtherType ? TrueType : FalseType;
如果SomeType是OtherType的一部分,则取值TrueType,反之则取值FalseType
举个例子
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // number
type Example2 = RegExp extends Animal ? number : string; // string
infer
infer 可以在 extends 的条件语句中推断待推断的类型
例如在以下示例中,使用 infer 来推断函数的返回值类型
type rtnType<T> = T extends (...args: any[]) => infer R ? R : any;
type func1 = () => number;
type func2 = () => string;
type func1ReturnType = rtnType<func1>; // number
type func2ReturnType = rtnType<func2>; // string
在这个例子中,infer R代表待推断的返回值类型,如果T是一个函数,则返回函数的返回值类型,否则返回any
unknown
unknown 和 any 有点像,他们都可以表示各种值,但是它比any更安全,因为你用它来执行任何操作都会给你提示报错
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// Object is of type 'unknown'.
}
Iterable
又回到了迭代器这个问题了,在文档中是这样写的:
Iterable表示可遍历的类型,也就是那些有Symbol.iterator属性的类型,在TS里主要指的是:Array, Map, Set, String, Int32Array, Uint32Array等等
所以 Iterable<infer T> 表示的是某种可遍历类型,里面的每一项具体类型不知,以T推断,如果ValueType符合这种情况,那么返回值为包含各种T的数组
function toArray<X>(xs: Iterable<X>): X[] {
return [...xs]
}
toArray(new Set([1,2,3,'a'])); // 返回值类型为(string | number)[]
一些npm包
tsd
用来测试你写的类型定义文件 .d.ts
ava
一个测试框架,但是根据npm trends来看Jest或者mocha用的人更多一些