1. 前言
-
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第33期,链接:arrify 转数组
-
github仓库地址 arrify
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];
}
内部执行逻辑如下:
-
如果
value是null或undefined,就会返回空数组。 -
如果
value是数组,返回value本身。 -
如果
value数据类型是string,将value放到数组中返回。 -
判断
value的Symbol.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);
}
3.2.3 迭代器使用
应用:比如vue的源码中这样一段代码keys.values().next().value实际就是用到迭代器。
例如:
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
返回是迭代对象,就可以使用next方法进行调用。
3.2.4 默认调用迭代器场景
-
for...of
-
数组解构赋值[]
对数组、字符串、和 Set、Map、自定义对象(实现迭代器)结构进行解构赋值时,会默认调用 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;
解构赋值的语法内部就是不断调用 Iterator 接口的 next() 方法,拿到 value 属性值(也就是该元素值)对解构赋值的变量进行一一对应的赋值。
- 扩展运算符
扩展运算符(…)也会调用默认的 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)
3.3 arrify对于string判断
对于arrify 第三步string的判断是必要的,原因是js中string类型解构默认调用 Iterator 接口,会导致返回出错。
4. 总结
通过arrify转数组的学习,学习迭代器有:
- 默认调用的场景
- 返回迭代器使用场景 了解每个写法有一定的考虑,不是简单一眼看就觉得可以省略。深入了解,才能理解作者的真实想法。O^O
参考文章: