资讯动态

JavaScript for循环星星模型教程:从嵌套循环到调试技巧

发布时间:2026/10/23 13:58:25 来源:尧图企业网站定制
写代码这几年我被问得最多的一句话就是“for 循环我好像看懂了但一到自己写就懵。” 每次遇到这种情况我都会让对方去用 for 循环在控制台打印星星模型。一个由星号和空格拼出来的三角形、菱形看着不起眼实际上把循环该考的东西全考了一遍外层循环管行、内层循环管列、循环变量怎么参与计算、边界条件怎么卡、字符串怎么拼接。所以这篇我就把平时最常练的几类星星模型从易到难拆开讲顺便把新手写循环时最容易翻车的几个点一并整理出来。不管是刚开始学 JavaScript 的人还是想找循环练习给新人练手的照着敲一遍应该会有收获。1. 先搞清楚 for 循环和星星模型之间的关系1.1 为什么偏偏用 for 循环画星星很多语言教程在讲循环时都绕不开星星图形JavaScript 也一样。for 循环天生适合“做固定次数”的事情而画星星模型本质上就是“有确定行数、有确定列数”的重复输出。比如我要画五行星星每行五颗这就是 5 次外层循环每次外层循环里再做 5 次内层循环。整个过程和数格子一模一样。for 循环的语法长这样for (let i 0; i 5; i) { // 循环体 }这里有一个新手容易忽略的细节i 从 0 开始到 i 5 成立i 依次取 0、1、2、3、4一共执行 5 次。你要习惯这种“半开区间”写法也就是用小于号而不是小于等于号。很多星星图形多一行少一行问题就出在把 i 5 写成了 i 5。画星星的时候外层循环变量 i 通常代表行号内层循环变量 j 通常代表列号。所谓“模型”就是行和列之间满足什么样的大小关系。你把行号列号当作坐标星星图形就是一堆符合条件的坐标点。1.2 先把“画纸”准备好控制台输出方式在浏览器控制台和 Node.js 环境中最直接的输出方式是 console.log。但 console.log 有一个脾气每次调用都会自动换行。所以你不能天真地这样做for (let i 0; i 5; i) { console.log(*); }这段代码会输出五颗星星但每颗独占一行根本拼不成一行五颗的效果。正确思路是先准备好一个字符串变量比如叫 row在内层循环里不断追加星星等这一行拼完了再 console.log 一次。let row ; for (let i 0; i 5; i) { row *; } console.log(row); // 输出 *****这个“先拼一行再输出一行”的习惯是所有星星模型的基础。后面的三角形、金字塔、菱形不管图形多复杂采用的都是一样的思路每一行都是一个字符串内层循环负责把这一行拼好外层循环负责把每一行打印出去。1.3 嵌套循环的核心心法外层管行内层管列有了单行输出的基础接下来就是嵌套循环。先看一个最朴素的例子打印一个五行五列的星星矩形function printRectangle(rows, cols) { for (let i 0; i rows; i) { let row ; for (let j 0; j cols; j) { row *; } console.log(row); } } printRectangle(5, 5);你仔细观察这段代码的执行顺序外层循环 i 等于 0 时进入内层循环内层循环 j 从 0 到 4 各执行一次把 5 颗星星拼到 row 上然后 console.log 输出第一行接着外层 i 变成 1重新定义一个新的空 row再走一遍内层循环输出第二行。这里有个特别关键的细节row 的定义必须放在外层循环里面。如果把 row 放在外层循环外面比如写成这样let row ; for (let i 0; i 5; i) { for (let j 0; j 5; j) { row *; } console.log(row); }后果是第一行输出 5 颗星第二行输出 10 颗星第三行输出 15 颗星因为 row 一直在累积没有被清空。我见过很多新手卡在这个问题上所以在这里先强调一遍每次开始新的一行row 都要重新初始化为空字符串。2. 从基础图形开始矩形与三角形2.1 矩形模型把行列数变成参数矩形是所有图形里最友好的因为它不涉及任何行列关系内层循环的次数固定外层循环的次数也固定。上面的 printRectangle 函数已经把矩形封装好了。这里我建议把行数和列数设计成函数参数而不是写死在代码里function printRectangle(rows, cols) { for (let i 0; i rows; i) { let row ; for (let j 0; j cols; j) { row * ; } console.log(row); } } printRectangle(4, 8);我习惯在星星之间加一个空格也就是每颗星后面跟一个空格这样输出到控制台以后看起来更像网格。如果想要星号紧挨着就把加号后面的空格去掉。做这种小图形时加不加空格纯看个人审美关键是理解循环结构。写完这个函数后你可以试着改变 rows 和 cols 的值观察输出变化。参数化思维很重要后面所有图形都会做成函数传不同的 n 就能得到不同尺寸。2.2 直角三角形模型内层循环次数跟着外层变量走矩形是固定列数直角三角形就不一样了。它每一行的星星数量都不同第 0 行 1 颗第 1 行 2 颗第 2 行 3 颗依此类推。也就是说内层循环的结束条件要跟外层循环变量 i 挂钩。function printRightTriangle(n) { for (let i 0; i n; i) { let row ; for (let j 0; j i; j) { row *; } console.log(row); } } printRightTriangle(5);输出效果* ** *** **** *****这里最容易被坑的是内层条件写成 j i。我们推导一下当 i 等于 0 时如果条件是 j 0那么 j 从 0 开始0 0 不成立内层循环一次都不执行第一行就空了这显然不对。当 i 等于 0 时我们希望输出 1 颗星星所以要满足 j 0也就是条件写成 j i或者写成 j i 1。记住这个加 1 的转换在很多图形推导里你会反复用到。2.3 反向三角形模型两种思路都可以反向三角形输出效果是倒过来的直角三角形第一行 n 颗星最后一行 1 颗星。实现方式有两种我都列出来你感受一下哪种更适合自己的思维习惯。第一种外层循环 i 依然从 0 递增内层循环的次数用 n - i 控制function printInvertedTriangle(n) { for (let i 0; i n; i) { let row ; for (let j 0; j n - i; j) { row *; } console.log(row); } }当 n 等于 5 时i 等于 0内层执行 5 次i 等于 1内层执行 4 次最后 i 等于 4内层执行 1 次。效果完全正确。第二种外层循环 i 直接从 n 递减到 1function printInvertedTriangle(n) { for (let i n; i 0; i--) { let row ; for (let j 0; j i; j) { row *; } console.log(row); } }两种写法输出结果一样。我个人更喜欢第一种因为所有变量的变化方向最直觉行号往下走列数跟着减循环边界不需要在脑子里“倒带”。做图形推导时你只要记住一点内层循环的次数等于当前行希望输出的星星个数具体用 i、n-i、还是 i1 表示画一张小表马上就能看出来。2.4 数字模型和字母模型循环变量不只是用来控制次数把星星换成数字图形突然就变成了另一种练习题也能帮你建立“循环变量不只会控制次数还能作为输出内容”的意识。比如打印这样的三角形1 22 333 4444 55555代码很简单只需要把拼接的星号换成 i 1function printNumberTriangle(n) { for (let i 0; i n; i) { let row ; for (let j 0; j i; j) { row i 1; } console.log(row); } } printNumberTriangle(5);同理想打印字母三角形就把内容换成 String.fromCharCode(65 i)65 是字母 A 的 Unicode 码i 是当前行号function printLetterTriangle(n) { for (let i 0; i n; i) { let row ; for (let j 0; j i; j) { row String.fromCharCode(65 i); } console.log(row); } }这个拓展的价值在于循环内部拼什么内容完全由你的业务决定。图形只是载体真正训练的是你对循环变量 i、j 的敏感度。3. 金字塔、空心图形与菱形3.1 金字塔模型空格和星号的完美配合直角三角形只需要考虑星星数量金字塔则多了一个角色空格。一个标准的金字塔每一行由“空格 星号”组成而且空格数和星号数都跟行号有关。假设总行数是 n第 i 行i 从 0 开始前面的空格数量是 n - i - 1星号数量是 2 * i 1为什么星号数量是 2 * i 1因为金字塔是对称的第一行 1 颗之后每往下走一行左右各多一颗所以每行多 2 颗。推导出来就是 2*i 1。代码function printPyramid(n) { for (let i 0; i n; i) { let row ; for (let j 0; j n - i - 1; j) { row ; } for (let j 0; j 2 * i 1; j) { row *; } console.log(row); } } printPyramid(5);输出效果* *** ***** ******* *********这里要注意循环顺序必须先拼空格再拼星号。你想想金字塔的样子星星往右凸出左边留白如果先拼星号再拼空格控制台会把星号顶到最左边空格全跑到右边去图形就变成另一个样子了。控制台看不到每一行的尾部所以很多人犯这个错后还挺困惑觉得自己明明加空格了怎么图形还是不对。调试的时候可以把空格换成下划线比如 row _这样能直观看到每一行前面有多少个占位符排完错再改回来。这个技巧后面还会细说。3.2 空心金字塔模型学会画“边界”实心金字塔会了空心金字塔就是只画轮廓不填充内部。第 i 行的星号数量还是 2 * i 1但除了最后一行外每一行只在第一个位置和最后一个位置放星星中间全部放空格。function printHollowPyramid(n) { for (let i 0; i n; i) { let row ; const starCount 2 * i 1; for (let j 0; j n - i - 1; j) { row ; } for (let j 0; j starCount; j) { if (i n - 1 || j 0 || j starCount - 1) { row *; } else { row ; } } console.log(row); } } printHollowPyramid(6);这段代码的关键在于边界判断。i n - 1 表示最后一行最后一行要全部用星号封底j 0 表示当前行的第一个星号j starCount - 1 表示当前行的最后一个星号。中间的部分用空格填充。空心图形训练的是 if 和循环的配合能力。在二维网格里你要能用某个点的“坐标” i 和 j 判断出它是否落在边界上。这个思路在后续做游戏地图、处理二维数组时都会用到。3.3 菱形模型拆成上下两半别想一口气画完菱形是最容易让人望而生畏的图形但它的本质很简单上半部分是正金字塔下半部分是倒金字塔。唯一需要注意的是中间那一行不能重复。假设菱形总共有 2n - 1 行那么上半部分用 n 行正金字塔下半部分用 n - 1 行倒金字塔。function printDiamond(n) { // 上半部分正金字塔共 n 行 for (let i 0; i n; i) { let row ; for (let j 0; j n - i - 1; j) { row ; } for (let j 0; j 2 * i 1; j) { row *; } console.log(row); } // 下半部分倒金字塔共 n - 1 行 for (let i n - 2; i 0; i--) { let row ; for (let j 0; j n - i - 1; j) { row ; } for (let j 0; j 2 * i 1; j) { row *; } console.log(row); } } printDiamond(5);输出效果* *** ***** ******* ********* ******* ***** *** *下半部分循环为什么从 n - 2 开始因为正三角已经输出了中间那一行也就是最大的一行。如果下半部分从 n - 1 开始中间那一行就会被输出两次菱形中间会出现一条明显的“胖”线。你可以自己试一下把从 n - 2 改成从 n - 1 开始输出结果会让你立刻理解边界的重要性。菱形还可以推广成空心菱形思路和空心金字塔一样把每一行的首尾和最后一行作为边界即可。这里我就不单独贴代码了你可以先尝试自己改。3.4 沙漏模型把菱形反过来沙漏本质上就是先把倒金字塔放前面再把正金字塔放后面。理解了菱形沙漏几乎不需要新知识function printHourglass(n) { // 上半部分倒金字塔 for (let i 0; i n; i) { let row ; for (let j 0; j i; j) { row ; } for (let j 0; j 2 * (n - i) - 1; j) { row *; } console.log(row); } // 下半部分正金字塔少一行 for (let i n - 2; i 0; i--) { let row ; for (let j 0; j i; j) { row ; } for (let j 0; j 2 * (n - i) - 1; j) { row *; } console.log(row); } } printHourglass(5);写到这里你应该已经发现规律了不管图形怎么变核心步骤都是先算空格数再算星号数然后套两层 for 循环。真正需要你动脑筋的是每一行空格和星号关于 i 的表达式。把这些表达式想明白图形就成功了一大半。4. 把星星模型玩出花样坐标思维、函数封装与页面渲染4.1 X 形星星和十字星开始用坐标判断前面几类图形都是“前面若干空格后面若干星星”属于堆砌型拼图。但如果你把整个图形看成一个二维网格用 i 表示行坐标、j 表示列坐标就能画出很多更有意思的图案。比如画一个大写的 Xfunction printX(n) { for (let i 0; i n; i) { let row ; for (let j 0; j n; j) { if (i j || i j n - 1) { row *; } else { row ; } } console.log(row); } } printX(7);这段代码的逻辑是行号等于列号也就是 i j代表从左上到右下的对角线i j n - 1 代表从右上到左下的另一条对角线。两条对角线上放星号其余位置放空格X 就出来了。同理十字星只需要找到中心点让中心点所在的行和列都放星号function printCross(n) { const center Math.floor(n / 2); for (let i 0; i n; i) { let row ; for (let j 0; j n; j) { if (i center || j center) { row *; } else { row ; } } console.log(row); } } printCross(7);这种“坐标判断法”比单纯数空格更灵活。很多字符画、心形图案、网格地图都是这个思路只是判断条件更复杂。等你熟练了可以尝试画空心菱形、五角星轮廓都是同一个套路。4.2 封装成通用函数一套代码管理多种模型写了这么多 print 函数后你会发现自己写了大量重复的循环结构。这时候就可以把“打印星星模型”这件事抽象成一个通用函数。比如用配置参数指定模型类型和大小function printStarModel(type, size) { switch (type) { case rectangle: printRectangle(size, size); break; case triangle: printRightTriangle(size); break; case pyramid: printPyramid(size); break; case diamond: printDiamond(size); break; case x: printX(size); break; default: console.log(暂不支持该模型); } } printStarModel(diamond, 7);这种做法非常适合给新人做练习工具或者用来快速验证不同尺寸下图形是否正常。在实际项目里这种“分发”结构也很常见它把调用方和实现细节解耦后面想加新模型只需要在 switch 里加一个 case。4.3 把计算和打印分开用二维数组存星星前面所有函数都是边拼字符串边 console.log。这个方式在图形简单时没问题但如果后续要对图形做旋转、镜像、拼接直接打印就很麻烦。更好的做法是先用二维数组把图形存下来最后一次性渲染。下面的代码创建一个二维数组然后把金字塔的星星坐标填充进去function createPyramidGrid(n) { const grid []; for (let i 0; i n; i) { const row new Array(2 * n - 1).fill( ); const start n - i - 1; const end n i - 1; for (let j start; j end; j) { row[j] *; } grid.push(row); } return grid; } const pyramid createPyramidGrid(5); console.log(pyramid.map(row row.join()).join(\n));new Array(2 * n - 1) 创建了一个长度为 2n - 1 的数组并全部填充为空格。每一行从 start 到 end 的位置放星号。最后用 map 把每个数组元素拼接成字符串再用 join(\n) 拼接成最终的图形文本。这种做法的好处是图形在内存中是一个可以继续操作的数据结构。你想镜像、旋转甚至逐帧生成动画都是在二维数组上做数学变换而不是重新拼字符串。等以后用 canvas 画星星、做小游戏这种网格思维会非常有用。4.4 从控制台到网页用 pre 标签在页面上显示星星如果你想把星星模型放到网页上展示直接使用 innerText 往元素里塞带换行符的字符串会遇到一个问题浏览器默认会合并连续空格和换行金字塔会变成一堆乱七八糟的星号。解决方案是使用 pre 标签或者给普通元素加 CSS white-space: pre。因为星星图形依赖空格对齐必须让浏览器保留这些空白字符。pre idstarBox/pre script function createPyramidText(n) { const lines []; for (let i 0; i n; i) { let row ; for (let j 0; j n - i - 1; j) { row ; } for (let j 0; j 2 * i 1; j) { row *; } lines.push(row); } return lines.join(\n); } document.getElementById(starBox).innerText createPyramidText(8); /script这里把打印逻辑改成了返回字符串数组再通过 join(\n) 合成带换行的字符串最后用 innerText 赋值。虽然只是把 console.log 改成了 return但这一步是由“控制台练习”进入“网页应用”的关键跨越。你也可以进一步把每个星星变成一个 span 元素设置不同颜色和大小做一个真正像夜空一样的星星模型页面。5. 常见问题与排查技巧实录5.1 图形多一行少一行边界条件必背口诀写 for 循环画星星最常见的错误就是多一行或者少一行。比如想输出 5 行条件却写成 i 5结果输出 6 行想输出第 i 行有 i1 个星条件却写成 j i结果第一行就空了。我的建议是每一段循环都先拿前两行做手算推演。比如 n 5外层条件是 i 5那么 i 取 0、1、2、3、4恰好 5 行。如果你看到“最后一行没有输出”或“最后多了一行”优先检查小于号和小于等于号。下面的速查表可以帮你快速对照期望写法 1写法 2行号从 0 开始共 n 行i ni n - 1行号从 1 开始共 n 行i ni n 1当前行输出 i 1 个星星j ij i 1当前行输出 n - i 个星星j n - ij n - i - 1这些不是要你背下来而是当你发现图形不对时照着这个思路去检查很快就能定位问题。5.2 图形歪了空格和星号顺序反了金字塔、菱形这类图形空格必须加在星号前面。有人会想“我明明加了空格为什么图形看起来左边还是齐的”因为控制台每一行按从左到右输出如果先输出星号再输出空格那么星号都在最左边空格全在行尾而终端很难看出行尾发生了什么。结果整个图形就没有金字塔的错落感。排查办法很直接把空格替换成下划线或者某个可见字符。假设你打印金字塔时用 _ 代替空格____* ___*** __***** _******* *********这样你就一眼能看出每行前面的占位符够不够。发现问题后把下划线改回空格即可。这个方法我到现在还在用尤其是调试复杂字符画的时候可视化占位符能救命。5.3 变量污染忘记重置行字符串前面已经提到过row 必须在外层循环内部初始化为空字符串。但还有一种更隐蔽的错误内层循环的计数变量和外层循环变量同名。for (let i 0; i 5; i) { for (let i 0; i 5; i) { // ... } }在 var 时代重复声明同名变量会相互覆盖导致外层循环被内层循环干扰这是一个非常经典的坑。现在用 let 声明可以避免一部分问题因为内层 i 会遮蔽外层 i但为了可读性绝对不建议内外层使用同名变量。我习惯外层用 i内层用 j如果还有第三层就用 k这已经是约定俗成的做法。5.4 网页上显示不对空格被浏览器吃掉很多人在控制台写得好好的星星图形一放到网页上就变形原因在前面的 pre 标签部分已经解释过。这里再补充一个判断经验如果你的页面元素里存了字符串检查一下元素样式是否设置了 white-space: pre。没有这个样式HTML 会把连续空格合并成一个金字塔自然就塌了。pre { white-space: pre; }如果不用 pre 标签给任意 div 加上这个样式也可以。还有一点使用 innerText 赋值时换行符 \n 会被保留但如果你用 innerHTML换行和空格同样是可能被折叠的。简单来说展示文本就用 innerText不要用 innerHTML。5.5 性能问题字符串拼接 versus 数组 join画星星模型规模一般很小性能差异可以忽略。但如果哪一天你想生成一个 1000 行的大星星图形频繁使用 row * 会在内存中反复创建新的字符串对象。JavaScript 字符串是不可变的每次拼接都生成一个新字符串旧字符串等待垃圾回收。这种情况下用数组收集元素最后 join 是更稳妥的做法。function createLargeTriangle(n) { const result []; for (let i 0; i n; i) { const line []; for (let j 0; j i; j) { line.push(*); } result.push(line.join()); } return result.join(\n); }对初学者而言不需要过度纠结性能但要建立这个意识凡是循环里不断增长的字符串都可以考虑用数组代替。先保证正确再谈优化。5.6 我的调试习惯先写一行再写全部每当我需要画一个新的星星模型我不会直接写完整的两层循环。我会先写一个 buildLine(spaces, stars) 函数专门负责生成一行内容function buildLine(spaces, stars) { return .repeat(spaces) *.repeat(stars); } console.log(buildLine(4, 1)); // 金字塔第一行前面 4 个空格 console.log(buildLine(3, 3)); console.log(buildLine(2, 5));先用几个固定参数验证 buildLine 是否正确再把 buildLine 放进外层循环把空格数和星号数替换成表达式。这样做的好处是如果最终图形出问题你能立刻确定是 buildLine 出错还是外层循环的行数计算出错不用对着两三层嵌套的代码发呆。这个习惯也适用于任何更复杂的逻辑。写一个几十行的函数前先拆出一个纯函数测一测能省掉大量排查时间。星星模型虽小但它真的能帮你养成好的编码习惯。后面你再去做冒泡排序的可视化、canvas 画图或者页面里的动态星空背景都会发现循环加坐标的思路是一样的。把基础图形吃透后面的路会顺很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