手写源码之学习arrify的源码
一、前言
-
-
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第33期,链接:详情点此查看。
-
二、arrify介绍
相信有部分童鞋跟我一样,之前没接触过arrify这个库,不清楚这个库是干嘛的。
arrify,顾名思义,就是使...数组化,具体来说:
- 如果传入null或者不传参数,返回一个空数组;
- 如果传入可迭代数据,则将迭代结果放到数组里面返回,其中要注意的是,字符串也是可迭代类型,但是字符串我们直接整个放在数组里面返回
- 如果又不可迭代,又不是null或者undefiend,那么直接整个放到数组里面返回
用代码来表示的话,就是:
import arrify from 'arrify';
arrify('🦄');
//=> ['🦄']
arrify(['🦄']);
//=> ['🦄']
arrify(new Set(['🦄']));
//=> ['🦄']
arrify(null);
//=> []
arrify(undefined);
//=> []
三、查漏补缺-什么是迭代器
简单来说,迭代器是一个对象,它有一个next方法,这个next方法返回一个有done和value属性的对象,这个done指示是否已经迭代到末尾了,value则是本次迭代的值。我们可以利用next方法来显式地迭代:
const arr = [1,2,3]
const ite = arr[Symbol.iterator]()
console.log(ite.next())
// { value: 1, done: false }
for ... of 关键字和扩展运算符,在本质上就是迭代器的语法糖。
大部分Javascript原生对象已经实现了迭代协议,常见的已经实现了迭代器协议的对象有: Array 、 String 、 Map 、Set 、 Arguments 、NodeList
关于迭代器更多的介绍,可以查看MDN迭代器的介绍。
四、自己动手实现一个arrify
看了上面的介绍,相信大家马上就有思路来实现一个这样的函数:
const arrify = value => {
if ([null, undefined].includes(value)) {
return []
}
if (value[Symbol.iterator]) {
if (typeof value === 'string') {
return [value]
}
return [...value]
}
return [value]
}
看上去应该是没有问题的,我们来看看arrify这个库是怎么实现的吧。
五、源码阅读思路及工具
一般流行库的源码都在GitHub上面,我们找到arrify的源码:arrify地址,按下句号键。就可以打开一个web版的vscode界面,方便我们切换查看代码文件。
拿到一个js代码库的时候,我们首先要查看package.json文件,重点关注里面的入口文件、运行脚本等信息
{
"name": "arrify",
"version": "3.0.0",
"description": "Convert a value to an array",
"license": "MIT",
"repository": "sindresorhus/arrify",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module", // 本库使用的模块化类型,默认是commonJS,这里指定了是ESModule
"exports": "./index.js", // 入口文件路径
"engines": { // 运行环境
"node": ">=12"
},
"scripts": { // 脚本
"test": "xo && ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"array",
"arrify",
"arrayify",
"convert",
"value",
"ensure"
],
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.39.1"
}
}
可以看到,入口文件就是根目录下面的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];
}
入口文件没有引用其他文件了,看来这里面就是这个库的核心代码了。
阅读源码之后,发现跟我们的实现方式还是有一定不同的。
区别1: 为什么数组要单独处理?
我们的需求是将传入的值转换为数组,如果本身就是数组,那自然不用处理,可以直接返回。如果不单独拿出来,在走有迭代器逻辑的时候,会遍历一次数组,增加不必要的操作。
区别2: 为什么迭代器的判断要用typeof判断是否是function?
我思考了一下,应该是因为迭代器属性可以被覆写。覆写之后失去了迭代能力,我们再用扩展运算符就会报错。
const a = [1,2,3]
a[Symbol.iterator] = '123'
if (a[Symbol.iterator]) {
const b = [...a]
// 报错 VM4221:2 Uncaught TypeError: a is not iterable
}
区别3: 字符串为什么要放到有迭代器逻辑的外面处理?
我们的需求是,字符串直接放在数组里面返回,跟他是否有迭代器无关,在传入的值是字符串类型时,放在外面可以少一个判断,提升代码运行效率。
延伸思考: 我们修改a=[1,2,3]的迭代器,会影响其他的数组吗?
照理说,迭代器Symbol.iterator是在Array.prototype上面的,我们修改了一个数组实例的迭代器,那么所有的数组的迭代器都应该会被修改。让我们试试:
const a = [1,2,3]
a[Symbol.iterator] = '123'
const b = [4,5,6]
typeof b[Symbol.iterator] // function
这是为什么呢?
我们分别打印一下a和b发现,a比b多了个Symbol.iterator属性。忽然之间恍然大悟,js里面万物皆对象,数组也是一个对象,我们用a[Symbol.iterator] = '123'以为改写了原型上面的方法,实际上只是给a=[1,2,3]添加了一个属性!
六、其他
我们可以看到,这个库里面除了index.js以外,还有index.t.ts以及test.js等文件,他们分别是干什么的呢?
这个index.t.ts应该就是这个库的ts类型声明文件
test.js应该就是单元测试相关的了,那index.test-d.ts应该就是单元测试的类型声明文件了。
license应该就是开源协议
其他的.开头的文件就是指定编辑器或者git或者npm的一些设定了。
看来优秀的开源项目都是要单元测试的,下次我们仔细学学单元测试吧。