展开运算符与可迭代对象

在ES6中引入了很多新的语法,三个点...的语法就是其中之一。该语法有两个用处,一个是作为展开运算符,还可以作为rest参数。其作为剩余参数的方法用法比较简单,也不是本文的关注内容,所以下边说一下关于展开运算符的一些操作以及引出迭代器与生成器。

基础用法

展开运算符正如其名,就是可以将目标展开,常用于展开数组。

1
2
let arr = [1,2,3,4,5]
console.log(...arr) // 1 2 3 4 5

不过我使用展开运算符最多的是用来合并两个数组

1
2
3
let arr1 = [1,2]
let arr2 = [...arr1, 3]
console.log(arr2) // [1,2,3]

当我们传递参数的时候,有时候也能用到它

1
2
3
4
5
function sum(a, b, c){
return a + b + c
}
let arr = [1, 2, 3]
let res = sum(...arr) // res = 6

展开运算符就只能用于展开数组吗?当然不是,在对象中也是可用的

1
2
3
4
5
6
7
8
9
let obj1 = {
name: 'tom',
age: 19
}
let obj2 = {
...obj1,
gender: 'male'
}
console.log(obj2) // {name: 'tom', age: 19, gender: 'male'}

自然,他也可以用来拷贝一个对象,不过要注意这并不是一个深拷贝

1
2
let obj = {name: 'tom'}
let objCopy = {...obj}

既然能合并数组,自然也可以合并对象了。

1
2
3
let obj1 = {name: 'tom'}
let obj2 = {age: 19, name: 'tony'}
let obj = {...obj1, ...obj2}

发现了吗?好像跟数组的合并不太一样了,有重复的属性怎么办呢

我希望你可以自己去试一下然后得出这个答案,不过在这里我就直说了,合并有重复的属性时会以右边的对象属性为准哦。

所以obj = {age: 19, name: 'tony'}

不过这里的坑不止这一个,我们合并对象跟拷贝对象是一样的,合并操作执行的也是一个浅拷贝,如果源对象中还有引用数据类型,使用过程中要注意。

展开运算符可以操作数组和对象,但不仅限于此。

我们将字符串展开为数组会怎么做呢?第一时间肯定想到了split()方法吧,其实展开运算符也可以实现这个效果。

1
2
let str = 'abc'
let arr = [...str]

常用操作就说这些了,展开运算符是不是很简单呢?好像是的

那,[...undefined]会输出什么呢?

可迭代对象

我们打开控制台输入[...undefined]会发生什么呢

1
2
3
[...undefined]

Uncaught TypeError: undefined is not iterable

控制台告诉我们undefined是不可迭代的

什么是可迭代对象呢?

在ES6中常用的集合对象和字符串都是可迭代对象

通过生成器创建的迭代器也是可迭代对象

要成为可迭代对象, 一个对象必须实现 iterator 方法。可迭代对象具有Symbol.iterator属性,即具有Symbol.iterator属性的对象都有默认迭代器。

我们可以用Symbol.iterator来访问对象的默认迭代器

迭代器

迭代器是一种特殊对象,每一个迭代器都有一个next方法,该方法会返回一个对象,对象包括value和done属性。

我们实现一个简单的迭代器来看一下方便理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 生成迭代器对象的函数
function createIterator(items){
let i = 0;
return {
next(){
// 判断i是否大于等于items的长度,也就是遍历是否完成的标识
let done = ( i >= items.length)
// 如果done为false,则尚未完成遍历
// 返回value为items[i]的值,然后将i自增,这里就不说i++和++i的区别了
// 如果done为true,则便利完成,返回value为undefined
let value = !done ? items[i++] : undefined;
return {
done,
value
}
}
}
}
// 上边的函数是用于返回迭代器对象的,函数不是迭代器,返回的对象才是
const a = createIterator([1,2,3])
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()); //{value: 2, done: false}
console.log(a.next()); //{value: 3, done: false}
console.log(a.next()); //{value: undefined, done: true}

生成器

