第三章:寻根究底
如果你已经阅读了第一和第二章,并花时间消化和思考,希望你对 JS 的理解有更多的收获。如果你跳过/略过它们(尤其是第二章),我建议你回去花更多的时间阅读这些材料。
在第二章中,我们在高层次上解释了语法、模式和行为。在这一章中,我们的注意力转移到 JS 的一些低层次的根本特征上,这些特征几乎是我们所写的每一行代码的基础。
请注意:这一章的内容比你可能习惯于思考的编程语言要深得多。我的目标是帮助你理解 JS 工作的核心,是什么让它运转。这一章应该开始回答一些你在探索 JS 时可能出现的「为什么?」的问题。然而,这些内容仍然不是对该语言的详尽阐述;这是本系列书的其他部分的目标,我们在这里的目标仍然是入门,并且更加适应 JS 的感觉,了解它是如何起伏的。
不要这么快就读完这些资料,以至于你迷失了方向。我已经说过十几次了,跬步千里。即使如此,你在读完这一章后可能还会有其他问题。这没关系,因为还有一整个系列的书在等着你继续探索呢!
迭代
由于程序本质上是为了处理数据(并对这些数据做出决定),用于浏览数据的模式对程序的可读性有很大影响。
迭代器模式已经存在了几十年,它提出了一种「标准化」的方法,从一个源头一次块的消费数据。这个想法是,对数据源进行迭代 。通过处理第一部分,然后是下一部分,以此类推,逐步处理数据集合,而不是一下子处理整个数据集,这样做更常见,更有帮助。
想象一下一个数据结构,它代表了一个关系型数据库的 SELECT
查询,它通常将结果组织成行。如果这个查询只有一条或几条记录,你可以一次处理整个结果集,并将每条记录分配给一个局部变量,然后对这些数据进行任何适当的操作。
但如果查询有 10 或 1000(或更多!)行,你就需要迭代处理来处理这些数据(通常是一个循环)。
迭代器模式定义了一个叫做「迭代器」的数据结构,它有一个对底层数据源(如查询结果行)的引用,它暴露了一个像 next()
的方法。调用 next()
返回下一个数据(即数据库查询的「记录」或「行」)。
你并不总是知道你需要遍历多少数据,所以一旦你遍历整个集合并超过终点,该模式通常以一些特殊的值或异常来表示完成。
迭代器模式的重要性在于坚持以标准的方式来迭代处理数据,这就创造了更干净、更容易理解的代码,而不是让每个数据结构/源都定义自己处理数据的自定义方式。
经过多年来 JS 社区围绕共同商定的迭代技术所做的各种努力,ES6 在语言中直接为迭代器模式规范了一个特定的协议。该协议定义了一个 next()
方法,其返回值是一个被称为 iterator result 的对象;该对象有 value
和 done
属性,其中 done
是一个布尔值,在对底层数据源的迭代完成之前为 false
。
消费迭代器
有了 ES6 的迭代协议,每次消费一个数据源的值是可行的,在每次 next()
调用后检查 done
是否为 true
来停止迭代。但这种方法是相当手动的,所以 ES6 也包括了几个机制(语法和 API),用于标准化地消费这些迭代器。
其中一个机制是 for..of
循环:
// 给定某个数据源的迭代器:
var it = /* .. */;
// 循环处理其结果,一次一个
for (let val of it) {
console.log(`Iterator value: ${
val }`);
}
// Iterator value: ..
// Iterator value: ..
// ..
注意: |
---|
这里我们将省略等效的手动循环,但它的可读性肯定不如 for..of 循环! |
另一个经常用于消费迭代器的机制是 ...
操作符。这个操作符实际上有两种对称的形式: 扩展和剩余。扩展形式是一个迭代器消费器。
要扩展一个迭代器,你必须要有东西来传递它。在 JS 中有两种可能性:一个数组或一个函数调用的参数列表。
一个数组的传递:
// 将一个迭代器分散到一个数组中,
// 每个迭代的值都占据一个数组元素的位置。
var vals = [...it];
一个函数调用传递:
// 将一个迭代器分散到一个函数中,
// 每个迭代的值占据一个参数位置。
doSomethingUseful(...it);
在这两种情况下,扩展运算符形式的 ...
遵循 iterator-consumption 协议(与 for..of
循环相同),从迭代中获取所有可用的值,并将它们放入(又称,扩展)接收上下文中(数组,参数列表)。
迭代器
迭代器消费 (iterator-consumption) 协议在技术上是为消费迭代器 (iterables) 而定义的;迭代器是一个可以被迭代的值。
该协议自动从一个可迭代的程序中创建一个迭代器实例,并且只消费该迭代器实例,直到其完成。这意味着一个迭代器可以被消费一次以上;每次都会创建并使用一个新的迭代器实例。
那么,我们在哪里可以找到可迭代项?
ES6 将 JS 中的基本数据结构/集合类型定义为 iterables。这包括字符串、数组、maps、sets 等。
假设以下代码:
// 数组是一个可迭代的对象
var arr = [10, 20, 30];
for (let val of arr) {
console.log(`Array value: ${
val}`);
}
// Array value: 10
// Array value: 20
// Array value: 30
由于数组是可迭代的,我们可以通过 ...
扩展运算符,使用迭代器消费浅层复制一个数组:
var arrCopy = [...arr];
我们也可以在一个字符串中一个一个地遍历字符:
var greeting = "Hello world!";
var chars = [...greeting];
chars;
// [ "H", "e", "l", "l", "o", " ",
// "w", "o", "r", "l", "d", "!" ]
一个 Map
数据结构使用对象作为键,将一个值(任何类型)与该对象相关联。Map 有一个不同于这里的默认迭代,因为迭代不仅仅是在 map 的值上,而是在它的 entries 上。一个 entry 是一个元组(两个元素数组),包括一个键和一个值。
假设以下代码:
// 给定两个DOM元素,`btn1`和`btn2`。
var buttonNames = new Map();
buttonNames.set(btn1, "Button 1"