ES6 迭代器(Iterator)和 for...of循环使用方法

一、什么是迭代器?

生成器概念在Java,Python等语言中都是具备的,ES6也添加到了JavaScript中。Iterator可以使我们不需要初始化集合,以及索引的变量,而是使用迭代器对象的 next 方法,返回集合的下一项的值,偏向程序化

迭代器是带有特殊接口的对象。含有一个next()方法,调用返回一个包含两个属性的对象,分别是value和done,value表示当前位置的值done表示是否迭代完,当为true的时候,调用next就无效了。

ES5中遍历集合通常都是 for循环,数组还有 forEach 方法,对象就是 for-in,ES6 中又添加了 MapSet,而迭代器可以统一处理所有集合数据的方法。迭代器是一个接口,只要你这个数据结构暴露了一个iterator的接口,那就可以完成迭代。ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

二、如何使用迭代器?

1、默认 Iterator 接口

数据结构只要部署了 Iterator 接口,我们就成这种数据结构为“可遍历”(Iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 数据,就可以认为是“可遍历的”(iterable)。

可以供 for...of 消费的原生数据结构

  • Array
  • Map
  • Set
  • String
  • TypedArray(一种通用的固定长度缓冲区类型,允许读取缓冲区中的二进制数据)
  • 函数中的 arguments 对象
  • NodeList 对象

可以看上面的原生数据结构中并没有对象(Object),为什么呢?
那是因为对象属性的遍历先后顺序是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口就等于部署一种线性变换
做如下处理,可以使对象供 for...of 消费:

// code1
function Obj(value) {
    this.value = value;
    this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
    var iterator = {
        next: next
    };
    var current = this;
    function next() {
        if (current) {
            var value = current.value;
            current = current.next;
            return {
                done: false,
                value: value
            };
        } else {
            return {
                done: true
            };
        }
    }
    return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one) {
    console.log(i);
}
// 1
// 2
// 3
2、调用 Iterator 接口的场合

(1) 解构赋值

// code2
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];

(2) 扩展运算符

// code3
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

(3)Generator 函数中的 yield* 表达式(下一章介绍)

// code4
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

(4)其它场合

  • for..of
  • Array.from
  • Map()、Set()、WeakMap()、WeakSet()
  • Promise.all()
  • Promise.race()
3、for...of 循环的优势

先看看,数组 forEach 方法的缺点:

// code5
myArray.forEach(function (value) {
  console.log(value);
});

这个写法的问题在于,无法中途跳出 forEach 循环,break 命令或 return 命令都不能生效

再看看,对象 for...in 的循环的缺点:

for (var index in myArray) {
  console.log(myArray[index]);
};
  • 数组的键名是数字,但是 for...in 循环是以字符串作为键名,“0”、“1”、“2”等。
  • for...in 循环不仅可以遍历数字键名,还会遍历手动添加的期推荐,甚至包括原型链上的键。
  • 某些情况下,for...in 循环会议任意顺序遍历键名
  • for...in 遍历主要是为遍历对象而设计的,不适用于遍历数组

那么,for...of 有哪些显著的优点呢

  • 有着同 for...in 一样的简洁语法,但是没有 for...in 那些缺点
  • 不同于 forEach 方法,它可以与 break、continue 和 return 配合使用
  • 提供了遍历所有数据结构的统一操作接口
for (var n of fibonacci) {
  if (n > 1000) {
    break;
    console.log(n);
  }
}
4、各数据类型如何使用 for...of 循环?

(1)数组

  • for...of 循环允许遍历数组获得键值
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
    console.log(a); // 0 1 2 3
}
for (let a of arr) {
    console.log(a); // a b c d
}
  • for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的值
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
    console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
    console.log(i); // "3", "5", "7"
}

(2)Map 和 Set 结构

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
    console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
    console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

由上述的代码可以看出,for...of 循环遍历Map 和 Set 结构时,遍历的顺序是按照各个成员被添加进数据结构的顺序,Set 结构遍历时返回的是一个值,而 Map 结构遍历时返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值

(3)类数组对象

  • 字符串
// 普通的字符串遍历
let str = "yuan";
for (let s of str) {
  console.log(s); // y u a n
}