迭代器是一个对象,我们前边通过一个函数来生成了迭代器

提到迭代器就不可避免的印出来生成器

生成器是一个函数,用来返回迭代器

生成器函数是ES6提出来的,声明的时候需要声明一个带有*星号的函数,同时你还会用到yield

还是拿到代码中看一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
// 生成器是函数,函数名前边加一个*
function *createIterator(){
yield 1;
yield 2;
yield 3;
}
// 生成一个迭代器,调用生成器的时候就不需要加*了
const a = createIterator();
// 生成的迭代器并不会自动执行,还是需要调用next才可以
console.log(a.next()); //{value: 1, done: false}
console.log(a.next()); //{value: 2, done: false}
console.log(a.next()); //{value: 3, done: false}
console.log(a.next()); //{value: undefined, done: true}

我们可以发现生成器与我们自己定义一个迭代器可以实现相同的效果,这就是yield的作用

生成器的yield关键字有一个神奇的功能,就是当你执行一次next(),那么就会执行一个yield后边的内容,然后语句终止运行

yield使用限制

当然yield使用是有限制的,只能在生成器函数内部使用,不能在非生成器的其他函数内部使用。

1
2
3
4
5
6
7
8
9
function *createInerator(items){
// 这里是生成器的内部,可以使用yield
items.map((value,key) => {
// 这里会出现语法错误,因为我们将yield用在了map的回调函数中,而不是生成器函数内部
yield value;
})
}
const a = createIterator([1,2,3])
console.log(a.next()) // 无输出

生成器函数表达式

函数表达式就是匿名函数,就这么叫没什么好纠结的

1
2
3
4
5
const createIterator = function *(){
yield 1;
yield 2;
}
const a = createIterator()

对象中添加生成器函数

一个普通对象通常是长这个样子的

1
2
3
const obj = {
a: 1
}

我们可以在对象中添加一个生成器,也就是一个带星号的方法

1
2
3
4
5
6
7
8
const obj = {
a: 1,
*createIterator(){
yield this.a
}
}
const a = obj.createIterator()
console.log(a.next) // {value: 1, done: false}

理解可迭代对象

前边我们说到过什么是可迭代对象,但描述并不容易理解,所以由此引出了迭代器与生成器,现在我们应该可以好好理解什么是可迭代对象了。

再次强调一遍,迭代器是一个对象,生成器是一个返回迭代器的函数

凡是通过生成器生成的迭代器,都是可迭代对象,也就可以通过for..of遍历。

1
2
3
4
5
6
7
8
9
function *createIterator(){
yield 1;
yield 2;
yield 3;
}
const a = createIterator()
for (let value of a){
console.log(value) // 1 2 3
}

我们前边也提到过可迭代对象都具有Symbol.iterator属性,我们可以通过这个属性访问迭代器

1
2
3
4
5
6
7
8
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
const a = createIterator();
const s = a[Symbol.iterator]();//使用Symbol.iterator访问迭代器
console.log(s.next()) //{value: 1, done: false}

通过Symbol.iterator来检测一个对象是否可迭代也是可行的

1
typeof obj[Symbol.iterator] === "function"

创建可迭代对象

前边已经提到过了,在es6中,数组、set、map、字符串都是可迭代对象。

但是默认情况下定义的对象是不可迭代的

1
2
3
4
5
6
7
cosnt obj = {
items: []
}
obj.items.push(1)
for(let x of obj){
console.log(x) // _iterator[Symbol.iterator] is not a function
}

所以就需要我们手动的给对象添加[Symbol.iterator]属性,使其成为一个可迭代的对象

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
items: [],
*[Symbol.iterator](){
for (let item of this.items){
yield item
}
}
}
obj.items.push(1)
for(let x of obj){
console.log(x) // 现在就可以通过for of来访问obj了
}

内建迭代器

我们上边提到过,数组、set、map默认都是可迭代对象,也就是说他们内部已经实现了迭代器,并且其提供了3中迭代器的函数调用

  1. entries():返回键值对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 数组
