这是一门带互动练习的学习课程,您可以在浏览器中直接完成练习。如果您只想浏览内容,请点击下面的按钮

JavaScript 中的函数式编程

这是一系列互动练习,用于学习 Microsoft 的 JavaScript 响应式扩展库 (Rx)。那么,为什么标题是“JavaScript 中的函数式编程”呢?事实证明,学习 Rx 的关键是训练自己使用函数式编程来操作集合。函数式编程为开发人员提供了将常见的集合操作抽象化为可重用、可组合的构建块的工具。您会惊讶地发现,大多数在集合上执行的操作都可以通过五个简单的函数(一些是 JavaScript 原生的,一些包含在 RxJS 库 中)来完成。

  1. map
  2. filter
  3. concatAll
  4. reduce
  5. zip

我对您的承诺是:如果您学习了这 5 个函数,您的代码将变得更短、更具自描述性,并且更持久。此外,出于目前可能不明显的原因,您将了解到这五个函数是简化异步编程的关键。完成本教程后,您还将拥有所有必要的工具,以便轻松避免竞争条件、传播和处理异步错误,以及对事件和 AJAX 请求进行排序。简而言之,这 5 个函数可能是您学过的最强大、最灵活、最有用的函数。

完成互动练习

这不仅是一个教程,而且是一系列您可以直接在浏览器中完成的互动练习! 完成这些练习很容易。只需编辑代码并按“运行”即可。如果代码有效,将在下面出现新的练习。否则,将出现错误。

注意: 使用“F4”键切换每个编辑器的全屏模式。

本教程可能存在错误,因此如果您遇到奇怪的状态,或者您确定自己有正确的答案但无法继续,只需刷新浏览器即可。如果您使用的是现代浏览器,并且假设您在这里,那么您的练习状态将被保存。如果需要,您也可以 重新开始实验室。

本教程在 GitHub 上,并且正在渐进式地接近完成。如果您想添加练习、澄清问题描述或修复错误,请随时 fork 并向我们发送拉取请求。我们会尝试将用户贡献的练习融入叙述中。

您的答案将保存在本地存储中。如果您想将答案转移到另一台机器,请使用下面的按钮。

使用数组

数组是 JavaScript 唯一的集合类型。数组无处不在。我们将把这五个函数添加到 Array 类型中,并在过程中使它变得更加强大和有用。实际上,Array 已经拥有 map、filter 和 reduce 函数!但是,我们将重新实现这些函数作为学习练习。

本节将遵循一个模式。首先,我们将按照您可能在学校学到的或通过阅读其他人的代码自己学到的方式来解决问题。换句话说,我们将使用循环和语句将集合转换为新的集合。然后,我们将实现五个函数之一,然后使用它来再次解决相同的问题,不使用循环。一旦我们学习了五个函数,您将学习如何将它们组合起来,使用很少的代码来解决复杂的问题。前两个练习已经提前完成,但请仔细阅读!

遍历数组

练习 1:打印数组中的所有名称

<- 点击此处尝试您的解决方案。如果有效,您将进入下一个练习。
			// Traverse array with for loop
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")");
				var items = [];
				var got;
				var expected = '["Ben","Brian","Jafar","Matt","Priya"]';
				fun({
					log:function(name) {
						items.push(name);
						console.log(name);
					}
				});

				got = JSON.stringify(items.sort());
				if(got === expected) {
					return "Success!"
				}
				else {
					showLessonErrorMessage(expected, got, 'Note: order does not matter');
				}
			}
		
			function(console) {
				var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"],
					counter;

				for(counter = 0; counter < names.length; counter++) {
					console.log(names[counter]);
				}
			}
		

问自己这个问题:我们需要指定打印名称的顺序吗? 如果不是,为什么这样做呢?

练习 2:使用 forEach 打印数组中的所有名称

让我们使用 forEach 函数重复上一个练习。

			// Traverse array with foreach
			function(str) {
				preVerifierHook();
				if (str.indexOf(".forEach") === -1) {
					return "You have to use forEach!"
				}
				var fun = eval("(" + str + ")");
				var items =[];
				fun({
					log:function(name) {
						items.push(name);
						console.log(name);
					}
				});
				if(JSON.stringify(items.sort()) === '["Ben","Brian","Jafar","Matt","Priya"]') {
					return "Success!"
				}
				else {
					throw 'console.log did not receive all of these values: "Ben","Brian","Jafar","Matt","Priya" (note: order does not matter)'
				}
			}
		
			function(console) {
				var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"];

				names.forEach(function(name) {
					console.log(name);
				});
			}
    	

请注意,forEach 让我们指定对数组中的每个项目执行的操作,但隐藏了遍历数组的方式

投影数组

将函数应用于值并创建新值称为投影。要将一个数组投影到另一个数组,我们将投影函数应用于数组中的每个项目,并将结果收集到一个新的数组中。

练习 3:使用 forEach() 将视频数组投影到 {id,title} 对数组中

对于每个视频,将投影的 {id, title} 对添加到 videoAndTitlePairs 数组中。

			// Projection with with forEach
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videoAndTitlePairs = fun(),
					expected = '[{\"id\":675465,\"title\":\"Fracture\"},{\"id\":65432445,\"title\":\"The Chamber\"},{\"id\":70111470,\"title\":\"Die Hard\"},{\"id\":654356453,\"title\":\"Bad Boys\"}]';

				// Sorting by video id
				videoAndTitlePairs = videoAndTitlePairs.sortBy(function(video) { return video.id });
				if (JSON.stringify(videoAndTitlePairs) === expected) {
					return true;
				}
				else {
					showLessonErrorMessage(expected, JSON.stringify(videoAndTitlePairs));
				}
			}
    	
			function() {
				var newReleases = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [4.0],
							"bookmark": []
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [5.0],
							"bookmark": [{ id: 432534, time: 65876586 }]
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [4.0],
							"bookmark": []
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [5.0],
							"bookmark": [{ id: 432534, time: 65876586 }]
						}
					],
					videoAndTitlePairs = [];

				newReleases.forEach(function(video) {
					videoAndTitlePairs.push({ id: video.id, title: video.title });
				});

				return videoAndTitlePairs;
			}
		

所有数组投影都具有两个共同的操作

  1. 遍历源数组
  2. 将每个项目的投影值添加到一个新数组中

为什么不将这些操作执行方式抽象出来呢?

练习 4:实现 map()

为了使投影更容易,让我们向 Array 类型添加一个map() 函数。Map 接受要应用于源数组中每个项目的投影函数,并返回投影后的数组。

			// Implement map()
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					arr = [1,2,3],
					result;

				result = arr.map(function(x) { return x + 1});

				if (JSON.stringify(arr) !== "[1,2,3]") {
					throw "Whoa! You changed the input array. Map never changes the value of the array passed in. It creates a new array with the results of applying the projection function to every value in the old array."
				}
				else if(JSON.stringify(result) !== '[2,3,4]') {
					throw 'Expected that [1,2,3].map(function(x) { return x + 1}) would equal [2,3,4].'
				}
			}
		
			Array.prototype.map = function(projectionFunction) {
				var results = [];
				this.forEach(function(itemInArray) {
					results.push(projectionFunction(itemInArray));

				});

				return results;
			};

			// JSON.stringify([1,2,3].map(function(x) { return x + 1; })) === '[2,3,4]'
		

练习 5:使用 map() 将视频数组投影到 {id,title} 对数组中

让我们重复收集 newReleases 数组中每个视频的 {id, title} 对的练习,但这次我们将使用我们的 map 函数。

			// Projection with map
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videoAndTitlePairs = fun(),
					expected = '[{\"id\":675465,\"title\":\"Fracture\"},{\"id\":65432445,\"title\":\"The Chamber\"},{\"id\":70111470,\"title\":\"Die Hard\"},{\"id\":654356453,\"title\":\"Bad Boys\"}]';

				// Sorting by video id
				videoAndTitlePairs = videoAndTitlePairs.sortBy(function(video) { return video.id });
				if (JSON.stringify(videoAndTitlePairs) === expected) {
					return true;
				}
				else {
					throw 'Expected: ' + expected;
				}
			}
		
			function() {
				var newReleases = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [4.0],
							"bookmark": []
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [5.0],
							"bookmark": [{ id: 432534, time: 65876586 }]
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [4.0],
							"bookmark": []
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": [5.0],
							"bookmark": [{ id: 432534, time: 65876586 }]
						}
					];

			  return newReleases.map(function(video) { return { id: video.id, title: video.title }; });
			}
		

请注意,map 允许我们指定要应用于数组的投影,但隐藏了操作执行方式

过滤数组

与投影类似,过滤数组也是一个非常常见的操作。要过滤数组,我们将对数组中的每个项目应用一个测试,并将通过测试的项目收集到一个新的数组中。

练习 6:使用 forEach() 收集评分为 5.0 的视频

使用 forEach() 遍历 newReleases 数组中的视频,如果视频的评分为 5.0,则将其添加到 videos 数组中。

			// Filter with forEach
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					expected = '[{"id":675465,"title":"Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture.jpg","uri":"http://api.netflix.com/catalog/titles/movies/70111470","rating":5,"bookmark":[{"id":432534,"time":65876586}]},{"id":654356453,"title":"Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys.jpg","uri":"http://api.netflix.com/catalog/titles/movies/70111470","rating":5,"bookmark":[{"id":432534,"time":65876586}]}]';

				// Sorting by video id
				videos = videos.sortBy(function(v) { return v.id; });
				if (JSON.stringify(videos) === expected) {
					return true;
				}
				else {
					throw 'Expected: ' + expected;
				}
			}
		
			function() {
				var newReleases = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
							"bookmark": []
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
							"bookmark": [{ id: 432534, time: 65876586 }]
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
							"bookmark": []
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
							"bookmark": [{ id: 432534, time: 65876586 }]
						}
					],
					videos = [];

				newReleases.forEach(function(video) {
					if (video.rating === 5.0) {
						videos.push(video);
					}
				});

				return videos;
			}
		

请注意,与 map() 一样,每个 filter() 操作都有一些共同的操作

  1. 遍历数组
  2. 将通过测试的对象添加到一个新数组中

为什么不将这些操作执行方式抽象出来呢?

练习 7:实现 filter()

为了使过滤更容易,让我们向 Array 类型添加一个 filter() 函数。filter() 函数接受一个谓词。谓词是一个函数,它接受数组中的一个项目,并返回一个布尔值,指示是否应将该项目保留在新数组中。

			// Implement filter()
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					arr = [1,2,3],
					result;

				result = arr.filter(function(x) { return x > 2});

				if (JSON.stringify(arr) !== "[1,2,3]") {
					throw "Whoa! You changed the input array. Filter never changes the value of the array passed in. It creates a new array that includes only those items in the old array that pass the predicate function."
				}
				else if(JSON.stringify(result) !== '[3]') {
					throw 'Expected that [1,2,3].filter(function(x) { return x > 2}) would equal [3].'
				}
			}
		
			Array.prototype.filter = function(predicateFunction) {
				var results = [];
				this.forEach(function(itemInArray) {
				  if (predicateFunction(itemInArray)) {
					results.push(itemInArray);
				  }
				});

				return results;
			};

			// JSON.stringify([1,2,3].filter(function(x) { return x > 2})) === "[3]"
		

与 map() 一样,filter() 让我们表达我们想要的数据,而无需我们指定我们想要收集数据的方式

通过链接方法调用查询数据

练习 8:链接 filter 和 map 来收集评分为 5.0 的视频的 id

			// Filter with filter()
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videoids = fun(),
					expected = '[675465,654356453]';

				// Sorting by video id
				videoids = videoids.sortBy(function(v) { return v; });
				if (JSON.stringify(videoids) === expected) {
					return true;
				}
				else {
					throw 'Expected: ' + expected;
				}
			}
			
			function() {
				var newReleases = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
							"bookmark": []
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
							"bookmark": [{ id: 432534, time: 65876586 }]
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
							"bookmark": []
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
							"bookmark": [{ id: 432534, time: 65876586 }]
						}
					];

				return newReleases.
					filter(function(video) {
						return video.rating === 5.0;
					}).
					map(function(video) {
						return video.id;
					});
			}
		

链接 map() 和 filter() 为我们提供了很大的表达能力。这些高级函数让我们表达我们想要的数据,但为底层库在查询执行方式方面提供了很大的灵活性。

查询树

有时,除了扁平数组之外,我们还需要查询树。树带来了挑战,因为我们需要将它们扁平化为数组,以便对它们应用 filter() 和 map() 操作。在本节中,我们将定义一个 concatAll() 函数,我们可以将其与 map() 和 filter() 组合起来以查询树。

练习 9:将 movieLists 数组扁平化为视频 id 数组

