news 2026/9/28 9:13:52

JS4阶段进阶指南:从语法到全栈开发的关键跨越

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS4阶段进阶指南:从语法到全栈开发的关键跨越

最近把全栈开发的学习推进到 JS_4 这个阶段,说是"学习",其实更像一次从"跟着教程抄示例"到"按需求写出能用的东西"的过渡。JavaScript 学到这个份上,很多人会有一种感觉:知识点都见过,但真要一个人从头写个小工具、处理一段真实数据,脑子还是发懵。这一篇我想把 JS_4 阶段最该练的东西梳理一遍,结合我自己的踩坑经历,讲清楚字符串判断、数组操作、函数作用域、事件循环这几块内容到底怎么串起来用,以及怎么把它们落进一个完整的练习项目里。

这个阶段适合两类人参考:一类是刚学完基本语法、准备往全栈方向走的初学者,另一类是写过一些前端页面但总觉得 JS 基础不扎实、想回头补课的同学。我不打算面面俱到地把所有 API 罗列一遍,那跟查文档没什么区别;我更想说的是"什么时候用哪个、为什么这么用、踩了哪些坑之后才明白"。

1. JS_4该练什么:从"跟着抄"到"自己写"的核心清单

1.1 为什么这个阶段是全栈开发的分水岭

我见过很多同学"JavaScript 教程刷完了",循环会写、函数会写,示例也能跑通,但一放到真实场景里就卡住。原因很简单:教程的示例都是"喂到嘴边"的,比如它告诉你includes可以判断字符串包含,但不会告诉你后端返回的字符串可能带空格、大小写不一致、换行符藏在里面;教程告诉你map可以遍历数组,但不会告诉你接口数据里有些字段可能是空的,遍历之前得先过滤。

全栈开发的特点恰恰在于,你要同时处理"数据怎么来""数据怎么校验""界面怎么展示""用户操作后怎么更新状态"这一整条链路。任何一环脱节,页面就会白屏、接口就会报错、数据就会对不上。JS_4 这个阶段真正要练的,不是再背十个新方法,而是把前面零散学过的语法,变成一套能应对真实脏数据的思维习惯。

1.2 我给自己列的阶段清单

我把这个阶段拆成了五个核心能力,每一项都对应实际开发中一定会遇到的场景:

核心能力对应的真实场景涉及的知识点
字符串处理校验用户输入、清洗后端返回的数据includes、trim、toLowerCase、slice
数组操作列表渲染、数据筛选、去重排序sort、map、filter、forEach、Set
控制流设计表单校验、权限判断、多分支逻辑if/else、switch、提前返回
函数与作用域抽公共方法、避免变量污染闭包、执行上下文、this
异步思维请求接口、定时任务、链式调用事件循环、Promise、async/await

这五块不是并列地学,而是有先后逻辑的。前面两块属于"处理数据",中间两块属于"组织代码",最后一块属于"理解运行时"。顺序走下来,一个人的 JS 才算真正能从"语法"上升到"工程"。

我自己当时练的方法是:不看教程,找一个实际的小需求硬写。比如"把接口返回的用户列表按年龄排序、过滤掉没有手机号的、再拼成页面",写不出来就翻文档,翻完再写。这个过程虽然慢,效果却比看十节视频课都好。

2. 字符串判断与处理:日常编码里最容易写顺手的一组能力

2.1 判断字符串是否包含:includes 与 indexOf 的选择

"JS 判断字符串是否包含"这个需求,前端和后端都会遇到。在 Node.js 里判断请求参数、在前端判断当前页面路径,都绕不开它。最常见的两个 API 是includes和indexOf。

const title = '前端全栈开发实战手册'; console.log(title.includes('全栈')); // true console.log(title.indexOf('全栈')); // 2 console.log(title.indexOf('后端')); // -1 console.log(title.includes('后端')); // false

二者都能判断"包不包含",但返回值不一样:includes返回布尔值,indexOf返回首次出现的索引,没找到返回 -1。在实际开发里,我的习惯是:只关心"有没有"就用includes,代码读起来更直白;需要知道"在哪个位置"或者要做字符串截取时,才用indexOf。

这里有个要注意的地方:includes和indexOf对空字符串的处理。'abc'.includes('')返回true,因为任意字符串都包含空字符串。如果你在写输入框过滤逻辑,用户可能什么都不填,这个特性容易造成误判,建议先判断输入值是否为空。

2.2 忽略大小写:先统一再比较

搜索热词里有一条是"js 忽略大小写",这个需求太真实了。用户输入验证码、邮箱、文件名、URL 路径时,大小写不一致几乎是必然的。includes本身是区分大小写的,所以常规做法是先统一成小写或大写再比较。

function containsIgnoreCase(source, target) { return source.toLowerCase().includes(target.toLowerCase()); } console.log(containsIgnoreCase('HelloWorld', 'hello')); // true console.log(containsIgnoreCase('helloWorld', 'WORLD')); // true

toLowerCase()是推荐做法,比toUpperCase()更稳,因为少数语言里大小写转换规则不对称。如果是在比较两个字符串是否相等,可以像上面这样先转换再比;但如果你要判断的是英文单词边界,比如abc不要匹配abcd,那includes就不够用了,得用正则表达式加i标志:

function containsWord(source, target) { const regex = new RegExp(`\\b${target}\\b`, 'i'); return regex.test(source); } console.log(containsWord('hello world', 'Hello')); // true console.log(containsWord('hello world', 'wor')); // false

虽然正则表达式的语法看着吓人,但处理"忽略大小写 + 单词边界"这类需求,它是最优雅的方案。记住\\b表示单词边界,i表示忽略大小写,这个组合在很多场景都适用。

2.3 字符串里的"隐形坑":空格、换行与中文

字符串判断出错,大多数时候不是方法选错了,而是"数据本身不干净"。后端返回的字段可能带首尾空格,用户粘贴进来的内容可能带着换行符,这些肉眼很难发现,但比较时就会出问题。

const userInput = ' 张伟 '; const savedName = '张伟'; console.log(userInput === savedName); // false console.log(userInput.trim() === savedName); // true console.log(userInput.trim().includes(savedName)); // true

所以我在项目里定了一个简单规则:所有从外部进来的字符串,先trim()再去参与判断和存储,能省掉大量莫名其妙的 bug。

中文字符在 JS 里的处理和英文没有本质区别,length直接按字符数计数,slice、substring也都能用。唯一的提醒是:如果要对中文做排序,默认的sort()是按 Unicode 编码排的,结果大概率不符合拼音顺序,需要用到localeCompare('zh-CN'),后文数组部分会细说。

3. 数组操作实战:排序、合并、去重和循环中断

数组是前端开发里操作频率最高的数据结构,后端响应数据结构化之后也大多是数组。这块练好了,全栈开发一大半的"数据处理"就算拿下了。

3.1 排序:sort 的默认行为是个大坑

很多人第一次用sort()就栽了跟头,因为 JavaScript 的sort默认把元素转成字符串再按 Unicode 顺序排。看这段代码:

const nums = [10, 5, 80, 2]; console.log(nums.sort()); // [10, 2, 5, 80]

按直觉应该输出[2, 5, 10, 80],结果实际是[10, 2, 5, 80]。原因就是字符串比较时,"10"排在"2"前面。正确的数字排序必须传一个比较函数:

const nums = [10, 5, 80, 2]; console.log(nums.sort((a, b) => a - b)); // [2, 5, 10, 80]

比较函数返回负数表示 a 排在 b 前,正数表示 a 排在 b 后。所以(a, b) => a - b是升序,(a, b) => b - a是降序。排序对象数组时也是同样道理:

