【源码阅读】arrify 转数组

207 阅读7分钟

arrify 介绍

  1. npm 地址

  2. 库的作用

    • 将传入的值转换为数组

读源码整体流程

从单元测试入手,一步一步跟着断点进行调试来阅读源码,在读的过程中产生的问题,如果不是阻塞性的问题都将其记下来,先将整体流程完成,然后再去解决之前产生的问题。

单元测试

该项目的单元测试是 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({})) 的问题

  1. TS 中的 unknown 是什么意思

  2. TS 中的 Record 是什么意思

  3. arrify({}) 中没有提到 string, unknown 为什么在 TS 中要这样声明

    由这段代码产生 expectError(arrify(['🦄'] as const).push('')) 的问题

  4. TSconst 是什么意思

将这些疑问滞后先去看源码,因为这些问题不阻塞阅读源码

源码

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 代码就比较简单了

  1. null 或者 undefined 则返回空数组
  2. 数组的话直接将其返回
  3. 字符串则将其转为数组返回

第四个 if 的代码需要看一下,在这里产生了较多的疑问

  1. Symbol.iterator 是什么
  2. ... 的作用

到这里源码就看完了,很简单的有一段代码,接下来解决前边的哪些问题

疑问

测试用例中的问题

TS 中的 unknown 是什么意思

  1. unknown 类型不能赋值给除了 unknownany 的其他任何类型,使用前必需显式进行指定 unknown 以外的其他类型,或是在有条件判断情况下能够隐式地进行类型推断的情况。
  2. 可以把任何值赋值给 unknown
  3. 如果联合类型中有 unknown ,那么最终得到的都是 unknown 类型;但如果联合类型中有 any 则联合类型会相当于 any
  4. 在交叉类型中(取多个类型的交集),任何类型都可以覆盖 unknown 类型。这意味着将任何类型与 unknown 相交不会改变结果类型。
  5. 对于 unknown 只可以使用 ==、!=、===、!== 四个运算符,因为如果我们不知道我们正在使用的值的类型,大多数运算符不太可能产生有意义的结果。

使用场景

获取一个 JSON 字符串,这个时候不知道 JSON 是一个什么类型,可以将其定义为 unknown,这样在后续使用的时候必须对值进行类型检查才可使用。

unknownany 的区别

unknown 是一种安全的 anyany 是什么类型都接受,而 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({}))

  1. Record<string, unknown> 中为什么要声明 string 类型

    这里其实 string/number/symbol 都可以(下边解答),给 string 猜测是因为 objectkey 默认是 string 的。

  2. Record<string, unknown> 中为什么要声明 unknown 类型

    unknown 的作用就是在不知道目标对象是一个什么类型的时候用的

  3. 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 三个类型

  4. keyof 的作用

    • 将某个类型中所有的 key 作为一个联合类型返回
    • 如果当前这个类型中有符串或数字索引签名(key 是动态的: [x: number]),keyof 将返回这些类型,(返回 x 的类型 number)

TSconst 是什么意思

  1. 对于基本类型的值,是将这个值定义为类型,例如这个值是 a,那么 a as const 就是将 a 定义为一个类型,在传入的时候只能是 a 这个类型,而不能传入 'a'
  2. 对于引用类型是将这个对象转换为一个只读对象,里边的 keyvalue 就被锁定了,就变成了一个只读的元组类型。

源码中的问题

Symbol.iterator 是什么?什么类型的值会有这个值

  1. 这个值指向了默认迭代器,Array、TypedArray、String、Map、Set 这些类型会默认拥有这个属性。

    TypedArray 的解释: 只是一个概念,指的是所有描述二进制 buffer 的类数组(Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array)

  2. 那这里为什么使用拥有默认迭代器的来判断,而不是通过是否可迭代,也就是说这俩有什么区别?(知道这个答案之后觉得自己挺搞笑的^_^)

    • 一个值只有拥有迭代器才是可迭代的值

解决 Symbol.iterator 是什么问题时引出的问题

  1. {} 没有默认迭代器,为什么采用 for...in 可以遍历

    • 因为后者是采用获取对象的可枚举属性来实现的遍历,而不是判断自身是否拥有默认迭代器。
  2. 在写代码的过程中不建议使用 for...in

    引用贺老知乎的回答

    • 一直以来 for...in 的行为就受到诟病,在许多场合我们只希望拿 own properties,因此不得不在 for...in 里用 hasOwnProperty/propertyIsEnumerable 之类的方法过滤。而这是一个低效又麻烦的方式。所以在ES5之后,我们一般不再使用 for...in,而是使用 Object.keys(obj).forEach(...) 来遍历所有 own properties。
  3. 为什么对象没有默认迭代器(为什么要这样设计)

    引用贺老知乎回答上一个问题的评论里的答复

    引用阮老师的阮一峰 ECMAScript 6 (ES6) 标准入门教程

    对象之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。(个人理解,对象 key 的类型不同,在遍历时顺序就会不一样)本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

... 的作用

MDN 展开语法

  1. 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。

    字面量一般指 [1, 2, 3] 或者 {name: "mdn"}

参考资料

  1. Typescript高级类型Record
  2. [译] TypeScript 3.0: unknown 类型
  3. TypeScript keyof 操作符
  4. TS 官网 - keyof
  5. MDN - iterator
  6. TypedArray
  7. MDN 展开语法
  8. 为什么es6里的object不可迭代?
  9. 阮一峰 ECMAScript 6 (ES6) 标准入门教程