JavaScript前端开发课后习题:隐式转换、闭包与调试实战

发布时间:2026/9/30 5:52:24
JavaScript前端开发课后习题:隐式转换、闭包与调试实战 1. 别急着对答案把课后题当成微型需求单来过一遍我拿到《JavaScript前端开发案例教程》这类教材的第一件事从来不是翻到书末找答案。原因很简单JavaScript前端开发这门手艺眼睛会了和手会了之间隔着一条很宽的沟——你能看懂一段代码在干什么不代表你能从一张空白的编辑器里把它敲出来更不代表它报错的第三分钟你还能保持冷静。教材的课后习题恰好是那条沟上成本最低的桥一道题通常只涉及一到两个知识点做砸了不会有人追责题干又往往自带一个小场景比如根据输入的年月日判断这一天是这一年的第几天这其实已经是一份缩到极致的需求文档了。所以我的做法是把每一道题按接需求的流程走一遍先读题干把输入、输出、边界条件在纸上写清楚再想数据结构是数组还是对象是字符串还是数字最后才动手敲。答案只在两种情况下才翻——一种是我写完并且自测通过后去比对思路差异另一种是我卡住超过二十分钟需要一点提示。这个次序看起来啰嗦但它把习题从背诵材料变成了训练场这两者的长期收益差得非常远。1.1 习题和真实业务代码之间到底差了什么很多人工作一两年之后回头看教材习题会觉得太简单了跟工作没关系。这个判断只对了一半。习题确实没有接口联调、没有构建配置、没有跨端兼容但它训练的恰恰是业务代码里最不能出错的那一层——对数据形态的直觉。真实的业务代码里一个诡异 bug 的源头经常不是什么高深机制而是某个字段本该是数字却成了字符串某个数组本该去重却出现了重复项某个对象被赋值传引用之后被下游函数悄悄改掉了。习题无法给你的是工程环境但它能给你的东西也不少一个明确的输入输出契约、一段可以独立验证的逻辑、一个不依赖任何框架的纯函数思维。我见过太多新人能熟练用框架把页面跑起来却写不出一个干净的去重函数也见过工作三年的人在for循环里嵌套了三层if导致同事接手时无从下手。这些问题本质上都是在基础阶段缺少独立完成一小段逻辑的刻意练习。1.2 我用了很多年的三栏笔记法做习题的时候我习惯开一个 Markdown 文件分三栏记题干要点、我的思路、翻车点。第一栏只写关键词比如输入数组输出去重后的新数组边界空数组、非数组输入、NaN。第二栏写我打算用什么方法、为什么比如用Set去重因为它天然不重复且NaN在Set里被认为是同一个值。第三栏最关键记我实际踩到的坑比如忘了Set转数组需要Array.from或者展开运算符。第三栏才是这份笔记真正的价值所在。我自己统计过前端基础题里我重复犯的错八成集中在四个地方类型判断、引用与值、this指向、异步时序。前三个在纯基础习题里都能练到第四个要等到后面的异步章节。把翻车点单独攒起来隔一周再回看你会发现那些坑在脑子里已经有了警示牌下次手还没敲完大脑就先拦住了。提示笔记别写成标准答案的抄写本。抄答案只会给你一种虚假的掌握感而人的记忆对自己犯过的错远比别人写对的东西牢固。2. 数据类型与运算符题面考语法底下考的是隐式转换的心智模型教材前几章的习题表面上是让你判断typeof的输出、让你算一个表达式的值实际考查的是一件更根本的事你对 JavaScript 隐式转换的心里有没有一张正确的图。这张图不清楚后面写任何业务逻辑都是踩雷。举个我最早被绊住的例子5 - 1得 4而5 1得51。当年我觉得这就是语言设计得怪后来才明白减号在 JavaScript 里只承担数值语义所以它必须把两边转成数字而加号身兼加法和字符串拼接两职一旦有一边是字符串它就优先走拼接。理解了这个动机很多看起来莫名其妙的题目就有了统一的解释而不是零散的记忆点。我在做这部分习题时有个习惯每遇到一个隐式转换就顺手写出它的等价显式写法。比如3等价于Number(3)!!等价于Boolean()。这个动作做上十几遍之后代码里就基本不会再出现靠隐式转换蒙过去的写法了。2.1typeof认不出 null 和数组这不是陷阱而是历史包袱typeof null object这个结论几乎每本教材都会提。很多习题用它来设置陷阱题但我觉得更重要的是知道它为什么这样。这是语言早期实现留下的问题因为当时的类型标记机制里null和对象用了同一个标记位。理解它是历史遗留而不是语言故意的设计你的心态就会从我怎么记住这个奇葩规则变成我知道它不可靠所以我不用它做判断。对于数组和null的判断我的实际做法是一张固定的对照表想判断的东西推荐写法不推荐写法原因是否为数组Array.isArray(x)typeof x object后者把 null、普通对象全算进来是否为 nullx null!x后者把 0、、undefined 也算进去是否为纯数字typeof x number !Number.isNaN(x)isNaN(x)全局isNaN会先做隐式转换是否为整数Number.isInteger(x)x % 1 0后者对Infinity会给出误判是否为可用值x ! null x ! undefinedBoolean(x)后者会误伤 0 和空字符串这张表我建议直接背下来比背十道题的答案有用得多。业务代码里0 被当成空值处理掉了这种 bug十个里面有七八个都能靠这张表提前拦掉。2.2 相等判断的两套规则以及练习题为什么偏爱它和的差别习题里出现频率极高。我的经验是不要在这上面玩花活业务代码里统一用只有在明确知道对面可能是字符串形式的数字、并且需要兼容时才考虑显式转换后再比。原因在于的转换规则涉及一长串优先级判断团队里每个人脑中的版本都不完全一样讨论成本比收益高得多。但做习题的时候我依然会把的规则过一遍因为它能帮你理解语言底层的类型转换链路。比如null undefined为真但null undefined为假NaN和任何值包括它自己都不相等。第二点特别重要因为它是Number.isNaN存在的原因——你没法用相等去检测一个不等于自身的值。2.3 短路求值和运算符优先级在页面里的真实用途运算符那一章很多人觉得枯燥我却觉得它是全书性价比最高的部分之一。短路求值在实际开发中到处都是给一个可能为空的配置取默认值、在渲染前判断数据是否就绪、在函数入口做参数兜底。你写const name user user.name这种形式的次数会比想象中多。不过这里有个必须提醒的点用做默认值是有坑的。const count input || 10在input为 0 的时候会错误地拿到 10。这就是为什么后来有了空值合并运算符??它只在左边是null或undefined时才取右边。做习题时如果遇到给 0 做默认值的题型我会刻意用两种写法各写一遍加深印象const raw 0; const wrong raw || 100; // 100被误判 const right raw ?? 100; // 0符合预期 const explicit raw undefined ? 100 : raw; // 0语义最直白排错时我特别偏爱第三种写法因为它不依赖读者对运算符优先级的记忆任何水平的同事拿起来都能一眼看懂。可读性和技巧性之间我通常优先选可读性除非那行代码在十万次循环里。3. 条件、循环与字符串把能跑通提到能读、能改基础语法章节的习题很容易让人产生我会了的错觉因为写出来的代码确实能输出正确结果。但能跑通只是及格线。我在带新人时最常问的一句话是如果需求改一个字你这行代码要动几处如果改一处需求要动五处代码那说明抽象层次没设计好。循环和条件这一块最容易出问题的不是语法而是嵌套深度。三层以上的嵌套循环加判断几乎必然藏着可优化的结构。做习题的时候不妨给自己加一条约束任何函数的嵌套层级不超过两层。这个约束会逼着你去拆函数、用提前返回、用数组方法替代手工索引管理长期下来代码质量的提升非常明显。3.1 循环三件套什么时候该换掉for、while、do...while在习题里通常分开考但真实项目里用得最多的是更高层的迭代方式。我的选择顺序大致是能用map、filter、reduce表达的优先用它们需要中途跳出或者性能敏感的用for不确定循环次数且依赖外部状态的用while。注意一个细节for...of和for...in完全是两回事。前者遍历的是可迭代对象的值后者遍历的是对象的可枚举属性名包括从原型链上继承来的。习题里如果考对象遍历正确姿势通常是先拿到键数组再遍历const user { name: 林一, age: 26, city: 杭州 }; // 只遍历自身属性顺序可控 Object.keys(user).forEach((key) { console.log(key, user[key]); }); // 需要 key 和 value 一起拿 for (const [key, value] of Object.entries(user)) { console.log(key, value); }我在早期的项目里因为for...in遍历到一个原型链上的方法名排查了大半天。那次之后我就给自己定了规矩遍历对象一律用Object.keys或Object.entries绝不用for...in除非我明确想要原型链上的属性。3.2 字符串处理的三个高频动作及其取舍字符串题的考点集中在截取、查找、替换、拼接。拼接这件事现在几乎没有理由再用去拼一长串内容模板字符串的可读性优势太大。截取方面有三个方法slice、substring、substr。我的建议是只用slice因为它对负索引的处理最符合直觉而且它是唯一一个在数组和字符串上行为一致的方法记忆负担最小。替换方面replace默认只替换第一个匹配项这一点习题里经常设坑。需要全局替换时要么用带g标志的正则要么用replaceAll。我更喜欢replaceAll因为不写正则就不会被正则里的特殊字符咬到——用户输入的内容里带个.或者(正则就得额外转义这是很常见的翻车点。const id user.1001.profile; // 用正则需要转义点号 const byRegex id.replace(/\./g, -); // replaceAll 直接写字符更省心 const byMethod id.replaceAll(., -); console.log(byRegex byMethod); // true3.3 用一个表单校验的小例子把前面几章串起来我最喜欢的一道综合练习是写一个注册表单的校验函数。它天然需要类型判断是不是字符串、字符串处理去掉首尾空格、判断长度、条件分支多个规则依次校验、以及返回值的结构设计。自己写一遍会发现很多书本上不会讲的取舍。function validateUser(input) { const errors []; const name typeof input.name string ? input.name.trim() : ; if (name.length 2 || name.length 12) { errors.push(昵称长度需在 2 到 12 个字符之间); } const age Number(input.age); if (!Number.isInteger(age) || age 12 || age 120) { errors.push(年龄需要是 12 到 120 之间的整数); } return { ok: errors.length 0, errors }; } console.log(validateUser({ name: 林一 , age: 26 })); console.log(validateUser({ name: A, age: abc }));这段代码值得琢磨的地方有三处。第一我先生成了错误数组再统一判断而不是一路return false因为实际页面需要把所有错误一次性展示给用户而不是让用户改一个提交一次。第二我显式做了类型转换并给了兜底值避免input.name是undefined时trim直接抛错。第三返回的是对象而不是布尔值调用方既能知道是否通过也能拿到具体的错误信息。这三点都是习题标准答案里通常不会写、但真实项目必须考虑的东西。4. 函数、参数与作用域习题里最容易被低估的一章函数那一章的习题往往是写出某个函数实现某个功能看起来简单但它是分水岭。一个人能不能写好函数基本能看出他的代码素养参数是不是合理、有没有副作用、边界情况怎么处理、返回值结构是否稳定。我自己的评判标准很朴素——如果我不能在完全不看实现的情况下仅凭函数名和参数就猜出它做什么、返回什么那这个函数的接口设计就不合格。做这部分习题时我会强迫自己给每个函数补一行契约注释输入什么、输出什么、什么情况下会抛错。这个动作看起来是多写了注释实际是在逼自己把职责边界想清楚。想不清楚边界的函数写出来一定是揉了一堆不相干逻辑的大杂烩。4.1 剩余参数和展开运算符一对容易搞混的兄弟这两者用同一个...符号方向却完全相反剩余参数是在函数形参位置把多个参数收成一个数组展开运算符是在调用或者字面量里把数组摊开。习题里经常把它们放在一起考很多人第一次做会绕晕。// 剩余参数收集 function sum(...nums) { return nums.reduce((acc, cur) acc cur, 0); } // 展开运算符摊开 const values [1, 2, 3, 4]; console.log(sum(...values)); // 10 console.log([0, ...values, 5]); // [0, 1, 2, 3, 4, 5] console.log(Math.max(...values)); // 4记忆的窍门是看位置写在函数定义的形参表里就是收集写在函数调用的实参里或者数组、对象字面量里就是摊开。我用这个办法之后就没再搞混过。还有一个实战要点剩余参数必须是最后一个形参因为它要吞掉后面所有的东西后面再跟别的形参就没有语义了。4.2 默认参数的求值时机比想象中更值得关注默认参数不只是语法糖它的求值时机有个很实用的特性只有当对应实参是undefined时才会求值。这意味着传null不会触发默认值传0也不会。习题里如果出现给空值兜底的需求用默认参数是解决不了null的这点必须清楚。另一个我常用的技巧是让后面的参数默认值引用前面的参数做参数联动function createCard(title, subtitle title 的副标题) { return { title, subtitle }; } console.log(createCard(周报)); // { title: 周报, subtitle: 周报的副标题 }这个特性在实际项目里用来做配置项的层层继承很方便但不要滥用因为参数之间的依赖会让接口变难理解。我的原则是只在参数存在明显的主从关系时才这么写。4.3 闭包题为什么反复出现它到底在考什么闭包是基础章节里最抽象的概念也是习题里出现频率最高、失分率也最高的题型。我最初的理解是函数里面套函数这个理解能应付一部分题但一到计数器、循环里绑定事件这种题就崩了。后来我换了个说法闭包是函数带着它出生时所在的那片变量环境一起旅行。函数被带出去执行的时候它依然能访问那片环境里的变量哪怕外层函数早就执行完了。一个典型场景就是给一组按钮绑定点击事件期望点击时拿到各自的序号const buttons [{ id: a }, { id: b }, { id: c }]; // 用 let 天然形成每次迭代独立的作用域 buttons.forEach((btn, index) { console.log(按钮 ${index} 的 id 是 ${btn.id}); }); // 需要延迟执行的场景把值先固定下来 const tasks buttons.map((btn, index) { return () ${index}:${btn.id}; }); console.log(tasks[0](), tasks[1](), tasks[2]());这段代码里map回调每执行一次都会生成一个新的index和btn绑定返回的箭头函数把这对值一起带走了。等后面调用tasks[0]()的时候拿到的依然是当初那一次迭代的值。这就是闭包在实战里的样子——它不是什么炫技工具而是让每个回调记住属于自己的那份状态的常规手段。5. 数组与对象前端日常数据的九成操作都在这里如果只允许我保留一章内容我会保留数组和对象这一章。前端业务说到底就是把后端给的一堆数据变成页面上的一堆界面元素中间的全部工作都是对数组和对象的遍历、筛选、映射、合并。这部分习题做得扎实后面学框架的时候会轻松很多因为框架里那些list、computed之类的概念本质上都是数组方法的高层封装。我练这一章的方法是一题多解。同一道题先用最朴素的手工索引写一遍再用数组方法写一遍最后比较两者在可读性和性能上的差异。这个过程比只写一遍有效得多因为它让你对方法的适用边界有了体感而不是死记硬背它的定义。5.1 数组方法选型对照表方法名字长得像的时候最容易选错我整理过一张自己常看的表需求推荐方法注意点每个元素变成新形态长度不变map必须有返回值忘了写会得到一堆 undefined挑出符合条件的元素filter回调返回真值即保留返回非布尔值会做隐式转换判断是否存在符合条件的一项some找到就短路返回不会遍历完整个数组判断是否全都符合every空数组调用返回 true这点常被忽略累加、汇总、统计reduce一定要传初始值否则空数组会直接抛错查找第一个匹配项find找不到返回 undefined记得判空查找匹配项的下标findIndex找不到返回 -1和indexOf语义不同原地增删改splice会改变原数组链式调用前必须确认这一点reduce是最容易劝退的一个方法很多人第一次看它的四个参数就晕了。我的建议是把它理解成一个滚雪球的过程第一个参数是当前累积起来的雪球第二个参数是刚滚进来的那片雪返回值是新的雪球。想清楚这个比喻reduce的题目基本就不会做了。5.2 对象的拷贝与看起来改了其实没改对象这一章最大的坑是引用。习题里如果出现复制一个对象然后修改副本直接赋值一定会出问题因为两个变量指向的是同一块内存。浅拷贝能解决一层的问题但嵌套对象还是共享的。这是我见过的新人 bug 高发区也是我得单独拿出来说的原因。const origin { name: 林一, tags: [前端, 读书] }; const shallow { ...origin }; shallow.name 赵二; shallow.tags.push(跑步); console.log(origin.name); // 林一第一层没被改 console.log(origin.tags); // [前端, 读书, 跑步]第二层被改了要彻底切断联系需要深拷贝。实际项目里我会优先选择结构化克隆// 浏览器环境下的深拷贝能处理嵌套对象、数组、日期 const deep structuredClone(origin); deep.tags.push(爬山); console.log(origin.tags.length); // 2原对象不受影响JSON.parse(JSON.stringify(obj))这种老办法也能深拷贝但它会丢掉函数、undefined、Date会变成字符串还会在遇到循环引用时直接报错。我在项目里已经基本不用它了除非是很简单的纯数据对象。5.3 从一道去重加排序的题看链式思维给一个数组去重并按指定规则排序是教材里非常经典的题目。用数组方法链式表达可以写得很清爽const raw [3, 1, 2, 3, 5, 1, 4]; const result [...new Set(raw)].sort((a, b) a - b); console.log(result); // [1, 2, 3, 4, 5] // 对象数组按字段排序注意不要直接改原数组 const list [{ score: 80 }, { score: 95 }, { score: 70 }]; const ranked [...list].sort((a, b) b.score - a.score); console.log(ranked.map((item) item.score)); // [95, 80, 70]这里有两个细节值得单独说。第一sort是原地排序会改变原数组所以我在排序前先展开复制了一份这在处理组件状态时非常重要直接改原数组可能导致界面不更新或者更新得莫名其妙。第二默认的sort是按字符串比较的[10, 9, 100]排出来会是[10, 100, 9]必须传比较函数。这个坑我在真实项目里踩过一次导致一个排行榜的数字顺序完全乱了排查了很久才想到是排序规则的问题。6. 把答案跑起来控制台、断点和最小复现做习题的过程中最有价值的技能不是写代码而是排错。教材的答案只能告诉你正确的代码长什么样但没法告诉你为什么我这段看起来差不多的代码结果不对。这个能力必须自己练而练习的场地就是浏览器的开发者工具。我的习惯是每道题都在控制台里手动跑一遍把中间变量打出来看。很多人写代码是一口气写完再运行出错之后两眼一抹黑只能靠猜。我更推荐增量式开发写两行跑一次确认这一步的输出符合预期再往下写。听起来慢实际上比写完再回头找 bug 快得多因为 bug 的可能范围被限制在最后几行里。6.1 console 家族的正确打开方式只用console.log做调试是效率很低的做法。我常用的几个方法各自有明确场景console.table(arr)数组或对象数组直接出表格字段对比一目了然比打印一堆对象友好太多。console.dir(obj)查看对象的完整结构尤其是 DOM 元素这种特殊对象。console.count(label)统计某段代码执行了多少次排查循环次数异常特别好用。console.time和console.timeEnd测量一段逻辑的耗时做性能对比时必备。console.trace()打印当前调用栈排查到底是谁调用了这个函数时非常有效。用console.log的时候也有个小技巧不要直接打印对象然后就放着不管因为控制台里打印的对象是引用快照后面代码改了它你在控制台看到的内容可能也跟着变容易产生误判。稳妥的做法要么是打断点看要么打印它的序列化结果const state { count: 0, list: [] }; console.log(JSON.stringify(state));6.2 断点怎么打才有用我的断点策略分三种。第一种是在报错行之前的那一行打上看看到底是哪个变量不符合预期。第二种是在函数入口打上配合条件断点只在特定参数下停下来避免在循环里停几百次。第三种是在debugger语句硬编码适合那种必须复现特定交互路径才能触发的问题。条件断点是很多人不知道的功能。在开发者工具的源码面板里右键断点可以设置表达式比如只在index 5的时候停下来。排查数组处理出错的时候这个功能能帮你直接跳到问题发生的那一次迭代而不是靠肉眼一次次数循环。6.3 一个报错定位的完整排查链路我拿一个很典型的报错走一遍思路。假设控制台给出TypeError: Cannot read properties of undefined (reading name)。第一步先不急着改代码而是读报错。它已经明确告诉你某个值是undefined你在它身上取name。所以问题不在name而在那个本该是对象的东西是 undefined。第二步看报错右侧的文件名和行号点进去定位到具体那一行。假设这一行是const title data.user.name;。第三步逐个往前推。data是不是 undefineddata.user是不是 undefined在断点里分别求值就能确认。我遇到的绝大多数情况是data.user为空根源是接口在某些场景下没有返回user字段。第四步想清楚这属于数据问题还是代码问题。如果接口确实可能不返回这个字段那代码就应该做防御如果是接口不该为空却空了那应该去查数据源。两种情况处理方式完全不同改代码之前必须判断清楚否则容易把真正的数据缺陷掩盖掉。第五步加防御并验证。我通常用可选链加兜底const title data?.user?.name ?? 未命名用户;可选链的好处是它只对null和undefined短路不会误伤 0 和空字符串这比早年的data data.user data.user.name既短又准确。沿着这条链路走几遍之后再看到类似的报错我基本上能在十几秒内定位到大致范围。这个能力在真实项目里的价值远高于多背几道题的答案。7. 刷完这一轮之后怎么判断自己是真的会了做完一本教材的习题不难难的是判断自己到底掌握了没有。我后来总结了一个比较靠谱的检验方式把题目关掉从零写一个综合性的小项目不查资料遇到卡壳就记下来。写完再对照卡壳的地方就是真正的薄弱点。这个过程比刷十道题更能暴露问题因为它不再给你这道题考什么的提示。我还会做一件事把之前做过的题目拿出来换个说法重新做一遍。比如原来题目是求数组最大值我就改成求一组交易记录里的最高金额并返回对应那条记录。题目变复杂了但核心逻辑没变。如果能顺利迁移说明掌握的是方法如果必须回到原题才能做出来说明记住的只是答案的模式。7.1 一份我常用的自测清单每次复习到这个阶段我会对着下面的清单过一遍答不上来的就回去补能不能在不查资料的情况下说清和的差别并给出一个不该用的例子能不能写出一个不会改变原数组的排序实现能不能解释为什么for...in不适合遍历数组能不能说清浅拷贝和深拷贝的区别并各写一种实现能不能用reduce完成一次分组统计能不能解释闭包捕获的是变量本身还是变量的值能不能说清typeof null和Array.isArray各自解决什么问题看到一个undefined相关的报错能不能在三十秒内说出排查步骤这八个问题覆盖了基础阶段最容易在真实项目里吃亏的几个点。它们没有一个是偏题怪题全是日常写业务代码绕不开的东西。7.2 看着会、写着废的高频清单最后分享几个我自己和身边同事反复出现的伪掌握现象这几种情况最容易让人误判自己的水平第一种是能读懂数组方法的链式调用但自己写的时候分不清哪个方法会改原数组。解决办法是每用一个方法前先想一下它的返回值是新数组还是原数组。第二种是能说出闭包的定义但写不出一个每个回调记住自己索引的例子。解决办法是手写三遍计数器或者事件绑定的场景直到不假思索。第三种是知道要用但遇到字符串数字比较时又犹豫。解决办法是把类型转换显式写出来永远不让读者猜。第四种是能背出this的几条规则但一放到对象方法或者事件回调里就判断不准。这种情况只能靠多写多跑用实际输出校正脑中的判断。我自己的体会是基础阶段最忌讳的就是用看答案的方式学代码。答案给你的是结论而你需要的是判断过程。做习题的时候多问自己一句如果不看答案我下一步会怎么做然后老老实实把手放上键盘敲出来哪怕敲错了再改。这一来一回的折腾才是真正长在身上的东西。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询