引言构建一个完整的数据库驱动界面在前两篇文章中我们学习了 JSON 的基础知识并通过猫舍数据解析的练习巩固了相关技能。现在是时候迎接一个更接近真实项目的综合挑战了。MDN 为我们准备了一个房产搜索与过滤页面的开发任务这并非一个简单的单步骤练习而是一个融合了数据获取、表单控件动态填充、多条件数据过滤、DOM 渲染以及面积计算等多项技术的完整小项目。完成这个挑战意味着你已经具备了构建数据库驱动型用户界面的核心能力。本文将带你完整走过这个房产搜索界面的实现过程。我们从分析项目起始文件和需求规格开始逐步讲解如何从远程服务器获取 JSON 数据并处理可能的错误如何根据数据内容动态填充下拉选择框以避免硬编码如何实现基于多条件组合的灵活过滤逻辑如何在数据渲染前正确处理 DOM 元素以避免结果累积以及如何深入对象内部计算嵌套数据并生成结构化的 HTML 内容。每一个步骤都建立在前文所学知识的基础上同时引入了数组方法、类型转换、防御性编程等新的实践技巧。项目起始点理解已有的代码框架项目为我们提供了一个完整的 HTML 页面和一个部分填充的 JavaScript 文件。HTML 页面包含一个表单表单中有三个下拉选择框分别用于按街道、卧室数量和浴室数量筛选房产还有一个提交按钮。表单下方有一个用于显示结果数量的段落元素和一个用于承载搜索结果的section容器。这些 HTML 结构在任务中无需修改我们所有的精力都集中在 JavaScript 功能的实现上。HTML 结构如下h1House search/h1pSearch for houses for sale. You can filter your search by street, number of bedrooms, and number of bathrooms, or just submit the search with no filters to display all available properties./pformdivlabelforchoose-streetStreet:/labelselectidchoose-streetnamechoose-streetoptionvalueNo street selected/option/select/divdivlabelforchoose-bedroomsNumber of bedrooms:/labelselectidchoose-bedroomsnamechoose-bedroomsoptionvalueAny number of bedrooms/option/select/divdivlabelforchoose-bathroomsNumber of bathrooms:/labelselectidchoose-bathroomsnamechoose-bathroomsoptionvalueAny number of bathrooms/option/select/divdivbuttonSearch for houses/button/div/formpidresult-countResults returned: 0/psectionidoutput/sectionJavaScript 起始文件已经声明了若干常量和变量为我们准备好了与 DOM 元素的连接桥梁conststreetSelectdocument.getElementById(choose-street);constbedroomSelectdocument.getElementById(choose-bedrooms);constbathroomSelectdocument.getElementById(choose-bathrooms);constformdocument.querySelector(form);constresultCountdocument.getElementById(result-count);constoutputdocument.getElementById(output);lethouses;functioninitializeForm(){}functionrenderHouses(e){// Stop the form submittinge.preventDefault();// Add rest of code here}// Add a submit listener to the form elementform.addEventListener(submit,renderHouses);// Call fetchHouseData() to initialize the appfetchHouseData();在这个起始代码中streetSelect指向街道选择框bedroomSelect指向卧室数量选择框bathroomSelect指向浴室数量选择框form指向整个表单元素resultCount指向显示结果数量的段落output指向承载结果的section容器。还有一个名为houses的变量初始值为空后续将用于存储从服务器获取并解析后的房产数据数组。此外文件还提供了两个骨架函数initializeForm函数负责根据数据动态填充选择框选项renderHouses函数负责处理表单提交、过滤数据并渲染结果。表单上已经绑定了一个提交事件监听器当用户点击搜索按钮时renderHouses函数会被调用。文件末尾还有一行fetchHouseData函数的调用但这个函数本身尚未定义需要我们来创建。理解这个起始框架的职责划分对于后续实现至关重要。数据获取是独立的步骤表单初始化依赖数据的存在而搜索渲染则响应用户的交互。这种分层设计使得每个函数的关注点单一且清晰。获取数据创建 fetchHouseData 函数整个应用的第一步是从服务器获取房产数据。我们需要在变量定义区域的下方创建一个名为fetchHouseData的新函数。这个函数的职责是向指定的 URL 发起网络请求获取 JSON 格式的房产数据将其解析为 JavaScript 对象数组然后存储在houses变量中并调用initializeForm函数来初始化表单选择框。数据来源的 URL 是https://mdn.github.io/shared-assets/misc/houses.json。在开始编写代码之前建议先在浏览器中打开这个链接仔细研究返回的 JSON 数据结构。每一栋房产都是一个对象包含number属性表示门牌号street属性表示街道名称bedrooms属性表示卧室数量bathrooms属性表示浴室数量price属性表示价格以及一个名为room_sizes的对象该对象内部的每个属性名是房间名称属性值是以平方米为单位的房间面积数值。了解这个结构对于后续的面积计算至关重要。fetchHouseData函数的完整实现如下functionfetchHouseData(){fetch(https://mdn.github.io/shared-assets/misc/houses.json).then((response){if(!response.ok){thrownewError(HTTP error! status:${response.status});}returnresponse.json();}).then((data){housesdata;initializeForm();}).catch((error){console.error(Failed to fetch house data:,error);});}在fetchHouseData函数内部首先使用fetch方法发起 GET 请求。fetch返回一个 Promise我们在第一个then回调中检查响应的ok属性。ok是一个布尔值当 HTTP 状态码在 200 到 299 之间时为true表示请求成功。如果ok为false说明服务器返回了错误状态此时使用throw语句抛出一个包含状态码信息的自定义Error对象以便于调试。如果响应正常则调用response的json方法将响应体解析为 JavaScript 对象。json方法本身也返回一个 Promise在下一个then回调中我们获得了解析后的数据数组将其赋值给houses变量然后立即调用initializeForm函数。这个调用顺序确保了表单的选择框只有在数据加载完成后才开始填充。最后链式调用的末尾添加了一个catch方法来捕获整个请求链路中可能出现的任何错误例如网络故障或 JSON 解析失败并将错误信息输出到控制台。完整的错误处理机制是一个健壮应用程序的必备要素。动态填充选择框实现 initializeForm 函数initializeForm函数的核心任务是读取houses数组中的数据为三个下拉选择框动态生成选项元素。题目明确指出不应当将选项硬编码在 HTML 中因为那只能适用于当前这一组特定数据。正确的做法是编写能够适应任意数据集的通用代码只要数据中的对象保持相同的结构即可。对于街道选择框我们需要从所有房产对象中提取出所有不重复的街道名称。推荐的实现方式是创建一个空数组作为临时存储然后遍历houses数组。对于每一个房产对象检查其street属性值是否已经存在于临时数组中。如果不存在则将该街道名称添加到临时数组中同时创建一个新的option元素将其value属性设置为街道名称将其textContent也设置为街道名称然后将这个option元素追加到街道选择框中。如果街道名称已经存在于临时数组中说明该街道已经添加过选项跳过即可。这种检查是否存在再决定是否添加的模式是处理去重逻辑的经典手法。对于卧室数量选择框处理策略有所不同。房产对象的bedrooms属性是一个数字而我们需要为从 1 到最大值之间的每一个整数创建一个选项。要实现这一点可以先遍历houses数组找出最大的卧室数量。一种方式是初始化一个maxBedrooms变量为 0然后在循环中比较每个房产的bedrooms值与当前最大值将较大的值赋给maxBedrooms。接着编写第二个循环从 1 到maxBedrooms为每一个数字创建一个option元素其value和textContent都设为该数字的字符串形式。浴室数量选择框的填充逻辑与此完全相同只是比较的属性换成bathrooms。initializeForm函数的完整实现如下functioninitializeForm(){// Populate street selectconststreets[];for(consthouseofhouses){if(!streets.includes(house.street)){streets.push(house.street);constoptiondocument.createElement(option);option.valuehouse.street;option.textContenthouse.street;streetSelect.appendChild(option);}}// Find maximum bedroomsletmaxBedrooms0;for(consthouseofhouses){if(house.bedroomsmaxBedrooms){maxBedroomshouse.bedrooms;}}// Populate bedroom selectfor(leti1;imaxBedrooms;i){constoptiondocument.createElement(option);option.valuei;option.textContenti;bedroomSelect.appendChild(option);}// Find maximum bathroomsletmaxBathrooms0;for(consthouseofhouses){if(house.bathroomsmaxBathrooms){maxBathroomshouse.bathrooms;}}// Populate bathroom selectfor(leti1;imaxBathrooms;i){constoptiondocument.createElement(option);option.valuei;option.textContenti;bathroomSelect.appendChild(option);}}这段代码分为三个清晰的区块。第一个区块处理街道选择框使用streets数组记录已经添加过的街道名称利用includes方法检查是否重复从而实现去重填充。第二个区块处理卧室数量选择框先通过一轮循环找出最大卧室数量再通过第二轮循环从 1 到该最大值依次生成选项。第三个区块用完全相同的模式处理浴室数量选择框。每个选项都通过createElement创建通过设置value和textContent属性配置最后通过appendChild添加到对应的选择框元素中。这种纯 DOM 操作的方式比使用innerHTML更加安全可控。过滤数据在 renderHouses 中实现多条件筛选renderHouses函数在用户提交表单时被调用。它的首要任务是阻止表单的默认提交行为否则页面会刷新导致所有 JavaScript 状态丢失。这一操作通过调用事件对象e的preventDefault方法完成。接下来进入核心的数据过滤环节。我们需要根据三个选择框的当前值从houses数组中筛选出符合条件的房产对象。JavaScript 数组的filter方法非常适合这个场景。filter接受一个回调函数该回调函数对数组中的每个元素都执行一次只有当回调返回true时该元素才会被保留在返回的新数组中。过滤条件的实现需要特别注意选择框值的特性。表单元素的值始终是字符串类型即使我们之前将数字转为字符串赋值给option的value它依然是字符串。而房产对象中的bedrooms和bathrooms属性很可能是数字类型。如果直接用严格相等运算符比较字符串和数字结果永远是false会导致过滤失败。因此在比较数值类属性时需要将选择框的值转换为数字类型。可以使用Number函数来实现类型转换。另一个关键需求是空值表示全选的逻辑。每个选择框的第一个选项value为空字符串表示用户不希望按该字段过滤。在过滤回调函数中对于每一个筛选条件如果对应的选择框值为空字符串该条件应当直接视为满足也就是短路返回true。只有当选择框值不为空时才去比较房产对象的对应属性是否与选择框值相等。三个条件需要用逻辑与运算符连接这意味着只有三个条件全部为真时当前房产才会被纳入筛选结果。过滤逻辑的完整代码如下functionrenderHouses(e){e.preventDefault();constfilteredHouseshouses.filter((house){conststreetMatchstreetSelect.value||house.streetstreetSelect.value;constbedroomMatchbedroomSelect.value||house.bedroomsNumber(bedroomSelect.value);constbathroomMatchbathroomSelect.value||house.bathroomsNumber(bathroomSelect.value);returnstreetMatchbedroomMatchbathroomMatch;});resultCount.textContentResults returned:${filteredHouses.length};output.innerHTML;functionrenderHouse(house){constroomSizesObject.values(house.room_sizes);consttotalArearoomSizes.reduce((sum,size)sumsize,0);constarticledocument.createElement(article);consth2document.createElement(h2);h2.textContent${house.number}${house.street};article.appendChild(h2);constuldocument.createElement(ul);constbedroomLidocument.createElement(li);bedroomLi.textContent️ Bedrooms:${house.bedrooms};ul.appendChild(bedroomLi);constbathroomLidocument.createElement(li);bathroomLi.textContent Bathrooms:${house.bathrooms};ul.appendChild(bathroomLi);constareaLidocument.createElement(li);areaLi.textContentRoom area:${totalArea}m²;ul.appendChild(areaLi);constpriceLidocument.createElement(li);priceLi.textContentPrice: £${house.price};ul.appendChild(priceLi);article.appendChild(ul);output.appendChild(article);}for(consthouseoffilteredHouses){renderHouse(house);}}在filter回调函数中streetMatch变量使用了短路求值如果streetSelect.value是空字符串则直接返回true跳过后续比较。bedroomMatch和bathroomMatch在比较前使用Number函数将选择框的值从字符串转换为数字确保与对象中的数字属性类型一致。三个匹配结果用逻辑与连接只有全部为真时该房产才被保留。渲染结果更新计数、清空容器并创建卡片过滤完成后我们得到了一个经过筛选的新数组。接下来需要将这个数组的内容展示在页面上。第一步是更新结果计数resultCount段落的textContent被设置为模板字符串Results returned: 后跟筛选数组的长度。这直接告知用户当前筛选条件下有多少套房产符合要求。第二步是清空output容器。这一步极其容易被初学者忽略但如果不做每次搜索的结果会不断追加到容器的末尾而不是替换之前的结果。这里使用output.innerHTML等于空字符串的方式来清空容器。由于我们是删除内容而非添加内容使用innerHTML不存在 XSS 攻击风险是安全且高效的。第三步是创建负责渲染单套房产信息的renderHouse函数。题目建议将这个函数定义在renderHouses函数内部这样做可以形成闭包使renderHouse能够直接访问外部函数的变量同时保持全局作用域的整洁。renderHouse函数接收一个房产对象作为参数需要完成两项任务计算房间总面积和构建 HTML 结构。计算房间总面积需要我们仔细审视数据结构。room_sizes是一个对象而非数组这意味着我们不能直接使用for循环遍历也不能直接使用数组的reduce方法。但我们可以使用Object.values方法将对象的所有值提取为一个数组。例如如果一个房产的room_sizes对象是{ Living Room: 25, Bedroom: 15, Kitchen: 10 }那么Object.values会返回数组[25, 15, 10]。接下来对这个数组使用reduce方法即可轻松求得总和。reduce方法接受一个回调函数和一个初始值回调函数有两个参数累加器和当前值返回值会成为下一次迭代的累加器。当我们把初始值设为 0并在回调中返回累加器加当前值时最终得到的结果就是所有数值的总和 50。构建 HTML 结构方面我们创建一个article元素作为容器内部包含一个h2标题和一个ul列表。h2的文本内容由房产的number和street属性用模板字符串拼接而成。ul列表中包含四个li条目分别展示卧室数量、浴室数量、房间总面积和价格。创建这些元素时全部使用createElement和appendChild等 DOM 操作方法通过textContent设置文本内容而非直接设置innerHTML。这样做可以有效防止潜在的跨站脚本攻击尤其是在数据来源不完全可信的情况下。每一个li元素的文本内容都通过textContent设置确保任何特殊字符都被当作纯文本处理不会被解析为 HTML 标签。完成renderHouse函数的定义后最后一步是使用for循环遍历筛选后的数组对每一个房产对象调用renderHouse函数。这样每一套符合条件的房产都会生成一张信息卡片被追加到output容器中。整个过程完成后用户就能在页面上看到清晰、结构化的搜索结果。完整代码整合与关键技术回顾将上述所有步骤串联起来整个应用的数据流非常清晰。应用启动时最后一行代码fetchHouseData()触发整个流程。fetchHouseData函数发起网络请求获取 JSON 数据解析后存入houses变量并触发initializeForm函数。initializeForm函数遍历数据提取不重复的街道名称和数值范围动态为三个选择框填充选项。用户选择筛选条件并提交表单时renderHouses函数被调用。该函数阻止默认提交行为使用filter方法根据选择框的值对houses数组进行多条件筛选将筛选结果的长度更新到结果计数段落清空输出容器然后遍历筛选结果数组对每个房产对象调用renderHouse函数生成信息卡片并追加到页面中。在这个综合实战中多个 JavaScript 核心概念被有机地整合在一起。数组的filter方法实现了声明式的数据筛选Object.values和reduce方法协作完成了嵌套对象数据的聚合计算DOM 的createElement和appendChild方法构建了安全的动态 HTML 内容。此外类型转换的细节处理、短路求值的逻辑设计、数组includes方法的去重应用、以及catch方法的错误处理机制都体现了从简单练习走向真实项目开发所需具备的综合素养。总结从数据到界面的完整闭环本文详细拆解了 MDN 房产搜索界面挑战的完整实现过程。我们从一个包含 HTML 骨架和 JavaScript 起始文件的半成品项目出发依次完成了fetchHouseData网络数据获取、initializeForm动态表单填充、renderHouses多条件数据筛选、以及renderHouse嵌套数据计算与安全 DOM 渲染等关键任务。这个项目虽然规模不大但完整地覆盖了前端开发中获取数据、处理数据、展示数据的核心流程。通过这个实战练习你应该对 JSON 在实际项目中的运用有了更立体的认识。JSON 不仅仅是静态的数据格式它贯穿了从服务器到客户端、从 JavaScript 对象到用户可见界面的整个链条。fetchAPI 负责将 JSON 从网络引入程序JSON 解析将其转化为可操作的对象数组数组方法对其进行分析和过滤DOM 操作将其转化为用户可读的视觉内容。每一个环节都紧密相连构成了现代 Web 应用数据驱动界面的基础范式。掌握了这些技能你已经为后续学习更复杂的 JavaScript 框架打下了坚实的原生语言基础。在接下来的学习中我们将正式进入 JavaScript 面向对象编程的领域探索如何用类和对象来组织更大型、更复杂的应用程序逻辑。
跟着 MDN 学 JavaScript Day 31:房产搜索界面——JSON 数据过滤与动态渲染综合实战
发布时间:2026/6/14 14:27:15
引言构建一个完整的数据库驱动界面在前两篇文章中我们学习了 JSON 的基础知识并通过猫舍数据解析的练习巩固了相关技能。现在是时候迎接一个更接近真实项目的综合挑战了。MDN 为我们准备了一个房产搜索与过滤页面的开发任务这并非一个简单的单步骤练习而是一个融合了数据获取、表单控件动态填充、多条件数据过滤、DOM 渲染以及面积计算等多项技术的完整小项目。完成这个挑战意味着你已经具备了构建数据库驱动型用户界面的核心能力。本文将带你完整走过这个房产搜索界面的实现过程。我们从分析项目起始文件和需求规格开始逐步讲解如何从远程服务器获取 JSON 数据并处理可能的错误如何根据数据内容动态填充下拉选择框以避免硬编码如何实现基于多条件组合的灵活过滤逻辑如何在数据渲染前正确处理 DOM 元素以避免结果累积以及如何深入对象内部计算嵌套数据并生成结构化的 HTML 内容。每一个步骤都建立在前文所学知识的基础上同时引入了数组方法、类型转换、防御性编程等新的实践技巧。项目起始点理解已有的代码框架项目为我们提供了一个完整的 HTML 页面和一个部分填充的 JavaScript 文件。HTML 页面包含一个表单表单中有三个下拉选择框分别用于按街道、卧室数量和浴室数量筛选房产还有一个提交按钮。表单下方有一个用于显示结果数量的段落元素和一个用于承载搜索结果的section容器。这些 HTML 结构在任务中无需修改我们所有的精力都集中在 JavaScript 功能的实现上。HTML 结构如下h1House search/h1pSearch for houses for sale. You can filter your search by street, number of bedrooms, and number of bathrooms, or just submit the search with no filters to display all available properties./pformdivlabelforchoose-streetStreet:/labelselectidchoose-streetnamechoose-streetoptionvalueNo street selected/option/select/divdivlabelforchoose-bedroomsNumber of bedrooms:/labelselectidchoose-bedroomsnamechoose-bedroomsoptionvalueAny number of bedrooms/option/select/divdivlabelforchoose-bathroomsNumber of bathrooms:/labelselectidchoose-bathroomsnamechoose-bathroomsoptionvalueAny number of bathrooms/option/select/divdivbuttonSearch for houses/button/div/formpidresult-countResults returned: 0/psectionidoutput/sectionJavaScript 起始文件已经声明了若干常量和变量为我们准备好了与 DOM 元素的连接桥梁conststreetSelectdocument.getElementById(choose-street);constbedroomSelectdocument.getElementById(choose-bedrooms);constbathroomSelectdocument.getElementById(choose-bathrooms);constformdocument.querySelector(form);constresultCountdocument.getElementById(result-count);constoutputdocument.getElementById(output);lethouses;functioninitializeForm(){}functionrenderHouses(e){// Stop the form submittinge.preventDefault();// Add rest of code here}// Add a submit listener to the form elementform.addEventListener(submit,renderHouses);// Call fetchHouseData() to initialize the appfetchHouseData();在这个起始代码中streetSelect指向街道选择框bedroomSelect指向卧室数量选择框bathroomSelect指向浴室数量选择框form指向整个表单元素resultCount指向显示结果数量的段落output指向承载结果的section容器。还有一个名为houses的变量初始值为空后续将用于存储从服务器获取并解析后的房产数据数组。此外文件还提供了两个骨架函数initializeForm函数负责根据数据动态填充选择框选项renderHouses函数负责处理表单提交、过滤数据并渲染结果。表单上已经绑定了一个提交事件监听器当用户点击搜索按钮时renderHouses函数会被调用。文件末尾还有一行fetchHouseData函数的调用但这个函数本身尚未定义需要我们来创建。理解这个起始框架的职责划分对于后续实现至关重要。数据获取是独立的步骤表单初始化依赖数据的存在而搜索渲染则响应用户的交互。这种分层设计使得每个函数的关注点单一且清晰。获取数据创建 fetchHouseData 函数整个应用的第一步是从服务器获取房产数据。我们需要在变量定义区域的下方创建一个名为fetchHouseData的新函数。这个函数的职责是向指定的 URL 发起网络请求获取 JSON 格式的房产数据将其解析为 JavaScript 对象数组然后存储在houses变量中并调用initializeForm函数来初始化表单选择框。数据来源的 URL 是https://mdn.github.io/shared-assets/misc/houses.json。在开始编写代码之前建议先在浏览器中打开这个链接仔细研究返回的 JSON 数据结构。每一栋房产都是一个对象包含number属性表示门牌号street属性表示街道名称bedrooms属性表示卧室数量bathrooms属性表示浴室数量price属性表示价格以及一个名为room_sizes的对象该对象内部的每个属性名是房间名称属性值是以平方米为单位的房间面积数值。了解这个结构对于后续的面积计算至关重要。fetchHouseData函数的完整实现如下functionfetchHouseData(){fetch(https://mdn.github.io/shared-assets/misc/houses.json).then((response){if(!response.ok){thrownewError(HTTP error! status:${response.status});}returnresponse.json();}).then((data){housesdata;initializeForm();}).catch((error){console.error(Failed to fetch house data:,error);});}在fetchHouseData函数内部首先使用fetch方法发起 GET 请求。fetch返回一个 Promise我们在第一个then回调中检查响应的ok属性。ok是一个布尔值当 HTTP 状态码在 200 到 299 之间时为true表示请求成功。如果ok为false说明服务器返回了错误状态此时使用throw语句抛出一个包含状态码信息的自定义Error对象以便于调试。如果响应正常则调用response的json方法将响应体解析为 JavaScript 对象。json方法本身也返回一个 Promise在下一个then回调中我们获得了解析后的数据数组将其赋值给houses变量然后立即调用initializeForm函数。这个调用顺序确保了表单的选择框只有在数据加载完成后才开始填充。最后链式调用的末尾添加了一个catch方法来捕获整个请求链路中可能出现的任何错误例如网络故障或 JSON 解析失败并将错误信息输出到控制台。完整的错误处理机制是一个健壮应用程序的必备要素。动态填充选择框实现 initializeForm 函数initializeForm函数的核心任务是读取houses数组中的数据为三个下拉选择框动态生成选项元素。题目明确指出不应当将选项硬编码在 HTML 中因为那只能适用于当前这一组特定数据。正确的做法是编写能够适应任意数据集的通用代码只要数据中的对象保持相同的结构即可。对于街道选择框我们需要从所有房产对象中提取出所有不重复的街道名称。推荐的实现方式是创建一个空数组作为临时存储然后遍历houses数组。对于每一个房产对象检查其street属性值是否已经存在于临时数组中。如果不存在则将该街道名称添加到临时数组中同时创建一个新的option元素将其value属性设置为街道名称将其textContent也设置为街道名称然后将这个option元素追加到街道选择框中。如果街道名称已经存在于临时数组中说明该街道已经添加过选项跳过即可。这种检查是否存在再决定是否添加的模式是处理去重逻辑的经典手法。对于卧室数量选择框处理策略有所不同。房产对象的bedrooms属性是一个数字而我们需要为从 1 到最大值之间的每一个整数创建一个选项。要实现这一点可以先遍历houses数组找出最大的卧室数量。一种方式是初始化一个maxBedrooms变量为 0然后在循环中比较每个房产的bedrooms值与当前最大值将较大的值赋给maxBedrooms。接着编写第二个循环从 1 到maxBedrooms为每一个数字创建一个option元素其value和textContent都设为该数字的字符串形式。浴室数量选择框的填充逻辑与此完全相同只是比较的属性换成bathrooms。initializeForm函数的完整实现如下functioninitializeForm(){// Populate street selectconststreets[];for(consthouseofhouses){if(!streets.includes(house.street)){streets.push(house.street);constoptiondocument.createElement(option);option.valuehouse.street;option.textContenthouse.street;streetSelect.appendChild(option);}}// Find maximum bedroomsletmaxBedrooms0;for(consthouseofhouses){if(house.bedroomsmaxBedrooms){maxBedroomshouse.bedrooms;}}// Populate bedroom selectfor(leti1;imaxBedrooms;i){constoptiondocument.createElement(option);option.valuei;option.textContenti;bedroomSelect.appendChild(option);}// Find maximum bathroomsletmaxBathrooms0;for(consthouseofhouses){if(house.bathroomsmaxBathrooms){maxBathroomshouse.bathrooms;}}// Populate bathroom selectfor(leti1;imaxBathrooms;i){constoptiondocument.createElement(option);option.valuei;option.textContenti;bathroomSelect.appendChild(option);}}这段代码分为三个清晰的区块。第一个区块处理街道选择框使用streets数组记录已经添加过的街道名称利用includes方法检查是否重复从而实现去重填充。第二个区块处理卧室数量选择框先通过一轮循环找出最大卧室数量再通过第二轮循环从 1 到该最大值依次生成选项。第三个区块用完全相同的模式处理浴室数量选择框。每个选项都通过createElement创建通过设置value和textContent属性配置最后通过appendChild添加到对应的选择框元素中。这种纯 DOM 操作的方式比使用innerHTML更加安全可控。过滤数据在 renderHouses 中实现多条件筛选renderHouses函数在用户提交表单时被调用。它的首要任务是阻止表单的默认提交行为否则页面会刷新导致所有 JavaScript 状态丢失。这一操作通过调用事件对象e的preventDefault方法完成。接下来进入核心的数据过滤环节。我们需要根据三个选择框的当前值从houses数组中筛选出符合条件的房产对象。JavaScript 数组的filter方法非常适合这个场景。filter接受一个回调函数该回调函数对数组中的每个元素都执行一次只有当回调返回true时该元素才会被保留在返回的新数组中。过滤条件的实现需要特别注意选择框值的特性。表单元素的值始终是字符串类型即使我们之前将数字转为字符串赋值给option的value它依然是字符串。而房产对象中的bedrooms和bathrooms属性很可能是数字类型。如果直接用严格相等运算符比较字符串和数字结果永远是false会导致过滤失败。因此在比较数值类属性时需要将选择框的值转换为数字类型。可以使用Number函数来实现类型转换。另一个关键需求是空值表示全选的逻辑。每个选择框的第一个选项value为空字符串表示用户不希望按该字段过滤。在过滤回调函数中对于每一个筛选条件如果对应的选择框值为空字符串该条件应当直接视为满足也就是短路返回true。只有当选择框值不为空时才去比较房产对象的对应属性是否与选择框值相等。三个条件需要用逻辑与运算符连接这意味着只有三个条件全部为真时当前房产才会被纳入筛选结果。过滤逻辑的完整代码如下functionrenderHouses(e){e.preventDefault();constfilteredHouseshouses.filter((house){conststreetMatchstreetSelect.value||house.streetstreetSelect.value;constbedroomMatchbedroomSelect.value||house.bedroomsNumber(bedroomSelect.value);constbathroomMatchbathroomSelect.value||house.bathroomsNumber(bathroomSelect.value);returnstreetMatchbedroomMatchbathroomMatch;});resultCount.textContentResults returned:${filteredHouses.length};output.innerHTML;functionrenderHouse(house){constroomSizesObject.values(house.room_sizes);consttotalArearoomSizes.reduce((sum,size)sumsize,0);constarticledocument.createElement(article);consth2document.createElement(h2);h2.textContent${house.number}${house.street};article.appendChild(h2);constuldocument.createElement(ul);constbedroomLidocument.createElement(li);bedroomLi.textContent️ Bedrooms:${house.bedrooms};ul.appendChild(bedroomLi);constbathroomLidocument.createElement(li);bathroomLi.textContent Bathrooms:${house.bathrooms};ul.appendChild(bathroomLi);constareaLidocument.createElement(li);areaLi.textContentRoom area:${totalArea}m²;ul.appendChild(areaLi);constpriceLidocument.createElement(li);priceLi.textContentPrice: £${house.price};ul.appendChild(priceLi);article.appendChild(ul);output.appendChild(article);}for(consthouseoffilteredHouses){renderHouse(house);}}在filter回调函数中streetMatch变量使用了短路求值如果streetSelect.value是空字符串则直接返回true跳过后续比较。bedroomMatch和bathroomMatch在比较前使用Number函数将选择框的值从字符串转换为数字确保与对象中的数字属性类型一致。三个匹配结果用逻辑与连接只有全部为真时该房产才被保留。渲染结果更新计数、清空容器并创建卡片过滤完成后我们得到了一个经过筛选的新数组。接下来需要将这个数组的内容展示在页面上。第一步是更新结果计数resultCount段落的textContent被设置为模板字符串Results returned: 后跟筛选数组的长度。这直接告知用户当前筛选条件下有多少套房产符合要求。第二步是清空output容器。这一步极其容易被初学者忽略但如果不做每次搜索的结果会不断追加到容器的末尾而不是替换之前的结果。这里使用output.innerHTML等于空字符串的方式来清空容器。由于我们是删除内容而非添加内容使用innerHTML不存在 XSS 攻击风险是安全且高效的。第三步是创建负责渲染单套房产信息的renderHouse函数。题目建议将这个函数定义在renderHouses函数内部这样做可以形成闭包使renderHouse能够直接访问外部函数的变量同时保持全局作用域的整洁。renderHouse函数接收一个房产对象作为参数需要完成两项任务计算房间总面积和构建 HTML 结构。计算房间总面积需要我们仔细审视数据结构。room_sizes是一个对象而非数组这意味着我们不能直接使用for循环遍历也不能直接使用数组的reduce方法。但我们可以使用Object.values方法将对象的所有值提取为一个数组。例如如果一个房产的room_sizes对象是{ Living Room: 25, Bedroom: 15, Kitchen: 10 }那么Object.values会返回数组[25, 15, 10]。接下来对这个数组使用reduce方法即可轻松求得总和。reduce方法接受一个回调函数和一个初始值回调函数有两个参数累加器和当前值返回值会成为下一次迭代的累加器。当我们把初始值设为 0并在回调中返回累加器加当前值时最终得到的结果就是所有数值的总和 50。构建 HTML 结构方面我们创建一个article元素作为容器内部包含一个h2标题和一个ul列表。h2的文本内容由房产的number和street属性用模板字符串拼接而成。ul列表中包含四个li条目分别展示卧室数量、浴室数量、房间总面积和价格。创建这些元素时全部使用createElement和appendChild等 DOM 操作方法通过textContent设置文本内容而非直接设置innerHTML。这样做可以有效防止潜在的跨站脚本攻击尤其是在数据来源不完全可信的情况下。每一个li元素的文本内容都通过textContent设置确保任何特殊字符都被当作纯文本处理不会被解析为 HTML 标签。完成renderHouse函数的定义后最后一步是使用for循环遍历筛选后的数组对每一个房产对象调用renderHouse函数。这样每一套符合条件的房产都会生成一张信息卡片被追加到output容器中。整个过程完成后用户就能在页面上看到清晰、结构化的搜索结果。完整代码整合与关键技术回顾将上述所有步骤串联起来整个应用的数据流非常清晰。应用启动时最后一行代码fetchHouseData()触发整个流程。fetchHouseData函数发起网络请求获取 JSON 数据解析后存入houses变量并触发initializeForm函数。initializeForm函数遍历数据提取不重复的街道名称和数值范围动态为三个选择框填充选项。用户选择筛选条件并提交表单时renderHouses函数被调用。该函数阻止默认提交行为使用filter方法根据选择框的值对houses数组进行多条件筛选将筛选结果的长度更新到结果计数段落清空输出容器然后遍历筛选结果数组对每个房产对象调用renderHouse函数生成信息卡片并追加到页面中。在这个综合实战中多个 JavaScript 核心概念被有机地整合在一起。数组的filter方法实现了声明式的数据筛选Object.values和reduce方法协作完成了嵌套对象数据的聚合计算DOM 的createElement和appendChild方法构建了安全的动态 HTML 内容。此外类型转换的细节处理、短路求值的逻辑设计、数组includes方法的去重应用、以及catch方法的错误处理机制都体现了从简单练习走向真实项目开发所需具备的综合素养。总结从数据到界面的完整闭环本文详细拆解了 MDN 房产搜索界面挑战的完整实现过程。我们从一个包含 HTML 骨架和 JavaScript 起始文件的半成品项目出发依次完成了fetchHouseData网络数据获取、initializeForm动态表单填充、renderHouses多条件数据筛选、以及renderHouse嵌套数据计算与安全 DOM 渲染等关键任务。这个项目虽然规模不大但完整地覆盖了前端开发中获取数据、处理数据、展示数据的核心流程。通过这个实战练习你应该对 JSON 在实际项目中的运用有了更立体的认识。JSON 不仅仅是静态的数据格式它贯穿了从服务器到客户端、从 JavaScript 对象到用户可见界面的整个链条。fetchAPI 负责将 JSON 从网络引入程序JSON 解析将其转化为可操作的对象数组数组方法对其进行分析和过滤DOM 操作将其转化为用户可读的视觉内容。每一个环节都紧密相连构成了现代 Web 应用数据驱动界面的基础范式。掌握了这些技能你已经为后续学习更复杂的 JavaScript 框架打下了坚实的原生语言基础。在接下来的学习中我们将正式进入 JavaScript 面向对象编程的领域探索如何用类和对象来组织更大型、更复杂的应用程序逻辑。