【若川视野 x源码共读】第33期 | arrify 转数组 | A20分组

113 阅读1分钟

image.png

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

  • 这是源码共读的第33期,链接:juejin.cn/post/710021…

  • 算是头一次参加若川源码共读在掘金的活动,唯一的期望就是打开自己学习源码的思路、以及打开自己的视野,后续进行不断的优化和学习

  • 真正的热爱学习,多参与、多查阅文档、多思考、形成自己的思维思想

  • 如果或许对于你有一点点启发,那么也算是另外的一份收获

  • 最后希望自己能坚持下去

  • 本文阅读思路提示

    • 1、本期要学习源码仓库地址
    • 2、在线查看源码或者本地克隆代码
    • 3、查看package.json文件
    • 4、从入口文件的入口函数开始
    • 5、查看typescript声明文件
    • 6、查看测试用例文件
    • 7、总结

1、本期要学习的源码仓库地址

2、在线查看源码或者本地克隆代码

  • 1、在线查看源码

    • mac: 在github代码仓库页面通过快捷键 command + k,再在文本框中输入 ">",即可查看到在线打开的命令行
    • window:同mac,只是快捷键改为ctrl + k
    • 通用方法: 【<>Code】 按钮点击即可查看到Codespaces对应的Tab分组
  • 2、本地克隆代码

    • 直接在本地文件夹,克隆代码

git clone git@github.com:sindresorhus/arrify.git

3、查看package.json文件

"devDependencies": {
		"ava": "^3.15.0",
		"tsd": "^0.14.0",
		"xo": "^0.39.1"
	}
  • 突然发现,这三个依赖都没见过,这里推荐一个小工具npm.devtool.tech, 这是山月大佬的自制小工具,感觉非常棒,安利一波。跳转网页后,直接输入npm库名即可查看,包的相关信息。

    • ava: 19.8k star,通过查看readme.md可以发现,原来是一个单元测试的类库,还有中文翻译版本,具体翻译课查看 github.com/avajs/ava-d… ,它最大的优势应该就是可以并行的执行测试用例,这对于IO繁重的测试就特别的友好了。
    • tsd: 1.4k star,也是star比较高的库,看似其貌不扬,但对于我个人来说,貌似又发现了新用法(以前从来没见过,涨姿势了)。检查typescript类型声明定义,让你通过xxx.test-d.ts后缀名,去为.xxx.d.ts类型声明去做测试(当然前提是你要是typescript)。
    • xo: 6.6k star,xo基于ESLint。这个项目最初只是一个可共享的ESLint配置,但它很快就发展了.我想要更简单的东西.只是输入xo就完成.没有争论.没有配置.我也有一些令人兴奋的未来计划.但是,在使用ESLint时,您仍然可以获得大部分ESLint可共享配置,来自xo优势.
  • scripts 脚本

"test": "xo && ava && tsd"
// &&相当于串行(上一个执行完毕后才会继续执行下一个)、&相当于并行
- xo: 对代码进行检查
- ava: 执行根目录的test.js文件中的测试用例
- tsd: 通过index.test-d.ts文件测试index.d.ts定义    

4、从入口文件的入口函数开始

终于找到入口文件和入口函数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、判断 null 或者undefined,则返回空的数组
  • 2、判断传入的是一个数组的话,则返回它本身
  • 3、判断传入的类型为string字符串的话,则返回一个数组长度为1的数组,第一个元素就为传入的字符串值
  • 4、应该是本文要讲解的重点

| 对于两个单词分别的理解可以查看ES6文档,阮一峰老大讲解的应该非常清楚了,这里我就不进行意义赘述

| 那么我在这里要强调一下什么呢:

  • ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值。(引用自ES6)

  • 原生具备 Iterator 接口的数据结构如下。

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

| 通过上面的描述你或许还没能理解,我们通过代码简单的来学习一下

let iteratorArray = new Set()
iteratorArray.add(1)
iteratorArray.add('2')
iteratorArray.add(['11', 22])

console.log(iteratorArray) /// Set(3) {1, '2', Array(2)}                
console.log(typeof iteratorArray )  /// function

| 我们对下面的obj对象封装一个简单的遍历器

let obj = {
   a: 'aa',
   b: 11,
   c: 'hellowrold'
 }

 obj = {
   ...obj,
   *[Symbol.iterator]() {
     console.log(this, 'this-item')
     yield { a: 'aa' }
     yield { b: 11 }
     yield { c: 'hellowrold' }
   }
 }

 for (const item of obj) {
   console.log(item, 'item')
 }
 // {a: 'aa'} 'item'
 // {b: 11} 'item'
 // {c: 'hellowrold'} 'item'
 
 
 // 那么obj 如果调用 arrify,对于其他的Object不生效
 // 可以统一在Object.prototype[Symbol.iterator] 上实现
 console.log(arrify(obj))
 
 //(3[{…}, {…}, {…}]0: {a: 'aa'}1: {b: 11}2: {c: 'hellowrold'}length: 3[[Prototype]]: Array(0)

| 再回到源码

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

现在再来看这个判断条件,其实就是判断value本身是否具有 Symbol.iterator属性,并且类型为function;也可以说成value是否是可遍历的,是否拥有接口 Iterator;还可以说成是否属于哪几种数据类型: Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象,其中Array和String已经单独判断就可以不考虑了。

  • 5、其他类型默认返回跟上述第三条一致

