likes
comments
collection
share

源码学习—arrify 转数组

作者站长头像
站长
· 阅读数 14

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与

【若川视野 x 源码共读】第33期 | arrify 转数组 点击参与本期源码共读

源码地址

github.com/sindresorhu…

arrify 的用法

在看源代码之前,我们要熟悉一下 arrify 包的用法。下面是 arrify 包官方的参考用例(点击上述的源码地址,滑到下面的readme文件,就会看到这段用例代码)。

import arrify from 'arrify';

arrify('🦄');
//=> ['🦄']

arrify(['🦄']);
//=> ['🦄']

arrify(new Set(['🦄']));
//=> ['🦄']

arrify(null);
//=> []

arrify(undefined);
//=> []

在这段用例代码中,我们可以看到从 arrify 包中导入了 arrify 函数,使用 arrify 函数,对字符串,数组,Set,nullundefined这些类型的数据都转换为了数组,其中 nullundefined转换为了空数组。

源码解析

明白了它是大概怎么用的之后,我们再来看看它的源码都做了些什么。

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

arrify 函数是通过 export default 向外暴露的,如果是通过 export 向外暴露的,那么在使用 import 语句导入时,需要对导入的包进行解构,导入语句就变成了 import {arrify} from 'arrify'

arrify 函数只接收一个名为value的参数,我们现在来一步一步分析函数体内部有什么逻辑:

  1. 首先对 value 进行 nullundefined 的判断,如果 value 等于其中一个值,就会返回空数组,否则进行第二个 if 判断。
  2. 在第二个 if 判断逻辑中,使用 Array.isArray()方法对 value 是否是数组的判断,如果 value 是数组,那么就返回 value 本身,否则进行第三个 if 判断。
  3. 在第三个 if 判断逻辑中,会判断 value 数据类型是否是 string,如果是 string,就将 value 放到数组中,并返回该数组,否则进行第四个 if 判断。
  4. 在第四个 if 判断逻辑中,会判断 valueSymbol.iterator 属性是否是一个函数,如果是,就用扩展运算符(...)将 value 的每个元素放入数组中,并返回该数组,否则执行最后一个 return 语句。对于 Symbol.iterator 属性,我们暂时把它理解成为唯一标识属性,下文会说明关于 iterator 的相关知识。
  5. 最后一个 return 语句,实际上跟第三个 if 判断的返回值一样,在这里相当于返回 arrify 函数的默认值。

细心的小伙伴可能注意到了一个问题,既然第三个 if 判断的返回值和最后的 return 语句返回值是一样的,为什么不把它们两个的返回值合并到一起?也就是为什么不去掉第三个 if 判断的逻辑呢?这跟接下来要说明的 iterator 也有关系。

Iterator(遍历器)

iterator,中文被翻译为遍历器,它到底是个啥东西,怎么使用它,别着急,喝口水先,我们即将打开 iterator 的神奇大门!

说到遍历两个字,我们最先想到的是数组,在 ES6 之前,数组确实是最经常被遍历的一种数据结构,在 ES6 之后,又出现了 MapSet 的数据结构,拿 Set 类型数据来说,我们会发现不能像数组那样用最原始的 for 循环去遍历元素,比如下面的这个例子:

// for 循环遍历数组
cons arr = [1,2,3];
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // 1 2 3
}

// for 循环遍历 Set
const s = new Set([1,2,3])
for (let j = 0; j < s.size; j++) {
    console.log(s[j]); // undefined undefined undefined
}

那怎么办?那怎么遍历 Set 类型数据?基于这种情况,就需要一种统一的接口机制,来处理所有数据结构的遍历,遍历器(Iterator)就是这样一种机制,它是一个接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要有了 Iterator 接口,就可以通过 for...of 进行遍历。

Set 默认具有遍历器接口,所以可以使用 for...of 遍历!

// for 循环遍历 Set
const s = new Set([1,2,3])
for (const ele of s) {
    console.log(ele); // 1 2 3
}

默认的 Iterator 接口是部署在该数据结构的 [Symbol.iterator] 属性,Symbol.iterator 是一个表达式,返回 Symbol 对象的 iterator 属性,这是一个 Symbol 对象预定义好的特殊值,所以要放在方括号内;[Symbol.iterator] 属性的值是一个函数,它返回一个 Iterator 接口,所以 [Symbol.iterator] 函数又称 Iterator 生成函数。

Iterator 接口其实就是一个对象,该对象具有 next() 方法,当使用 for...of 进行遍历时,内部原理每次都会调用 next() 方法,该方法返回一个代表当前元素信息的对象,这个对象具有 valuedone 两个属性,value 属性代表当前元素的值,done 属性是一个布尔值,表示是否遍历结束。

