Published on

33-arrify转数组

Github 仓库地址

学习目标

  • 使用测试用例调试源码
  • Symbol.iterator 迭代器属性的作用
  • 迭代器属性的实现原理
  • ava 是 Node 环境下的测试运行器
  • xo 开箱即用的 Lint
  • tsd 为类型定义编写测试,创建一个 .test-d.ts后缀的文件就行
  • github 使用 cmd + k打开快捷指令,可以快速克隆到本地

参考文章

源码目录

.
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .npmrc
├── index.d.ts
├── index.js
├── index.test-d.ts
├── license
├── package.json
├── readme.md
└── test.js

我们大概了解一下这些文件都是干嘛用的。

  • .editorconfig:它在项目中可能不太被注意到,主要是用来定义代码编辑器格式和风格的配置文件,可以帮助开发者在不同的编辑器和 IDE 中保持一致的代码风格和格式。比如一些基本的格式和规则:缩进方式、缩进字符数、换行符类型还有文件编码等等。当编辑器打开一个项目之后,会自动对这个文件进行加载和解析,根据里面的规则来设置项目的编辑器选项,从而保证多人协作开发时能有统一的代码格式和规则,提高代码的可读性和可维护性。

  • .gitattributes:简单来说它可以定义不同类型的文件如何在 Git 中得到统一的处理,比如文本中的换行符、还有二进制文件中的 diff 合并等等。它可以让 Git 在处理文件时能更加灵活、方便,也能够解决一些文件在不同操作系统中出现的兼容性问题。同时呢,该文件也可以被用于 Git Hooks 中,在提交、合并等操作时可以自动执行一些特定的操作。

  • .npmrc:看文件名就知道,它是 npm 的配置文件,可以设置 npm 命令的行为和行为参数,比如指定 npm 从哪个源获取依赖包、设置缓存路径和设置代理等等。

    • 文件存放位置:

      • 全局的 .npmrc 文件:会位于用户的 home 目录下,对所有的项目都生效
      • 本地项目的 .npmrc 文件:位于项目根目录下,仅对项目本身生效
      • .npmrc 文件:位于某个包的目录下,仅对该包生效
    • 常用的配置内容:

      • registry:指定安装依赖包的源
      • cache:指定缓存路径
      • proxy:设置代理服务器
      • prefix:设置全局包的安装路径
      • save-prefix:指定在使用 npm install 安装依赖包的时候,依赖包的版本前缀
  • index.test-d.ts:这种文件通常是 TS 中的类型测试文件。主要是用来测试 TS 类型的正确性,确保它们是符合预期的,并且在编译时不会出现错误。具体来说,.test-d.ts 文件中通常会包含一些 TypeScript 类型的定义和测试用例。这些测试用例可以使用一些 TypeScript 工具和库(如 ts-node、Jest 等)来运行和测试,以验证类型定义是否正确,并且能够在代码编译时通过。

其他没有介绍的文件,都是大家熟知的,就不再多赘述。

package.json

相比较之前看过的源码,这里多了一个 engines 字段,它是干嘛的呢?

它的作用是用于指定我们的应用程序或者包所依赖的 Node.js 版本范围和其他运行时依赖项的版本要求。这个字段可以确保我们的应用程序或者包在安装及运行时可以拥有所需的最小版本。

而此项目中的开发依赖只有三个,虽然只有三个,但是我都没有见过(现在的工具真的太多了),分别是:

  • ava:它也是一款测试包,用于前端开发过程中的单测和集测。主打:简洁、快速、并行和轻量;
  • tsd:全名 TypeScript Definition manager,是一个用于管理 TS 类型定义文件的工具。在 TS 中,类型定义文件是用来描述 JS 库和框架中的类型信息的文件。通过使用类型定义文件,我们可以为那些没有 TS 类型声明的 JS 库添加类型支持。
  • xo:它则是一个代码规格的检查工具,主要用来在前端项目中检查代码里潜在的问题,比如语法错误、代码格式等等。ESLint 是其核心引擎,可以通过配置规则来检查代码是否符合一定的代码风格,以及是否存在潜在的错误。

源码解析

// 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];
}

要将一个值转为数组,首先我们要知道在 JS 中都有哪些数据类型,笼统一点来讲有两类:基本数据类型和引用数据类型。

在基本数据类型中有三类比较特殊的值,它们分别是:nullundefinedstring,这里为什么说 string 也比较特殊呢?不要着急,这是针对 arrify方法而言的,后面会有解释。

对于基本数据类型的值来说,我们要将其转为一个数组,最简单的做法莫过于直接包在中括号里 [我是基本数据类型]这样,但是对于 nullundefined这两类就需要特殊处理一下了,毕竟总不能返回去一个内容为 nullundefined 的数组吧,所以这里直接返回一个空数组。

而对于引用数据而言,又有很多细化的类型,为了能统一对这一类数据进行处理,arrify 的作者利用迭代器属性以及扩展运算符实现了数组的转换,也就是源码中的第四个判断。

⚠️注意:

  1. string 字符串类型是迭代器接口的,所以在源码中的第三个判断处,专门做了一次判断,为的就是不让进入到第四个判断中,对字符串使用扩展运算符,会得到一组拆开后的结果;
  2. 对于 Object 对象而言,它本身是没有迭代器接口属性的,所以它会走到最后一个 return 的地方,没错,直接被扔进了一个数组中然后返回了

至于第二个判断,就很简单了,如果传进来的参数本身就是一个数组,那直接返回就行了。

可迭代对象

这是该源码中的重点知识了,在《JavaScript 高级程序设计》第四版中是这样介绍它的:

可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解为数组或者集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

这样来看,它似乎有一点数组那味儿了,但不仅限于此。实际上迭代器的出现正是为了解决 JS 中遍历数据结构的问题,相较于数组的遍历而言,ES6 之前似乎没有一个统一的方式来遍历对象、Map、Set 等数据结构,我们需要手动遍历它们的属性或者使用一些库来实现遍历。这样的遍历其实代码量大、复杂度高且不利于维护。

所以在 ES6 中引入了迭代器的概念,可以让我们更方便地遍历各种数据结构。很多的内置类型都实现了迭代器的接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等 DOM 集合类型

我们如何检查某个值是否存在默认的迭代器属性呢——我是要被检查的值[Symbol.iterator]。在 ES6 中规定:

  • 迭代器属性必须使用 Symbol.iterator 作为键
  • 它的值是一个函数,且返回一个迭代器对象
  • 该迭代器对象包含一个 next()方法,用于返回迭代过程中的下一个值和迭代是否结束的标识,结构为 { value: any, done: boolean }

来看一个简单的例子:

const obj = {
  values: [1, 2, 3, 4, 5],
  [Symbol.iterator]() {
    let index = 0;
    const values = this.values;
    return {
      next() {
        if (index < values.length) {
          return {
            value: values[index++],
            done: false
          };
        } else {
          return {
            done: true
          };
        }
      }
    };
  }
};

for (const value of obj) {
  console.log(value);
}

这个例子中,我们为 obj 对象定义了一个迭代器属性 Symbol.iterator,它返回一个包含 next() 方法的迭代器对象。在使用 for..of 时会默认调用迭代器接口,从而实现对 obj.values 的遍历。

而除了 for...of 之外,以下的 API 都接收可迭代对象的原生语言特性:

  • 数组解构
  • 扩展运算符
  • Array.from
  • Promise.all 接收由 Promise 组成的可迭代对象
  • Promise.race 接收由 Promise 组成的可迭代对象
  • yield*操作符,在生成器中使用

这些 API 实际会在后台调用提供的可迭代对象的工厂函数,从而创建一个迭代器。

项目脚本

在该项目中只有一个 test: xo && ava && tsd运行脚本,它会先执行 xo来检查代码规范,然后执行 ava 来跑 test.js 测试用例,最后跑 tsd 来检查项目中的 ts 文件。

总结

  1. 数组外的其他数据结构的遍历,离不开迭代器,也复习了如何自定义一个迭代器属性;
  2. github 使用 cmd + k打开快捷指令,可以快速克隆到本地
  3. 其实第四步中的扩展运算符也可以使用 Array.from
  4. 如同参考文章第三篇里所说,使用 arrify 来做兜底处理,其实在实际开发中是一个不错的选择,对于一些边界 case,不能完全信任接口的返回值或者说数据源。