5、拓展一下这个es5 迭代协议

  1. 迭代协议的定义:迭代协议具体分为两个协议:可迭代协议迭代器协议
  2. 我们先理解下什么是可迭代协议,也可以看上面的MDN介绍。
let someString = "hi";
typeof someString[Symbol.iterator];          // "function"

可以看到string内置了Symbol.iterator这个属性,是一个方法。

let iterator = someString[Symbol.iterator]();
iterator + "";                               // "[object String Iterator]"

iterator.next();                             // { value: "h", done: false }
iterator.next();                             // { value: "i", done: false }
iterator.next();                             // { value: undefined, done: true }

这个写法是不是很眼熟,如果了解过生成器函数,应该就能看懂了。 我们顺便来写一个可迭代对象吧

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
[...myIterable]; // [1, 2, 3]

for ( let item of myIterable){
    console.log(item)
} 
// 1
// 2
// 3
[...myIterable]
//(3) [1, 2, 3]
//可见可以通过生成器函数,实现可迭代对象,

我们可以通过提供自己的 @@iterator 方法,重新定义迭代行为:

// 必须构造 String 对象以避免字符串字面量 auto-boxing
var someString = new String("hi");
someString[Symbol.iterator] = function() {
  return { // 只返回一次元素,字符串 "bye",的迭代器对象
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

我们再写一个简单的迭代器

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

let it = makeIterator(['哟', '呀']);

console.log(it.next().value); // '哟'
console.log(it.next().value); // '呀'
console.log(it.next().done);  // true

使用生成器

function* makeSimpleGenerator(array) {
    let nextIndex = 0;

    while(nextIndex < array.length) {
        yield array[nextIndex++];
    }
}

let gen = makeSimpleGenerator(['哟', '呀']);

console.log(gen.next().value); // '哟'
console.log(gen.next().value); // '呀'
console.log(gen.next().done);  // true



function* idMaker() {
    let index = 0;
    while (true) {
        yield index++;
    }
}

let gen = idMaker();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...

1.看到这里的小伙伴如果不了解迭代器的,估计也已经又看蒙了。

2.生成器对象到底是一个迭代器,还是一个可迭代对象?

let aGeneratorObject = function* (){
    yield 1;
    yield 2;
    yield 3;
}();

typeof aGeneratorObject.next;
// 返回"function", 因为有一个 next 方法,所以这是一个迭代器

typeof aGeneratorObject[Symbol.iterator];
// 返回"function", 因为有一个 @@iterator 方法,所以这是一个可迭代对象

aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// 返回 true,因为 @@iterator 方法返回自身(即迭代器),所以这是一个格式良好的可迭代对象

[...aGeneratorObject];
// 返回 [1, 2, 3]

console.log(Symbol.iterator in aGeneratorObject)
// 返回 true,因为 @@iterator 方法是 aGeneratorObject 的一个属性

强烈建议大家读一下高程4第七章节,顺便再看下不可枚举属性。

总结:


    1.迭代器对象的概念实现了正式的`Iterable`接口,而且可以通过迭代器`Iterator`消费。
    2.任何实现`Iterable`接口的数据结构都可以被实现`Iterator`接口的结构“消费”(consume)。**迭代器**(iterator)是按需创建的一次性对象。每个迭代器都会关联一个**可迭代对象**,而迭代器会暴露迭代其关联可迭代对象的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是`Iterable``Iterator`的强大之处。

3.可迭代协议 
实现`Iterable`接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现`Iterator`接口的对象的能力。
在ECMAScript中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的`Symbol.iterator`作为键。
这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
let num = 1;
let obj = {};
// 这两种类型没有实现迭代器工厂函数 
console.log(num[Symbol.iterator]); // undefined 
console.log(obj[Symbol.iterator]); // undefined
let str = 'abc'; 
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c'); 
let els = document.querySelectorAll('div');
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] } 
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
console.log(map[Symbol.iterator]); // f values() { [native code] } 
console.log(set[Symbol.iterator]); // f values() { [native code] } 
console.log(els[Symbol.iterator]); // f values() { [native code] }
// 调用这个工厂函数会生成一个迭代器 
console.log(str[Symbol.iterator]()); // StringIterator {} 
console.log(arr[Symbol.iterator]()); // ArrayIterator {} 
console.log(map[Symbol.iterator]()); // MapIterator {} 
console.log(set[Symbol.iterator]()); // SetIterator {} 
console.log(els[Symbol.iterator]()); // ArrayIterator {}

6、查看测试用例文件

import test from 'ava';
import arrify from './index.js';

test('main', t => {
	t.deepEqual(arrify('foo'), ['foo']);  // 判断字符串
	t.deepEqual(arrify(new Map([[1, 2], ['a', 'b']])), [[1, 2], ['a', 'b']]);  //Map对象
	t.deepEqual(arrify(new Set([1, 2])), [1, 2]);  //Set对象
	t.deepEqual(arrify(null), []);   // null
	t.deepEqual(arrify(undefined), []);  // undefined

	const fooArray = ['foo'];
	t.is(arrify(fooArray), fooArray);  //正常的数组
});

测试用例本身并没有什么花期呼哨的边界值