“用 JavaScript 解答练习题”,听起来像是个学生时代的课后任务,但真把它当回事的人,往往能在这些“小题”里学到比刷十遍文档更扎实的东西。我自己带过不少新人,也面试过候选人,发现一个规律:能把基础练习题写得干净、严谨、不踩坑的人,写业务逻辑的代码通常也不会差到哪去。JavaScript 这门语言太灵活了,同一个题目能写出十种风格,但哪一种是“对”的,哪一种是“能跑但会埋雷”的,这中间的差别,正好是经验的分水岭。
这篇文章我不想讲那些虚的“编程思想”,而是结合我实际解题、出题、改代码的经验,把一套用 JavaScript 解答练习题的方法论拆开揉碎。从最基础的变量陷阱,到函数、字符串、事件处理、运行时报错排查,再到跨浏览器兼容那点破事,咱们一道题一道题地过,把每一步为什么这么做、底层是什么原理、还有哪些坑没填,全给你说清楚。无论你是刚摸键盘的初学者,还是想帮别人改作业的老手,应该都能在里面翻到点有用的东西。
1. 整体设计与思路拆解
拿到一套练习题,最忌讳的就是上来就敲代码。哪怕题目只是“交换两个变量的值”这种幼儿园级别,我也建议你先在脑子里过一遍:这道题考的到底是语法,还是算法,还是某个 API 的边界条件?JavaScript 的练习题通常不会只考一个点,它是把“变量声明”“类型转换”“作用域”这些东西缝在同一个场景里,你拆解得越清楚,写出来的代码就越有章法。
1.1 核心需求解析:练习题到底在练什么
JavaScript 练习题按考察目标可以粗暴分成三类:
第一类是语法熟练度题,考的是“你知不知道这个写法存在”。比如考箭头函数和普通函数里 this 的区别,考 const 声明后能不能改对象属性,考 == 和 === 的差异。这类题的标准答案是唯一的,属于死记硬背加理解,但偏偏很多人挂在上面,因为平时写业务代码压根不去想这些细节。
第二类是逻辑思维题,考的是“你能不能把一个问题拆解成步骤”。比如数组去重、字符串翻转、斐波那契数列,这类题的解法可能有好几种,暴力枚举、双指针、递归、迭代,各有优劣。练习题要的往往不是最炫技的解法,而是可读性最高、边界最稳的那一个。
第三类是综合应用与坑点题,考的是“你知不知道这里有个坑”。比如 DOM 操作里取到的元素是实时集合还是静态快照,事件绑定在动态添加的元素上为什么会失效,字符串拼接和模板字符串在性能上有什么差别。能发现这些坑的人,通常都有不少线上事故经验,练习题的价值就在于用很小的成本把这种经验预演一遍。
我自己的习惯是,拿到题目之后先在注释里写三行话:输入是什么,输出是什么,边界条件是什么。这三个问题想明白了,代码基本不会跑偏。
1.2 方案选型:为什么选择原生 JavaScript 而非框架
现在前端圈子里“开口闭口 React/Vue”的氛围很重,导致很多初学者一上来就学框架,结果连querySelector和getElementById的区别都说不清楚。练习题这东西恰恰相反,它逼着你用原生 JavaScript 去解决问题,因为出题人不会允许你npm install一个答案下来。
用原生 JavaScript 解题有三个别的东西替代不了的好处:
第一是逼你理解浏览器 API 的原生行为。比如那句热搜词里提到的document.querySelector('video'),你只有写过原生选择器,才知道它返回的是第一个匹配的元素,而querySelectorAll返回的是静态 NodeList,如果不小心用getElementsByTagName,拿到的就是实时集合,删掉一个节点它自己会变。这种差异只有原生环境能让你感受到。
第二是练出面试手写题的基本功。好多面试官特别喜欢让你手写一个debounce、手写一个Promise.all、手写一个深拷贝,你用框架写倒是快,但手写的时候就露馅了。原生 JavaScript 练多了,这些题也就是顺手的事。
第三是调试思路会变得清晰。用框架的时候,报错信息一层层套着组件栈,新手往往懵在原地。用原生 JavaScript,报错就是TypeError: Cannot read properties of null,你一眼就能定位到那一行,这种排查能力是后续一切工程能力的地基。
所以,如果你正拿练习题练手,别急着引入 lodash 或者 jQuery,先用纯 JavaScript 把它写完,再想一想“如果非要引入一个库,应该引入哪个、为什么”。这个思考过程,就是工程师和码农的区别。
2. 基础核心语法拆解与实操要点
练习题里最阴险的往往不是算法,而是基础语法里那些看似不起眼的细节。我一贯的立场是:能把这些细节讲明白的教程,比给你抄一百道题的答案有用得多。下面挑几个热点里反复出现的模块,结合题目实际操作点,逐个拆给你看。
2.1 变量声明与类型转换:别在 const 和 let 上栽跟头
有一道出现频率极高的练习题:给定一个对象const obj = { name: '张三' },请问能不能执行obj.name = '李四'?很多人一拍脑袋说不能,因为声明用的是 const。但正确答案是“能”,因为 const 锁定的只是这个变量对对象的引用关系,而不是对象的内部结构。你要做的是obj = { name: '李四' },那才叫报错。
这道题背后真正想考的是 JavaScript 的“按引用传递”机制。对象存在内存堆里,变量只是一个指向堆内存的地址,const 冻结的只是这个地址不能变。如果你想彻底冻结对象结构,得用到Object.freeze,而且它还是浅冻结,嵌套对象照样能改。我在练习题里见过有人天真地以为Object.freeze就万事大吉,结果内部数组被 push 得飞起——这其实也提醒我们,解答练习题的时候,不要只记结论,要记住结论的边界。
类型转换也是练习题的重灾区。我记得有道题是算'5' + 3和'5' - 3的结果,前者是字符串'53',后者是数字2。原因在于加号在有字符串参与的时候优先做拼接,而减号会强制把两边的操作数转成数字。这个不对称的设计让无数初学者怀疑人生,但它恰恰是 JavaScript 的“隐式转换”规则里最经典的考点。
实操建议就一条:在写业务代码和练习题时,尽量用===代替==,用显式转换函数(Number()、String()、Boolean())代替依赖隐式规则。练习题里如果明确考隐式转换,再单独把它当知识点突击。口诀是“不猜隐式,显式优先”,这样至少能避免一大半头痛。
2.2 字符串操作:掌握常用 API 但更要看准返回值
热搜词里有一组是“javascript学习手册九:字符串”,可见字符串处理在练习题里的地位。字符串题型的套路比较固定:翻转、截取、替换、统计字符出现次数、判断回文等等。你光背 API 是不够的,关键是要清楚每个 API 的返回值类型和是否改变原字符串。
举几个实际例子:slice()、substring()、substr()都能截取字符串,但参数含义和边界行为各不相同。substr()的第二个参数是长度,已经被标准废弃,不建议再用;slice()支持负数索引,从末尾往前数;substring()不支持负数,传负数会被当成 0。我见过笔试现场写混这三个函数的人,改了半天才发现是索引语义错了。
另外,字符串方法里有一个高频考点:split()拆分的结果是个数组,但分隔符如果传的是空字符串'',会把字符串拆成每一个字符;如果字符串里有连续分隔符,比如'a,,b'.split(','),结果里会出现空字符串元素['a', '', 'b'],很多人会忘记这个细节,导致统计结果出错。
还有replace()方法。一个字面量字符串作为第一个参数,它只替换第一个匹配项,你需要用全局正则/pattern/g才能替换所有。练习题里如果出现“把所有空格替换成下划线”,第一版代码经常写str.replace(' ', '_'),然后只换掉了第一个空格,这就是对正则参与方法的语义不熟。
字符串这块我给的实操建议是:把String.prototype上的常用方法分成两类——纯查询类(indexOf、includes、charAt)和生成新字符串类(slice、substring、concat、replace),然后记住一条铁律:字符串是不可变的,所有方法都不会修改原字符串,而是返回一个新字符串。基于这条铁律,你在写链式调用时才不会把原值搞丢。
2.3 函数与作用域:考清楚 this 和闭包
“javascript函数”和“javascript学习手册八:js函数”这两组热搜词说明,函数是练习题绕不过去的山。函数题里最经典的几大类:普通函数、箭头函数、立即执行函数、闭包、回调函数、递归。每一类背后都有几个特定的考点。
先说 this 指向。普通函数里的 this 是指向调用者,而箭头函数里的 this 继承自定义时的上下文。那道经典练习题“this 指向解析”,往往给出一连串嵌套对象和方法调用,问你输出顺序是什么。我的解题套路是三步走:第一步确认每个函数是不是箭头函数;第二步找到每个函数被调用时点号前面是哪个对象;第三步把没被对象调用的函数 this 视为全局(或者 undefined,取决于是否开启严格模式)。这套扣手流程,能解决大部分 this 指向题。
闭包更是重中之重。练习题里常见的写法是for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100) },问输出是什么。答案是五个 5,因为var没有块级作用域,循环结束后 i 已经变成 5,五个定时器回调读的是同一个 i。解法是改用let声明,或者用立即执行函数把 i 的值封进去。这道题把所有考点全串起来了:作用域、闭包、事件循环、定时器延迟执行。搞清楚它,基本就能理解“为什么闭包能保存变量”这件事。
函数题的实操建议是:写完函数后多问自己一句“如果不传参数会怎样”,再问一句“如果传错类型会怎样”。练习题经常会在默认参数、参数个数、arguments 对象上做文章,把这些边界想清楚,函数的质量直接上一个档次。另外,能用默认参数就用默认参数,不要在自己函数里手写一堆if (!param) param = 1的逻辑,那样既不优雅还容易出 bug。
2.4 数组与对象操作:高性能解法的取舍之道
数组和对象操作在练习题里占的篇幅最大,因为它们最能反映一个人对数据结构理解的深度。数组去重、扁平化、排序、聚合统计,这些题能写出正确答案的人很多,但能在“答案正确”的基础上再说清楚时间复杂度和空间复杂度的人,少之又少。
以数组去重为例:最简单的解法是用Set,[...new Set(arr)]一行搞定,时间复杂度 O(n),代码可读性极高。但如果你被要求“只能使用 ES5 语法”,就得退回indexOf循环,或者用对象当哈希表。如果题目里面还混着NaN,indexOf的严格相等语义会直接失效,你用includes才能处理 NaN 的去重,因为includes用的是 SameValueZero 比较。这些细节不是靠背,是靠一道道题踩出来的。
对象操作的题型里,最常见的是浅拷贝和深拷贝区分。很多人不知道Object.assign和展开运算符都是浅拷贝,嵌套对象里修改属性,原对象也跟着变。真要写深拷贝,练习题时代可以先 JSON 两连:JSON.parse(JSON.stringify(obj)),但你得背着它的大坑——函数、undefined、Symbol、循环引用都会被干掉。所以比较扎实的练习路线是:先能手写一个递归深拷贝,再去理解structuredClone这种浏览器原生 API。
3. 实操过程与核心环节实现
光说不练假把式。这一节我挑了几道有代表性的练习题,从读题到调试,完整走一遍流程。每道题我都会给出我实际敲过的代码,以及我当时注意到的问题,方便你直接参考。
3.1 练习题一:数组扁平化并去重排序
题目要求把嵌套数组[1, [2, [3, [4]], 5], 6]处理成一个升序且去重的数组。
我第一次做这题时的第一版解法是递归 + Set + sort:
function flattenAndSort(arr) { const result = []; function walk(list) { for (let item of list) { if (Array.isArray(item)) { walk(item); } else { result.push(item); } } } walk(arr); return [...new Set(result)].sort((a, b) => a - b); } console.log(flattenAndSort([1, [2, [3, [4]], 5], 6])); // 输出 [1, 2, 3, 4, 5, 6]代码能跑,但仔细想想有两个可以优化的点:一是用递归在数据量极大时可能栈溢出,这时可以用栈模拟迭代;二是sort默认按字典序,排数字必须传(a, b) => a - b,不然[1, 10, 2]会被排成[1, 10, 2]。后来我测试的时候发现用flat(Infinity)一行就把嵌套打平了,但面试如果问原理,还得能说出它底层实现的大致思路。
这道题我实际跑完还有一个体会:别在递归函数外面定义一个共享数组result,如果函数被调用两次,第二次会在第一次的基础上继续 push,结果就脏了。最稳的写法是让递归函数返回值,一层层把结果拼接上来。这类“共享状态污染”的坑,在真实项目里也特别常见,练习题提前踩一踩挺好。
3.2 练习题二:模拟实现防抖与节流
防抖和节流是前端面试的高频手写题,也是日常滚动监听、输入框搜索里躲不开的工具函数。它们都是在限制函数的执行频率,但场景不同:防抖是“停止操作后等待一段时间再执行”,节流是“固定时间间隔内最多执行一次”。
我的防抖实现:
function debounce(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }这里的关键点是:回调函数里必须用apply(this, args),因为如果不传 this,直接写fn(args),防抖后的函数就丢了调用者上下文。比如你是给某个对象的方法做防抖,this 指向错了,方法里的属性就访问不到。
节流的典型实现:
function throttle(fn, interval = 300) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }时间戳版节流有个特点:第一次触发会立即执行,最后一次触发若还没到时间间隔,会被跳过。想要“尾执行”效果,就得用定时器版本,或者把两者缝合。练习题不会要求你背下所有版本,但至少你要能解释清楚两种写法的行为差异,这也是出题人最喜欢追问的地方。
实操时我建议你在本地写一个测试页面验证效果:放一个按钮,点击后打日志,然后分别用防抖和节流包一层,观察日志频率的差异。你会非常直观地感受到这两个函数的意义。
3.3 练习题三:DOM 事件监听与动态元素绑定
还有一道经典的“N 个按钮点击输出序号”题目。HTML 结构通常是循环生成五个按钮,要求点击第 i 个按钮时弹出 i。
很多新手用for (var i = 0; i < 5; i++) { btn[i].onclick = function() { alert(i); } },结果发现点哪个都弹出 5。原因跟前面那个 setTimeout 题一模一样,var 的全局作用域让所有函数闭包共用同一个 i。解决方法是把 var 换成 let,或者用立即执行函数传参,或者用>document.querySelector('.container').addEventListener('click', function (e) { if (e.target.tagName === 'BUTTON') { const index = e.target.dataset.index; console.log('点击了第', index, '个按钮'); } });
事件委托的好处是一劳永逸,新加的按钮不用额外绑定也能触发,而且内存里只放一个监听器。我在实际项目中,列表操作按钮几乎全是这么写的。练习题里如果出现“动态列表 + 事件”,优先考虑事件委托,这个思路比循环绑定高级不少。
3.4 练习题四:字符串统计字符出现次数
最后一道题比较亲民:输入一段英文,统计每个字符出现的次数,忽略大小写,按次数降序输出。
我常用的解法:
function countChars(str) { const cleaned = str.toLowerCase(); const map = {}; for (let ch of cleaned) { if (ch === ' ') continue; map[ch] = (map[ch] || 0) + 1; } return Object.entries(map).sort((a, b) => b[1] - a[1]); } console.log(countChars('Hello World')); // 输出 [ [ 'l', 3 ], [ 'o', 2 ], [ 'h', 1 ], [ 'e', 1 ], [ 'w', 1 ], [ 'r', 1 ], [ 'd', 1 ] ]这道题的考点有两个:一是for...of遍历字符串时按字符迭代,能正确处理大部分 Unicode 字符(但碰到 emoji 这类代理对字符仍需小心);二是Object.entries能把对象转成数组,配合sort排序。如果你想统计的是单词而不是字符,改成split(/\s+/)再遍历就行。
这里有一个很典型的“字典累加”模式:map[ch] = (map[ch] || 0) + 1,第一次遇到某字符时map[ch]是 undefined,undefined || 0得到 0,加 1 之后塞回去。第二次再遇到,map[ch]就是上次的数字。这种模式在写统计数据时能用上百次,建议肌肉记忆。
4. 常见问题与排查技巧实录
写练习题的报错和线上运行的报错,本质上没有区别,只是规模不同。我把我见过的高频报错和隐蔽问题整理成一张速查表,再挑几个详细说说排查思路。这部分内容,实打实是吃了亏换来的。
4.1 运行时报错的核心类型速查
| 错误类型 | 典型场景 | 解决方向 |
|---|---|---|
ReferenceError: xxx is not defined | 变量名拼错、未声明就使用 | 检查拼写;确认在作用域内 |
TypeError: Cannot read property 'xxx' of null | 查询 DOM 节点失败后直接访问属性 | 打印节点确认是否为空;考虑执行时机(DOMContentLoaded) |
TypeError: xxx is not a function | 误把对象属性当函数,或函数名覆盖 | 打印 typeof;检查变量是否被赋值为非函数 |
SyntaxError: Unexpected token | 括号不匹配、中英文符号混用 | 利用 IDE 顶层的语法高亮快速定位 |
RangeError: Maximum call stack size exceeded | 递归没有出口或死循环 | 检查递归终止条件;改用循环或尾递归 |
这张表别看简单,我每次帮新人排查问题,百分之八十都能落在前两行。特别是“Cannot read property of null”,十个里八个是脚本放在 head 里提前执行,DOM 还没解析完。标准做法是把脚本放到 body 末尾,或者监听DOMContentLoaded事件,或者用defer加载。
4.2 事件循环与异步顺序问题
练习题一旦涉及setTimeout、Promise、async/await,顺序题就会变成噩梦。经典题目是打印以下代码的输出顺序:
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);正确输出是1, 4, 3, 2。很多人不理解为什么 3 在 2 前面,因为虽然setTimeout的延迟是 0,但它进的是宏任务队列,而 Promise 的回调进的是微任务队列。JavaScript 引擎执行完当前同步代码后,会先把微任务队列清空,再取一个宏任务执行。这就是“微任务优先于宏任务”的规则。
如果你遇到的练习题是“求执行顺序”,我的排查技巧是三步走:第一步把所有同步代码标出来,第二步把所有微任务(Promise、queueMicrotask、MutationObserver)标出来,第三步把所有宏任务(setTimeout、setInterval、I/O)标出来。然后按“同步 → 全部微任务 → 一个宏任务 → 再清空微任务”的顺序排序。这套规则理清楚了,任何异步顺序题都不在话下,日常排查线上定时器错乱也能直接用。
4.3 跨浏览器兼容性问题的处理原则
热搜词里有“javascript 框架或库是一组能轻松生成跨浏览器兼容的 javascript 代码的工具和函”,这句话本身没问题,但它背后的意思需要掰开揉碎。在练习题阶段,我不会鼓励你套一层框架来糊弄,因为你需要知道浏览器之间到底在哪里分叉。
实际工作中最常遇到的分叉点:旧版本浏览器不支持Array.prototype.flat,不支持String.prototype.includes,不支持可选的 catch 绑定等。处理原则是:如果你知道目标浏览器很老,用babel做语法降级,或者手写一个兼容函数。如果你只是写练习题,那你应该能在纯原生环境里,用条件判断检测 API 是否存在,来决定走哪条分支。
比如你要兼容不支持NodeList.forEach的环境,稳妥的写法是:
const nodes = document.querySelectorAll('.item'); if (nodes.forEach) { nodes.forEach(node => console.log(node)); } else { Array.prototype.forEach.call(nodes, node => console.log(node)); }这种 feature detection(特性检测)的思路,比 userAgent 判断靠谱得多。练习题可能不会考这么细,但一旦你养成了“先检测能力再调用 API”的习惯,你在业务代码里就很少会写出那种“在我电脑上是好的,到用户电脑上就白屏”的代码。
4.4 容易忽略的边界条件和隐性陷阱
很多时候练习题不是难在算法,而是难在隐形条件。我想把自己的排查清单分享给你,每次写完代码逐条自查,能少踩很多坑:
- 空数组与空对象:
reduce在空数组上不带初始值会直接报错;sort空数组不会报错但也没意义。 - NaN 的比较:
NaN === NaN是 false,需要判断是否 NaN 时,用Number.isNaN()。 - 浮点数精度:
0.1 + 0.2不等于0.3,比较时用Math.abs(a - b) < Number.EPSILON。 - string 与 number 混合比较:数组
sort默认字典序,数字排序必须传回调函数。 - 循环绑定事件的 var 泄漏:前面已经说过,优先 let,或者闭包,或者事件委托。
- 函数参数个数不一致:用
arguments.length或默认参数处理缺省情况。
这几条是我的“高频犯错清单”,每次写完代码至少过一遍。时间久了,你会发现自己犯错的概率显著下降。不是因为你变聪明了,而是因为你的检查清单变长了。
5. 调试技巧与工具链推荐
工欲善其事,必先利其器。练习题虽然规模小,但调试工具用的顺手,能省下不少瞎猜的时间。这里讲几个我一直在用的方法和工具,都是经过实战筛选的。
5.1 浏览器开发者工具的正确用法
很多新手打开 DevTools 只知道看 Console,实际上完整的调试姿势是“Console + Sources + Network”三件套配合。
Console 里我常用的是console.table,打印数组和对象数组时可比console.log清楚十倍。排查循环内部状态时,不要只打最终结果,要在循环体里打当前迭代的索引和值。条件断点也很重要,在 Sources 面板里右键断点可以添加表达式,比如i === 3才停下来,这样就不会断一屏幕了。
如果你在排查涉及到时间或动画的问题,Performance 面板可以看主线程的占用情况。练习题用不到这么深,但一旦你开始处理性能题(比如大数组操作、频繁 DOM 操作),就会发现 Performance 给的数据比直觉可靠得多。
5.2 Node.js 环境跑练习题
有的练习题不涉及 DOM,纯逻辑操作,我建议直接在 Node.js 环境里跑,不用开浏览器。方法很简单,装好 Node 之后,在项目目录里建一个test.js,写完代码在终端运行node test.js,结果立刻输出。Node 环境的报错堆栈信息比浏览器更简洁明了,适合纯算法逻辑题的反复试错。
如果你还想自动跑断言,可以用 Node 内置的node:assert模块,或直接装一个 Vitest 跑单测。练习题阶段用不着完整的测试框架,但写几个assert.strictEqual(实际值, 期望值)绝对是好习惯,它逼着你在写代码之前先想清楚输出到底该是什么。
5.3 在线代码沙箱的取舍
在线代码沙箱(比如 CodePen、JSBin、CodeSandbox 这类)胜在零配置,适合快速验证一段代码的效果,尤其是涉及 HTML + CSS + JavaScript 联动的场景。你想验证一个querySelector('video')之后设置旋转样式,开个沙箱放一段真实的视频标签,比在本地搭环境快得多。
我的建议是:逻辑题在本地 Node 跑,DOM 题在浏览器 DevTools 跑,联调的视觉题用在线沙箱。每个工具都有它最舒服的场景,不要一把梭。真实项目里最常见的效率杀手,就是在不合适的工具里死磕一个本该几分钟解决的问题。
6. 实战心得与进阶建议
题目刷到一定数量之后,你自然会意识到:练习题的核心不是为了“会做”,而是为了建立浏览器与 JavaScript 之间的那种“肌肉记忆”。这跟学骑自行车一个道理,光看教学视频不蹬踏板,永远感受不到平衡点在哪里。
6.1 一题多解与复杂度分析是分水岭
如果你做完一道题就急着做下一道,那我建议你改改节奏。同一道题,写出暴力解之后,再想一想能不能用哈希表降低时间复杂度;写出循环法之后,再想一想能不能用递归或者减少变量使用。这种“多解推敲”的过程,才是刷题真正的收益。
比如找数组里出现次数最多的元素,暴力解法是双重循环,时间复杂度 O(n²)。用对象统计一遍,时间复杂度降到 O(n)。看起来只是多了一个中间对象,但数据量从一万变成一百万的时候,运行时间差距是几个数量级。练习题规模小体现不出来,但分析能力的养成就是从这种小地方开始的。
6.2 写注释和伪代码的习惯越早养成越好
我现在看新人交上来的代码,最喜欢看到的是函数开头有一段简短注释,说明这个函数做什么、参数是什么、返回什么。练习题阶段,我更建议你在动手之前先用伪代码把流程写出来。比如统计字符次数这题,伪代码就是“遍历字符串 → 计数 → 排序 → 输出”。写代码的时候照着伪代码一行行填,出错的概率会小得多。
还有一点个人心得:给自己写的代码留“修改空间”,不要在函数名和变量名上随便用a、b、c。练习题的函数可能不大,但你取的每一个名字,都在训练未来写大型代码时的命名直觉。const countMap = {}比const obj = {}更容易让人一眼读懂。
6.3 从练习题过渡到真实项目的最后一公里
有些人在练习题里再厉害,一上手公司项目就懵,原因在于项目里的代码不是一道孤立题,而是几十个文件互相纠缠。练习题给你的是一种“单点能力”,但真实项目还需要“组合能力”。不过,练习题里的技巧几乎都能迁移过去:你学会的防抖,能用在搜索框里;你学会的事件委托,能用在长列表渲染里;你学会的边界条件检查,能帮你写出更健壮的工具函数。
所以,我的建议很直接:练习题刷到你觉得“大部分题都有思路”的时候,就去找一个小型真实项目来重构一遍。比如写一个待办事项应用,需要增删改查、本地存储、事件交互,这些功能拆开了全是练习题,合在一起就是一个微型的真实业务。做完这个项目,你再回头看不明白的练习题,基本都有一种“降维打击”的感觉。
这个内容后续还可以这样扩展:把每道练习题的解法整理成一个小型工具库,比如array-utils.js、string-utils.js,以后写项目直接从里面抽函数用。我就是这么过来的,那些文档里干巴巴的 API,在一次次实战中被消化成了自己的东西,往后写代码时的底气完全不一样。