Iterator 和 for...of 循环以及部分代码理解

目录

Iterator 的概念

默认 Iterator 接口

默认调用 Iterator 接口的场合

字符串的 Iterator 接口

Iterator 接口与 Generator 函数

遍历器对象的 return()、throw()

数组中的 for...in 和 for...of

对象中使用 for...of

与其他遍历语法比较


Iterator 的概念

JavaScript 表示“集合”的数据结构,主要有:Array、Object、Map、Set。

遍历器(Iterator),为各种不同的机制提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 作用有三个:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构中每个成员能够按照某种次序排列;
  • ES6 创建了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

Iterator 的遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上是一个指针对象。
  2. 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用,就指向数据结构的第二个成员。
  4. 不断的调用指针的 next 方法,直至它指向数据结构的结束位置。

每一次调用 next 方法,都会返回数据结构的当前成员信息。具体来说就是返回一个包含 value 和 done 两个属性的对象。其中 value 属性是当前成员的值,done 是一个布尔值表示遍历是否结束。

模拟 next 返回:

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

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

// makeIterator 其实就是返回了一个对象,这个对象有 next 方法,next 方法每次返回数组的下一项。
// next 方法还是一个闭包,每次访问的都是同一个 nextIndex,所以可以实现依次逐个访问数组每一项
// 我们自己可以随便写一个这样的方法,比如倒着遍历...

对于遍历器来说,done: false 和 value: undefined 都是可以省略的(也就是说 done 默认是 false,value 默认是 undefined)。所以上面函数可以简写:

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

默认 Iterator 接口

当用 for...of 循环遍历某种数据结构时,该循环就会自动去找 Iterator 接口。

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”。

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

对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法。

注意:

let obj = { a: 1, b: 2 };
console.log(obj[Symbol.iterator]);

// undefined

可以看到对象其实并没有 Symbol.iterator 属性。可遍历也只是自己写的遍历方法。

写一个可遍历的对象示例:

const obj = {
    [Symbol.iterator]: function () {
        return {
            next: function () {
                return {value: 1, done: true};
            }
        };
    }
};

for (var i of obj) {
    console.log(i);     // 无输出...
}

解读:对象 obj 是可遍历的(iterable),因为具有 Symbol.iterator 属性。执行这个属性,就会返回一个遍历器对象,该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 value 和 done 属性。

ES6 有些数据结构原生具备 Iterator 接口,即不用做任何处理,就可以被 for...of 循环遍历。原因在于这些数据结构原生部署了 Symbol.iterator 属性。凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口,调用这个接口就会返回一个遍历器对象。

原生具备 Iterator 接口的数据结构

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

数组的 Symbol.iterator 属性

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

对于原生部署的 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of 循环会自动遍历它们。其他数据结构(比如对象)的 Iterator 接口,需要自己在 Symbol.iterator 属性上面部署,这样才会被 for...of 循环遍历。

对象之所以没有部署 Iterator 接口,是因为对象的哪个属性先遍历哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署了一种线性转换。

给一个对象部署遍历器接口 ------ 在 Symbol.Iterator 的属性上部署遍历器生成方法

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

range(0, 3) //  RangeIterator {value: 0, stop: 3}

for (var val of range(0, 3)) {
  console.log(val ); // 0, 1, 2
}

解读(个人理解):

for...of 循环内部调用的是数据结构的 Symbol.iterator 方法,拿到返回的遍历器对象,在调用遍历器对象的 next 方法;for (var variable of iterable) {}:variable:当前成员的 value 值,iterable:部署的 Iterator 接口的数据结构。

new RangeIterator(start, stop) 调用构造函数,返回 this(构造函数的返回值如果是引用类型就返回这个引用,如果不是或者没有,就返回 new 创建的实例;在调用构造函数时,这实例指向了 this);

第一次循环,调用 Symbol.iterator 方法返回 this (遍历器对象),再调用这个遍历器对象的 next 方法,返回第一个成员 {done: false, value: 1}(此时 this.value 已经++),将该成员的 value 值赋给 val;

第二次循环调用 next 方法,返回第二个成员...

直至 done 为 true 停止。

var opr = range(0, 3)[Symbol.iterator]();
console.log(opr.next());
console.log(opr.next());
console.log(opr.next());
console.log(opr.next());

依次打印:
{done: false, value: 0}
{done: false, value: 1}
{done: false, value: 2}
{done: true, value: undefined}

for...of 先调用 Symbol.iterator 方法返回遍历器对象,再用这个对象依次调用 next 方法并将返回值的 value 值赋值给 val(此时 done 属性值必为 false),实现遍历(done 为 true 时应该是自动截止了并且不会将此时的值赋值给 val)。

有一点是必须的,就是返回值对象必须包含 value 属性和 done 属性(若没有还用 for...of 遍历可能会陷入死循环)

通过遍历器实现指针结构的例子:

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
}

解读(个人理解):

one、two、three 三个变量分别为:
one { value: 1, next: two }
two { value: 2, next: three }
three { value: 3, next: null }

for...of 
第一次调用 one 实例的 Symbol.iterator 方法:
此时 this 指针指向 one 这个实例
很明显返回 { done: false, value: 1 }
再返回之前,有个关键点:current = current.next; 在这里,this 指针的指向改变,指向了 current.next
current.next == this.next == one.next == two(实例)

当第二次调用 next 方法时,this 指向 two 实例,value 值为 2,同时再次改变 this 指针指向

调用过程:
var opr = one[Symbol.iterator]();
console.log(opr.next());    // {done: false, value: 1}
console.log(opr.next());    // {done: false, value: 2}
console.log(opr.next());    // {done: false, value: 3}

另一个为对象添加 Iterator 接口的例子:

let obj = {
    data: ['hello', 'world'],
    [Symbol.iterator]() {
        const self = this;
        let index = 0;
        return {
            next() {
                if (index < self.data.length) {
                    return { value: self.data[index++], done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

for (var k of obj) {
    console.log(k);
}

// hello
// world

解读:

最开始以为 for...of 内部都是这样调用:
console.log(obj[Symbol.iterator]().next());
console.log(obj[Symbol.iterator]().next());

但是仔细一想加上一打印发现,打印的都是 {value: "hello", done: false}
所以上面的一些想法应该错了...(目前上面已改)

可能是这样:
var opr = obj[Symbol.iterator]();
console.log(opr.next());
console.log(opr.next());

也就是,for...of 第一次循环会调用 Symbol.iterator,然后将返回值(遍历器对象)保存,每次遍历都直接用这个返回值来调用 next() 方法

对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口的简便方法,将数组的 Iterator 接口直接部署到此类数据结构的 Symbol.iterator。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

如果 Symbol.iterator 方法对应的不是遍历器生成函数(即不会返回一个遍历器对象),则解释引擎会报错。

var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function

有了遍历器接口,数据接口就可以用 for...of 循环遍历,也可以用 while 循环。

let obj = {
    data: ['hello', 'world'],
    [Symbol.iterator]() {
        const self = this;
        let index = 0;
        return {
            next() {
                if (index < self.data.length) {
                    return { value: self.data[index++], done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

var $iterator = obj[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
    var x = $result.value;
    console.log(x);
    $result = $iterator.next();
}
// hello
// world

上面代码中,obj 是可遍历的对象,$iterator 是它的遍历器对象。遍历器对象每次移动指针(next 方法),都会检查一下返回值的 done 属性,如果遍历还没结束,就移动遍历器的指针到下一步(next 方法),不断循环。

(看到这里,上面自己的推测好像是对的...^_^)

默认调用 Iterator 接口的场合

1)解构赋值

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)扩展运算符

// 例一
var str = 'hello';
[...str] //  ['h', 'e', 'l', 'l', 'o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

可以将任何部署了 Iterator 接口的数据结构,转为数组。

3)yield*

// yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
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()
  • Promise.all()
  • Promise.race()

字符串的 Iterator 接口

var someString = "hi";
typeof someString[Symbol.iterator] // "function"
var iterator = someString[Symbol.iterator]();

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

可以覆盖字符串的 Symbol.iterator 方法

var str = new String('world');
str[Symbol.iterator] = function () {
    var flag = true;
    return {
        next() {
            if (flag) {
                flag = false;
                return { value: 'hello', done: false };
            } else {
                return { done: true }
            }
        }
    };
}
console.log([...str] + '');     // hello
console.log(str.toString());    // world

Iterator 接口与 Generator 函数

Symbol.iterator 最简单实现

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

// 或者采用下面的简洁写法
let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};
for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

遍历器对象的 return()、throw()

遍历器对象除了具有 next() 方法,还可以具有 return() 方法和 throw() 方法。自己写遍历器对象生成函数,next() 是必须部署的,其它是可选的。

return() 方法的使用场景是,如果 for...of 循环提前退出(出错或者有 break),就会调用 return()。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return() 方法。

function readLinesSync(iterator) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          return { done: true };
        }
      };
    },
  };
}

会触发 return() 的情况

// 情况一
for (let val of iterater) {
  console.log(val);
  break;
}

// 情况二
for (let val of iterater) {
  console.log(val);
  throw new Error();
}

注意:return() 方法必须返回一个对象,这是 Generator 语法决定的。

数组中的 for...in 和 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"
}

for...of 循环数组,只返回具有数字索引的属性。

对象中使用 for...of

对于普通的对象,for...of 不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。

var obj = { a: 1, b: 2 };
obj[Symbol.iterator] = function () {
    let keys = Object.keys(obj);
    let len = keys.length;
    let that_ = this;
    let index = 0;
    return {
        next() {
            if (index < len) {
                var val = that_[keys[index]];
                index++;
                return { value: val, done: false };
            } else {
                return { done: true };
            }
        }
    }
}
for (var val of obj) {
    console.log(val);
}

// 1 2
let obj = {a: 1, b: 2};
for (var key of Object.keys(obj)) {
  console.log(key + ': ' + obj[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

与其他遍历语法比较

(偷下懒...v_v)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值