JavaScript练习题解题框架:从审题建模到代码调试全指南
最近在带几个新手朋友刷 JavaScript 练习题发现一个特别普遍的问题题目看得懂思路也有但一落到代码上就卡壳或者好不容易写完一运行就报错改来改去头都大了。这篇文章不是语法文档而是把“运用 JavaScript 解答练习题”这件事完整拆开——从审题建模、工具选型、常见题型套路到真实调试过程再到练完之后怎么往项目里迁移。不管你是刚学完基础语法、准备刷题巩固的初学者还是写过几个页面但一碰到逻辑题就心虚的前端这篇都能给你一套能直接上手的解题框架。我自己这几年面试别人、带新人、自己也刷过不少题最大的感受是JavaScript 解题真正的难点从来不是语法而是“把自然语言翻译成代码逻辑”的那一步。这一步有方法可以练而且练完之后写业务代码也会顺手很多。下面我从头到尾捋一遍。1. 解题第一步审题建模比写代码更重要很多人拿到练习题就急着打开编辑器开始敲结果写了一半发现理解偏了推倒重来。这就像盖房子不打地基直接砌墙迟早要返工。我一般会先花几分钟把题目“翻译”成代码世界里的三样东西输入、输出、边界条件。1.1 先把题目拆成“输入-处理-输出”三段练习题无论长短本质上都是在描述一个数据处理过程。拿到题目之后先在脑子里回答三个问题输入是什么是一个数字、一个字符串、一个数组还是一个对象如果是多个参数它们的顺序和类型分别是什么输出是什么要返回一个新值还是要修改原数据返回值是单个结果还是一个集合中间要做什么处理这一步就是算法的核心通常可以拆成几个小的子问题。举个最常见的例子“给定一个字符串统计每个字符出现的次数返回一个对象。”题干一句话但拆开之后就很清楚输入是字符串输出是对象处理逻辑是遍历字符串的每个字符用对象做计数器。有了这个拆解代码结构基本就定下来了。1.2 边界条件决定代码的鲁棒性我见过太多人代码逻辑正确但一提交就挂问题多半出在边界条件上。字符串为空怎么办数组只有一个元素怎么办输入是 null 或者 undefined 怎么办数字包含负数怎么办这些不是刁难而是真实开发中一定会遇到的情况——用户不会按你的预期输入数据。一个实用的做法是在写主逻辑之前先列几个“极端用例”空输入、[]、0、null单元素输入[a]、[5]重复值输入aaaa、[1, 1, 1]超大值或负数-1、1000000把这些用例在心里过一遍再去写代码能避免大量低级错误。而且面试或笔试时主动写出处理边界条件的代码本身就是加分项。2. JavaScript 解题的核心工具函数、字符串与数组方法JavaScript 的练习题主要集中在几个领域字符串处理、数组操作、函数与递归、DOM 操作、异步流程。这些领域对应的核心工具说白了就是一组内置方法和函数式编程技巧。把这套工具用熟大部分题目都能迎刃而解。2.1 函数封装每个练习都是一个独立单元写练习题的时候别一上来就把所有代码平铺在全局作用域里。正确的做法是定义一个函数来封装解题逻辑输入作为参数输出作为返回值。// 不好的写法直接在全局跑逻辑没法复用 let str hello; let result {}; for (let ch of str) { result[ch] (result[ch] || 0) 1; } console.log(result); // 好的写法封装成函数输入输出清晰 function countChars(str ) { const result {}; for (const ch of str) { result[ch] (result[ch] || 0) 1; } return result; } console.log(countChars(hello)); console.log(countChars()); console.log(countChars(null)); // 注意这里需要处理函数封装有三个明显好处第一你可以方便地在不同输入上反复测试第二代码结构清晰评审或面试时容易讲明白第三养成这个习惯之后写业务代码时抽象能力会自然提升。2.2 字符串与数组出题最多的两大题型字符串和数组是练习题里的“常客”因为它们的 API 足够丰富能组合出很多花样。我这里整理几个高频方法每一个都是必会的字符串常用split()把字符串按分隔符拆成数组includes()判断是否包含子串toUpperCase()/toLowerCase()大小写转换replace()/replaceAll()替换trim()去掉首尾空格charAt()/ 方括号索引取字符数组常用map()逐个转换元素返回等长新数组filter()筛选符合条件的元素reduce()把数组归并为单个值find()/findIndex()查找元素sort()排序注意默认按字符串排序需要传比较函数includes()/indexOf()判断存在或找位置slice()/splice()切片和增删这里特别提一下sort()的坑。默认的sort()是把元素转成字符串再按字典序排的所以[10, 9, 100].sort()得到的是[10, 100, 9]。想正确排序必须传比较函数const nums [10, 9, 100]; nums.sort((a, b) a - b); // 升序[9, 10, 100] nums.sort((a, b) b - a); // 降序[100, 10, 9]2.3 高阶技巧解构、展开、可选链除了基础 API还有几个语法层面的技巧能在解题时大幅减少代码量解构赋值从数组或对象里快速取值展开运算符...复制数组、合并数组、展开参数可选链?.安全访问嵌套属性空值合并??处理默认值// 数组去重的高阶写法 const arr [1, 2, 2, 3, 3, 3]; const unique [...new Set(arr)]; // [1, 2, 3] // 对象取值防报错 const user { profile: { name: 张三 } }; const name user?.profile?.name ?? 匿名; // 张三这些写法不仅是“炫技”在真实项目里也是高频使用的。练习题阶段早点接触后面看别人代码的时候就不会一脸懵。3. 五道典型练习题完整拆解从思路到实现光讲方法论不够还得落到具体题目上。下面我挑了五道覆盖不同知识点的练习题每道题都按“审题 → 思路 → 实现 → 反思”的顺序走一遍。这几道题难度递增正好串起从基础语法到异步处理的路径。3.1 练习题一统计字符串中字符出现次数题目写一个函数countChars(str)输入一个字符串返回一个对象键为字符值为该字符出现的次数。这道题考察的是字符串遍历和对象操作。核心逻辑很直接遍历每个字符如果对象里还没有这个键就初始化为 1如果已经有了就加 1。function countChars(str) { if (typeof str ! string || str.length 0) { return {}; } const result {}; for (const char of str) { result[char] (result[char] || 0) 1; } return result; } console.log(countChars(hello)); // { h: 1, e: 1, l: 2, o: 1 }这里有个小细节for...of遍历字符串是按字符遍历的比传统的for (let i 0; i str.length; i)更简洁。(result[char] || 0) 1这种写法利用了“如果取不到值就取 0”的短路逻辑少写了一个if判断。3.2 练习题二数组去重并排序题目输入一个可能包含重复元素的数字数组[3, 1, 2, 3, 4, 1]输出去重后按升序排列的新数组。这道题考察数组 API 和 Set 数据结构。最简单的写法是组合Set和sort()function uniqueSort(nums) { if (!Array.isArray(nums)) { return []; } return [...new Set(nums)].sort((a, b) a - b); } console.log(uniqueSort([3, 1, 2, 3, 4, 1])); // [1, 2, 3, 4]如果要求不能用Set也可以用filter()手写function uniqueSort(nums) { return nums .filter((item, index) nums.indexOf(item) index) .sort((a, b) a - b); }这两种写法背后的原理不一样Set是利用哈希集合去重性能更好filter indexOf是逐个查找数据量大时会慢一些。练习题阶段两种都建议写一遍理解其中的区别。3.3 练习题三斐波那契数列的三种写法题目实现一个函数fib(n)返回斐波那契数列的第 n 项n 从 0 开始fib(0) 0fib(1) 1。这道题考察函数、递归、循环和性能优化意识。一般有三种实现方式方式一朴素递归function fib(n) { if (n 2) return n; return fib(n - 1) fib(n - 2); }代码最简洁但性能很差。fib(40)就会明显卡顿因为存在大量重复计算。方式二记忆化递归function fib(n, memo {}) { if (n 2) return n; if (memo[n] ! undefined) return memo[n]; memo[n] fib(n - 1, memo) fib(n - 2, memo); return memo[n]; }用一个对象缓存计算结果避免重复递归性能大幅提升。方式三迭代循环function fib(n) { if (n 2) return n; let prev 0; let curr 1; for (let i 2; i n; i) { [prev, curr] [curr, prev curr]; } return curr; }迭代法是性能最好的方案时间复杂度为 O(n)而且不用额外空间。这里用到了数组解构赋值来同时更新两个变量非常简洁。这道题最值得学习的地方在于同一个需求可以有不同的思路面试时如果你能给出从递归到迭代的演进过程会显得非常扎实。3.4 练习题四DOM 交互题——点击按钮旋转视频画面题目页面上有一个video元素和一个按钮点击按钮时把视频画面逆时针旋转 90 度。这道题考察 DOM 操作和事件绑定也是真实开发里很常见的场景——比如有些视频源方向不对需要一个按钮让用户手动旋转。我见过很多人写出来的代码要么没生效要么报错问题往往出在选择器的写法上。先看完成版代码video idmyVideo srcmovie.mp4 width480/video button idrotateBtn旋转视频/button script const video document.querySelector(#myVideo); const btn document.querySelector(#rotateBtn); btn.addEventListener(click, () { const currentRotate video.style.rotate || 0deg; video.style.rotate calc(${currentRotate} 90deg); video.style.transition transform 0.3s ease; }); /script这段代码有几个关键点值得展开讲选择器写法document.querySelector()必须传入完整的 CSS 选择器。很多人写成document.queryselector(video)注意querySelector里的S是大写如果写错会直接报TypeError: document.queryselector is not a function。事件绑定用addEventListener而不是onclick ...前者的好处是同一个事件可以绑定多个处理函数而且在需要移除时也方便。样式操作element.style只能读取和设置内联样式。如果你想读取的是外部 CSS 里定义的样式需要用getComputedStyle()。旋转属性的兼容性rotate属性是较新的 CSS 特性老浏览器可能不支持。更好的兼容写法是用transform: rotate(...)然后在 JS 里修改transform值。下面给出一个更稳妥的版本btn.addEventListener(click, () { let current video.style.transform; // 解析当前的旋转角度默认是 0 let currentDeg 0; if (current current.includes(rotate)) { currentDeg parseInt(current.replace(/[^0-9]/g, )) || 0; } const newDeg (currentDeg 90) % 360; video.style.transform rotate(${newDeg}deg); });这里多了一个“解析当前角度”的步骤看起来麻烦一点但避免了多次点击后角度参数叠加出错的隐患。练习题阶段能想到这里说明你不是在死记 API而是真的理解状态管理。3.5 练习题五异步流程——按顺序打印题目有三个异步任务task1、task2、task3它们完成时间不同要求无论谁先完成最终打印结果必须按 1、2、3 的顺序输出。这道题考察事件循环、回调函数、Promise 和 async/await。真实场景里很常见比如同时请求三个接口但必须按顺序渲染结果。先看一个典型的错误写法// 错误示范输出顺序不可控 function task1() { return new Promise((resolve) setTimeout(() resolve(任务1), 300)); } function task2() { return new Promise((resolve) setTimeout(() resolve(任务2), 100)); } function task3() { return new Promise((resolve) setTimeout(() resolve(任务3), 200)); } task1().then(console.log); task2().then(console.log); task3().then(console.log); // 实际输出任务2、任务3、任务1正确写法之一是串联执行让每个任务完成后才启动下一个task1() .then((res) { console.log(res); return task2(); }) .then((res) { console.log(res); return task3(); }) .then((res) { console.log(res); });如果用 async/await代码更接近同步写法async function runInOrder() { const res1 await task1(); console.log(res1); const res2 await task2(); console.log(res2); const res3 await task3(); console.log(res3); } runInOrder();但如果任务是“同时发起结果按序输出”也就是并发请求、结果按顺序展示那就需要用不同的策略// 并发发起但收集结果统一按顺序输出 Promise.all([task1(), task2(), task3()]).then(([res1, res2, res3]) { console.log(res1); console.log(res2); console.log(res3); });Promise.all保持结果顺序和传入数组一致哪怕任务 2 先完成输出时也排在第二个位置。这是异步练习题里非常高频的一个考点。4. 运行时报错与调试实录刷题时最该掌握的技能练习题写好了不一定代表结束运行时报错才是新手最头疼的部分。我整理了几类高频报错每个都对应一个常见的踩坑场景。4.1 高频报错速查表报错信息常见原因检查方向ReferenceError: xxx is not defined变量名拼写错误或变量未声明看变量是否在使用前声明过TypeError: xxx is not a function把不是函数的值当函数调用查函数名是否拼错或这个值到底是对象还是函数TypeError: Cannot read properties of undefined访问了undefined的属性确认对象是否存在用可选链?.保护SyntaxError: Unexpected token语法错误多为括号、引号不匹配检查括号匹配、中英文引号RangeError: Maximum call stack size exceeded递归没有终止条件或递归过深检查递归的基准条件考虑改用循环这些报错信息本身就是线索不要一看到红色报错就慌按图索骥去找对应的代码行大多数问题都能自己定位。4.2 用 console 和断点定位问题定位问题最快的方式是分段打印。我写练习题的时候习惯在关键节点加console.log确认每一步的数据是否符合预期打印输入确认参数有没有正确传入打印中间结果确认循环或方法调用后的状态打印输出确认最终结果是否和预期一致比如数组去重那道题如果结果不对我会这样调试function uniqueSort(nums) { console.log(输入:, nums); const unique [...new Set(nums)]; console.log(去重后:, unique); const sorted unique.sort((a, b) a - b); console.log(排序后:, sorted); return sorted; }如果输入没问题去重后没问题排序后出问题那问题一定出在sort()上——大概率是没传比较函数。浏览器开发者工具里的 Sources 面板还支持打断点点击行号即可程序运行到那一行会暂停可以逐行查看变量变化。练习阶段不要求每个问题都用断点但至少要会用——这是后面排查复杂问题的基本功。4.3 边界用例让代码在“压力”下露馅很多人写完代码测试一个正常用例就觉得完事了结果上交后评分才挂。一个简单有效的习惯是每个函数至少测试五个用例——正常输入、空输入、单元素、重复元素、特殊值。以fib为例console.log(fib(0)); // 0 console.log(fib(1)); // 1 console.log(fib(10)); // 55 console.log(fib(-1)); // 需要额外处理或者约定返回 undefined console.log(fib(1000)); // 大数检查是否会栈溢出如果fib(-1)没有处理递归会无限进行下去直到栈溢出。针对负数或非整数输入可以在函数开头加一个类型校验function fib(n) { if (typeof n ! number || n 0 || !Number.isInteger(n)) { throw new Error(Invalid input: n must be a non-negative integer); } // ... }练习题阶段就开始考虑校验和容错这种习惯带到工作上写出来的代码质量会明显高于平均水平。5. 从练习题到真实项目JavaScript 解题能力的迁移刷题不是终点能把这种“拆解问题、设计逻辑、处理边界”的能力迁移到实际开发里才算真正有效。我在这里举几个真实的迁移方向可能和你之后的工作直接相关。5.1 从“数组排序题”到日程日历面板很多前端项目里都有日历或日程组件比如用 FullCalendar 这类库。它内部处理事件数据的时候本质上就是在做“数组排序 时间过滤 冲突检测”。你练习过的sort()比较函数、filter()条件筛选、map()数据格式化全都能直接派上用场。比如把后端返回的日程数组按开始时间排序const events [ { title: 开会, start: 2025-03-20T15:00:00 }, { title: 写周报, start: 2025-03-20T09:00:00 }, { title: 代码评审, start: 2025-03-20T14:00:00 }, ]; events.sort((a, b) new Date(a.start) - new Date(b.start));这里用new Date()把字符串转成时间戳再相减思路和数字排序一模一样只是比较函数里多了一步转换。5.2 从“字符串统计题”到数据处理脚本在数据处理工具比如 Kettle 这类 ETL 工具里JavaScript 常被用来写自定义转换逻辑。你写的countChars这类字符串处理函数稍加扩展就变成数据清洗脚本// 清洗用户输入去掉首尾空格、合并连续空格、统一大小写 function cleanInput(str) { return str.trim().replace(/\s/g, ).toLowerCase(); }很多自动化流程里的小逻辑本质上就是这些基础练习题的应用。能把字符串、数组玩熟练写数据处理脚本会非常顺手。5.3 从“事件绑定题”到宿主环境交互在部分混合应用或小程序环境里JavaScript 需要和原生宿主互相调用。核心思路和 DOM 事件绑定类似通过注入的全局对象调用原生能力或者通过特定协议让原生感知到 JS 的调用。你在练习题里练过的“事件监听 回调函数”模型就是理解这种桥接机制的底层基础。所以别小看练习题里那些简单的 click 事件理解了“触发—响应”的模型后面接触任何宿主环境交互都能快速上手。最后说点实在的我自己刷题有个习惯每道题做完之后会再问自己三个问题能不能换一种写法能不能处理更极端的输入如果数据量变成一万倍当前写法还扛得住吗这三个问题逼着我从“能跑”走向“跑得对、跑得快”。这个习惯也是我从只会复制代码到能独立设计解决方案的关键转折。如果你现在正处于“看得懂答案但自己写不出来”的阶段别焦虑这是正常的。拿上面这五道题自己动手敲一遍卡住了再看解析然后把代码删掉重写一遍。两三轮下来手感自然就出来了。JavaScript 的练习题世界并不复杂套路就那么多你只需要用足够多的练习把这些套路变成肌肉记忆。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →