本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第33期,链接
源码功能解析
在阅读源码的时候首先从package.json开始,找到项目的入口文件以及依赖的资源。
导出的的是index.js文件的内容,index.js内容如下
export default function arrify(value) {
if (value === null || value === undefined) {
return [];
}
if (Array.isArray(value)) {
return value;
}
// 标记1
if (typeof value === "string") {
return [value];
}
// 标记2
if (typeof value[Symbol.iterator] === "function") {
console.log("Symbol.iterator", Symbol.iterator);
return [...value];
}
// 默认返回
return [value];
}
从上可以看出arrify方法主要是将一个值转为数组的方法,转化规则如下:
- 数据值为 null,undefined就返回空数组
- 数据类型是数组的话就返回本身
- 如果数据类型是字符串的话就返回[value]
- 如果
value[Symbol.iterator] === "function"返回[...value] - 其他返回 [value]
问题/疑惑
问题1: 为什么单独判断字符串类型?
尝试去掉字符串类型的判断
// 修改前
arrify('foo') // ['foo']
// 修改后
arrify('foo') // ['f', 'o', 'o']
可以看到,字符串类型的值被拆解,结果和想象有了偏差。说明它执行了标记2处的逻辑,也就是字符串类型的数据typeof value[Symbol.iterator] === "function"
问题2: typeof value[Symbol.iterator] === "function"判断是什么意思呢?
判断value是否是可迭代对象
什么是可迭代对象?
可迭代对象实现了 @@iterator 方法,该方法返回一个迭代器对象,再根据迭代器对象获取要迭代的值。
可迭代对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性
通俗的说就是:可迭代对象的原型对象都拥有一个Symbol.iterator方法
引用MDN的一个例子,自定义一个可迭代对象:
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
在js中内置的可迭代对象有: String、Array、TypedArray、Map 和 Set,函数的 arguments 对象 NodeList 对象 都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator 方法
所以字符串类型进入了[标记2]的判断
是不是可以用Array.from代替...呢?
Array.from和...都是可以实现数组浅拷贝。可以根据类数组对象或可迭代对象创建一个新的数组。
尝试使用Array.from,替换以后
if (typeof value[Symbol.iterator] === 'function') {
// return [...value];
return Array.from(value);
}
运行单元测试出现以下错误
对应eslint 的 prefer-spread校验,eslint,关于这个的讨论github issues
eslint 对 prefer-spread校验的描述
// ES5 apply 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2)
// ES6 扩展运算符 写法
arr1.push(...arr2);
通俗的讲就是: 某些场景扩展运算符可以替代Function.prototype.apply调用可变参数 ,但是Array.from不可以,而且apply 方法使用起来并不是很方便
个人理解的就是在代码层面更建议使用扩展运算符,可以更简洁的编写。所以没有使用Array.from
为什么不转换类数组对象?
类数组对象
- 有length属性;
- 能通过索引访问;
- 不是数组,无法使用 Array.prototype 上的方法。
常见的类数组对象有内置的
arguments、NodeList,还有自定义对象例如{ length: 1, 0: 'a' }
arrify转换类数组对象的结果是[value]
在 github.com/sindresorhu… 里作者解释了不转换类数组对象的原因: 为了保持api的单一性,避免增加使用门槛。(如果把类数组对象看作是数组而非对象,假设用户希望类数组对象被转换为数组中的元素该怎么办。)如果需要对类数组对象进行转换,开发者应该在外部自行转换它。
项目配置工具方面
TS的类型配置
面对这种参数类型不定的数据类型定义根据具体的参数类型定义具体的函数返回值。
使用的是ts条件类型、高阶类型 和extends结合三元运算符联合定义函数的输入值类型
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];
TS的推导泛型参数 infer
只能在 extends 的右边使用【因为必须保证这个已知类型是由右侧的泛型推出来的】
示例:
type getIntersection<T> = T extends (a: infer P,b: infer P) => void ? P : never;
// 推导泛型 函数返回值类型由 a,b的数据类型决定
type Intersection = getIntersection<(a: string, b: number)=> void> // string & number
推导遵循以下几点:
- P 只在一个位置占位:直接推出类型
- P 都在协变(A | B -> A & B)位置占位: 推出占位类型的联合
- P 都在逆变(类型推导到其超类型的过程,函数本身的变型规则和参数相反的话)位置占位:推出占位类型的交叉(目前主要是函数的参数逆变)
- P 既在顺变位置又在逆变位置(双变):只有占位类型相同才能使 extends 为 true,且推出这个占位类型
package.json文件
项目的开发依赖有 xo, ava, tsd
- xo: 一个开箱即用的 Linter,内部是
ESLint,但 Lint 规则都预置好了,不接受 eslintrc 配置 - ava: Node.js 环境下的测试运行器,执行根目录下
test.js测试文件 - tsd: 为类型定义编写测试,创建一个
.test-d.ts后缀的文件就行
项目中的脚本命令 test: "xo && ava && tsd":
- 先让 xo 对 js 和 ts 文件做 Lint
- 再交给 ava 执行 test.js单元测试, 测试 index.js
- 最后是 tsd 执行 index.test-d.ts, 测试ts类型配置 index.d.ts
总结
arrify源码阅读学习到的知识点
- 什么是可迭代对象?如何判断可迭代对象
可迭代对象的原型对象都拥有一个
Symbol.iterator方法,返回一个迭代器对象
可以通过typeof value[Symbol.iterator] === 'function' 判断是否是可迭代对象
- Array.from和...更推荐使用扩展运算符
- 复杂函数参数的类型定义,readonly修饰符,
infer是ts的推导泛型参数 - 项目工具方面
- 使用xo,代替eslint,减少eslint配置;xo是 JavaScript/TypeScript (ESLint 包装器)具有很好的默认值
- 使用ava,简单便捷的测试运行器
- 使用tsd,为ts类型定义文件编写测试