const arr = ['a', 'b', 'c']
for(let v of arr.entries()){
console.log(v)
}
// [0, 'a'] [1, 'b'] [2, 'c']

// Set
const arr = new Set(['a', 'b', 'c'])
for(let v of arr.entries()) {
console.log(v)
}
// ['a', 'a'] ['b', 'b'] ['c', 'c']

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.entries()) {
console.log(v)
}
// ['a', 'a'] ['b', 'b']
  1. values():返回键值对的value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//数组
const arr = ['a', 'b', 'c'];
for(let v of arr.values()) {
console.log(v)
}
//'a' 'b' 'c'

//Set
const arr = new Set(['a', 'b', 'c']);
for(let v of arr.values()) {
console.log(v)
}
// 'a' 'b' 'c'

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.values()) {
console.log(v)
}
// 'a' 'b'
  1. keys:返回键值对的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//数组
const arr = ['a', 'b', 'c'];
for(let v of arr.keys()) {
console.log(v)
}
// 0 1 2

//Set
const arr = new Set(['a', 'b', 'c']);
for(let v of arr.keys()) {
console.log(v)
}
// 'a' 'b' 'c'

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.keys()) {
console.log(v)
}
// 'a' 'b'

for…of循环解构对象

对象本身不支持迭代,我们前边给对象添加生成器使其成为可迭代对象。这次我们给对象添加新的生成器使其可以使用for…of循环解构key和value

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
a: 1,
b: 2,
*[Symbol.iterator](){
for(let i in obj){
yield [i, obj[i]]
}
}
}

for (let [key, value] of obj){
console.log(key, value) // 'a' 1 'b' 2
}

高级功能

高级功能就是涉及到一些传参、抛出异常、委托生成器等等功能,由于自己也不是特别理解,避免误导就不写太多,简单写一下传参和生成器的返回语句的用法吧,具体原理不太懂,如果后续有了比较好的理解,会修改这篇文章的。

传参

不是特别理解这个概念,给大家看一下示例

1
2
3
4
5
6
7
8
9
function *createIterator(){
let first = yield 1;
yield first + 2
}
const a = createIterator()
console.log(a.next()) // {value: 1, done: false}
// 为什么是NaN呢?此时first为undefined,并不是第一句next返回的1
console.log(a.next()) // {value: NaN, done: false}
console.log(a.next()) // {value: undefined, done: true}

我们可以给next传入参数,它会替代掉上一个next的yield返回值

1
2
3
4
5
6
7
8
9
function *createIterator(){
let first = yield 1;
yield first + 2
}
const a = createIterator()
console.log(a.next()) // {value: 1, done: false}
// 此时first为5,传入的参数代替了上一句的yield返回值first
console.log(a.next(5)) // {value: 7, done: false}
console.log(a.next()) // {value: undefined, done: true}

生成器返回语句

生成器中添加return表示退出操作,函数中使用return应该没什么好说的。

1
2
3
4
5
6
7
8
function *createIterator(){
yield 1;
return;
yield 2;
}
cosnt a = createIterator()
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()) // {value: undefined, done: true}

委托生成器

委托生成器就是生成器之间进行嵌套

只能给一个简单示例,太深入的用法不懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 生成器1
function *aIterator(){
yield 1;
yield 2;
}
// 生成器2
function *bIterator(){
yield 3;
}
// 生成器3
function *cIterator(){
yield *aIterator()
yield *bIterator()
}
// 会把内部的迭代器按顺序依次进行迭代,只有前一个的内容迭代完成,才会对后一个迭代器的内容进行迭代
const i = cIterator()
console.log(i.next()); // {value: 1, done: false}
console.log(i.next()); // {value: 2, done: false}
console.log(i.next()); // {value: 3, done: false}
console.log(i.next()); // {value: undefined, done: true}

好吧,就先到这里了,后续有新理解再更新内容吧。

作者

胡兆磊

发布于

2021-12-13

更新于

2022-10-23

许可协议