第十二章:迭代器和生成器
可迭代对象及其相关的迭代器是 ES6 的一个特性,在本书中我们已经多次见到。数组(包括 TypedArrays)、字符串以及 Set 和 Map 对象都是可迭代的。这意味着这些数据结构的内容可以被迭代——使用for/of
循环遍历,就像我们在§5.4.4 中看到的那样:
let sum = 0; for(let i of [1,2,3]) { // Loop once for each of these values sum += i; } sum // => 6
迭代器也可以与...
运算符一起使用,将可迭代对象展开或“扩展”到数组初始化程序或函数调用中,就像我们在§7.1.2 中看到的那样:
let chars = [..."abcd"]; // chars == ["a", "b", "c", "d"] let data = [1, 2, 3, 4, 5]; Math.max(...data) // => 5
迭代器可以与解构赋值一起使用:
let purpleHaze = Uint8Array.of(255, 0, 255, 128); let [r, g, b, a] = purpleHaze; // a == 128
当你迭代 Map 对象时,返回的值是[key, value]
对,这与for/of
循环中的解构赋值很好地配合使用:
let m = new Map([["one", 1], ["two", 2]]); for(let [k,v] of m) console.log(k, v); // Logs 'one 1' and 'two 2'
如果你只想迭代键或值而不是键值对,可以使用keys()
和values()
方法:
[...m] // => [["one", 1], ["two", 2]]: default iteration [...m.entries()] // => [["one", 1], ["two", 2]]: entries() method is the same [...m.keys()] // => ["one", "two"]: keys() method iterates just map keys [...m.values()] // => [1, 2]: values() method iterates just map values
最后,一些常用于 Array 对象的内置函数和构造函数实际上(在 ES6 及更高版本中)被编写为接受任意迭代器。Set()
构造函数就是这样一个 API:
// Strings are iterable, so the two sets are the same: new Set("abc") // => new Set(["a", "b", "c"])
本章解释了迭代器的工作原理,并演示了如何创建自己的可迭代数据结构。在解释基本迭代器之后,本章涵盖了生成器,这是 ES6 的一个强大新功能,主要用作一种特别简单的创建迭代器的方法。
12.1 迭代器的工作原理
for/of
循环和展开运算符与可迭代对象无缝配合,但值得理解实际上是如何使迭代工作的。在理解 JavaScript 中的迭代过程时,有三种不同的类型需要理解。首先是可迭代对象:这些是可以被迭代的类型,如 Array、Set 和 Map。其次,是执行迭代的迭代器对象本身。第三,是保存迭代每一步结果的迭代结果对象。
可迭代对象是任何具有特殊迭代器方法的对象,该方法返回一个迭代器对象。迭代器是任何具有返回迭代结果对象的next()
方法的对象。而迭代结果对象是具有名为value
和done
的属性的对象。要迭代可迭代对象,首先调用其迭代器方法以获取一个迭代器对象。然后,重复调用迭代器对象的next()
方法,直到返回的值的done
属性设置为true
为止。关于这一点的棘手之处在于,可迭代对象的迭代器方法没有传统的名称,而是使用符号Symbol.iterator
作为其名称。因此,对可迭代对象iterable
进行简单的for/of
循环也可以以较困难的方式编写,如下所示:
let iterable = [99]; let iterator = iterable[Symbol.iterator](); for(let result = iterator.next(); !result.done; result = iterator.next()) { console.log(result.value) // result.value == 99 }
内置可迭代数据类型的迭代器对象本身也是可迭代的。(也就是说,它有一个名为Symbol.iterator
的方法,该方法返回自身。)这在以下代码中偶尔会有用,当你想要遍历“部分使用过”的迭代器时:
let list = [1,2,3,4,5]; let iter = list[Symbol.iterator](); let head = iter.next().value; // head == 1 let tail = [...iter]; // tail == [2,3,4,5]
12.2 实现可迭代对象
在 ES6 中,可迭代对象非常有用,因此当它们表示可以被迭代的内容时,你应该考虑使自己的数据类型可迭代。在第 9-2 和第 9-3 示例中展示的 Range 类是可迭代的。这些类使用生成器函数使自己可迭代。我们稍后会介绍生成器,但首先,我们将再次实现 Range 类,使其可迭代而不依赖于生成器。
要使类可迭代,必须实现一个方法,其名称为符号Symbol.iterator
。该方法必须返回具有next()
方法的迭代器对象。而next()
方法必须返回具有value
属性和/或布尔done
属性的迭代结果对象。示例 12-1 实现了一个可迭代的 Range 类,并演示了如何创建可迭代、迭代器和迭代结果对象。
示例 12-1. 一个可迭代的数字范围类
/* * A Range object represents a range of numbers {x: from <= x <= to} * Range defines a has() method for testing whether a given number is a member * of the range. Range is iterable and iterates all integers within the range. */ class Range { constructor (from, to) { this.from = from; this.to = to; } // Make a Range act like a Set of numbers has(x) { return typeof x === "number" && this.from <= x && x <= this.to; } // Return string representation of the range using set notation toString() { return `{ x | ${this.from} ≤ x ≤ ${this.to} }`; } // Make a Range iterable by returning an iterator object. // Note that the name of this method is a special symbol, not a string. [Symbol.iterator]() { // Each iterator instance must iterate the range independently of // others. So we need a state variable to track our location in the // iteration. We start at the first integer >= from. let next = Math.ceil(this.from); // This is the next value we return let last = this.to; // We won't return anything > this return { // This is the iterator object // This next() method is what makes this an iterator object. // It must return an iterator result object. next() { return (next <= last) // If we haven't returned last value yet ? { value: next++ } // return next value and increment it : { done: true }; // otherwise indicate that we're done. }, // As a convenience, we make the iterator itself iterable. [Symbol.iterator]() { return this; } }; } } for(let x of new Range(1,10)) console.log(x); // Logs numbers 1 to 10 [...new Range(-2,2)] // => [-2, -1, 0, 1, 2]
除了使您的类可迭代之外,定义返回可迭代值的函数也非常有用。考虑这些基于迭代的替代方案,用于 JavaScript 数组的map()
和filter()
方法:
// Return an iterable object that iterates the result of applying f() // to each value from the source iterable function map(iterable, f) { let iterator = iterable[Symbol.iterator](); return { // This object is both iterator and iterable [Symbol.iterator]() { return this; }, next() { let v = iterator.next(); if (v.done) { return v; } else { return { value: f(v.value) }; } } }; } // Map a range of integers to their squares and convert to an array [...map(new Range(1,4), x => x*x)] // => [1, 4, 9, 16] // Return an iterable object that filters the specified iterable, // iterating only those elements for which the predicate returns true function filter(iterable, predicate) { let iterator = iterable[Symbol.iterator](); return { // This object is both iterator and iterable [Symbol.iterator]() { return this; }, next() { for(;;) { let v = iterator.next(); if (v.done || predicate(v.value)) { return v; } } } }; } // Filter a range so we're left with only even numbers [...filter(new Range(1,10), x => x % 2 === 0)] // => [2,4,6,8,10]
可迭代对象和迭代器的一个关键特性是它们本质上是惰性的:当需要计算下一个值时,该计算可以推迟到实际需要该值时。例如,假设您有一个非常长的文本字符串,您希望将其标记为以空格分隔的单词。您可以简单地使用字符串的split()
方法,但如果这样做,那么必须在使用第一个单词之前处理整个字符串。并且您最终会为返回的数组及其中的所有字符串分配大量内存。以下是一个函数,允许您惰性迭代字符串的单词,而无需一次性将它们全部保存在内存中(在 ES2020 中,使用返回迭代器的matchAll()
方法更容易实现此函数,该方法在 §11.3.2 中描述):
function words(s) { var r = /\s+|$/g; // Match one or more spaces or end r.lastIndex = s.match(/[^ ]/).index; // Start matching at first nonspace return { // Return an iterable iterator object [Symbol.iterator]() { // This makes us iterable return this; }, next() { // This makes us an iterator let start = r.lastIndex; // Resume where the last match ended if (start < s.length) { // If we're not done let match = r.exec(s); // Match the next word boundary if (match) { // If we found one, return the word return { value: s.substring(start, match.index) }; } } return { done: true }; // Otherwise, say that we're done } }; } [...words(" abc def ghi! ")] // => ["abc", "def", "ghi!"]
12.2.1 “关闭”迭代器:返回方法
想象一个(服务器端)JavaScript 变体的words()
迭代器,它不是以源字符串作为参数,而是以文件流作为参数,打开文件,从中读取行,并迭代这些行中的单词。在大多数操作系统中,打开文件以从中读取的程序在完成读取后需要记住关闭这些文件,因此这个假设的迭代器将确保在next()
方法返回其中的最后一个单词后关闭文件。
但迭代器并不总是运行到结束:for/of
循环可能会被break
、return
或异常终止。同样,当迭代器与解构赋值一起使用时,next()
方法只会被调用足够次数以获取每个指定变量的值。迭代器可能有更多值可以返回,但它们永远不会被请求。
如果我们假设的文件中的单词迭代器从未完全运行到结束,它仍然需要关闭打开的文件。因此,迭代器对象可能会实现一个return()
方法,与next()
方法一起使用。如果在next()
返回具有done
属性设置为true
的迭代结果之前迭代停止(通常是因为您通过break
语句提前离开了for/of
循环),那么解释器将检查迭代器对象是否具有return()
方法。如果存在此方法,解释器将以无参数调用它,使迭代器有机会关闭文件,释放内存,并在完成后进行清理。return()
方法必须返回一个迭代结果对象。对象的属性将被忽略,但返回非对象值是错误的。
for/of
循环和展开运算符是 JavaScript 的非常有用的特性,因此在创建 API 时,尽可能使用它们是一个好主意。但是,必须使用可迭代对象、其迭代器对象和迭代器的结果对象来处理过程有些复杂。幸运的是,生成器可以极大地简化自定义迭代器的创建,我们将在本章的其余部分中看到。
12.3 生成器
生成器是一种使用强大的新 ES6 语法定义的迭代器;当要迭代的值不是数据结构的元素,而是计算结果时,它特别有用。
要创建一个生成器,你必须首先定义一个生成器函数。生成器函数在语法上类似于普通的 JavaScript 函数,但是用关键字function*
而不是function
来定义。(从技术上讲,这不是一个新关键字,只是在关键字function
之后和函数名之前加上一个*
。)当你调用一个生成器函数时,它实际上不会执行函数体,而是返回一个生成器对象。这个生成器对象是一个迭代器。调用它的next()
方法会导致生成器函数的主体从头开始运行(或者从当前位置开始),直到达到一个yield
语句。yield
在 ES6 中是新的,类似于return
语句。yield
语句的值成为迭代器上next()
调用返回的值。通过示例可以更清楚地理解这一点:
// A generator function that yields the set of one digit (base-10) primes. function* oneDigitPrimes() { // Invoking this function does not run the code yield 2; // but just returns a generator object. Calling yield 3; // the next() method of that generator runs yield 5; // the code until a yield statement provides yield 7; // the return value for the next() method. } // When we invoke the generator function, we get a generator let primes = oneDigitPrimes(); // A generator is an iterator object that iterates the yielded values primes.next().value // => 2 primes.next().value // => 3 primes.next().value // => 5 primes.next().value // => 7 primes.next().done // => true // Generators have a Symbol.iterator method to make them iterable primes[Symbol.iterator]() // => primes // We can use generators like other iterable types [...oneDigitPrimes()] // => [2,3,5,7] let sum = 0; for(let prime of oneDigitPrimes()) sum += prime; sum // => 17
在这个例子中,我们使用了function*
语句来定义一个生成器。然而,和普通函数一样,我们也可以以表达式形式定义生成器。再次强调,我们只需在function
关键字后面加上一个星号:
const seq = function*(from,to) { for(let i = from; i <= to; i++) yield i; }; [...seq(3,5)] // => [3, 4, 5]
在类和对象字面量中,我们可以使用简写符号来完全省略定义方法时的function
关键字。在这种情况下定义生成器,我们只需在方法名之前使用一个星号,而不是使用function
关键字:
let o = { x: 1, y: 2, z: 3, // A generator that yields each of the keys of this object *g() { for(let key of Object.keys(this)) { yield key; } } }; [...o.g()] // => ["x", "y", "z", "g"]
请注意,没有办法使用箭头函数语法编写生成器函数。
生成器通常使得定义可迭代类变得特别容易。我们可以用一个更简短的*Symbol.iterator]()
生成器函数来替换[示例 12-1 中展示的[Symbol.iterator]()
方法,代码如下:
*[Symbol.iterator]() { for(let x = Math.ceil(this.from); x <= this.to; x++) yield x; }
查看第九章中的示例 9-3 以查看上下文中基于生成器的迭代器函数。
12.3.1 生成器示例
如果生成器实际上生成它们通过进行某种计算来产生的值,那么生成器就更有趣了。例如,这里是一个产生斐波那契数的生成器函数:
function* fibonacciSequence() { let x = 0, y = 1; for(;;) { yield y; [x, y] = [y, x+y]; // Note: destructuring assignment } }
注意,这里的fibonacciSequence()
生成器函数有一个无限循环,并且永远产生值而不返回。如果这个生成器与...
扩展运算符一起使用,它将循环直到内存耗尽并且程序崩溃。然而,经过谨慎处理,可以在for/of
循环中使用它:
// Return the nth Fibonacci number function fibonacci(n) { for(let f of fibonacciSequence()) { if (n-- <= 0) return f; } } fibonacci(20) // => 10946
这种无限生成器与这样的take()
生成器结合使用更有用:
// Yield the first n elements of the specified iterable object function* take(n, iterable) { let it = iterable[Symbol.iterator](); // Get iterator for iterable object while(n-- > 0) { // Loop n times: let next = it.next(); // Get the next item from the iterator. if (next.done) return; // If there are no more values, return early else yield next.value; // otherwise, yield the value } } // An array of the first 5 Fibonacci numbers [...take(5, fibonacciSequence())] // => [1, 1, 2, 3, 5]
这里是另一个有用的生成器函数,它交错多个可迭代对象的元素:
// Given an array of iterables, yield their elements in interleaved order. function* zip(...iterables) { // Get an iterator for each iterable let iterators = iterables.map(i => i[Symbol.iterator]()); let index = 0; while(iterators.length > 0) { // While there are still some iterators if (index >= iterators.length) { // If we reached the last iterator index = 0; // go back to the first one. } let item = iterators[index].next(); // Get next item from next iterator. if (item.done) { // If that iterator is done iterators.splice(index, 1); // then remove it from the array. } else { // Otherwise, yield item.value; // yield the iterated value index++; // and move on to the next iterator. } } } // Interleave three iterable objects [...zip(oneDigitPrimes(),"ab",[0])] // => [2,"a",0,3,"b",5,7]
12.3.2 yield*
和递归生成器
除了在前面的示例中定义的zip()
生成器之外,可能还有一个类似的生成器函数很有用,它按顺序而不是交错地产生多个可迭代对象的元素。我们可以这样编写这个生成器:
function* sequence(...iterables) { for(let iterable of iterables) { for(let item of iterable) { yield item; } } } [...sequence("abc",oneDigitPrimes())] // => ["a","b","c",2,3,5,7]
在生成器函数中产生其他可迭代对象的元素的过程在生成器函数中是很常见的,ES6 为此提供了特殊的语法。yield*
关键字类似于yield
,不同之处在于,它不是产生单个值,而是迭代一个可迭代对象并产生每个结果值。我们使用的sequence()
生成器函数可以用yield*
简化如下:
function* sequence(...iterables) { for(let iterable of iterables) { yield* iterable; } } [...sequence("abc",oneDigitPrimes())] // => ["a","b","c",2,3,5,7]
数组的forEach()
方法通常是遍历数组元素的一种优雅方式,因此你可能会尝试像这样编写sequence()
函数:
function* sequence(...iterables) { iterables.forEach(iterable => yield* iterable ); // Error }
然而,这是行不通的。yield
和yield*
只能在生成器函数内部使用,但是这段代码中的嵌套箭头函数是一个普通函数,而不是function*
生成器函数,因此不允许使用yield
。
yield*
可以与任何类型的可迭代对象一起使用,包括使用生成器实现的可迭代对象。这意味着yield*
允许我们定义递归生成器,你可以使用这个特性来允许对递归定义的树结构进行简单的非递归迭代,例如。
12.4 高级生成器功能
生成器函数最常见的用途是创建迭代器,但生成器的基本特性是允许我们暂停计算,产生中间结果,然后稍后恢复计算。这意味着生成器具有超出迭代器的功能,并且我们将在以下部分探讨这些功能。
12.4.1 生成器函数的返回值
到目前为止,我们看到的生成器函数没有return
语句,或者如果有的话,它们被用来导致早期返回,而不是返回一个值。不过,与任何函数一样,生成器函数可以返回一个值。为了理解在这种情况下会发生什么,回想一下迭代的工作原理。next()
函数的返回值是一个具有value
属性和/或done
属性的对象。对于典型的迭代器和生成器,如果value
属性被定义,则done
属性未定义或为false
。如果done
为true
,则value
为未定义。但是对于返回值的生成器,最后一次调用next
会返回一个同时定义了value
和done
的对象。value
属性保存生成器函数的返回值,done
属性为true
,表示没有更多的值可迭代。这个最终值被for/of
循环和展开运算符忽略,但对于手动使用显式调用next()
的代码是可用的:
function *oneAndDone() { yield 1; return "done"; } // The return value does not appear in normal iteration. [...oneAndDone()] // => [1] // But it is available if you explicitly call next() let generator = oneAndDone(); generator.next() // => { value: 1, done: false} generator.next() // => { value: "done", done: true } // If the generator is already done, the return value is not returned again generator.next() // => { value: undefined, done: true }
12.4.2 yield 表达式的值
在前面的讨论中,我们将yield
视为接受值但没有自身值的语句。实际上,yield
是一个表达式,它可以有一个值。
当调用生成器的next()
方法时,生成器函数运行直到达到yield
表达式。yield
关键字后面的表达式被评估,该值成为next()
调用的返回值。此时,生成器函数在评估yield
表达式的过程中停止执行。下次调用生成器的next()
方法时,传递给next()
的参数成为暂停的yield
表达式的值。因此,生成器通过yield
向其调用者返回值,调用者通过next()
向生成器传递值。生成器和调用者是两个独立的执行流,来回传递值(和控制)。以下代码示例:
function* smallNumbers() { console.log("next() invoked the first time; argument discarded"); let y1 = yield 1; // y1 == "b" console.log("next() invoked a second time with argument", y1); let y2 = yield 2; // y2 == "c" console.log("next() invoked a third time with argument", y2); let y3 = yield 3; // y3 == "d" console.log("next() invoked a fourth time with argument", y3); return 4; } let g = smallNumbers(); console.log("generator created; no code runs yet"); let n1 = g.next("a"); // n1.value == 1 console.log("generator yielded", n1.value); let n2 = g.next("b"); // n2.value == 2 console.log("generator yielded", n2.value); let n3 = g.next("c"); // n3.value == 3 console.log("generator yielded", n3.value); let n4 = g.next("d"); // n4 == { value: 4, done: true } console.log("generator returned", n4.value);
当运行这段代码时,会产生以下输出,展示了两个代码块之间的来回交互:
generator created; no code runs yet next() invoked the first time; argument discarded generator yielded 1 next() invoked a second time with argument b generator yielded 2 next() invoked a third time with argument c generator yielded 3 next() invoked a fourth time with argument d generator returned 4
注意这段代码中的不对称性。第一次调用next()
启动了生成器,但传递给该调用的值对生成器不可访问。
12.4.3 生成器的 return()和 throw()方法
我们已经看到可以接收生成器函数产生的值。您可以通过在调用生成器的next()
方法时传递这些值来向正在运行的生成器传递值。
除了使用next()
向生成器提供输入外,还可以通过调用其return()
和throw()
方法来更改生成器内部的控制流。如其名称所示,调用这些方法会导致生成器返回一个值或抛出异常,就好像生成器中的下一条语句是return
或throw
一样。
在本章的前面提到,如果迭代器定义了一个return()
方法并且迭代提前停止,那么解释器会自动调用return()
方法,以便让迭代器有机会关闭文件或进行其他清理工作。对于生成器来说,你不能定义一个自定义的return()
方法来处理清理工作,但你可以结构化生成器代码以使用try/finally
语句,在生成器返回时确保必要的清理工作已完成(在finally
块中)。通过强制生成器返回,生成器的内置return()
方法确保在生成器不再使用时运行清理代码。
就像生成器的next()
方法允许我们向正在运行的生成器传递任意值一样,生成器的throw()
方法给了我们一种向生成器发送任意信号(以异常的形式)的方法。调用throw()
方法总是在生成器内部引发异常。但如果生成器函数编写了适当的异常处理代码,异常不必是致命的,而可以是改变生成器行为的手段。例如,想象一个计数器生成器,产生一个不断增加的整数序列。这可以被编写成使用throw()
发送的异常将计数器重置为零。
当生成器使用yield*
从其他可迭代对象中产生值时,那么对生成器的next()
方法的调用会导致对可迭代对象的next()
方法的调用。return()
和throw()
方法也是如此。如果生成器在可迭代对象上使用yield*
,那么在生成器上调用return()
或throw()
会导致依次调用迭代器的return()
或throw()
方法。所有迭代器必须有一个next()
方法。需要在不完整迭代后进行清理的迭代器应该定义一个return()
方法。任何迭代器可以定义一个throw()
方法,尽管我不知道任何实际原因这样做。
12.4.4 关于生成器的最后说明
生成器是一种非常强大的通用控制结构。它们使我们能够使用yield
暂停计算,并在任意后续时间点以任意输入值重新启动。可以使用生成器在单线程 JavaScript 代码中创建一种协作线程系统。也可以使用生成器掩盖程序中的异步部分,使你的代码看起来是顺序和同步的,尽管你的一些函数调用实际上是异步的并依赖于网络事件。
尝试用生成器做这些事情会导致代码难以理解或解释。然而,已经做到了,唯一真正实用的用例是管理异步代码。然而,JavaScript 现在有async
和await
关键字(见第十三章)用于这个目的,因此不再有任何滥用生成器的理由。
12.5 总结
在本章中,你学到了:
for/of
循环和...
扩展运算符适用于可迭代对象。- 如果一个对象有一个名为
[Symbol.iterator]
的方法返回一个迭代器对象,那么它就是可迭代的。 - 迭代器对象有一个
next()
方法返回一个迭代结果对象。 - 迭代结果对象有一个
value
属性,保存下一个迭代的值(如果有的话)。如果迭代已完成,则结果对象必须将done
属性设置为true
。 - 你可以通过定义一个
[Symbol.iterator]()
方法返回一个具有next()
方法返回迭代结果对象的对象来实现自己的可迭代对象。你也可以实现接受迭代器参数并返回迭代器值的函数。 - 生成器函数(使用
function*
而不是function
定义的函数)是定义迭代器的另一种方式。 - 当调用生成器函数时,函数体不会立即运行;相反,返回值是一个可迭代的迭代器对象。每次调用迭代器的
next()
方法时,生成器函数的另一个块会运行。 - 生成器函数可以使用
yield
运算符指定迭代器返回的值。每次调用next()
都会导致生成器函数运行到下一个yield
表达式。该yield
表达式的值然后成为迭代器返回的值。当没有更多的yield
表达式时,生成器函数返回,迭代完成。
第十三章:异步 JavaScript
一些计算机程序,如科学模拟和机器学习模型,是计算密集型的:它们持续运行,不间断,直到计算出结果为止。然而,大多数现实世界的计算机程序都是显著异步的。这意味着它们经常需要在等待数据到达或某个事件发生时停止计算。在 Web 浏览器中,JavaScript 程序通常是事件驱动的,这意味着它们等待用户点击或轻触才会实际执行任何操作。而基于 JavaScript 的服务器通常在等待客户端请求通过网络到达之前不会执行任何操作。
这种异步编程在 JavaScript 中很常见,本章记录了三个重要的语言特性,帮助简化处理异步代码。Promise 是 ES6 中引入的对象,表示尚未可用的异步操作的结果。关键字async
和await
是在 ES2017 中引入的,通过允许你将基于 Promise 的代码结构化为同步的形式,简化了异步编程的语法。最后,在 ES2018 中引入了异步迭代器和for/await
循环,允许你使用看似同步的简单循环处理异步事件流。
具有讽刺意味的是,尽管 JavaScript 提供了这些强大的功能来处理异步代码,但核心语法本身没有异步特性。因此,为了演示 Promise、async
、await
和for/await
,我们将首先进入客户端和服务器端 JavaScript,解释 Web 浏览器和 Node 的一些异步特性。(你可以在第十五章和第十六章了解更多关于客户端和服务器端 JavaScript 的内容。)
13.1 使用回调进行异步编程
在 JavaScript 中,异步编程的最基本层次是通过回调完成的。回调是你编写并传递给其他函数的函数。当满足某些条件或发生某些(异步)事件时,另一个函数会调用(“回调”)你的函数。你提供的回调函数的调用会通知你条件或事件,并有时,调用会包括提供额外细节的函数参数。通过一些具体的例子更容易理解,接下来的小节演示了使用客户端 JavaScript 和 Node 进行基于回调的异步编程的各种形式。
13.1.1 定时器
最简单的异步之一是当你想在一定时间后运行一些代码时。正如我们在§11.10 中看到的,你可以使用setTimeout()
函数来实现:
setTimeout(checkForUpdates, 60000);
setTimeout()
的第一个参数是一个函数,第二个是以毫秒为单位的时间间隔。在上述代码中,一个假设的checkForUpdates()
函数将在setTimeout()
调用后的 60,000 毫秒(1 分钟)后被调用。checkForUpdates()
是你的程序可能定义的回调函数,setTimeout()
是你调用以注册回调函数并指定在何种异步条件下调用它的函数。
setTimeout()
调用指定的回调函数一次,不传递任何参数,然后忘记它。如果你正在编写一个真正检查更新的函数,你可能希望它重复运行。你可以使用setInterval()
而不是setTimeout()
来实现这一点:
// Call checkForUpdates in one minute and then again every minute after that let updateIntervalId = setInterval(checkForUpdates, 60000); // setInterval() returns a value that we can use to stop the repeated // invocations by calling clearInterval(). (Similarly, setTimeout() // returns a value that you can pass to clearTimeout()) function stopCheckingForUpdates() { clearInterval(updateIntervalId); }
13.1.2 事件
客户端 JavaScript 程序几乎普遍是事件驱动的:而不是运行某种预定的计算,它们通常等待用户执行某些操作,然后响应用户的动作。当用户在键盘上按键、移动鼠标、点击鼠标按钮或触摸触摸屏设备时,Web 浏览器会生成一个事件。事件驱动的 JavaScript 程序在指定的上下文中为指定类型的事件注册回调函数,当指定的事件发生时,Web 浏览器会调用这些函数。这些回调函数称为事件处理程序或事件监听器,并使用addEventListener()
进行注册:
// Ask the web browser to return an object representing the HTML // <button> element that matches this CSS selector let okay = document.querySelector('#confirmUpdateDialog button.okay'); // Now register a callback function to be invoked when the user // clicks on that button. okay.addEventListener('click', applyUpdate);
在这个例子中,applyUpdate()
是一个我们假设在其他地方实现的虚构回调函数。调用document.querySelector()
返回一个表示 Web 页面中单个指定元素的对象。我们在该元素上调用addEventListener()
来注册我们的回调。然后addEventListener()
的第一个参数是一个字符串,指定我们感兴趣的事件类型——在这种情况下是鼠标点击或触摸屏点击。如果用户点击或触摸 Web 页面的特定元素,那么浏览器将调用我们的applyUpdate()
回调函数,传递一个包含有关事件的详细信息(如时间和鼠标指针坐标)的对象。
JavaScript 权威指南第七版(GPT 重译)(五)(2)https://developer.aliyun.com/article/1485372