源码学习—arrify 转数组
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
【若川视野 x 源码共读】第33期 | arrify 转数组 点击参与本期源码共读。
源码地址
arrify 的用法
在看源代码之前,我们要熟悉一下 arrify 包的用法。下面是 arrify 包官方的参考用例(点击上述的源码地址,滑到下面的readme文件,就会看到这段用例代码)。
import arrify from 'arrify';
arrify('🦄');
//=> ['🦄']
arrify(['🦄']);
//=> ['🦄']
arrify(new Set(['🦄']));
//=> ['🦄']
arrify(null);
//=> []
arrify(undefined);
//=> []
在这段用例代码中,我们可以看到从 arrify 包中导入了 arrify 函数,使用 arrify 函数,对字符串,数组,Set,null
,undefined
这些类型的数据都转换为了数组,其中 null
和 undefined
转换为了空数组。
源码解析
明白了它是大概怎么用的之后,我们再来看看它的源码都做了些什么。
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
的参数,我们现在来一步一步分析函数体内部有什么逻辑:
- 首先对
value
进行null
或undefined
的判断,如果value
等于其中一个值,就会返回空数组,否则进行第二个 if 判断。 - 在第二个 if 判断逻辑中,使用
Array.isArray()
方法对value
是否是数组的判断,如果value
是数组,那么就返回value
本身,否则进行第三个 if 判断。 - 在第三个 if 判断逻辑中,会判断
value
数据类型是否是string
,如果是string
,就将value
放到数组中,并返回该数组,否则进行第四个 if 判断。 - 在第四个 if 判断逻辑中,会判断
value
的Symbol.iterator
属性是否是一个函数,如果是,就用扩展运算符(...)将value
的每个元素放入数组中,并返回该数组,否则执行最后一个 return 语句。对于Symbol.iterator
属性,我们暂时把它理解成为唯一标识属性,下文会说明关于iterator
的相关知识。 - 最后一个 return 语句,实际上跟第三个 if 判断的返回值一样,在这里相当于返回 arrify 函数的默认值。
细心的小伙伴可能注意到了一个问题,既然第三个 if 判断的返回值和最后的 return 语句返回值是一样的,为什么不把它们两个的返回值合并到一起?也就是为什么不去掉第三个 if 判断的逻辑呢?这跟接下来要说明的 iterator
也有关系。
Iterator(遍历器)
iterator
,中文被翻译为遍历器,它到底是个啥东西,怎么使用它,别着急,喝口水先,我们即将打开 iterator
的神奇大门!
说到遍历两个字,我们最先想到的是数组,在 ES6 之前,数组确实是最经常被遍历的一种数据结构,在 ES6 之后,又出现了 Map
和 Set
的数据结构,拿 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()
方法,该方法返回一个代表当前元素信息的对象,这个对象具有 value
和 done
两个属性,value
属性代表当前元素的值,done
属性是一个布尔值,表示是否遍历结束。
Iterator 对象的具体遍历过程是这样的:
- 第一次调用
next()
方法,返回数据结构中第一个元素的信息对象。 - 第二次调用
next()
方法,返回数据结构中第二个元素的信息对象。 - 不断调用
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 接口;Array
,Map
,Set
,String
,TypedArray
,函数的 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];
}
// ......
}
它主要是把 Map
,Set
,TypedArray
,函数的 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'
判断虽然返回的值看似是一样的,但是前者是返回该数组本身,后者是返回一个新的数组,新数组每个元素的值等于原数组每个元素的值。
总结
- Iterator 接口为各种不同的数据结构提供统一的访问机制,任何数据结构只要有了 Iterator 接口,就可以通过
for...of
进行遍历。 - 对象(
Object
)没有默认的 Iterator 接口;Array
,Map
,Set
,String
,TypedArray
,函数的arguments
对象,NodeList
对象有默认的 Iterator 接口。 - 解构赋值,扩展运算符会默认调用 Iterator 接口。
- 函数体内对参数进行多个判断时,顺序很重要,应该最先判断特殊的情况,再接着判断比较广泛的情况。
转载自:https://juejin.cn/post/7201500589758136376