《JavaScript高级程序设计》读书笔记 28

发布于:2024-12-18 ⋅ 阅读:(92) ⋅ 点赞:(0)

感谢点赞、关注和收藏!

上一篇讲的是Set和WeakSet以及简单介绍了迭代,这一篇是书的第 7 章迭代器与生成器的迭代器部分。

理解迭代

        在 JavaScript 中,计数循环就是一种最简单的迭代。循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。那么思考下面两个点。
  • 迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。
  • 遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。
        ES5 新增了 Array.prototype.forEach()方法,向通用迭代需求迈进了一步(但仍然不够理想)。这个方法解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。
        JavaScript 在 ECMAScript 6 以后支持了迭代器模式。这个模式是为了开发者无须事先知道如何迭代就能实现迭代操作。

迭代器模式

        迭代器模式(特别是在 ECMAScript 这个语境下)描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序
        任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构“消费”(consume)。迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是 Iterable 和 Iterator 的强大之处。
可迭代协议
        实现 Iterable 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。下面看看哪些内置类型实现了这个迭代器:
let num = 1; 
let obj = {}; 
// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined 
console.log(obj[Symbol.iterator]); // undefined 
let str = 'abc'; 
let arr = ['a', 'b', 'c']; 
let map = new Map().set('a', 1).set('b', 2).set('c', 3); 
let set = new Set().add('a').add('b').add('c'); 
let els = document.querySelectorAll('div'); 
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] } 
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
console.log(map[Symbol.iterator]); // f values() { [native code] } 
console.log(set[Symbol.iterator]); // f values() { [native code] } 
console.log(els[Symbol.iterator]); // f values() { [native code] } 
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {} 
console.log(arr[Symbol.iterator]()); // ArrayIterator {} 
console.log(map[Symbol.iterator]()); // MapIterator {} 
console.log(set[Symbol.iterator]()); // SetIterator {} 
console.log(els[Symbol.iterator]()); // ArrayIterator {}
        实际写代码过程中,不需要显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括:
  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收由期约组成的可迭代对象
  • Promise.race()接收由期约组成的可迭代对象
  • yield*操作符,在生成器中使用

另外,如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口

迭代器协议
        迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置。
        next()方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。done 是一个布尔值,表示是否还可以再次调用 next()取得下一个值;value 包含可迭代对象的下一个值(done 为false),或者 undefined(done 为 true)。done: true 状态称为“耗尽”。可以通过以下简单的数组来演示:
// 可迭代对象
let arr = ['foo', 'bar']; 
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
// 迭代器
let iter = arr[Symbol.iterator](); 
console.log(iter);                 // ArrayIterator {} 
// 执行迭代
console.log(iter.next());          // { done: false, value: 'foo' } 
console.log(iter.next());          // { done: false, value: 'bar' } 
console.log(iter.next());          // { done: true, value: undefined }
        迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达 done: true 状态,后续调用 next()就一直返回同样的值了:
let arr = ['foo']; 
let iter = arr[Symbol.iterator](); 
console.log(iter.next()); // { done: false, value: 'foo' } 
console.log(iter.next()); // { done: true, value: undefined } 
console.log(iter.next()); // { done: true, value: undefined } 
console.log(iter.next()); // { done: true, value: undefined }
        迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
let arr = ['foo', 'baz']; 
let iter = arr[Symbol.iterator](); 
console.log(iter.next()); // { done: false, value: 'foo' } 
// 在数组中间插入值
arr.splice(1, 0, 'bar'); 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: false, value: 'baz' } 
console.log(iter.next()); // { done: true, value: undefined }

自定义迭代器
        任何实现 Iterator 接口的对象都可以作为迭代器使用。为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,可以把计数器变量放到闭包里,然后通过闭包返回迭代器:
class Counter { 
 constructor(limit) { 
  this.limit = limit; 
 } 
 [Symbol.iterator]() { 
  let count = 1, 
  limit = this.limit; 
  return { 
   next() { 
    if (count <= limit) { 
     return { done: false, value: count++ }; 
    } else { 
     return { done: true, value: undefined }; 
    } 
   } 
  }; 
 } 
} 
let counter = new Counter(3); 
for (let i of counter) { console.log(i); } 
// 1 
// 2 
// 3 
for (let i of counter) { console.log(i); } 
// 1 
// 2 
// 3
// 可以多次迭代
        每个以这种方式创建的迭代器也实现了 Iterable 接口。Symbol.iterator 属性引用的工厂函数
会返回相同的迭代器:
let arr = ['foo', 'bar', 'baz']; 
let iter1 = arr[Symbol.iterator](); 
console.log(iter1[Symbol.iterator]); // f values() { [native code] } 
let iter2 = iter1[Symbol.iterator](); 
console.log(iter1 === iter2); // true
        因为每个迭代器也实现了 Iterable 接口,所以它们可以用在任何期待可迭代对象的地方,比如
for-of 循环:
let arr = [3, 1, 4]; 
let iter = arr[Symbol.iterator]();
for (let item of arr ) { console.log(item); } 
// 3 
// 1 
// 4 
for (let item of iter ) { console.log(item); } 
// 3 
// 1 
// 4
提前终止迭代器
         可选的 return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:
  • for-of 循环通过 break、continue、return 或 throw 提前退出;
  • 解构操作并未消费所有值。
        return()方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回{ done: true }。因为这个返回值只会用在生成器的上下文中,所以后面说生成器的时候再讨论这种情况。如下面的代码所示,内置语言结构在发现还有更多值可以迭代,但不会消费这些值时,会自动调用return()方法。
class Counter {
    constructor(limit) {
        this.limit = limit;
    }
    [Symbol.iterator]() {
        let count = 1,
            limit = this.limit;
        return {
            next() {
                if (count <= limit) {
                    return { done: false, value: count++ };
                } else {
                    return { done: true };
                }
            },
            return() {
                console.log('Exiting early');
                return { done: true };
            }
        };
    }
}
let counter1 = new Counter(5);
for (let i of counter1) {
    if (i > 2) {
        break;
    }
    console.log(i);
}
// 1 
// 2 
// Exiting early 
let counter2 = new Counter(5);
try {
    for (let i of counter2) {
        if (i > 2) {
            throw 'err';
        }
        console.log(i);
    }
} catch (e) { }
// 1 
// 2 
// Exiting early 
let counter3 = new Counter(5);
let [a, b] = counter3;
// Exiting early
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关
闭的:
let a = [1, 2, 3, 4, 5]; 
let iter = a[Symbol.iterator](); 
for (let i of iter) { 
 console.log(i); 
 if (i > 2) { 
  break;
 } 
} 
// 1 
// 2 
// 3 
for (let i of iter) { 
 console.log(i); 
} 
// 4 
// 5
        因为 return()方法是可选的,所以并非所有迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。这是因为调用 return()不会强制迭代器进入关闭状态。即便如此,return()方法还是会被调用。
let a = [1, 2, 3, 4, 5]; 
let iter = a[Symbol.iterator](); 
iter.return = function() { 
 console.log('提前退出'); 
 return { done: true };
}; 
for (let i of iter) { 
 console.log(i); 
 if (i > 2) { 
  break;
 } 
} 
// 1 
// 2 
// 3 
// 提前退出
for (let i of iter) { 
 console.log(i); 
} 
// 4 
// 5

不知道这篇看下来大家有没有比较懵,其实就是描述了迭代器应该长什么样子,然后有哪些方法要实现(Symbol.iterator),有哪些是可选的(return()),想怎样定义迭代完全看需求,没有规定就一定要能反复迭代,也没规定一定要可关闭。

下一篇介绍生成器。


网站公告

今日签到

点亮在社区的每一天
去签到