让我们首先使用两个嵌套的 forEach 循环来收集二维 movieLists 数组中每个视频的 id。

			// Use filter and map to collect video ids with rating of 5.0
			function(str) {
				var fun = eval("(" + str + ")"),
					videos = fun(),
					expected = '[675465,65432445,70111470,654356453]';

				videos = videos.sortBy(function(v) { return v });
				if (JSON.stringify(videos) !== expected) {
					throw "Expected " + expected;
				}
			}
		
			function() {
				var movieLists = [
						{
							name: "New Releases",
							videos: [
								{
									"id": 70111470,
									"title": "Die Hard",
									"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 654356453,
									"title": "Bad Boys",
									"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						},
						{
							name: "Dramas",
							videos: [
								{
									"id": 65432445,
									"title": "The Chamber",
									"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 675465,
									"title": "Fracture",
									"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						}
					],
					allVideoIdsInMovieLists = [];

				movieLists.forEach(function(movieList) {
					movieList.videos.forEach(function(video) {
						allVideoIdsInMovieLists.push(video.id);
					});
				});

				return allVideoIdsInMovieLists;

			}
		

使用嵌套的 forEach 表达式扁平化树很容易,因为我们可以显式地将项目添加到数组中。不幸的是,正是这种类型的低级操作,我们一直在尝试使用 map() 和 filter() 之类的函数将其抽象出来。我们能否定义一个足够抽象的函数来表达我们扁平化树的意图,而无需指定太多关于如何执行操作的信息呢?

练习 10:实现 concatAll()

让我们向 Array 类型添加一个 concatAll() 函数。concatAll() 函数遍历数组中的每个子数组,并将结果收集到一个新的扁平数组中。请注意,concatAll() 函数期望数组中的每个项目都是另一个数组。

			// Flatten movieLists into an array of video ids
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					arr = [[1,2,3],[4,5,6],[7,8,9]],
					result,
					expected = "[1,2,3,4,5,6,7,8,9]";

				result = arr.concatAll();
				result = result.sortBy(function(x) { return x; });
				if (JSON.stringify(result) !== expected) {
					throw 'Expected that [[1,2,3],[4,5,6],[7,8,9]].concatAll() would equal [1,2,3,4,5,6,7,8,9].'
				}
			}
		
			Array.prototype.concatAll = function() {
				var results = [];
				this.forEach(function(subArray) {
					results.push.apply(results, subArray);
				});

				return results;
			};

			// JSON.stringify([ [1,2,3], [4,5,6], [7,8,9] ].concatAll()) === "[1,2,3,4,5,6,7,8,9]"
			// [1,2,3].concatAll(); // throws an error because this is a one-dimensional array
		

concatAll 是一个非常简单的函数,以至于可能还不明显它如何与 map() 组合起来以查询树。让我们尝试一个示例...

练习 11:使用 map() 和 concatAll() 将 movieLists 投影并扁平化为视频 id 数组

提示:使用两次嵌套的 map() 调用和一次 concatAll() 调用。

			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					expected = '[675465,65432445,70111470,654356453]';

				videos = videos.sortBy(function(v) { return v });
				if (JSON.stringify(videos) !== expected) {
					throw "Expected " + expected + "\n\nReceived " + JSON.stringify(videos);
				}
			}
		
			function() {
				var movieLists = [
						{
							name: "New Releases",
							videos: [
								{
									"id": 70111470,
									"title": "Die Hard",
									"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 654356453,
									"title": "Bad Boys",
									"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						},
						{
							name: "Dramas",
							videos: [
								{
									"id": 65432445,
									"title": "The Chamber",
									"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 675465,
									"title": "Fracture",
									"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
									"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						}
					];

				return movieLists.
				  map(function(movieList) {
					return movieList.videos.map(function(video) {
						return video.id;
					  });
				  }).
				  concatAll();

			}
		

哇!干得好。 掌握 map() 和 concatAll() 的组合是有效函数式编程的关键。您已经完成了一半!让我们尝试一个更复杂的示例...

练习 12:检索每个视频的 id、标题和 150x200 像素的盒装封面 URL

您已经成功地扁平化了一个深度为 2 的树,让我们尝试深度为 3 的树!假设每个视频上没有一个盒装封面 URL,而是有一个盒装封面对象的集合,每个对象都有不同的尺寸和 URL。创建一个查询,为 movieLists 中的每个视频选择 {id, title, boxart}。但是这一次,结果中的 boxart 属性将是尺寸为 150x200px 的盒装封面对象的 URL。让我们看看您是否可以使用 map()、concatAll() 和 filter() 来解决这个问题。

还有一件事:您不能使用索引器。换句话说,这是非法的

				var itemInArray = movieLists[0];
			

此外,在任何剩余的练习中您都不允许使用索引器,除非您正在实现五个函数之一。之所以有此限制,是有一个很好的理由,而且这个理由最终会得到解释。现在,您只需要相信这个限制是有目的的即可。:-)

			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					got,
					expected = JSON.stringify([
						{"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
						{"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" },
						{"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" },
						{"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }
					].sortBy(function(v) { return v.id }));

				if (str.indexOf('[0]') !== -1) {
					throw "You're not allowed to index into the array. You might be creating the object too early. Instead of using an indexer to get the boxart out of the array, try adding a call to map() and creating the object inside the projection function.";
				}

				videos = videos.sortBy(function(v) { return v.id });
				got = JSON.stringify(videos);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var movieLists = [
						{
							name: "Instant Queue",
							videos : [
								{
									"id": 70111470,
									"title": "Die Hard",
									"boxarts": [
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 654356453,
									"title": "Bad Boys",
									"boxarts": [
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }

									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						},
						{
							name: "New Releases",
							videos: [
								{
									"id": 65432445,
									"title": "The Chamber",
									"boxarts": [
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" },
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 675465,
									"title": "Fracture",
									"boxarts": [
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
										{ width: 300, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						}
					];

				return movieLists.
				  map(function(movieList) {
					return movieList.videos.
					  map(function(video) {
						return video.boxarts.
						  filter(function(boxart) {
							return boxart.width === 150 && boxart.height === 200;
						  }).
						  map(function(boxart) {
							return {id: video.id, title: video.title, boxart: boxart.url};
						  });
					  }).
					  concatAll();
				  }).
				  concatAll();

			}
		

太棒了!现在您已经学会了使用 concatAll() 以及 map() 和 filter() 来查询树。请注意,map() 和 concatAll() 通常被链接在一起。让我们创建一个小型辅助函数来帮助我们处理这种常见模式。

练习 13:实现 concatMap()

几乎每次我们扁平化树时,都会链接 map() 和 concatAll()。有时,如果我们正在处理深度为多层的树,我们会多次在代码中重复此组合。为了节省输入,让我们创建一个 concatMap 函数,它只是一个 map 操作,后面跟着一个 concatAll。

			// Implement concatAll
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					spanishFrenchEnglishWords = [ ["cero","rien","zero"], ["uno","un","one"], ["dos","deux","two"] ],
					allWords = [0,1,2],
					result,
					expected = '["cero","rien","zero","uno","un","one","dos","deux","two"]';


				var allWords = [0,1,2].
					concatMap(function(index) {
						return spanishFrenchEnglishWords[index];
					});

				if (JSON.stringify(allWords) !== expected) {
					throw "Expected " + expected;
				}
			}
		
			Array.prototype.concatMap = function(projectionFunctionThatReturnsArray) {
				return this.
					map(function(item) {
						return projectionFunctionThatReturnsArray(item);
					}).
					// apply the concatAll function to flatten the two-dimensional array
					concatAll();
			};

			/*
				var spanishFrenchEnglishWords = [ ["cero","rien","zero"], ["uno","un","one"], ["dos","deux","two"] ];
				// collect all the words for each number, in every language, in a single, flat list
				var allWords = [0,1,2].
					concatMap(function(index) {
						return spanishFrenchEnglishWords[index];
					});

				return JSON.stringify(allWords) === '["cero","rien","zero","uno","un","one","dos","deux","two"]';
			*/
		

现在,我们可以使用 concatMap 辅助函数,而不是使用 map().concatAll() 来扁平化树。

练习 14:使用 concatMap() 检索每个视频的 id、标题和 150x200 像素的盒装封面 URL

让我们重复我们刚刚执行的练习。但是这次我们将通过用 concatMap() 替换 map().concatAll() 调用来简化代码。

			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					got,
					expected = JSON.stringify([
						{"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
						{"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" },
						{"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" },
						{"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }
					].sortBy(function(v) { return v.id }));

				videos = videos.sortBy(function(v) { return v.id });
				got = JSON.stringify(videos);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var movieLists = [
						{
							name: "Instant Queue",
							videos : [
								{
									"id": 70111470,
									"title": "Die Hard",
									"boxarts": [
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 654356453,
									"title": "Bad Boys",
									"boxarts": [
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }

									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						},
						{
							name: "New Releases",
							videos: [
								{
									"id": 65432445,
									"title": "The Chamber",
									"boxarts": [
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" },
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"bookmark": []
								},
								{
									"id": 675465,
									"title": "Fracture",
									"boxarts": [
										{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
										{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
										{ width: 300, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"bookmark": [{ id: 432534, time: 65876586 }]
								}
							]
						}
					];

				return movieLists.concatMap(function(movieList) {
					return movieList.videos.concatMap(function(video) {
						return video.boxarts.
							filter(function(boxart) {
								return boxart.width === 150 && boxart.height === 200;
						  	}).
						  	map(function(boxart) {
								return {id: video.id, title: video.title, boxart: boxart.url};
							});
					  });
				  });
			}
		

看到几个嵌套的 concatMap 操作,最后一个操作是 map,这是一种非常常见的模式。您可以将这种模式视为嵌套 forEach 的函数式版本。

缩减数组

有时我们需要对数组中的多个元素进行操作同时。例如,假设我们需要在数组中找到最大的整数。我们不能使用 `filter()` 操作,因为它一次只检查一个元素。要找到最大的整数,我们需要将数组中的元素相互比较。

一种方法可能是选择数组中的一个元素作为假设最大数(可能是第一个元素),然后将该值与数组中的所有其他元素进行比较。每次遇到一个大于我们假设最大数的数字时,我们都会用更大的值替换它,并继续此过程,直到遍历整个数组。

如果我们将特定的大小比较替换为闭包,我们可以编写一个函数来为我们处理数组遍历过程。在每一步中,我们的函数都会将闭包应用于最后一个值和当前值,并将结果用作下一次的最后一个值。最后,我们将只剩下一个值。此过程称为减少,因为我们将许多值减少为一个值。

练习 15:使用 forEach 查找最大的盒子图片

在这个例子中,我们使用 forEach 来查找最大的盒子图片。每次我们检查一个新的盒子图片时,我们都会用当前已知的最大尺寸更新一个变量。如果盒子图片小于最大尺寸,我们就会丢弃它。如果它更大,我们会记录它。最后,我们只留下一个盒子图片,它一定是最大的。

			// Find largest box art
			function(str){
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					boxart = fun(),
					got = JSON.stringify(boxart),
					expected = JSON.stringify({ width: 425, height:150, url:"http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" });

				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var boxarts = [
						{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
						{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
						{ width: 300, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" },
						{ width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" }
					],
					currentSize,
					maxSize = -1,
					largestBoxart;

				boxarts.forEach(function(boxart) {
					currentSize = boxart.width * boxart.height;
					if (currentSize > maxSize) {
						largestBoxart = boxart;
						maxSize = currentSize;
					}
				});

				return largestBoxart;
			}
		

这个过程是减少,因为我们使用从最后一次计算中得出的信息来计算当前值。但是,在上面的例子中,我们仍然需要指定遍历方法。如果我们只需要指定想要对最后一个值和当前值执行的操作,那不是很好吗?让我们创建一个辅助函数来对数组进行减少。

练习 16:实现 reduce()

让我们在 Array 类型中添加一个 `reduce()` 函数。与 map 一样。注意,这与 **ES5 中的 reduce 不同**,后者返回一个值而不是一个数组!

			// Implement reduce
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					numbers = [1,2,3],
					sum = numbers.reduce(function(acc,curr) { return acc + curr }),
					expected = JSON.stringify([6]),
					sum2 = numbers.reduce(function(acc,curr) { return acc + curr },10),
					expected2 = JSON.stringify([16]);

				if (JSON.stringify(sum) !== expected) {
					throw "Expected that [1,2,3].reduce(function(accumulated,current) { return accumulated + current; }) === [6]. Instead got " + JSON.stringify(sum);
				}
				if (JSON.stringify(sum2) !== expected2) {
					throw "Expected that [1,2,3].reduce(function(accumulated,current) { return accumulated + current; }, 10) === [16]. Instead got " + JSON.stringify(sum2);
				}
			}
		
			// [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }); === [6];
			// [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }, 10); === [16];

			Array.prototype.reduce = function(combiner, initialValue) {
				var counter,
					accumulatedValue;

				// If the array is empty, do nothing
				if (this.length === 0) {
					return this;
				}
				else {
					// If the user didn't pass an initial value, use the first item.
					if (arguments.length === 1) {
						counter = 1;
						accumulatedValue = this[0];
					}
					else if (arguments.length >= 2) {
						counter = 0;
						accumulatedValue = initialValue;
					}
					else {
						throw "Invalid arguments.";
					}

					// Loop through the array, feeding the current value and the result of
					// the previous computation back into the combiner function until
					// we've exhausted the entire array and are left with only one function.
					while(counter < this.length) {
						accumulatedValue = combiner(accumulatedValue, this[counter])
						counter++;
					}

					return [accumulatedValue];
				}
			};
		

练习 17:获取最大的评分。

让我们使用我们新的 reduce 函数来隔离评分数组中的最大值。

			 // Find largest rating
			function(str){
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					boxarts = fun(),
					got = JSON.stringify(boxarts),
					expected = JSON.stringify([5]);


				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var ratings = [2,3,1,4,5];

				return ratings.
				  reduce(function(acc, curr) {
					if(acc > curr) {
					  return acc;
					}
					else {
					  return curr;
					}
				  });
			}
		

干得好。现在让我们尝试将 reduce() 与我们的其他函数结合起来构建更复杂的查询。

练习 18:获取最大盒子图片的 url

让我们尝试将 reduce() 与 map() 结合起来,将多个盒子图片对象减少为一个值:最大盒子图片的 url。

			// Find largest box art with reduce
			function(str){
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					boxarts = fun(),
					got = JSON.stringify(boxarts),
					expected = JSON.stringify(["http://cdn-0.nflximg.com/images/2891/Fracture425.jpg"]);


				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var boxarts = [
						{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
						{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
						{ width: 300, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" },
						{ width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" }
					];

				return boxarts.
				  reduce(function(acc,curr) {
					if (acc.width * acc.height > curr.width * curr.height) {
					  return acc;
					}
					else {
					  return curr;
					}
				  }).
				  map(function(boxart) {
					return boxart.url;
				  });
			}
		

练习 19:使用初始值进行减少

有时,当我们减少一个数组时,我们希望减少的值与数组中存储的元素具有不同的类型。假设我们有一个视频数组,我们想将它们减少为一个单一的映射,其中键是视频 ID,而值是视频的标题。

			// Reducing with an initial value
			function(str){
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videoMap = fun()[0],
					expected = [
						{
							"65432445": "The Chamber",
							"675465": "Fracture",
							"70111470": "Die Hard",
							"654356453": "Bad Boys"
						}
					];

				if (!(videoMap["65432445"] === "The Chamber" && videoMap["675465"] === "Fracture" && videoMap["70111470"] === "Die Hard" && videoMap["654356453"] === "Bad Boys")) {
					throw "Expected " + JSON.stringify(expected);
				}
			}
		
			function() {
				var videos = [
					{
						"id": 65432445,
						"title": "The Chamber"
					},
					{
						"id": 675465,
						"title": "Fracture"
					},
					{
						"id": 70111470,
						"title": "Die Hard"
					},
					{
						"id": 654356453,
						"title": "Bad Boys"
					}
				];

				// Expecting this output...
				// [
				//     {
				//         "65432445": "The Chamber",
				//         "675465": "Fracture",
				//         "70111470": "Die Hard",
				//         "654356453": "Bad Boys"
				//     }
				// ]
				return videos.
					reduce(function(accumulatedMap, video) {
					var obj = {};

					// ----- INSERT CODE TO ADD THE VIDEO TITLE TO THE ----
					// ----- NEW MAP USING THE VIDEO ID AS THE KEY	 ----
					obj[video.id] = video.title;

					// Object.assign() takes all of the enumerable properties from
					// the object listed in its second argument (obj) and assigns them
					// to the object listed in its first argument (accumulatedMap).
					return Object.assign(accumulatedMap, obj);
					},
					// Use an empty map as the initial value instead of the first item in
					// the list.
					{});
			}
		

干得好。现在让我们尝试将 reduce() 与我们的其他函数结合起来构建更复杂的查询。

练习 20:获取每个视频的 ID、标题和最小盒子图片 url。

这是我们之前解决问题的变体,我们检索了宽度为 150px 的盒子图片的 url。这次我们将使用 `reduce()` 而不是 `filter()` 来检索 `boxarts` 数组中最小的盒子图片。

			// Find the id, title, and smallest box art.
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					got,
					expected = JSON.stringify([
						{"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" },
						{"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" },
						{"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" },
						{"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }
					].sortBy(function(v) { return v.id }));

				if (str.indexOf('[0]') !== -1){
					throw "You're not allowed to index into the array. You might be creating the object too early. Instead of using an indexer to get the boxart out of the array, try adding a call to map() and creating the object inside the projection function.";
				}
				videos = videos.sortBy(function(v) { return v.id });
				got = JSON.stringify(videos);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var movieLists = [
					{
						name: "New Releases",
						videos: [
							{
								"id": 70111470,
								"title": "Die Hard",
								"boxarts": [
									{ width: 150, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
									{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }
								],
								"url": "http://api.netflix.com/catalog/titles/movies/70111470",
								"rating": 4.0,
								"bookmark": []
							},
							{
								"id": 654356453,
								"title": "Bad Boys",
								"boxarts": [
									{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
									{ width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }

								],
								"url": "http://api.netflix.com/catalog/titles/movies/70111470",
								"rating": 5.0,
								"bookmark": [{ id:432534, time:65876586 }]
							}
						]
					},
					{
						name: "Thrillers",
						videos: [
							{
								"id": 65432445,
								"title": "The Chamber",
								"boxarts": [
									{ width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" },
									{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }
								],
								"url": "http://api.netflix.com/catalog/titles/movies/70111470",
								"rating": 4.0,
								"bookmark": []
							},
							{
								"id": 675465,
								"title": "Fracture",
								"boxarts": [
									{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
									{ width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" },
									{ width: 300, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }
								],
								"url": "http://api.netflix.com/catalog/titles/movies/70111470",
								"rating": 5.0,
								"bookmark": [{ id:432534, time:65876586 }]
							}
						]
					}
				];


				// Use one or more concatMap, map, and reduce calls to create an array with the following items (order matters)
				// [
				//     {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" },
				//     {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" },
				//     {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" },
				//     {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }
				// ];

				return movieLists.concatMap(function(movieList) {
				  return movieList.videos.concatMap(function(video) {
				    return video.boxarts.
					  reduce(function(acc,curr) {
						if (acc.width * acc.height < curr.width * curr.height) {
						  return acc;
						}
						else {
						  return curr;
						}
					  }).
					  map(function(boxart) {
						return {id: video.id, title: video.title, boxart: boxart.url};
					  });
				  });
				});

			}
		

压缩数组

有时我们需要通过逐个取两个数组中的元素并组合成对来组合两个数组。如果你想象一个拉链,其中每边都是一个数组,每个齿都是一个元素,那么你就知道 zip 操作是如何工作的了。

练习 21:按索引组合视频和书签

使用 for 循环同时遍历 `videos` 和 `bookmarks` 数组。对于每个视频和书签对,创建一个 `{videoId, bookmarkId}` 对,并将其添加到 `videoIdAndBookmarkIdPairs` 数组中。

			// Zip imperatively
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					pairs = fun(),
					got,
					expected = '[{"videoId":65432445,"bookmarkId":445},{"videoId":70111470,"bookmarkId":470},{"videoId":654356453,"bookmarkId":453}]';

				pairs = pairs.sortBy(function(v) { return v.videoId });
				got = JSON.stringify(pairs);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var videos = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
						}
					],
					bookmarks = [
						{id: 470, time: 23432},
						{id: 453, time: 234324},
						{id: 445, time: 987834}
					],
				counter,
				videoIdAndBookmarkIdPairs = [];

				for(counter = 0; counter < Math.min(videos.length, bookmarks.length); counter++) {
				  videoIdAndBookmarkIdPairs.push({videoId: videos[counter].id, bookmarkId: bookmarks[counter].id});
				}

				return videoIdAndBookmarkIdPairs;
			}
		

练习 22:实现 zip

让我们在 Array 类型中添加一个静态 `zip()` 函数。`zip` 函数接受一个组合函数,同时遍历每个数组,并对左侧和右侧的当前元素调用组合函数。`zip` 函数需要每个数组中的一个元素才能调用组合函数,因此 `zip` 返回的数组的大小将仅与最小的输入数组一样大。

			// Implement zip
			function(str) {
				preVerifierHook();
				var fun = eval(str),
					left = [1,2,3],
					right = [4,5,6],
					sum = Array.zip(left, right, function(left, right){ return left + right; }),
					expected = '[5,7,9]';

				if (JSON.stringify(sum) !== expected) {
					showLessonErrorMessage(expected, JSON.stringify(sum));
				}
			}
		
			// JSON.stringify(Array.zip([1,2,3],[4,5,6], function(left, right) { return left + right })) === '[5,7,9]'

			Array.zip = function(left, right, combinerFunction) {
				var counter,
					results = [];

				for(counter = 0; counter < Math.min(left.length, right.length); counter++) {
					results.push(combinerFunction(left[counter],right[counter]));
				}

				return results;
			};
        

练习 23:按索引组合视频和书签

让我们重复练习 21,但这次让我们使用你的新 `zip()` 函数。对于每个视频和书签对,创建一个 `{videoId, bookmarkId}` 对。

			// Combine videos and bookmarks
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					pairs = fun(),
					got,
					expected = '[{"videoId":65432445,"bookmarkId":445},{"videoId":70111470,"bookmarkId":470},{"videoId":654356453,"bookmarkId":453}]';

				pairs = pairs.sortBy(function(v) { return v.videoId });
				got = JSON.stringify(pairs);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var videos = [
						{
							"id": 70111470,
							"title": "Die Hard",
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
						},
						{
							"id": 65432445,
							"title": "The Chamber",
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 4.0,
						},
						{
							"id": 675465,
							"title": "Fracture",
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg",
							"uri": "http://api.netflix.com/catalog/titles/movies/70111470",
							"rating": 5.0,
						}
					],
					bookmarks = [
						{id: 470, time: 23432},
						{id: 453, time: 234324},
						{id: 445, time: 987834}
					];

				return Array.
					zip(
					  videos,
					  bookmarks,
					  function(video, bookmark) {
						return {videoId: video.id, bookmarkId: bookmark.id};
					  });
			}
        

练习 24:获取每个视频的 ID、标题、中间精彩时刻时间和最小盒子图片 url。

这是我们之前解决问题的变体。这次每个视频都有一个精彩时刻集合,每个集合代表一个截图有趣或代表标题整体的时间。请注意,`boxarts` 和 `interestingMoments` 数组都位于树中的相同深度。同时使用 `zip()` 检索中间精彩时刻的时间和最小的盒子图片 url。对于每个视频,返回一个 `{id, title, time, url}` 对象。

			// Find id, title, smallest box art, and bookmark id
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					videos = fun(),
					got,
					expected = '[{"id":675465,"title":"Fracture","time":3453434,"url":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg"},{"id":65432445,"title":"The Chamber","time":3452343,"url":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg"},{"id":70111470,"title":"Die Hard","time":323133,"url":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg"},{"id":654356453,"title":"Bad Boys","time":6575665,"url":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg"}]';

				videos = videos.sortBy(function(v) { return v.id });
				got = JSON.stringify(videos);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var movieLists = [
						{
							name: "New Releases",
							videos: [
								{
									"id": 70111470,
									"title": "Die Hard",
									"boxarts": [
										{ width: 150, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
										{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"interestingMoments": [
										{ type: "End", time:213432 },
										{ type: "Start", time: 64534 },
										{ type: "Middle", time: 323133}
									]
								},
								{
									"id": 654356453,
									"title": "Bad Boys",
									"boxarts": [
										{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
										{ width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }

									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"interestingMoments": [
										{ type: "End", time:54654754 },
										{ type: "Start", time: 43524243 },
										{ type: "Middle", time: 6575665}
									]
								}
							]
						},
						{
							name: "Instant Queue",
							videos: [
								{
									"id": 65432445,
									"title": "The Chamber",
									"boxarts": [
										{ width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" },
										{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 4.0,
									"interestingMoments": [
										{ type: "End", time:132423 },
										{ type: "Start", time: 54637425 },
										{ type: "Middle", time: 3452343}
									]
								},
								{
									"id": 675465,
									"title": "Fracture",
									"boxarts": [
										{ width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
										{ width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" },
										{ width: 300, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }
									],
									"url": "http://api.netflix.com/catalog/titles/movies/70111470",
									"rating": 5.0,
									"interestingMoments": [
										{ type: "End", time:45632456 },
										{ type: "Start", time: 234534 },
										{ type: "Middle", time: 3453434}
									]
								}
							]
						}
					];

				//------------ COMPLETE THIS EXPRESSION --------------
				return movieLists.concatMap(function(movieList) {
					return movieList.videos.concatMap(function(video) {
						return Array.zip(
							video.boxarts.reduce(function(acc,curr) {
								if (acc.width * acc.height < curr.width * curr.height) {
							  	  	return acc;
								}
								else {
							  		return curr;
								}
						  	}),
							video.interestingMoments.filter(function(interestingMoment) {
								return interestingMoment.type === "Middle";
							}),
						  	function(boxart, interestingMoment) {
								return {id: video.id, title: video.title, time: interestingMoment.time, url: boxart.url};
						  	});
					});
				});
			}
		

强大的查询

现在我们已经学习了五个运算符,让我们锻炼一下我们的肌肉,写一些强大的查询。

练习 25:从数组到树的转换

当信息以类似 JSON 表达式的树状结构组织时,关系是从父节点指向子节点的。在像数据库这样的关系系统中,关系是从子节点指向父节点的。这两种组织信息的方式是等效的,根据情况,我们可能会以这种或那种方式获得组织的信息。你可能会惊讶地发现,你可以使用你已经知道的五个查询函数轻松地在这两种表示之间转换。换句话说,**你不仅可以从树中查询数组,还可以从数组中查询树**。

我们有两个数组,分别包含列表和视频。每个视频都有一个 `listId` 字段,表示其父列表。我们想要构建一个列表对象的数组,每个对象都有一个名称和一个视频数组。视频数组将包含视频的 ID 和标题。换句话说,我们想要构建以下结构

			[
				{
					"name": "New Releases",
					"videos": [
						{
							"id": 65432445,
							"title": "The Chamber"
						},
						{
							"id": 675465,
							"title": "Fracture"
						}
					]
				},
				{
					"name": "Thrillers",
					"videos": [
						{
							"id": 70111470,
							"title": "Die Hard"
						},
						{
							"id": 654356453,
							"title": "Bad Boys"
						}
					]
				}
			]
        

注意:请确保在创建对象(列表和视频)时,按照上面的顺序添加属性。这不会影响代码的正确性,但验证器期望属性按照此顺序创建。

			// Combine videos and bookmarks
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					pairs = fun(),
					got,
					expected = '[{"name":"New Releases","videos":[{"id":65432445,"title":"The Chamber"},{"id":675465,"title":"Fracture"}]},{"name":"Thrillers","videos":[{"id":70111470,"title":"Die Hard"},{"id":654356453,"title":"Bad Boys"}]}]';

				got = JSON.stringify(pairs);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var lists = [
						{
							"id": 5434364,
							"name": "New Releases"
						},
						{
							"id": 65456475,
							name: "Thrillers"
						}
					],
					videos = [
						{
							"listId": 5434364,
							"id": 65432445,
							"title": "The Chamber"
						},
						{
							"listId": 5434364,
							"id": 675465,
							"title": "Fracture"
						},
						{
							"listId": 65456475,
							"id": 70111470,
							"title": "Die Hard"
						},
						{
							"listId": 65456475,
							"id": 654356453,
							"title": "Bad Boys"
						}
					];

				return lists.map(function(list) {
					return {
						name: list.name,
						videos:
							videos.
								filter(function(video) {
									return video.listId === list.id;
								}).
								map(function(video) {
									return {id: video.id, title: video.title};
								})
					};
				});
			}
        

看起来你已经弄清楚可以使用 map 和 filter 通过键连接两个不同的数组。现在让我们尝试一个更复杂的示例...

练习 26:从数组到更深的树的转换

让我们尝试创建一个更深的树结构。这次我们有四个单独的数组,分别包含列表、视频、盒子图片和书签。每个对象都有一个父 ID,表示其父节点。我们想要构建一个列表对象的数组,每个对象都有一个名称和一个视频数组。视频数组将包含视频的 ID、标题、书签时间和最小的盒子图片 url。换句话说,我们想要构建以下结构

			[
				{
					"name": "New Releases",
					"videos": [
						{
							"id": 65432445,
							"title": "The Chamber",
							"time": 32432,
							"boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg"
						},
						{
							"id": 675465,
							"title": "Fracture",
							"time": 3534543,
							"boxart": "http://cdn-0.nflximg.com/images/2891/Fracture120.jpg"
						}
					]
				},
				{
					"name": "Thrillers",
					"videos": [
						{
							"id": 70111470,
							"title": "Die Hard",
							"time": 645243,
							"boxart": "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg"
						},
						{
							"id": 654356453,
							"title": "Bad Boys",
							"time": 984934,
							"boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg"
						}
					]
				}
			]
        

注意:请确保在创建对象(列表和视频)时,按照上面的顺序添加属性。这不会影响代码的正确性,但验证器期望属性按照此顺序创建。

			// Combine videos and bookmarks
			function(str) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					pairs = fun(),
					got,
					expected = '[{"name":"New Releases","videos":[{"id":65432445,"title":"The Chamber","time":32432,"boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg"},{"id":675465,"title":"Fracture","time":3534543,"boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg"}]},{"name":"Thrillers","videos":[{"id":70111470,"title":"Die Hard","time":645243,"boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg"},{"id":654356453,"title":"Bad Boys","time":984934,"boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg"}]}]';

				got = JSON.stringify(pairs);
				if (got !== expected) {
					showLessonErrorMessage(expected, got);
				}
			}
		
			function() {
				var lists = [
						{
							"id": 5434364,
							"name": "New Releases"
						},
						{
							"id": 65456475,
							name: "Thrillers"
						}
					],
					videos = [
						{
							"listId": 5434364,
							"id": 65432445,
							"title": "The Chamber"
						},
						{
							"listId": 5434364,
							"id": 675465,
							"title": "Fracture"
						},
						{
							"listId": 65456475,
							"id": 70111470,
							"title": "Die Hard"
						},
						{
							"listId": 65456475,
							"id": 654356453,
							"title": "Bad Boys"
						}
					],
					boxarts = [
						{ videoId: 65432445, width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" },
						{ videoId: 65432445, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" },
						{ videoId: 675465, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
						{ videoId: 675465, width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" },
						{ videoId: 675465, width: 300, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" },
						{ videoId: 70111470, width: 150, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
						{ videoId: 70111470, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" },
						{ videoId: 654356453, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
						{ videoId: 654356453, width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }
					],
					bookmarks = [
						{ videoId: 65432445, time: 32432 },
						{ videoId: 675465, time: 3534543 },
						{ videoId: 70111470, time: 645243 },
						{ videoId: 654356453, time: 984934 }
					];

				return lists.map(function(list) {
					return {
						name: list.name,
						videos:
							videos.
								filter(function(video) {
									return video.listId === list.id;
								}).
								concatMap(function(video) {
									return Array.zip(
										bookmarks.filter(function(bookmark) {
											return bookmark.videoId === video.id;
										}),
										boxarts.filter(function(boxart) {
											return boxart.videoId === video.id;
										}).
										reduce(function(acc,curr) {
											return acc.width * acc.height < curr.width * curr.height ? acc : curr;
										}),
										function(bookmark, boxart) {
											return { id: video.id, title: video.title, time: bookmark.time, boxart: boxart.url };
										});
							})
					};
				});

			}
        

哇!这是一个大型查询,但代码相对于它所做的工作量来说仍然很小。如果我们用一系列循环重写这个查询,我们的代码将变得不那么自描述。循环不会向读者提供有关正在执行的操作类型的任何信息。每次你看到循环时,都需要仔细阅读代码以找出正在做什么。是投影吗?过滤器?减少?为什么要使用循环来查询数据,因为我们已经证明了五个函数可以用来创建我们想要的任何输出?

练习 27:股票行情

让我们尝试一个更简单的例子。假设我们有一个包含所有纳斯达克股票价格随时间变化的集合。每次纳斯达克股票行情上的股票价格发生变化时,都会在这个集合中添加一个条目。假设十天前你购买了微软股票,现在你想打印所有那以后的微软股票价格。从十天前开始过滤微软交易的集合,并使用 `print()` 函数打印每个价格记录(包括时间戳)。**注意:这不是一个技巧问题。它看起来很简单,就和它一样简单。**

			// The pricesNASDAQ collection looks something like this...
			var pricesNASDAQ = [
				// ... from the NASDAQ's opening day
				{name: "ANGI", price: 31.22, timeStamp: new Date(2011,11,15) },
				{name: "MSFT", price: 32.32, timeStamp: new Date(2011,11,15) },
				{name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)},
				{name: "ANGI", price: 28.44, timeStamp: new Date(2011,11,16)},
				{name: "GOOG", price: 199.33, timeStamp: new Date(2011,11,16)},
				// ...and up to the present.
			];
		

控制台

			// Combine videos and bookmarks
			function(str, lesson) {
				preVerifierHook();
				var output = $(".output", lesson)[0],
					fun = eval("(" + str + ")"),
					stockSymbols = ["MSFT", "GOOG","NFLX","OSTK"],
					input = [{name: "MSFT", price: 32.32, timeStamp: new Date() }, {name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)}],
					expected = [input[0]],
					items = [],
					counter = 0;
					confirmPrint = function(item) {
						items.push(item);
					},
					print = function(item) {
						output.innerHTML += "MSFT " + item.price + " at " + item.timeStamp.toString() + "<" + "br" + ">";
						output.scrollTop = output.scrollHeight;
						counter++;
						if (counter % 100 === 0) {
							output.innerHTML = "";
						}
					},
					stocks =
						Rx.Observable.
							interval(250).
							map(function() {
								var symbol = stockSymbols[Math.floor(Math.random() * stockSymbols.length)];
								return {name: symbol, price: 30 + ((Math.floor(Math.random() * 100))/200), timeStamp: new Date()};
							});

				fun(input, confirmPrint);

				if (JSON.stringify(items) !== JSON.stringify(expected)) {
					throw "Got " + JSON.stringify(items, null, 4) + ", expected " + JSON.stringify(expected, null, 4);
				}

				fun(stocks, print);
			}
		
			function(pricesNASDAQ, printRecord) {
				var microsoftPrices,
					now = new Date(),
					tenDaysAgo = new Date( now.getFullYear(), now.getMonth(), now.getDate() - 10);

				// use filter() to filter the trades for MSFT prices recorded any time after 10 days ago
				microsoftPrices =
					pricesNASDAQ.
						filter(function(priceRecord) {
						  return priceRecord.name === 'MSFT' && priceRecord.timeStamp > tenDaysAgo;
						});

				// Print the trades to the output console
				microsoftPrices.
					forEach(function(priceRecord) {
						printRecord(priceRecord);
					});
			}
        

**请注意,控制台正在随时间变化。**现在看看股票价格上的时间戳。我们正在显示在运行程序之后采样的股票价格!我们的数组怎么会包含来自未来的股票价格记录?我们是不是不小心撕裂了时空连续体?

谜题的答案是,**`pricesNASDAQ` 不是一个数组**。与只能存储股票价格快照的数组不同,这种新类型可以对变化做出反应并随着时间的推移进行更新。

在下一节中,我将揭示这种神奇类型的内部机制。你将学习如何使用它来模拟从鼠标事件到异步 JSON 请求的一切。最后,我将向你展示如何**使用你已经知道的五个查询函数来查询这种类型。**是时候给这种神奇类型起个名字了...

使用 Observable

微软的开源 Reactive Extensions 库为 Javascript 引入了一种新的集合类型:**Observable。**Observable 很像事件。就像事件一样,**Observable 是数据生产者推送给消费者的值序列。**但是与事件不同的是,**Observable 可以向监听器发出已完成的信号,**并且不会再发送任何数据。

Observable 可以异步地将数据发送给消费者。与 Array 不同的是,没有 Javascript 字面量语法来创建 Observable 序列。但是,我们可以构建一个辅助方法,直观地描述序列的内容以及每个元素到达之间的间隔。`seq` 函数从一个元素数组创建一个 Observable,并为遇到的每个空元素添加一个延迟。每个 `, , ,` 加起来为一秒。

				// An array of numbers 1,2,3
				var numbers123Array =      [1,2,3];

				// A sequence that returns 1, and then after 4 seconds returns 2,
				// and then after another second returns 3, and then completes.
				var numbers123Observable = seq([1,,,,,,,,,,,,2,,,3]);

				// Like Arrays, Observables can contain any object - even Arrays.
				var observableOfArrays = seq([ [1,2,3],,,,,,[4,5,6],,,,,,,,,,,[1,2] ]);
			

Observable 是一个值序列,一个接一个地传递。因此,一个 Observable 可能永远向监听器发送数据,就像鼠标移动事件一样。要创建一个不完成的序列,你可以在传递给 `seq()` 的元素末尾添加一个尾随 `, , ,`。

				// The trailing ,,, ensures that the sequence will not complete.
				var mouseMovesObservable =
					seq([ {x: 23, y: 55},,,,,,,{x: 44, y: 99},,,{x:55,y:99},,,{x: 54, y:543},,, ]);

				// No trailing ,,, indicates that sequence will complete.
				var numbers123Observable = seq([1,,,2,,,3]);
			

查询数组只会给我们一个快照。相反,查询 Observable 允许我们创建随着系统变化而反应和更新的数据集。这使得一种非常强大的编程类型成为可能,称为响应式编程

让我们从对比 Observable 和事件开始...

练习 28:订阅事件

你可能习惯于将事件视为存储在对象中的处理程序列表。在这个例子中,我们订阅了一个按钮点击事件,然后在第一次点击按钮时取消订阅。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					button = $('.button', lesson)[0];

				fun(button);
			}
		

问问自己这个问题:**订阅事件与遍历数组有什么不同?**两种操作都涉及通过重复调用一个函数,将一个项目的序列发送给一个监听器。那么为什么我们不能以相同的方式遍历数组和事件呢?

练习 29:遍历事件

订阅事件和遍历数组从根本上来说是相同的操作。唯一的区别是**数组遍历是同步的且完成的,而事件遍历是异步的且永远不会完成。**如果我们将按钮点击事件转换为一个 Observable 对象,我们可以使用 `do()` 来遍历事件。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					button = $('.button', lesson)[0];

				fun(button);
			}
		

请注意,**Observable 的 `forEach()` 函数返回一个 Subscription 对象。**处置 Subscription 对象会取消订阅事件,并防止内存泄漏。处置订阅相当于异步地停止执行一个计数 for 循环。

销毁一个订阅对象基本上等同于调用 removeEventListener()。表面上,这两种事件处理方法似乎并没有太大区别。在这种情况下,为什么要费心将事件转换为 Observable 呢?原因是**如果我们将事件转换为 Observable 对象,我们就可以使用强大的函数来转换它们。** 在下一节练习中,我们将学习如何使用其中一个函数来避免在许多情况下处理订阅……

练习 30:使用 take() 完成序列

**你是否曾经希望能够监听事件的下一个发生,然后立即取消订阅?** 例如,开发人员经常将事件处理程序附加到 window.onload,期望他们的事件处理程序只会被调用一次。

			window.addEventListener(
				"load",
				function()
					// do some work here, but expect this function will only be called once.
				})
		

在这种情况下,最好在事件触发后取消订阅。未能取消订阅会导致**内存泄漏**。根据具体情况,内存泄漏可能会严重破坏应用程序的稳定性,并且很难追踪。不幸的是,在一次发生后取消订阅事件可能很麻烦。

			var handler = function() {
				// do some work here, then unsubscribe from the event
				window.removeEventListener("load", handler)
			};
			window.addEventListener("load", handler);
		

如果有一种更简单的方法来编写代码,那岂不是很好?这就是 Observable 有 take() 函数的原因。take() 函数的工作原理如下……

			seq([1,,,2,,,3,,,4,,,5,,,6,,,]).take(3) === seq([1,,,2,,,3]);
		

基于事件的 Observable 永远不会自行完成。take() 函数创建一个新的序列,该序列在收到一定数量的项目后完成。这很重要,因为与事件不同,当 Observable 序列完成时,它会取消订阅其所有监听器。 这意味着**如果我们使用 take() 来完成我们的事件序列,我们就不需要取消订阅!**

让我们重复之前的练习,我们在其中监听了一个按钮点击并取消订阅。这次让我们使用 take() 函数在点击按钮后完成序列。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					button = $('.button', lesson)[0];

				fun(button);
			}
		
			function(button) {
				var buttonClicks = Observable.fromEvent(button, "click");

				// Use take() to listen for only one button click
				// and unsubscribe.
				buttonClicks.
					take(1).
					forEach(function() {
						alert("Button was clicked once. Stopping Traversal.");
					});
			}
		

take() 函数是监听一定数量的事件然后取消订阅的绝佳方式,但 Observable 有一个更灵活的函数,我们可以用来完成序列……

练习 31:使用 takeUntil() 完成序列

**你是否曾经想在另一个事件触发时取消订阅一个事件?** Observable 的 takeUntil() 函数是在另一个事件发生时完成序列的便捷方式。以下是 takeUntil() 的工作原理

			var numbersUntilStopButtonPressed =
				seq(              [ 1,,,2,,,3,,,4,,,5,,,6,,,7,,,8,,,9,,, ]).
					takeUntil(seq([  ,,, {eventType: "click"},,, ]) )                    === seq([ 1,,,2 ])
		

早些时候,我们(无意中)使用 Observable 构建了一个动态的微软股票价格走势图。该股票走势图的问题是,它会一直运行下去。如果任其发展,日志中的所有条目可能会耗尽页面上的所有内存。在下面的练习中,过滤 MSFT 股票价格的 NASDAQ 价格 Observable 序列,使用 fromEvent() 函数创建一个 Observable 。

<-按此按钮完成微软股票价格序列。

控制台

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					output = $(".output", lesson)[0],
					stopButton = $('.stop', lesson)[0],
					stockSymbols = ["MSFT", "GOOG","NFLX","OSTK"],
					input = [{name: "MSFT", price: 32.32, timeStamp: new Date() }, {name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)}],
					expected = [input[0]],
					items = [],
					counter = 0,
					print = function(item) {
						output.innerHTML += "MSFT " + item.price + " at " + item.timeStamp.toString() + "<" + "br" + ">";
						output.scrollTop = output.scrollHeight;
						counter++;
						if (counter % 100 === 0) {
							output.innerHTML = "";
						}
					},
					stocks =
						Rx.Observable.
							interval(250).
							map(function() {
								var symbol = stockSymbols[Math.floor(Math.random() * stockSymbols.length)];
								return {name: symbol, price: 30 + ((Math.floor(Math.random() * 100))/200), timeStamp: new Date()};
							});

				fun(stocks, print, stopButton);
			}
		
			function(pricesNASDAQ, printRecord, stopButton) {
				var stopButtonClicks = Observable.fromEvent(stopButton,"click"),
					microsoftPrices =
						pricesNASDAQ.
							filter(function(priceRecord) {
								return priceRecord.name === "MSFT";
							}).
							takeUntil(stopButtonClicks);

				microsoftPrices.
					forEach(function(priceRecord) {
						printRecord(priceRecord);
					});
			}
		

我们已经了解到 Observable 序列比原始事件强大得多,因为它们可以完成。**take() 和 takeUntil() 函数足够强大,以确保我们再也不用取消订阅任何事件!** 这降低了内存泄漏的风险,并使我们的代码更易读。

以下是我们在本节中学到的内容

  • 我们可以使用 forEach() 遍历 Observable。
  • 我们可以使用 fromEvent() 将事件转换为永不完成的 Observable。
  • 我们可以将 take() 和 takeUntil() 应用于 Observable 以创建一个确实完成的新序列。

在下一节中,我们将学习如何组合事件以创建更复杂的事件。你一定会对使用这些方法轻松解决复杂异步问题感到惊讶!

查询 Observable

以下两个任务有什么区别?

  • 从多个电影列表中创建一个评分为 5.0 的电影的扁平列表。
  • 创建一个包含来自 mouseDown、mouseMove 和 mouseUp 事件的所有鼠标拖动事件的序列。

你可能会认为它们不同,并且可能会使用完全不同的代码来编写它们,但**从根本上来说,这些任务是相同的。** 这两个任务都是查询,并且可以使用你在这些练习中学到的函数来解决。

**遍历数组和遍历 Observable 之间的区别在于数据移动的方向。** 遍历数组时,客户端从数据源拉取数据,阻塞直到获得结果。遍历 Observable 时,数据源在数据到达时将数据推送到客户端。

事实证明,数据移动的方向与查询数据正交。换句话说,**当我们查询数据时,数据是拉取还是推送并不重要。** 在任何情况下,查询方法都会执行相同的转换。唯一改变的是输入和输出类型。如果我们过滤一个数组,我们将得到一个新的数组。如果我们过滤一个 Observable,我们将得到一个新的 Observable,等等。

看看查询方法如何转换 Observable 和数组

				// map()

				[1,2,3].map(function(x) { return x + 1 })                       === [2,3,4]
				seq([1,,,2,,,3]).map(function(x) { return x + 1})               === seq([2,,,3,,,4])
				seq([1,,,2,,,3,,,]).map(function(x) { return x + 1 })           === seq([2,,,3,,,4,,,])

				// filter()

				[1,2,3].filter(function(x) { return x > 1; })                   === [2,3]
				seq([1,,,2,,,3]).filter(function(x) { return x > 1; })          === seq([2,,,3])
				seq([1,,,2,,,3,,,]).filter(function(x) { return x > 1; })       === seq([2,,,3,,,])

				// concatAll()

				[ [1, 2, 3], [4, 5, 6] ].concatAll()                             === [1,2,3,4,5,6]
				seq([ seq([1,,,2,,,3]),,,seq([4,,,5,,,6]) ]).concatAll()         === seq([1,,,2,,,3,,,4,,,5,,,6])

				// If a new sequence arrives before all the items
				// from the previous sequence have arrived, no attempt
				// to retrieve the new sequence's elements is made until
				// the previous sequence has completed. As a result the
				// order of elements in the sequence is preserved.
				seq([
					seq([1,,,, ,2, ,3])
					,,,seq([,,4, ,5, ,,6]) ]).
					concatAll()                                                  === seq([1,,,,,2,,3,,4,,5,,,6])

				// Notice that as long as at least one sequence being
				// concatenated is incomplete, the concatenated sequence is also
				// incomplete.
				seq([
					seq([1,, ,,, ,,,2,,,3])
					,,,seq([4,,,5,,, ,,, ,,6,,,]) ]).
					concatAll()                                                  === seq([1,,,,,,,,2,,,3,4,,,5,,,,,,,,6,,,])

				// reduce()

				[ 1, 2, 3 ].reduce(sumFunction)                                 === [ 6 ]
				seq([ 1,,,2,,,3 ]).reduce(sumFunction)                          === seq([,,,,,,6])

				// Reduced sequences do not complete until the
				// sequence does.
				seq([ 1,,,2,,,3,,, ]).reduce(sumFunction)                       === seq([ ,,,,,,,,,])

				// zip()

				// In both Arrays and Observables, the zipped sequence
				// completes as soon as either the left or right-hand
				// side sequence completes.
				Array.zip([1,2],[3,4,5], sumFunction)                           === [4,6]
				Observable.zip(seq([1,,,2]),seq([3,,,4,,,5]), sumFunction)      === seq([4,,,6])

				// take()
				[1,2,3].take(2)                                                 === [1, 2]
				seq([ 1,,,2,,,3 ]).take(2)                                      === seq([ 1,,,2 ])
				seq([ 1,,,2,,,3,,, ]).take(2)                                   === seq([ 1,,,2 ])

				// takeUntil()

				// takeUntil works for Arrays, but it's not very useful
				// because the result will always be an empty array.
				[1,2,3].takeUntil([1])                                          === []

				seq([1,,,2,,,3,,, ]).takeUntil(
				seq([ ,,, ,,4 , ]))                                             === seq([ 1,,,2 ])

			

还记得我禁止使用数组索引器吗? 现在你应该更清楚地了解这一限制的原因。虽然这 5 个函数可以应用于任何集合,但索引器只能应用于支持随机访问的集合(如 Array)。如果你避免使用索引器,并且坚持使用在本教程中学到的函数,你将拥有一个统一的编程模型来转换任何集合。拥有一个统一的编程模型使得将同步代码转换为异步代码变得轻而易举,否则这个过程将非常困难。正如我们所演示的,你不必使用索引器来执行复杂的集合转换。

现在我们已经了解到可以使用相同的编程模型来查询异步和同步数据源,让我们使用 Observable 和我们的查询函数来创建复杂的新事件。

练习 32:创建鼠标拖动事件

还记得我们之前解决的练习吗?那个从电影列表数组中检索所有评分为 5.0 的电影的练习?如果我们用伪代码描述解决方案,它可能看起来像这样……

"对于每个电影列表,只检索评分为 5.0 的视频"

			var moviesWithHighRatings =
				movieLists.
					concatMap(function(movieList) {
						return movieList.videos.
							filter(function(video) {
								return video.rating === 5.0;
							});
					});
		

现在我们将为 DOM 对象创建一个 mouseDrag 事件。如果我们用伪代码描述这个问题,它可能看起来像这样……

"对于每个电影列表在精灵上的鼠标按下事件,只检索在下一个鼠标抬起事件之前发生的评分为 5.0 的视频鼠标移动事件."

精灵 精灵容器
			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					output = $(".output", lesson)[0],
					container = $(".container", lesson)[0],
					sprite = $(".sprite",lesson)[0],
					moveSprite = function(point) {
						sprite.style.left = point.pageX + "px";
						sprite.style.top = point.pageY + "px";
					}

				fun(sprite, container, moveSprite);
			}
		
			function(sprite, spriteContainer) {
				var spriteMouseDowns = Observable.fromEvent(sprite, "mousedown"),
					spriteContainerMouseMoves = Observable.fromEvent(spriteContainer, "mousemove"),
					spriteContainerMouseUps = Observable.fromEvent(spriteContainer, "mouseup"),
					spriteMouseDrags =
						// For every mouse down event on the sprite...
						spriteMouseDowns.
							concatMap(function(contactPoint) {
								// ...retrieve all the mouse move events on the sprite container...
								return spriteContainerMouseMoves.
									// ...until a mouse up event occurs.
									takeUntil(spriteContainerMouseUps);
							});

				// For each mouse drag event, move the sprite to the absolute page position.
				spriteMouseDrags.forEach(function(dragPoint) {
					sprite.style.left = dragPoint.pageX + "px";
					sprite.style.top = dragPoint.pageY + "px";
				});
			}
        

现在我们真的开始上手了。我们只用几行代码就创建了一个复杂的事件。我们不必处理任何订阅对象,也不必编写任何有状态的代码。让我们尝试更难一些的操作。

练习 33:改进我们的鼠标拖动事件

我们的鼠标拖动事件有点简单了。请注意,当我们拖动精灵时,它总是定位在鼠标的左上角。理想情况下,我们希望我们的拖动事件根据鼠标按下时在精灵上的位置来偏移其坐标。这将使我们的鼠标拖动更像用手指移动真实物体。

让我们看看你是否可以根据鼠标在精灵上的按下位置调整鼠标拖动事件中的坐标。鼠标事件是序列,它们看起来像这样

			spriteContainerMouseMoves =
				seq([ {x: 200, y: 400, layerX: 10, layerY: 15},,,{x: 210, y: 410, layerX: 20, layerY: 26},,, ])
		

鼠标事件序列中的每个项目都包含一个 x、y 值,表示鼠标事件在页面上的绝对位置。moveSprite() 函数使用这些坐标来定位精灵。序列中的每个项目包含一对 layerX 和 layerY 属性,它们指示鼠标事件相对于事件目标的位置。

精灵 精灵容器
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					output = $(".output", lesson)[0],
					container = $(".container", lesson)[0],
					sprite = $(".sprite",lesson)[0],
					moveSprite = function(point) {
						sprite.style.left = point.pageX + "px";
						sprite.style.top = point.pageY + "px";
					}

				fun(sprite, container, moveSprite);
			}
		
			function(sprite, spriteContainer) {
				// All of the mouse event sequences look like this:
				// seq([ {pageX: 22, pageY: 3423, layerX: 14, layerY: 22} ,,, ])
				var spriteMouseDowns = Observable.fromEvent(sprite, "mousedown"),
					spriteContainerMouseMoves = Observable.fromEvent(spriteContainer, "mousemove"),
					spriteContainerMouseUps = Observable.fromEvent(spriteContainer, "mouseup"),
					// Create a sequence that looks like this:
					// seq([ {pageX: 22, pageY:4080 },,,{pageX: 24, pageY: 4082},,, ])
					spriteMouseDrags =
						// For every mouse down event on the sprite...
						spriteMouseDowns.
							concatMap(function(contactPoint) {
								// ...retrieve all the mouse move events on the sprite container...
								return spriteContainerMouseMoves.
									// ...until a mouse up event occurs.
									takeUntil(spriteContainerMouseUps).
									map(function(movePoint) {
										return {
											pageX: movePoint.pageX - contactPoint.layerX,
											pageY: movePoint.pageY - contactPoint.layerY
										};
									});
							});

				// For each mouse drag event, move the sprite to the absolute page position.
				spriteMouseDrags.forEach(function(dragPoint) {
					sprite.style.left = dragPoint.pageX + "px";
					sprite.style.top = dragPoint.pageY + "px";
				});
			}
        

练习 34:HTTP 请求

事件不是应用程序中唯一异步数据的来源。还有 HTTP 请求。大多数情况下,HTTP 请求通过**基于回调的 API** 公开。为了从基于回调的 API 异步接收数据,客户端通常会将成功和错误处理程序传递给该函数。当异步操作完成时,将使用数据调用相应的处理程序。在本练习中,我们将使用 jQuery 的 getJSON api 来异步检索数据。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")");
				fun(jQueryMock);
			}
		

练习 35:使用回调对 HTTP 请求进行排序

假设我们正在为 Web 应用程序编写启动流程。启动时,应用程序必须执行以下操作

  1. 下载用于所有后续 AJAX 调用的 URL 前缀。此 URL 前缀将根据用户注册的 AB 测试而有所不同。
  2. 使用 url 前缀并行执行以下操作
    • 检索电影列表数组
    • 检索配置信息,并且……
      • 如果 config 属性 "showInstantQueue" 为真,则进行后续调用以获取即时队列列表
  3. 如果检索到即时队列列表,则将其追加到电影列表的末尾。
  4. 如果所有操作都成功,则在窗口加载显示电影列表。否则,通知用户出现连接错误。
			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					NOOP = function() {};

				fun(
					{
						addEventListener: function(event, handler) {
							window.setTimeout(handler, 200)
						},
						removeEventListener: NOOP
					},
					jQueryMock,
					function(output) { alert(output) },
					function(output) { alert(output) });
			}
		

可以肯定地说,**使用回调对 HTTP 请求进行排序非常困难。** 为了并行执行两个任务,我们必须引入一个变量来跟踪每个任务的状态。每次并行任务完成时,它都必须检查其兄弟任务是否也已完成。如果两者都已完成,我们才能继续执行。在上面的示例中,每次任务完成时,都会调用 tryToDisplayOutput() 函数来检查程序是否已准备好显示输出。该函数检查所有任务的状态,并在可能的情况下显示输出。

使用基于回调的 API,异步错误处理也非常复杂。在同步程序中,当抛出异常时,工作单元会被取消。相比之下,在我们的程序中,我们必须显式地跟踪是否在并行中发生了错误,以防止不必要的即时队列调用。Javascript 为我们提供了对带有 try/catch/throw 关键字的同步错误处理的特殊支持。不幸的是,异步程序没有这样的支持。

Observable 接口是处理异步 API 的一种比回调更强大的方式。我们将看到 Observable 可以让我们免于跟踪并行运行的任务的状态,就像 Observable 可以让我们免于跟踪事件订阅一样。我们还将看到 Observable 在异步程序中为我们提供了我们在同步程序中预期的相同错误传播语义。最后,我们将学习到通过将基于回调的 API 转换为 Observable,我们可以将它们与事件一起查询以构建更具表达性的程序。

练习 36:遍历基于回调的异步 API

**如果回调 API 是一个序列,它将是什么样的序列?** 我们已经看到 UI 事件序列可以包含 0 到无限个项目,但不会自行完成。

			mouseMoves === seq([ {x: 23, y: 55},,,,,,,{x: 44, y: 99},,,{x:55,y:99},,,{x: 54, y:543},,, ]);
		

相反,如果我们将我们一直在使用的 $.getJSON() 函数的输出转换为一个序列,它将始终返回一个在发送单个项目后完成的序列。

			getJSONAsObservable("http://api-global.netflix.com/abTestInformation") ===
				seq([ { urlPrefix: "billboardTest" } ])
		

创建一个只包含一个对象的序列似乎很奇怪。我们可以引入一个专门用于标量值的 Observable 类,但这会使基于回调的 API 更难与事件一起查询。谢天谢地,Observable 序列足够灵活,可以对两者进行建模。

**那么,我们如何将回调 API 转换为 Observable 序列?** 不幸的是,由于基于回调的 API 在其接口方面差异很大,因此我们无法像使用 fromEvent() 一样创建转换函数。但是,我们可以使用一个更灵活的函数来构建 Observable 序列……

Observable.create() 非常强大,可以将任何异步 API 转换为 Observable。 Observable.create() 依赖于所有异步 API 都具有以下语义的事实

  1. 客户端需要能够接收数据。
  2. 客户端需要能够接收错误信息。
  3. 客户端需要能够被提醒操作已完成。
  4. 客户端需要能够指示他们不再对操作的结果感兴趣。

在下面的示例中,我们将使用 Observable.create() 函数来创建一个在遍历时发出 getJSON 请求的 Observable。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					NOOP = function() {};

				fun(
					{
						addEventListener: function(event, handler) {
							window.setTimeout(handler, 200)
						},
						removeEventListener: NOOP
					},
					jQueryMock,
					function(output) { alert(output) },
					function(output) { alert(output) });
			}
		

传递到 Observable.create() 中的参数被称为 subscribe 函数。对创建的 Observable 可能产生的数据感兴趣的事物(例如 Observer)可以通过订阅来表达这种意愿。为了让 Observable 推送的通知能够被传递,它们必须符合 Observer 接口。然后将 Observer 作为参数传递到创建的 Observable 的 subscribe 函数中。

请注意,为 Observable 定义的 subscribe 函数代表着懒惰求值,只有在每个 Observer 订阅时才会发生。一旦 Observer 不再对 Observable 提供的数据感兴趣,它应该取消订阅自己。使用一些 Observer 在 Observable 上调用 subscribe 的返回值是 Subscription,它代表一个可处置的资源。在 Subscription 对象上调用 unsubscribe 将为相应的 Observer 清理 Observable 执行。

注意上面的 Observer 定义了三个方法

  • next(),Observable 用它来传递新数据
  • error(),Observable 用它来传递错误信息
  • complete(),Observable 用它来表示数据序列已完成

Observer 不需要实现上面所有方法(例如,它们可以是部分的)。对于未提供的回调,Observable 执行仍然正常进行,只是某些类型的通知会被忽略。

在 RxJS 4 和 5 之间,有一些需要注意的 API 细微差异与这里讨论的内容有关。请参阅此迁移指南以了解更改的详细列表。

现在我们已经构建了返回 Observable 序列的 getJSON 函数版本,让我们使用它来改进我们之前练习的解决方案...

练习 37:使用 Observable 顺序执行 HTTP 请求

让我们使用返回 Observable 的 getJSON 函数和 Observable.fromEvent() 来完成我们之前完成的练习。

			// Combine videos and bookmarks 2
			function(str, lesson) {
				preVerifierHook();
				var fun = eval("(" + str + ")"),
					getJSON = function(url) {
						return Observable.create(function(observer) {
							var subscribed = true;

							jQueryMock.getJSON(url,
								{
									success:
										function(data) {
											// If client is still interested in the results, send them.
											if (subscribed) {
												// Send data to the client
												observer.next(data);
												// Immediately complete the sequence
												observer.complete();
											}
										},
									error: function(ex) {
										// If client is still interested in the results, send them.
										if (subscribed) {
											// Inform the client that an error occurred.
											observer.error(ex);
										}
									}
								});

							// Definition of the Subscription objects dispose() method.
							return function() {
								subscribed = false;
							}
						})
					},
					NOOP = function() {};

				fun(
					{
						addEventListener: function(event, handler) {
							window.setTimeout(function(){
								var fakeEvent = { preventDefault: NOOP };
								handler(fakeEvent);
						}, 200);						},
						removeEventListener: NOOP
					},
					getJSON,
					function(output) { alert(JSON.stringify(output)) },
					function(output) { alert(output) });
			}
		

几乎所有 Web 应用程序中的工作流程都以一个事件开始,接着是一个 HTTP 请求,最后导致状态变化。现在我们知道如何优雅地表达前两个任务。

练习 38:节流输入

在处理用户输入时,有时用户输入过于频繁,可能会用无关的请求堵塞服务器。我们希望能够节流用户输入,以便如果他们在 1 秒内交互,那么我们会获得用户输入。例如,假设用户在保存时点击了一个按钮太多次,我们只希望在他们停止了 1 秒钟后才触发。

			seq([1,2,3,,,,,,,4,5,6,,,]).throttleTime(1000 /* ms */) === seq([,,,,,,,3,,,,,,,,,,6,,,]);
		
<<-- 点击这里保存你的数据
			function(str, lesson) {
				preVerifierHook();
				var $inputName = $('.inputName', lesson),
					$savedValue = $('.savedValue', lesson);

				var counter = 0;
				var data = null;
				var clicks = Observable.fromEvent($('.submitInputName', lesson)[0], 'click');

				var code =  eval("(" + str + ")")
				var code = code(clicks, function(name) { return Rx.Observable.of(name.val()); }, $inputName);

				code
					.subscribe(function (data) {
						$savedValue.text('Name Saved: ' + data);
					});
			}
		
			function (clicks, saveData, name) {
				return clicks.
					throttleTime(1000).
					concatMap(function () {
						return saveData(name);
					})
			}
        

现在我们知道如何节流输入,让我们看看另一个需要节流数据的关键问题...

练习 39:自动完成框

Web 开发中最常见的问题之一是自动完成框。这似乎应该是一个简单的问题,但实际上非常具有挑战性。例如,我们如何节流输入?如何确保我们不会得到来自服务的乱序请求?例如,如果我输入 "react" 然后输入 "reactive",我希望 "reactive" 成为我的结果,无论哪个实际先从服务返回。

在下面的示例中,您将收到一系列按键、一个文本框,以及一个函数,当调用该函数时,它将返回一个搜索结果数组。

			getSearchResultSet('react') === seq[,,,["reactive", "reaction","reactor"]]
			keyPresses === seq['r',,,,,'e',,,,,,'a',,,,'c',,,,'t',,,,,]
		
			function(str, lesson) {
				preVerifierHook();
				var wordlist = window.wordlist;
				wordlist.sort();

				var searchText = function (text) {

					var matched = wordlist.filter(function (x) {
						return x.indexOf(text) === 0;
					});

					return Rx.Observable.of(
						matched.slice(0, 10)
					);
				};

				var $inputName = $('.inputName', lesson)[0],
					$searchResults = $('.searchResultsForAutoComplete', lesson);

				var clicks = Rx.Observable.fromEvent($inputName, 'keyup');

				var code =  eval("(" + str + ")")
				var code = code(searchText, clicks, $inputName);

				code
					.subscribe(function (results) {
						var s = results.map(function (r) {
							return '<li>' + r + '</li>';
						});
						$searchResults.html(s.join(''));
					});
			}
		
			function (getSearchResultSet, keyPresses, textBox) {

				var getSearchResultSets =
					keyPresses.
						map(function () {
							return textBox.value;
						}).
						throttleTime(1000).
						concatMap(function (text) {
							return getSearchResultSet(text).takeUntil(keyPresses);
						});

				return getSearchResultSets;
			}
        

现在我们能够使用节流后的输入进行查询,您仍然会注意到一个细微的问题。如果您点击箭头键或任何其他非字符键,请求仍然会触发。我们如何防止这种情况?

练习 40:区分直到改变输入

您将在前面的练习中注意到,如果您在文本框内按箭头键,查询仍然会触发,无论文本是否实际更改。我们如何防止这种情况?distinctUntilChanged 过滤掉连续重复的值。

			seq([1,,,1,,,3,,,3,,,5,,,1,,,]).distinctUntilChanged() ===
			seq([1,,,,,,,3,,,,,,,5,,,1,,,]);
		
通过 distinctUntilChanged 过滤的键:
			function(str, lesson) {
				preVerifierHook();
				var $inputName = $('.inputName', lesson),
					$filtered = $('.filteredKeysByDistinct', lesson);

				var keyups = Rx.Observable.fromEvent($inputName[0], 'keypress');

				var isAlpha = function (x) {
					return 'abcdefghijklmnopqrstuvwxyz'.indexOf(x.toLowerCase()) !== -1;
				};

				var code =  eval("(" + str + ")")
				var code = code(keyups, isAlpha);

				code
					.subscribe(function (text) {
						$filtered.text(text);
					});
			}
		
			function (keyPresses, isAlpha) {

				return keyPresses.
					map(function (e) { return String.fromCharCode(e.keyCode); }).
					filter(function (character) { return isAlpha(character); }).
					distinctUntilChanged().
					scan(function (stringSoFar, character) {
						return stringSoFar + character;
					}, '');
			}
        

现在我们知道如何只获取不同的输入,让我们看看它如何应用于我们的自动完成示例...

练习 41:自动完成框第二部分:重磅回归

在自动完成框的先前版本中,存在两个错误

下面的示例与上面相同,但这次修复了这些错误!

			getSearchResultSet('react') === seq[,,,["reactive", "reaction","reactor"]]
			keyPresses === seq['r',,,,,'e',,,,,,'a',,,,'c',,,,'t',,,,,]
		
			function(str, lesson) {
				preVerifierHook();
				var wordlist = window.wordlist;
				wordlist.sort();

				var searchText = function (text) {

					var matched = wordlist.filter(function (x) {
						return x.indexOf(text) === 0;
					});

					return Rx.Observable.of(
						matched.slice(0, 10)
					);
				};

				var $inputName = $('.inputName', lesson),
					$searchResults = $('.searchResultsForAutoComplete', lesson);

				var clicks = Rx.Observable.fromEvent($inputName[0], 'keyup');

				var code =  eval("(" + str + ")")
				var code = code(searchText, clicks, $inputName[0]);

				code
					.subscribe(function (results) {
						var s = results.map(function (r) {
							return '<li>' + r + '</li>';
						});
						$searchResults.html(s.join(''));
					});
			}
		
			function (getSearchResultSet, keyPresses, textBox) {

				var getSearchResultSets =
					keyPresses.
						map(function () {
							return textBox.value;
						}).
						throttleTime(1000).
						distinctUntilChanged().
						filter(function (s) { return s.length > 0; }).
						concatMap(function (text) {
							return getSearchResultSet(text).takeUntil(keyPresses);
						});

				return getSearchResultSets;
			}
        

仅仅使用这么少量的代码,我们就能够生成一个完全正常的自动完成场景。但是还有一些悬而未决的问题,比如错误处理。我们如何处理错误并在必要时重试?

练习 42:错误后重试

正在进行

恭喜!您已经走到了这一步,但您还没有完成。学习是一个持续的过程。开始使用您在日常编码中学习的功能。随着时间的推移,我将在这个教程中添加更多练习。如果您对更多练习有任何建议,请向我发送一个拉取请求!