Iterator 对象的具体遍历过程是这样的:

  1. 第一次调用 next() 方法,返回数据结构中第一个元素的信息对象。
  2. 第二次调用 next() 方法,返回数据结构中第二个元素的信息对象。
  3. 不断调用 next() 方法,直到返回的信息对象中 done 属性为 true。

我们可以自己写一个 Iterator 生成函数,并模拟遍历过程:

function myIterator(array) {
    const nextIndex = 0;
    return {
        next: function() {
            return nextIndex < array.length ?
                {value: array[nextIndex++], done: false} : 
                {value: undefined, done: true}
        }
    }
}

// 遍历器接口
const myIt = myIterator([1,2,3]);
myIt.next() // { value: 1, done: false }
myIt.next() // { value: 2, done: false }
myIt.next() // { value: 3, done: false }
myIt.next() // { value: undefined, done: true }

默认的 Iterator 接口

哪些数据结构具有默认的 Iterator 接口,哪些数据结构没有呢?

对象(Object)是没有默认的 Iterator 接口;ArrayMapSetStringTypedArray,函数的 arguments 对象,NodeList 对象是具有默认的 Iterator 接口。

下面是获取 Set 的默认的 Iterator 接口和手动遍历的例子:

const s = new Set(['a','b','c']);
const iter = s[Symbol.iterator]();
iter.next(); // { value: 'a', done: false }
iter.next(); // { value: 'b', done: false }
iter.next(); // { value: 'c', done: false }
iter.next(); // { value: undefined, done: true }

默认调用 Iterator 接口的场景

除了上文讲到 for...of 遍历会默认调用 Iterator 接口之外,还有两个常见的场景也有默认调用。

解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用 Iterator 接口。

const s = new Set(['a','b','c']);
const [x,y,z] = set;
// x='a', y='b', z='c'

解构赋值的语法内部就是不断调用 Iterator 接口的 next() 方法,拿到 value 属性值(也就是该元素值)对解构赋值的变量进行一一对应的赋值。

扩展运算符

扩展运算符(…)也会调用默认的 Iterator 接口。

const str = 'hello';
[...str]  // ['h','e','l','l','o']

const arr = ['b', 'c'];
['a', ...arr, 'd']  // ['a', 'b', 'c', 'd']

扩展运算符其实可以看成是 for...of 遍历的语法糖。

回看 arrify 函数

至此,我们已经充分了解了 Iterator 的知识,现在再到回头来看第四个 if 判断到底做了些什么。

export default function arrify(value) {
    // ......
    
    if (typeof value[Symbol.iterator] === 'function') {
        return [...value];
    }

    // ......
}

它主要是把 MapSetTypedArray,函数的 arguments 对象,NodeList 对象这些类型的数据转换为数组,这也是我们在业务中常用的将具有 Iterator 接口的数据结构转换数组的方法!对于对象(Object),arrify 函数返回的值是 [value],也就是直接把这个对象放到一个数组中。

那又为什么不能去掉第三个 if 判断的逻辑呢?

因为 String 是具有默认的 Iterator 接口,如果把第三个 if 判断去掉了,那就只会走原本的第四个 if 判断逻辑,它会把字符串的每个字符作为数组元素的值,而不是把整个字符串作为数组的第一个元素值。举个例子:

原来的 arrify 函数:

import arrify from 'arrify';
arrify('123');  // ['123']

去掉第三个 if 判断的 arrify 函数:

import arrify from 'arrify';
arrify('123');  // ['1', '2', '3']

两者返回值截然不同,所以不能去掉第三个 if 判断。不仅不能去掉,而且 typeof value === 'string' 的判断必须在 typeof value[Symbol.iterator] === 'function' 前面,否则返回值还是同去掉第三个 if 判断的一样。

既然说到顺序了,那能不能改变这四个 if 语句的判断顺序呢?答案是可以的!但是 Array.isArray(value)typeof value === 'string'判断必须在 typeof value[Symbol.iterator] === 'function'判断前面。

Array.isArray(value) 判断和 typeof value[Symbol.iterator] === 'function' 判断虽然返回的值看似是一样的,但是前者是返回该数组本身,后者是返回一个新的数组,新数组每个元素的值等于原数组每个元素的值。

总结

  1. Iterator 接口为各种不同的数据结构提供统一的访问机制,任何数据结构只要有了 Iterator 接口,就可以通过 for...of 进行遍历。
  2. 对象(Object)没有默认的 Iterator 接口;ArrayMapSetStringTypedArray,函数的 arguments 对象,NodeList 对象有默认的 Iterator 接口。
  3. 解构赋值,扩展运算符会默认调用 Iterator 接口。
  4. 函数体内对参数进行多个判断时,顺序很重要,应该最先判断特殊的情况,再接着判断比较广泛的情况。