// 遍历含有 32位 utf-16字符的字符串
for (let x of 'a\uD83D\uDC0A') {
  console.log(x);
}
// 'a'
// '\uD83D\uDC0A'
  • DOM NodeList 对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
  p.classList.add("test");
}
  • arguments 对象
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs("a", "n");
// "a"
// "n"
  • 没有 Iterator 接口类数组对象的遍历处理
    借用 Array.from 方法处理
let arrayLike = {
    length: 2,
    0 : 'a',
    1 : 'b'
};
// 报错
for (let x of arrayLike) {
    console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
    console.log(x);
}

(4)对象
对于普通对象,不能直接使用 for...of 遍历,否则会报错,必须部署了 Iterator 接口才能使用。如下两种方法部署:

// 方法一:使用 Object.keys 方法讲对象的键名生成一个数组
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

// 方法二:使用Generator 函数将对象重新包装一下
function * entries(obj) {
    for (let key of Object.keys(obj)) {
        yield[key, obj[key]];
    }
}
for (let[key, value] of entries(obj)) {
    console.log(key, "->", value);
}
// a -> 1
// b -> 2
// c -> 3

三、迭代器应用实例

1、斐波那契数列
下面我们就使用迭代器来自定义自己的一个斐波那契数列组,我们直到斐波那契数列有两个运行前提,第一个前提是初始化的前两个数字为0,1,第二个前提是将来的每一个值都是前两个值的和。这样我们的目标就是每次都迭代输出一个新的值。

var it = { [Symbol.iterator]() {
        return this
    },
    n1: 0,
    n2: 1,
    next() {
        let temp1 = this.n1,
        temp2 = this.n2;
        [this.n1, this.n2] = [temp2, temp1 + temp2]
        return {
            value: temp1,
            done: false
        }
    }
}

for (var i = 0; i < 20; i++) {
    console.log(it.next())
}

// 
    "value": 0,
    "done": false
} {
    "value": 1,
    "done": false
} {
    "value": 1,
    "done": false
} {
    "value": 2,
    "done": false
} {
    "value": 3,
    "done": false
} {
    "value": 5,
    "done": false
}... {
    "value": 2584,
    "done": false
} {
    "value": 4181,
    "done": false
}

2、任务队列迭代器
我们可以定义一个任务队列,该队列初始化时为空,我们将待处理的任务传递后,传入数据进行处理。这样第一次传递的数据只会被任务1处理,第二次传递的只会被任务2处理… 代码如下:

var Task = {
    actions: [],
    [Symbol.iterator]() {
        var steps = this.actions.slice();
        return { [Symbol.iterator]() {
                return this;
            },
            next(...args) {
                if (steps.length > 0) {
                    let res = steps.shift()(...args);
                    return {
                        value: res,
                        done: false
                    }
                } else {
                    return {
                        done: true
                    }
                }
            }
        }
    }
}

Task.actions.push(function task1(...args) {
    console.log("任务一:相乘") return args.reduce(function(x, y) {
        return x * y
    })
},
function task2(...args) {
    console.log("任务二:相加") return args.reduce(function(x, y) {
        return x + y
    }) * 2
},
function task3(...args) {
    console.log("任务三:相减") return args.reduce(function(x, y) {
        return x - y
    })
});

var it = Task[Symbol.iterator]();
console.log(it.next(10, 100, 2));
console.log(it.next(20, 50, 100)) console.log(it.next(10, 2, 1))
 // 
任务一:相乘 {
    "value": 2000,
    "done": false
}任务二:相加 {
    "value": 340,
    "done": false
}任务三:相减 {
    "value": 7,
    "done": false
}

**3、延迟执行
假设我们有一个数据表,我们想按大小顺序依次的获取数据,但是我们又不想提前给他排序,有可能我们根本就不去使用它,所以我们可以在第一次使用的时候再排序,做到延迟执行代码:

var table = {
    "d": 1,
    "b": 4,
    "c": 12,
    "a": 12
}
table[Symbol.iterator] = function() {
    var _this = this;
    var keys = null;
    var index = 0;

    return {
        next: function() {
            if (keys === null) {
                keys = Object.keys(_this).sort();
            }

            return {
                value: keys[index],
                done: index++>keys.length
            };
        }
    }
}

for (var a of table) {
    console.log(a)
} 
// a b c d

戳我博客

参考:

章节目录

1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for...of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容