源码共读 | arrify

966 阅读7分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与

课程: 源码共读第一期|arrify

项目依赖


先来看看项目package.json文件中的依赖配置:

  • "ava": "^3.15.0",

  • "tsd": "^0.14.0",

  • "xo": "^0.39.1"

  • ava 单元测试工具,相比于Mocha主打性能,支持并发测试。

  • tsd d.ts类型文件的检测工具,检测手写的类型文件是否正确。

  • xo 基于eslint的代码质量检测工具,默认使用约定配置,不用去手动添加大量的配置,做到开箱即用。

目录结构


.
├── .eslintignore
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .npmrc
├── index.d.ts
├── index.js
├── index.test-d.ts
├── license
├── package.json
├── readme.md
├── test.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];
}

整个逻辑比较清晰:

  • 首先判断value如果是nullundefined直接返回空数组;
  • 如果本身就是数组那么直接返回自身;
  • 如果是字符串,则返回一个长度为1的字符串数组;
  • 如果拥有迭代器,则执行迭代器返回结果;
  • 如果都不满足,则返回元素是自身的长度为1的数组。

Symbol

ES6中引入了一种新的原始数据类型 Symbol,相比于其他类型,他的作用是表示独一无二的值。

Symbol("123") == Symbol("123"); // false

正是因为这种特性,Symbol可以用来定义对象唯一的属性名,来标识具有唯一性的值。

需要把Symbol用作属性名时可以这样做:

const key = Symbol("key1")

// 1
let obj = {}
obj[key] = "value"

// 2
let obj = {
 [key]: "value"
}

// 3
let obj = {}
Object.defineProperty(obj,key,{value: 'value'})

不过需要注意的是,Symbol作为属性名时,该属性不会被for...in...访问,也不会被Object.keys,Object.getOwnPropertyNames返回,如果你需要读取一个对象的Symbol属性,可以通过使用Object.getWonPropertySybolsReflect.ownKeys获取到, 有时也可以使用Symbol来定义私有属性或方法,来避免通过for...in被暴露。

const key = Symbol("key1")

let obj = {}
obj[key] = "value"

for(let i in obj){
	console.log(i) // output nothing
}

Object.keys(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(key1)]
Reflect.ownKeys(obj) // [Symbol(key1)]

Symbol值是唯一的,而Symbol.for类型类似Symbol的单例模式,他会存储在全局共享的Symbol, 可以传递一个key,如果该key存在,那么返回对应的Symbol,如果不存在就创建返回新Symbol

const key1 = Symbol("key")
const key2 = Symbol.for("key")
const key3 = Symbol.for("key")

key1 === key2 // false
key2 === key3 // true

Symbol还有一个方法keyFor,它可以传入一个Symbol.for生成的Symbol,返回相应的key,如果传入非Symbol.for生成的Symbol则不会有返回

const key1 = Symbol.for("key")
const key2 = Symbol("key")

Symbol.keyFor(key1) // key
Symbol.keyFor(key2) // undefined

除了自己创建的 symbol,JavaScript 还内建了一些在 ECMAScript 5 之前没有暴露给开发者的 symbol,它们代表了内部语言行为。它们可以使用以下属性访问:

  • Symbol.asyncIterator

一个返回对象默认的异步迭代器的方法。被 for await of 使用。

  • Symbol.hasInstance

一个确定一个构造器对象识别的对象是否为它的实例的方法。被 instanceof 使用。

  • Symbol.isConcatSpreadable

一个布尔值,表明一个对象是否应该 flattened 为它的数组元素。被 Array.prototype.concat() 使用。

  • Symbol.iterator

一个返回一个对象默认迭代器的方法。被 for...of 使用。

  • Symbol.match 一个用于对字符串进行匹配的方法,也用于确定一个对象是否可以作为正则表达式使用。被 String.prototype.match() 使用。

  • Symbol.matchAll

一个用于对字符串中所有匹配项进行匹配的方法。被 String.prototype.matchAll() 使用。

  • Symbol.replace

一个替换匹配字符串的子串的方法。被 String.prototype.replace() 使用。

  • Symbol.search

一个返回一个字符串中与正则表达式相匹配的索引的方法。被 String.prototype.search() 使用。

  • Symbol.species

一个用于创建派生对象的构造器函数。

  • Symbol.split

一个在匹配正则表达式的索引处拆分一个字符串的方法.。被 String.prototype.split() 使用。

  • Symbol.toPrimitive

一个将对象转化为基本数据类型的方法。

  • Symbol.toStringTag

用于对象的默认描述的字符串值。被 Object.prototype.toString() 使用。

  • Symbol.unscopables

拥有和继承属性名的一个对象的值被排除在与环境绑定的相关对象外。

迭代协议

说完了Symbol我们再来看一下Symbol.iterator, 这之前我们先了解一下什么是迭代协议

迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现,迭代协议具体分为两个协议:可迭代协议迭代器协议

  • 可迭代协议
  • 迭代器协议
  1. 可迭代协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为, 以便在for...of语句中使用,而for...of语句在可迭代对象上创建一个迭代循环,以调用相应的迭代器来实现循环。要成为可迭代对象,该对象必须实现 @@iterator方法,这意味着对象必须有一个键为 [Symbol.iterator]的属性,属性值是一个无参数的函数,其返回值为一个符合迭代器协议的对象。

简单的说可迭代协议就是☞实现 [Symbol.iterator]属性,并且该属性值得方法需要返回一个符合迭代器协议对象。

那么什么是迭代器协议呢?

  1. 迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

一个对象只有实现了满足要求的 next() 方法,一个对象才能成为迭代器,要求就是next()方法是无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象。

我们来看看IteratorResult的定义:

  • done 可选 如果迭代器能够生成序列中的下一个值,则返回 false 布尔值。(这等价于没有指定 done 这个属性。) 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

  • value 可选 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

实际上,两者都不是严格要求的;如果返回没有任何属性的对象,则实际上等价于 { done: false, value: undefined }

如果需要手动实现迭代器如下即可:

const obj = {}

// 实现可迭代协议
obj[Symbol.iterator] = function(){
    // 实现迭代器协议
	let index = 0
	return {
		next:()=>{
			if(index<3){
				index++;
				return { value: index }
			}else{
				return { done: true }
			}
		}
	}
}

[...obj] // 1,2,3

也可以将包含next()方法的自身返回,这样结构更清晰:

const obj = {}

obj['index'] = 0
// 实现迭代器协议
obj['next'] = function(){
	if(this.index<3){
		this.index++;
		return { value: this.index }
	}else{
		return { done: true }
	}
}
// 实现可迭代协议
obj[Symbol.iterator] = function(){
    return this
}

[...obj] // 1,2,3

当然也可以使用生成器对象来实现:

const obj = {}

// 实现可迭代协议
obj[Symbol.iterator] = function* (){
    yield 1;
	yield 2;
	yield 3;
}

[...obj] // 1,2,3

在JavaScript中一些类型原生具有Iterator接口:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

类型推断

来看看函数的类型推断

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

基本结构与实现的逻辑一致,需要了解的就是Iterable<inter T>

Iterable<T>typescript内置的迭代器接口,其定义如下:

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface Iterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

type IteratorResult<T, TReturn = any> = 
IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

可以看到相应结构与之前介绍的迭代协议一致,所有通过观察内置类型Iterable,也可以帮助我们理解迭代协议。

单元测试

arrify库使用ava完成单元测试,作法是jest区别不大。

但它还有一个index.test-d.ts的测试文件,似乎使用typescript类型推断来进行测试.

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));

expectType<[number]>(arrify(1));

expectAssignable<[Record<string, unknown>]>(arrify({}));

expectType<[number, string]>(arrify([1, 'foo']));

expectType<Array<string | boolean>>(arrify(new Set<string | boolean>(['🦄', true])));

expectType<number[]>(arrify(new Set([1, 2])));

expectError(arrify(['🦄'] as const).push(''));

expectType<[number, number] | []>(arrify(false ? [1, 2] : null));

expectType<[number, number] | []>(arrify(false ? [1, 2] : undefined));

expectType<[number, number] | [string]>(arrify(false ? [1, 2] : '🦄'));

expectType<[number, number] | [string]>(arrify(false ? [1, 2] : ['🦄']));

expectAssignable<number[] | [boolean]>(arrify(false ? [1, 2] : true));

expectAssignable<number[] | [number]>(arrify(false ? [1, 2] : 3));

expectAssignable<number[] | [Record<string, unknown>]>(arrify(false ? [1, 2] : {}));

expectAssignable<number[] | [number, string]>(arrify(false ? [1, 2] : [1, 'foo']));

expectAssignable<number[] | Array<string | boolean>>(arrify(false ? [1, 2] : new Set<string | boolean>(['🦄', true])));

expectAssignable<number[] | [boolean] | [string]>(arrify(false ? [1, 2] : (false ? true : '🦄')));
  • expectType 期望类型是否完全匹配
  • expectAssignable 期望类型满足匹配,类似extends约束
  • expectError 期望抛出错误