const users = [ { name: '小明', age: 30 }, { name: '小红', age: 22 }, { name: '小刚', age: 25 }, ]; users.sort((a, b) => a.age - b.age); // 按年龄升序:小红、小刚、小明

数组排序还有个细节:sort()是原地排序,会修改原数组。如果之后还要用到原始顺序,记得先[...arr]拷贝一份再排。

3.2 合并数组:concat、展开运算符与扁平化

合并数组有多种写法,我推荐展开运算符,可读性最高,也最灵活:

const list1 = [1, 2, 3]; const list2 = [3, 4, 5]; const merged = [...list1, ...list2]; console.log(merged); // [1, 2, 3, 3, 4, 5]

concat也能实现同样的效果,但展开运算符可以在多个数组合并时做到"想加什么就在哪加",比如在中间插入一个数组:[...list1, ...extraList, ...list2],concat写起来就绕一些。

还有一种常见的场景是"数组套数组"——比如接口返回了按月份分组的数据,你要拿到所有元素。这就是数组扁平化:

const monthly = [ [1, 2, 3], [4, 5], [6], ]; console.log(monthly.flat()); // [1, 2, 3, 4, 5, 6]

flat()默认只展开一层,如果嵌套更深需要传数字参数,比如flat(Infinity)会展开所有层级。日常开发里其实很少有人会写超过两层的嵌套,知道flat()能解决 90% 的情况就够了。

3.3 去重:Set 是首选方案

数组去重在笔试和日常工作里都经常出现。最简单可靠的方式是利用Set的元素唯一性:

const list = [1, 2, 2, 3, 4, 4, 4]; const unique = [...new Set(list)]; console.log(unique); // [1, 2, 3, 4]

这里把数组传入Set构造函数,重复值自动被丢弃,再用展开运算符转回数组。缺点是只能处理基本类型——对象去重不能用Set,因为每个对象引用都不同。如果要对对象数组按某个字段去重,可以用Map或filter配合find实现,但这属于进阶场景,先不做展开。

还有一种常见的去重需求是"字符串按逗号分隔后再去重"。我踩过坑的地方在于:如果原始字符串里有的地方带空格(比如"a, b, a"),直接按逗号split(',')会留下" b"而不是"b"。所以一定要先拆分、再trim()、最后去重:

const raw = 'apple, banana, apple,orange'; const cleaned = [...new Set( raw.split(',').map(item => item.trim()) )]; console.log(cleaned); // ['apple', 'banana', 'orange']

3.4 forEach 能"打断"吗:不能,但可以换个思路

搜索热词里有一条"js foreach 打断",这个问题几乎每个写 JS 的人都会撞上。直觉上,我们希望在遍历到某个元素时提前结束循环,像for里的break那样。但forEach没有break语法,在里面写return也只是跳过当前这一次迭代,不会停止整个循环。

比如找第一个大于 3 的数字:

const list = [1, 2, 3, 4, 5]; // 错误示范:return 只是跳过当前这一轮 list.forEach(item => { if (item > 3) { // 你以为循环结束了,其实后面还在跑 } });

真正想要"提前终止",最简单的是直接用for...of:

let found = null; for (const item of list) { if (item > 3) { found = item; break; } } console.log(found); // 4

如果你确实需要在"类 forEach 的回调风格"里实现中断,也可以借助some()或every():在回调里返回true(some)或返回false(every)来提前结束遍历。不过我用下来还是觉得for...of最直观。这个细节非常重要,因为很多新手在写异步流程时也想让forEach配合await顺序执行,结果完全没生效,后面事件循环的部分我会专门讲。

4. 条件与循环:把 if 套娃改造成可读的控制流

4.1 用"提前返回"替代深层嵌套的 if

写业务逻辑时,最常见的问题就是越写越嵌套。比如一个下单逻辑,先判断用户是否登录,再判断是否有商品,再判断余额是否够,层层if下来,代码缩进越来越深,阅读成本直线上升。

这种场景我推荐"提前返回"(Guard Clause)写法:

// 嵌套写法:一旦逻辑变多就很难读 function processOrder(order, user) { if (user) { if (order) { if (order.items.length > 0) { return doPay(user, order); } else { return '订单里没有商品'; } } else { return '订单不存在'; } } else { return '用户未登录'; } } // 提前返回写法:每个异常先走,剩下的自然就是正常流程 function processOrder(order, user) { if (!user) return '用户未登录'; if (!order) return '订单不存在'; if (order.items.length === 0) return '订单里没有商品'; return doPay(user, order); }

这两种写法逻辑完全等价,但可读性天差地别。提前返回的核心思想是:先把所有异常情况"挡在外面",主流程就不会被else打断。这是我在代码评审里最常提的一条建议,对新手尤其有用。

4.2 switch 的适用场景与容易忽略的穿透问题

if / else是万能的,但有些场景用switch更清晰。典型场景是"一个变量对应多种离散值",比如把百分制分数转成等级:

function getGrade(score) { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 60) return 'C'; return 'D'; }

这里用if没问题,因为分数是连续范围。但如果是"不同的订单状态字段对应不同文案",switch就很好看:

function getStatusText(status) { switch (status) { case 'pending': return '待支付'; case 'paid': return '已支付'; case 'shipped': return '已发货'; case 'done': return '已完成'; default: return '未知状态'; } }

用switch时最容易踩的坑是"穿透"——如果不写break或return,匹配到某个case后会继续往下执行。我曾经在代码评审里见过一个案例:有人拿switch做了权限判断,忘记在每个case后面写return,结果"普通用户"匹配之后把管理员逻辑也跑了一遍。如果是有返回值的写法,建议每个case都写成return xxx;,避免穿透导致的隐性 bug。

4.3 循环里容易忽略的两个细节

除了forEach不能break之外,循环还有两个高频坑值得单独说。

第一个坑是在遍历的同时删除了数组元素。比如你想把数组中值等于 4 的元素全部删掉:

const list = [1, 2, 3, 4, 5, 4]; for (let i = 0; i < list.length; i++) { if (list[i] === 4) { list.splice(i, 1); } } console.log(list); // [1, 2, 3, 5]? 不对,是 [1, 2, 3, 5, 4]

问题在于splice会让后面的元素往前挪,而i已经自增了,于是第二个 4 被漏掉了。更安全的做法是用filter生成新数组,或者从后往前遍历。

第二个坑是使用for...in遍历数组。for...in是为对象设计的,它遍历的是"可枚举属性名",用在数组上会拿到字符串形式的索引,而且可能包含一些意外属性:

const arr = ['a', 'b']; arr.custom = 'c'; for (const key in arr) { console.log(key); // "0"、"1"、"custom" }

看到那个"custom"了吗?这是for...in最容易坑人的地方。遍历数组请优先用for...of,它遍历的是元素值,更符合直觉。

5. 函数背后的作用域、执行上下文与 this 指向

5.1 函数声明与函数表达式:var 的"幽灵"

函数有两种写法:函数声明和函数表达式,它们最本质的区别是提升(hoisting)行为不同。

console.log(add(1, 2)); // 3 function add(a, b) { return a + b; } console.log(addFn(1, 2)); // TypeError: addFn is not a function var addFn = function (a, b) { return a + b; };

函数声明会被整体提升——整个函数在代码执行前就存在,所以可以在声明前调用。函数表达式只有变量声明被提升,赋值没被提升,所以调用时addFn还是undefined,于是报错"不是函数"。

这个例子完美解释了为什么现在都推荐用const或let声明变量而不是var:var的提升行为太反直觉,容易写出"看起来对、运行时炸"的代码。

5.2 执行上下文与作用域链:变量去哪儿找

要理解闭包,先要理解作用域链。JavaScript 在执行代码时,每个函数都有自己的执行上下文,里面记录了变量、this、作用域指向。查找变量时,先从当前作用域找,找不到就往上层作用域找,一直到全局作用域。这条链就是"作用域链"。

const globalName = 'global'; function outer() { const outerName = 'outer'; function inner() { const innerName = 'inner'; console.log(innerName); // inner console.log(outerName); // outer console.log(globalName); // global } inner(); } outer();

这段代码能依次打印出inner、outer、global,因为inner的作用域链上挂着inner→outer→ 全局。理解这个查找过程之后,闭包就好懂了:闭包就是"函数 + 函数定义时所在的外部变量的引用"。即使外部函数已经执行完毕,内部函数依然能访问到那些变量。

5.3 闭包经典问题:为什么循环打印全是最后一个值

很多人在面试里遇到过这道题:

for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 输出 3 3 3

为什么不是 0、1、2?因为var i是函数级变量,整个循环共享同一个i。循环很快结束,i变成 3;然后三个定时器回调才依次执行,读取到的都是同一个 "3"。

改成let i就好了:

for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 输出 0 1 2

let是块级作用域,每次循环都会创建一个新的绑定,每个回调都捕获了自己那一轮的i。这个例子既说明了闭包,也说明了let比var合理的地方。

5.4 this 的指向规则:调用方式决定一切

this是 JavaScript 里最容易被误解的概念之一。核心规律是:函数里的this不取决于它在哪定义,而取决于它被"谁"调用。

const user = { name: '小明', sayName() { console.log(this.name); }, }; user.sayName(); // 小明,因为是通过 user 调用的 const fn = user.sayName; fn(); // undefined,因为直接调用,this 指向全局/undefined

箭头函数是例外:它自己没有this,会继承外层作用域的this。所以如果你在对象方法里嵌套一个普通函数,this会丢失:

const user = { name: '小明', sayNameLater() { setTimeout(function () { console.log(this.name); // undefined }, 0); setTimeout(() => { console.log(this.name); // 小明 }, 0); }, }; user.sayNameLater();

第一个定时器用了普通函数,this指向定时器运行时环境,拿不到user.name;第二个用了箭头函数,自动继承sayNameLater的this,所以能正确打印。日常开发里,凡是回调函数里要访问外层this,无脑用箭头函数就对了。

6. 事件循环与异步:一个必须搞懂的执行顺序问题

6.1 setTimeout(0) 为什么不是立刻执行

很多人第一次写setTimeout(fn, 0)时都以为会立即执行,结果发现它总是排在同步代码后面。看这段代码:

console.log('start'); setTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => console.log('promise')); console.log('end');

我一开始也是凭感觉猜顺序,实际输出是start、end、promise、timeout。原因要从事件循环说起。

JavaScript 同一时间只能执行一段代码。当执行一段脚本时,代码会依次进入调用栈,同步代码一条条执行。遇到异步任务(比如setTimeout、Promise),先不执行,而是交到浏览器的任务队列里,等当前调用栈清空后再回来处理。队列还分两种:Promise.then这类微任务优先级更高,会排在宏任务(如setTimeout)之前。所以顺序是:同步代码先跑完,微任务先清空,再执行宏任务。

6.2 为什么全栈开发要懂事件循环

很多人觉得事件循环只是前端面试题,实际写 Node.js 后台也一样绕不开。Node.js 的单线程模型决定了:如果你的代码里有一段同步阻塞操作(比如大文件读取、复杂的循环计算),整个进程都会被卡住,其他用户的请求全部排队等待。事件循环的意义在于,把耗时的 I/O 操作交给系统内核,主线程不傻等,这样同样一台服务器能同时服务大量请求。

所以你在 Node.js 里监听接口、读写数据库时,底层全是事件循环在工作。理解了这个模型,你才会明白为什么"写异步代码要记得return"、为什么"在异步回调里抛异常要格外小心",因为异步错误如果没人捕获,可能直接导致进程退出。

6.3 async/await:让异步代码像同步一样可读

Promise刚开始流行时,大家用.then()链式调用,比回调地狱好一些,但链一长还是费劲。async/await是更进一步的语法糖,它让异步流程读起来几乎就是同步逻辑:

async function getUserData() { try { const userResp = await fetch('/api/user'); const user = await userResp.json(); const orderResp = await fetch(`/api/orders?userId=${user.id}`); const orders = await orderResp.json(); console.log('用户的订单:', orders); } catch (err) { console.error('请求失败:', err); } }

注意await只能在async函数内部使用。它会暂停函数执行,等背后的Promise出结果后再继续。这解决了"嵌套回调"的阅读问题,但线程没有真的被阻塞,事件循环依然在服务其他任务。

我用下来最重要的一个心得是:async/await不是万能药。如果你需要并发请求多个接口,用Promise.all更合适,因为逐个await会串行等待,白白浪费带宽和时间。

7. 把零碎知识点串成一个小项目:URL 检查工具的完整实现

学到这里,我建议你别继续看下一节教程,而是停下来做一个小工具练手。我当时做的练习是一个"URL 检查器",输入多行网址,点击按钮后逐行校验,列出哪些是合法 URL、哪些格式有问题。这个项目虽小,却把字符串处理、数组遍历、DOM 操作、函数封装全用上了,非常适合作为 JS_4 的综合练习。

7.1 需求拆解

这个工具核心逻辑有三步:

  1. 读取文本框里的多行字符串,按换行符拆成数组,去掉空行和首尾空格。
  2. 对每一行用URL构造函数校验是否合法,同时判断协议必须是http或https。
  3. 把结果渲染到页面上,合法和不合法的分别展示。

需求很明确,不用框架,纯原生 JavaScript 就能完成。这样做的好处是强迫你把 DOM 操作、事件监听、数组方法全部亲手过一遍。

7.2 核心代码实现

页面部分只需要一个<textarea>、一个按钮、一个结果列表:

<textarea id="urlList" rows="6" placeholder="每行输入一个网址"></textarea> <button id="checkBtn">开始检查</button> <ul id="resultList"></ul>

脚本部分,先写一个独立的校验函数,方便复用和测试:

function isValidUrl(value) { try { const url = new URL(value.trim()); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (err) { return false; } }

这里用new URL()做校验是一个很实用的技巧。如果传入的字符串不是合法 URL,构造函数会抛异常,用try/catch捕获即可。这个函数既用到了字符串处理(trim),也用到了异常处理,是很好的综合练习。

然后写点击事件和渲染逻辑:

const btn = document.getElementById('checkBtn'); const result = document.getElementById('resultList'); btn.addEventListener('click', () => { const textarea = document.getElementById('urlList'); const lines = textarea.value .split('\n') .map(line => line.trim()) .filter(Boolean); result.innerHTML = ''; lines.forEach(line => { const ok = isValidUrl(line); const li = document.createElement('li'); li.textContent = ok ? '[通过] ' + line : '[无效] ' + line; if (ok) { li.style.color = '#2e7d32'; } else { li.style.color = '#c62828'; } result.appendChild(li); }); });

这段代码把前几章的知识点串起来了:split拆分、map清洗、filter去空行、forEach遍历、createElement和appendChild操作 DOM、textContent设置文本。

7.3 跑起来之后我发现的两个问题

任何项目,跑起来之前觉得都想到了,跑起来之后才有真相。这个练习让我发现两个值得记录的问题。

第一,用innerHTML还是innerText/textContent。最初我图省事写了li.innerHTML = '...',结果发现用户如果输入一句<img src=x onerror=alert(1)>,网页就会弹窗。可见永远不要用innerHTML拼接用户输入,textContent会把内容当纯文本处理,安全得多。

第二,URL构造函数能识别一些"意料之中但不期望"的格式。比如你输入javascript:alert(1),new URL不会抛异常,而且协议是javascript:,这要求我们必须额外校验协议是否属于http/https。这个细节不写代码是发现不了的,写一遍印象特别深。

做完这个练习之后,我又顺手给工具加了一个"统计有效数量"的功能,把filter的结果.length显示出来。这样对"数组处理后怎么继续用结果"也有了体会。如果还想再进一步,可以用async/await加一个"逐个请求检查对方服务器是否返回 200"的按钮,但前端跨域限制会比较多,建议把同样的逻辑放到 Node.js 里跑一次,正好感受一下前端和后端对同一个任务的不同做法。

8. 学习路上最典型的三个"卡壳现场"与排查思路

8.1 "数组和对象都叫 contains,怎么一个能用一个报错"

这是我在论坛答疑时经常看到的问题。有人先学到了array.includes(),又看到对象也想要"包含判断",于是写出了obj.includes(key),然后报错includes is not a function。

这里的关键是理清数据类型的不同。数组判断"是否包含某个值"用includes,对象判断"是否包含某个键"要用hasOwnProperty或Object.keys:

const arr = ['apple', 'banana']; const obj = { apple: 1, banana: 2 }; console.log(arr.includes('apple')); // true console.log(obj.hasOwnProperty('apple')); // true console.log('apple' in obj); // true,但会把原型链上的也算进来 console.log(Object.keys(obj).includes('apple')); // true,最通用的写法

我自己的经验是:遇到xxx.includes is not a function这类报错,第一反应先看xxx到底是什么类型,而不是怀疑浏览器有问题。在代码里加一行console.log(typeof xxx, xxxx),立刻真相大白。

8.2 "forEach 里写 await,接口还是不等我"

这个坑我在前面提过,但值得单独拎出来再说一遍,因为太普遍了。很多同学写代码时想把一个数组逐个异步处理:

async function fetchAll(urls) { urls.forEach(async url => { const resp = await fetch(url); console.log(url, resp.status); }); console.log('全部请求已发出,但还没等结果'); }

你期待的顺序是"请求一个、等一个、再请求下一个",实际上forEach根本不等待回调里的await。所有 fetch 几乎同时发出,主流程立刻打印出最后一行console.log。

如果想按顺序执行异步任务,要用for...of:

async function fetchSequentially(urls) { for (const url of urls) { const resp = await fetch(url); console.log(url, resp.status); } console.log('全部请求已完成'); }

判断"为什么没按预想顺序执行"时,我会把setTimeout、Promise、async/await三个概念放在一起想。凡是遇到这种"明明加了 await 怎么还乱跑"的现象,十有八九是把异步操作放进了一个不等待的同步循环或回调里。

8.3 "页面突然白屏,报错信息在哪里看"

初学 JavaScript 时,最害怕的就是页面一片空白,不知道去哪里找线索。其实每个浏览器都内置了开发者工具,按 F12 打开,切到"Console"(控制台)面板就能看到红色报错。这也是我在全栈学习过程中每天都要用的工具。

看到报错之后,不要急着改代码。我的排查顺序是固定的:

  1. 先看报错发生在哪个文件第几行,提示信息里通常有明确的关键词。
  2. 鼠标悬停到报错处,看详细堆栈;很多浏览器会把"调用链"一步步列出来。
  3. 在报错位置附近加上console.log或debugger,打印出疑似有问题的变量,确认是不是undefined、是不是类型不对。
  4. 修复一个错误后继续跑,往往下一个错误会自己浮现出来。

比如最常见的报错之一是Cannot read properties of undefined (reading 'length'),翻译过来就是"你想读的东西是 undefined,拿不到其 length 属性"。这通常说明接口没返回数据,或某个字段名字拼错了。解决办法就是在使用前先把数据打出来看看:

fetch('/api/users') .then(res => res.json()) .then(data => { console.log('接口返回的数据:', data); console.table(data.items); // 之后再对 data.items 做操作 });

console.table是我特别推荐的一个方法,它能用表格形式打印数组和对象,数据长什么样一目了然。有些同学一报错就慌了,把代码大段大段重写,这其实是效率最低的。定点打日志、逐层推进,几分钟就能定位问题。


最后再说一点个人体会。JS_4 这个阶段最大的价值,不在于多背了几个 API,而在于建立了"我写的代码在运行时到底发生了什么"的意识。知道变量为什么提升、知道定时器为什么要排队、知道异步不是立刻执行,很多报错在你眼里就会从"莫名其妙"变成"情理之中"。

如果你正好学到这里,我建议你挑一个身边的小需求,哪怕只是上面的 URL 检查器,完整地写一遍、跑一遍、改 bug 改到顺畅为止。这个过程走完,再回头看那些"原理"的文章,你会觉得它们说的都不是空话。这大概就是我理解的"从语法到工程"的第一步:不是会背 API,而是敢拿着 API 去解决一个具体的问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 9:13:40

输电线路金具检测:YOLO数据集制作与训练全流程指南

简介&#xff1a;这是一份面向YOLO目标检测实战的输电线路电力金具检测数据集&#xff0c;适用于电力巡检、工业视觉检测相关的研究人员与工程技术人员。数据集基于真实场景拍摄&#xff0c;包含一千张图片对应的VOC&#xff08;XML&#xff09;、COCO&#xff08;JSON&#xf…

作者头像 李华
网站建设 2026/9/28 9:12:42

C语言进阶实战:指针、内存与文件操作避坑指南

学C语言有个很有意思的分水岭&#xff1a;前几篇笔记里的变量、分支、循环、函数&#xff0c;上课时大家都觉得能跟上&#xff0c;一到指针、字符串和文件操作&#xff0c;例子看得懂&#xff0c;作业写不出来。这篇是《C语言学习笔记》系列的第五篇&#xff0c;我打算把进入“…

作者头像 李华
网站建设 2026/9/28 9:10:08

AURIX TC375开发环境搭建指南:ADS安装与多核调试技巧

手头躺着一块吃灰的TC375开发板&#xff1f;别让它继续吃灰了。这篇文章准备聊聊英飞凌AURIX TC375这个三核单片机&#xff0c;怎么从零开始搭建ADS&#xff08;AURIX Development Studio&#xff09;开发环境&#xff0c;以及我实际调试时沉淀下来的几个实用技巧。先强调一句&…

作者头像 李华
网站建设 2026/9/28 9:10:08

AURIX TC375 开发环境搭建:ADS 安装、工程创建与调试实战

做车规控制器开发&#xff0c;绕不开 AURIX。TC375 是英飞凌 AURIX TC3xx 家族里的中坚型号&#xff0c;很多做 BMS、网关、底盘域控制器的团队都在用这颗料。AURIX Development Studio&#xff08;也就是大家常说的 ADS&#xff09;是英飞凌官方推出的免费 IDE&#xff0c;和 …

作者头像 李华
网站建设 2026/9/28 9:09:47

Linux条件变量深度解析:从pthread_cond_wait到生产者消费者实战

1. 拿到这个Linux并发问题&#xff0c;先要懂它的内核门槛很多朋友学Linux并发编程&#xff0c;会先看进程、线程、锁&#xff0c;觉得理解了mutex就万事大吉。但真正动手写多线程应用&#xff0c;或者被Linux面试题问到"如何让一个线程等待某个条件成立"时&#xff…

作者头像 李华
网站建设 2026/9/28 9:08:58

Android图标黑边成因与修复指南:从自适应图标到PNG透明通道

1. 先搞清楚&#xff1a;图标黑边到底是怎么来的做Android开发的人&#xff0c;几乎都撞见过这个诡异的问题&#xff1a;明明设计师给的图标干干净净&#xff0c;资源文件里看着也正常&#xff0c;可一装到手机上&#xff0c;桌面图标边缘就多出一圈黑边&#xff0c;有的甚至整…

作者头像 李华