
“Js练习作业”这个标题看起来挺普通的既没有指名道姓的框架也不算一个具体项目。但正因为如此我反而觉得它值得好好聊一聊。当年我从第一次用JavaScript写完一个能跑的简单页面到后来给别人评审练习作业、再到自己不断地重复“写练习、重构练习、扔掉练习再重写”这个过程我发现很多人对JavaScript练习的认知都停在了“把题做出来”这一步。而我真正想通过这篇内容告诉你的是练习作业背后那套关于代码习惯、完成度判断、环境搭建、可复现意识、以及复盘方法的东西——这些东西才是决定一份练习值不值得做的关键。这篇文章不是那种一条条列语法知识的教科书也不是讲某个高大上框架的入门Demo。它适合谁看一种是把JavaScript基础语法学完、正在找题目练手的人另一种是身边有同学或团队成员在做JS练习、你负责检查或批改的人还有一种是自己手上有几个项目但写久了感觉心里没底、想回过头来用练习查漏补缺的人。不管你是哪一种我希望你看完之后把“练习作业”这四个字的理解从“完成一个功能”升级成“完成一次完整的代码交付”。1. 为什么我觉得“JS练习作业”是能力提升的隐形分水岭1.1 练习和实战之间其实隔着一道“完成”的墙先说一个我观察了很久的现象。很多人学JavaScript走的是这样一条路看视频、看文档、跟着教程敲一遍代码然后进入了一个“自我感觉良好”的阶段。可一旦让他脱离教程、自己动手去写一个练习作业哪怕是实现一个“数组去重”或“模拟实现一个简单的表单验证”他都会卡住。卡住的原因往往不是语法不会而是他从来没有独立面对过“从一个空页面/空文件开始把需求落到代码里”的过程。练习作业的本质其实就是把这个过程以可控的规模暴露出来。它像是给你一块小小的试验田面积不大但你必须完整地经历“翻土、播种、浇水、收获”这四个环节不能跳步。相比之下很多人做练习总喜欢“抄近道”从网上找现成的答案一看哎逻辑懂了就觉得自己会了。但等到他真正需要独立完成一个稍大一点的作业问题就会集中爆发而且爆发的点通常特别基础比如不会组织函数的拆分、不知道什么时候该把一段逻辑抽出来、不知道如何给变量起一个不需要回忆半天的名字。所以我的第一个判断是练习作业是能力提升的分水岭不是因为题目本身有多难而是因为它强制你走完一个完整的思考闭环。你会不会拆解需求、你会不会在没有提示的情况下选择一种实现方案、你写出来的代码能不能让三天后的自己一眼看懂这些在练习里都会暴露无遗。1.2 面试和带人时我从练习作业里看到了什么我参与过几次模拟面试和技术评审也帮朋友看了不少练习项目的代码。一个有意思的规律是面试者简历里写“熟练使用JavaScript”的和写“完成过XX练习作业并做过多轮重构”的在代码思路上经常有明显差异。前者往往能很快说出某个API的用法比如Array.prototype.reduce怎么传参、Promise.all和Promise.allSettled的区别但让他在白板上写一个完整的小函数他会犹豫参数怎么设计、要不要考虑边界情况、返回值怎么定比较合理。后者虽然可能不记得某个API的第二个参数但他写出来的代码结构通常更稳他会自然而然地处理空数组、考虑输入类型、把可复用的部分拆成独立函数。这种差异不是天赋造成的就是训练方式造成的。换句话说练习作业如果只是用来“练熟某个语法点”那它的价值很有限。真正有价值的是你在做练习时以一个“交付者”的心态去要求自己——哪怕这道题只有五分钟的编码量你也要把它当成一个需要提交给用户的完整功能来对待。时间长了这种习惯就会长在你身上。2. 从“能跑”到“能交付”一份练习作业的完成度坐标2.1 三档完成度能跑、能看、能扛我在评估一份JS练习作业时通常会先给它分档。这个分档方式和学校里的打分不一样学校看的是功能对不对我看的是这份代码在真实环境下能走多远。三档大概是这样的第一档叫“能跑”。功能是实现了点击按钮有反应数据能过滤页面能渲染。但代码可能是流水账式的一个函数写了两百行中间还夹杂着好几处让人摸不着头脑的魔法数字。这一档的代码属于“在作者的机器上、在作者的操作路径下”能跑换个场景可能立刻就碎。第二档叫“能看”。代码结构是清晰的函数拆得合理命名能看懂常量被提取出来有适当的注释。这一档的代码已经具备了可读性或可维护性的雏形别人接手时不需要从第一行猜到最后一行。第三档叫“能扛”。这一档在第二档的基础上还多了对边界情况的处理、对异常输入的兜底、以及对未来扩展的预留。比如写一个sum函数第二档会处理常规数组第三档会想一想“如果传入的不是数组怎么办”“如果元素里有字符串数字怎么办”“如果数组为空该返回什么”。写一个异步请求第三档会加上超时、错误状态和加载状态的区分而不是只写一个try...catch意思一下。我的建议是练习作业的最低目标应该是第二档第三档可以作为进阶追求但绝不能满足于第一档。第一档的练习做一百个给你的进步远不如把十个练习从第一档磨到第三档。2.2 一个反面案例A同学的数字转换作业说一个我印象比较深的例子。某位同学的练习是“把用户输入的人民币金额转换为大写形式”比如输入1234.56输出壹仟贰佰叁拾肆元伍角陆分。这位同学的代码功能测试完全通过输入各种数字都能正确转换。但我看完代码后觉得这份练习只能算“能跑”。问题出在哪呢第一他把所有逻辑都写进了唯一的一个function convertChineseMoney里面从处理整数部分到拼接小数部分全是一个接一个的if块和字符串拼接。第二里面充满了类似[零, 壹, 贰]这样的数组字面量以及2、10这种除了他自己没人知道含义的数字。第三他没有处理负数没有处理超大金额没有考虑用户输入0这种边界值。换句话说这份代码在“输入正常数字”这条路上走得很好但只要稍微偏离一点它就会给出错误结果或者干脆异常。后来我让他把数组提取成常量、把整数部分和小数部分的处理拆成两个函数、给每个函数加输入输出说明再补上对特殊输入的处理他只花了一个多小时就完成了。功能没有加一个但代码给人的信任感完全不一样了。这个案例想说明的就是练习作业的完成度不看功能列表有多长而看代码在边界条件下的存活率。功能写得多只能说明你花了时间边界处理做得好才能说明你有意识。2.3 如何判断自己的作业已经“到了第二档”如果你自己正在做练习想知道当前处于哪一档可以用一个简单的检查清单来自评每个函数是否只干一件事如果一个函数名里出现了“处理和转换”这种组合词汇它可能该拆了。函数名和变量名是否能在不看注释的情况下被理解如果a和arr1这种名字出现频率太高那就要警惕。重复的代码是否被抽出来了同一个逻辑如果写了三遍以上就必须抽成公共函数。处理输入的逻辑是否和业务逻辑分开了比如输入校验、格式清洗最好独立成一到两个函数而不是混在业务判断里。这五条不需要全部满足但满足得越多说明作业越接近“能看”这一档。每次写完练习花二十分钟过一遍这个清单比多写三个新练习有用得多。3. 练习里最容易翻车的基础角落从作用域到异步时序3.1 作用域和闭包只要绕进去一次就会绕进去很多次JavaScript的作用域是很典型的“看着简单、用起来全是坑”的知识。练习作业里最常见的翻车场景是循环里用var或let创建函数集合。比如写一个经典练习生成一个按钮列表点击第几个按钮就输出第几个序号。如果用的是var i点击后输出的永远是最后一个序号换成let i问题自然消失。我在帮人看这道题的时候经常发现一个情况学习者已经知道“用let代替var”这个结论却说不清楚为什么。这就是只看答案、不追原理造成的结果。let之所以能解决这个问题是因为它每次迭代都会创建一个独立的词法环境而var在整个函数作用域内共享一个变量绑定。用生活化的话说var像一块公用的白板大家往上面写数值后写的会覆盖先写的let像每个人手里发了一张独立便签各写各的互不干扰。所以我建议练习中一旦遇到作用域相关的题目不要只满足于把题目做对尽量自己解释一遍原理或者写一个小Demo验证自己的理解。只有当你能够对着一个陌生人把var和let在循环里的差异讲清楚这个知识点才算真正过手。闭包也是一样。我见过很多人的练习作业里写了一个类似“计数器自增”的功能用了闭包把状态藏在了一个内部函数里。功能是能跑的但如果问他“这个状态存在了哪里”“如果外层函数被调用两次状态会怎样”他往往说不上来。闭包的价值在于数据私有化和状态保持但代价是内存上的持续引用。练习里用一用是好事但必须搞清楚内部函数被外部引用之后原本的函数作用域为什么没有被垃圾回收掉。想通了这一点你的作用域理解才算真正“闭环”。3.2 this的指向问题写练习时最容易自我欺骗的地方this是JavaScript里另一个容易出问题的地方。很多练习作业用到了对象方法、事件回调、构造函数的组合但代码一复杂this的指向就变得扑朔迷离。一个常见的翻车案例是在对象方法内部设置了一个定时器定时器回调里要用到对象的一个属性于是直接用了this.xxx结果运行时告诉你undefined。问题出在定时器回调和事件回调里的this指向的是调用时的上下文而不是定义时的上下文。初学者最容易陷入的误区是“在哪个对象里写的this就指向哪个对象”——这个直觉是错误的。正确的思考方式是this的值取决于函数被调用时的方式而不是函数被定义时的位置。练习里遇到这个问题的正确处理方式不是死记“箭头函数没有自己的this”这种口诀而是想清楚为什么箭头函数能解决这个问题。箭头函数不绑定自己的this它会捕获定义时所处上下文的this值相当于把外层的this固定了下来。所以当你在对象方法里写一个箭头函数回调时回调里的this就是对象方法被调用时的this也就是这个对象本身。我见过好几种这样接近“背结论”的写法有人说“箭头函数就是方便”有人说“用bind(this)也行”但很少有人能解释清楚如果不用箭头函数用普通函数配合保存const self this本质上的区别是什么。弄明白这个问题之后你再去写事件监听、定时器、Promise链里的回调会踏实很多。3.3 异步时序练习里最容易“看似对了其实不对”的领域异步是JavaScript练习里最容易“假成功”的地方。比如一个练习是“请求一个模拟接口拿到数据后更新页面”。很多人的第一版代码是“请求发出后立刻在下一行更新页面”然后发现数据没出来接着改用then数据出来了于是他就认为“用了then就对了”。但实际上then只是把回调挂了上去代码的执行顺序并不是从上到下。很多人练了很久异步遇到面试题“打印顺序是什么先setTimeout后Promise的微任务还是反之”还是容易搞混。练习作业面对这种情况不能只满足于“结果对了”要尝试在浏览器控制台用console.log在每个关键环节打印一下顺序直观感受一下同步代码、微任务、宏任务之间的调度顺序。我自己做练习时比较喜欢加一个“自测日志”在练习文件里临时写几个console.log标记当前是第几步、数据状态是什么。跑一遍观察输出顺序再对照事件循环的机制去理解为什么是这个顺序。这个过程比做对题目更有价值因为它训练的是“调试思维”——你开始关心程序怎么运行的而不是只关心结果对不对。4. 环境、工具与可复现性练习作业里那堂隐形的工程课4.1 一份作业能跑不能跑先看环境描述是否完整很多JS练习卡住不是因为代码不会写而是因为环境不一致。举个最简单的例子你的练习作业是在浏览器里跑的那么是基于原生HTMLJS文件还是基于某个脚手架是在Node环境里跑脚本还是挂在某个页面里这些基础信息如果不写明别人拿过去很难复现。不少人的练习作业只有一个孤零零的JS文件没有说明依赖没有说明运行方式。你问他怎么跑他说“用浏览器打开就行”但他忘了自己的代码里用了ES Module直接用浏览器打开本地文件时会被安全策略拦截。这种“只能在我机器上跑”的练习恰恰暴露了工程上的不严谨。所以无论练习多小我强烈建议在项目里放一个README.md写清楚三件事这个练习是什么、环境需要什么、怎么运行。如果是Node脚本写明node index.js如果是浏览器效果写明需要在本地起一个静态服务或者直接说明用某个工具打开。这一行说明表面上看是给“别人”看的实际上是训练自己“以交付为标准”的思维方式。4.2 用package.json管理一个“小到只有两个文件”的练习有人觉得JS练习就那么一两百行代码根本不值得用package.json这一套。但我的实际经验恰恰相反从练习开始就给自己建立工程化的习惯哪怕是一个只有两个文件的练习也尽量用项目的方式去组织。你可以做这样一个最小化配置新建一个项目目录里面放一个index.js或src/main.js再放一个package.json。package.json里写上项目名、描述、脚本命令比如start: node index.js。如果练习需要用第三方包就在devDependencies里加依赖并固定版本。这个流程会让练习接近一个“可交付的微型项目”。一开始你可能觉得这是在浪费时间但当你拿着这份练习去给别人评审或者过几个月自己回看时你会发现“一条命令就能跑起来”之所以让人舒服是因为“可复现”本身就是工程上的核心诉求。养成这个习惯以后你后续做真正的项目时环境管理就不会觉得是额外负担而是顺手的事情。4.3 版本固定和“换台电脑还能跑”的分量我在带同学做练习时发现很少有人会把依赖版本写死。他们往往顺手npm install一个包装上然后用起来没问题就把这茬忘了。等到某天别人来运行这份作业发现新版库的接口变了代码报错于是卡在环境上明明写的算法没问题却跑不通。这里分享一个特别简单的实操建议在练习项目里给依赖固定版本或者如果完全不需要第三方依赖就尽量别引入。JavaScript练习的最大优势就是它往往可以是“零依赖”的纯标准库加纯JavaScript逻辑反而更能体现语言本身的功底。如果某个练习确实需要模拟数据、需要网络请求、需要某些库能力那就在package.json里锁定精确版本避免“意外升级导致作业瘫痪”。我在自己的练习项目里通常会额外加一个要求假设换一台电脑clone下来之后应该用一条命令安装依赖、一条命令跑起练习。如果做不到说明这份练习的“交付说明”还不够合格。这个标准听起来不高但能真正做到的人很少。4.4 把“输出可观察的结果”当作练习的一部分还有一点很有意思。许多练习作业在运行时不会输出任何带有“标志性”的结果——比如跑一个函数控制台静静退出返回值为undefined你根本不知道它对不对。这种作业写出来别说别人你自己过几天回看时都会呆住这玩意儿到底成功了吗我习惯把这类练习加一个“自测模式”或者“样例输出”在代码里预设几组输入把结果打印在控制台让人一眼看出“预期是什么、实际是什么、是否匹配”。这不仅是给读者看的更是给你自己的一种训练你在学习写代码就要开始学会“证明代码是对的”而不是默认它是对的。在练习里认真对待“输出可观察结果”你想要验证边界情况就也写在自测输出里。这样练习才算真正完整而不只是写了一堆自我感觉良好的函数。5. 练习做完以后复盘和沉淀决定了这份作业能给你留下多少东西5.1 一份练习做完至少要回答三个“为什么”我发现很多人在练习提交之后就立刻开始下一个练习。这个节奏看起来很勤奋但有一个问题练习带来的经验是“散装”的没有沉淀成自己的方法论。如果训练的目标是提升能力而不只是刷数量那你需要至少回答自己三个问题。第一个问题是为什么当时选择了这个方案而不是另一个比如写数组去重你可以用Set、可以用双重循环、可以用reduce为什么选了其中的一种是因为代码短因为性能好因为可读性强当你认真回答这个问题时你才会发现自己选择的依据是否成立。第二个问题是这道题有哪些“变种”值得警惕比如“数组去重”的进阶版本是“对象数组根据某个字段去重”再进阶一点是“大数据量下的去重性能优化”。你不一定需要全做一遍但要想一想现在这个写法能不能快速迁移到变种场景。第三个问题是这段代码里有没有哪几行是你“复制过来但没有完全理解”的很多练习代码里有大量从教程或AI对话里“拼”出来的片段功能跑通但自己并说不清楚每一行为什么存在。如果不揪出这些“不理解但能跑”的地方它们就会变成后续项目的定时炸弹。5.2 建立自己的“练习档案”从题目到答案到复盘我看过不少人的本地目录一堆以test1.js、demo1这种命名的文件躺在那里彼此之间没有关联也无法看出当时的思考过程。我后来自己调整了一套整理方式每个练习建一个文件夹目录里至少包含三样东西。第一样是“题目说明.md”把需求用自己的话写一遍包括输入、输出、边界条件。第二样是“思路.md”记录最初的解决思路以及碰到过的坑。第三样是代码本身。如果做了多轮重构再细分版本例如v1-simple.js、v2-refactor.js。这个整理过程会耗费一些时间但它能把一次练习从“做完就忘”变成“可检索的经验”。我甚至建议在“思路.md”末尾补一个“如果重新做一次”的清单。你会发现很多练习在刚写完时觉得“完美”过一个月再看你会列出三四条改进方向——这恰恰说明这段时间你在进步。如果没有这种存档进步是无法被量化的。5.3 从练习到博客/面试素材的转化路径练习作业还有一个容易被忽略的用途那就是它其实是最好的面试素材和博客素材来源。一个精心设计、边界完整、有复盘记录的练习远比简历上写着“熟悉JavaScript”更有说服力。你在面试时可以讲“我写过这样一个练习一开始我用双重循环去重后来意识到性能问题改成了哈希表并且封装了一层公共函数。”这种具体经验容易被记住。如果你有写技术博客的打算练习作业更是取之不尽的素材库。把练习题中关于闭包、作用域、异步时序、边界处理的踩坑整理成文字附上代码和运行结果就是一篇对同行有用的内容。我在不少技术社区看过这类文章印象深刻的往往不是那些讲大框架的教程而是作者记录自己如何从“能跑”走到“能看”、如何发现自己的第一次低效实现。5.4 关于“二刷练习作业”的一点个人体会最后聊一个我自己的习惯我会定期“二刷”以前的练习。所谓二刷不是把之前的代码背下来重写一遍而是打开当时的文件夹只看题目和思路记录不看代码然后从头再写一遍。写完之后对比旧版看看这次是不是会有不同的设计选择是不是能把边界处理做得更完整。这个过程特别有意思。有时候我会发现自己当年觉得“已经很优雅”的写法现在看其实可以用更简单的逻辑替代有时候又会发现现在的写法虽然更规范但当年某个“笨办法”里的一些直觉其实是对的。这种对照练习带来的进步常常比做新题更明显因为它让你同时看见两个时间点的自己。我自己的周期大概是一批练习做完过一到两个月再挑两三个重点的二刷。每次花的时间不长但收获远超预期。如果你现在手里正有一批写完的练习别急着把它们丢进回收站试着这样回看一次或许你对自己的成长速度会有一个比较直观的感知。这比继续闷头写新题目要划算得多。