【源码共读】第33期 | arrify 转数组

367 阅读4分钟

1. 前言

2. 项目结构说明

2.1 目录结构

arrify
├─index.js //入口文件
├─package.json
├─test.js // 测试文件

2.2 依赖命令说明

依赖:

"scripts": {
    "test": "xo && ava && tsd"
},
"devDependencies": {
    "ava": "^3.15.0",
    "tsd": "^0.14.0",
    "xo": "^0.39.1"
}
  • xo:一个开箱即用的 Linter,内部是 ESLint,但 Lint 规则都预置好了,不接受 eslintrc 配置
  • tsd:为类型定义编写测试,创建一个 .test-d.ts后缀的文件
  • ava:Node.js 环境下的测试运行器,执行根目录下 test.js 测试文件

此处只关注调试,执行ava即可。

3. 源码解析

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

内部执行逻辑如下:

  • 如果 valuenullundefined ,就会返回空数组。

  • 如果 value 是数组,返回 value 本身。

  • 如果 value 数据类型是 string,将 value 放到数组中返回。

  • 判断 valueSymbol.iterator 属性是一个函数,则进行扩展运算符操作。

  • 如果都不是,则返回 [ value ]

咋一看,第三步string的判断完全多余,如果你是这么想,证明你没有了解迭代器的相关知识。

3.2 迭代器

3.2.1 概念

提供一种统一的接口机制,来处理所有数据结构的遍历。有迭代器就可以使用for...of方法进行遍历。

判断一个类型是否可遍历,该类型的原型上是否含有Symbol.iterator属性。调用该属性,返回的一个对象含有next方法,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。

含有迭代器的类型有:

  • 字符串String
  • 数组Array
  • 映射Map
  • 集合Set
  • arguments对象
  • NodeList等DOM集合类型

3.2.2 手写迭代器

例如对象中没有,可以自己手动添加一个:

const obj = {
        0: "jack",
        length: 2,
        name: 'mark',
        [Symbol.iterator]: function () {
                let count = 0;
                let keys = Object.keys(obj);
                let length = keys.length;
                const that = this;
                return {
                        next() {
                                return {
                                        value: that[keys[count++]],
                                        done: count > length,
                                };
                        },
                };
        },
};

let iterator = obj[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

for (const iter of obj) {
    console.log(iter);
}

image.png

3.2.3 迭代器使用

应用:比如vue的源码中这样一段代码keys.values().next().value实际就是用到迭代器。

image.png

例如:

let cache = new Map()
cache.set('1', 1)
cache.set('2', 2)
cache.set('3', 3)
// 生成器的代码
// 使用cache.keys().next().value 相当于取了最先存进去的那个值
console.log(cache.keys()); // [Map Iterator] { '1', '2', '3' } 返回一个新的迭代对象,其中包含 Map 对象中所有的键
console.log(cache.keys().next()); //{ value: '1', done: false } 迭代器对象可以调用next方法获取{value,done}对象方绘制
console.log(cache.keys().next().value);//1

image.png

返回是迭代对象,就可以使用next方法进行调用。

3.2.4 默认调用迭代器场景

  1. for...of

  2. 数组解构赋值[]

对数组、字符串、和 SetMap、自定义对象(实现迭代器)结构进行解构赋值时,会默认调用 Iterator 接口。

// 数组
const arr = [1, 2, 3];
const [a, b, c] = arr;// 1,2,3
// 字符串
const str = "hello";
const [d, e, f, g, h] = str;
const s1List = [...str] 

const mySet = new Set([1, 2, 3]);
const [i, j, k] = mySet;

const myMap = new Map([
  ["key1", "value1"],
  ["key2", "value2"],
  ["key3", "value3"]
]);

for (const [key, value] of myMap) {
  console.log(key, value);
}

const myObj = {
  [Symbol.iterator]() {
    let count = 0;
    const values = [1, 2, 3, 4, 5];
    return {
      next() {
        if (count < values.length) {
          return { value: values[count++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

const [l, m, n, o, p] = myObj;

image.png

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

  1. 扩展运算符

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

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

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

// 如何看到调用

const myObj = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        console.log('打印***i',i)
        if (i < 5) {
          return { value: i++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

const arr = [...myObj];
console.log('打印***arr',arr)

image.png

3.3 arrify对于string判断

对于arrify 第三步string的判断是必要的,原因是js中string类型解构默认调用 Iterator 接口,会导致返回出错。

4. 总结

通过arrify转数组的学习,学习迭代器有:

  1. 默认调用的场景
  2. 返回迭代器使用场景 了解每个写法有一定的考虑,不是简单一眼看就觉得可以省略。深入了解,才能理解作者的真实想法。O^O

参考文章:

详解转数组 arrify 的源码

【源码阅读】arrify|如何把一个值转换为一个数组?