003-手写源码之学习arrify的源码

122 阅读5分钟

手写源码之学习arrify的源码

一、前言

二、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的一些设定了。

看来优秀的开源项目都是要单元测试的,下次我们仔细学学单元测试吧。