ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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)); // truetoLowerCase()是推荐做法比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...oflet found null; for (const item of list) { if (item 3) { found item; break; } } console.log(found); // 4如果你确实需要在类 forEach 的回调风格里实现中断也可以借助some()或every()在回调里返回truesome或返回falseevery来提前结束遍历。不过我用下来还是觉得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声明变量而不是varvar的提升行为太反直觉容易写出看起来对、运行时炸的代码。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 2let是块级作用域每次循环都会创建一个新的绑定每个回调都捕获了自己那一轮的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 需求拆解这个工具核心逻辑有三步读取文本框里的多行字符串按换行符拆成数组去掉空行和首尾空格。对每一行用URL构造函数校验是否合法同时判断协议必须是http或https。把结果渲染到页面上合法和不合法的分别展示。需求很明确不用框架纯原生 JavaScript 就能完成。这样做的好处是强迫你把 DOM 操作、事件监听、数组方法全部亲手过一遍。7.2 核心代码实现页面部分只需要一个textarea、一个按钮、一个结果列表textarea idurlList rows6 placeholder每行输入一个网址/textarea button idcheckBtn开始检查/button ul idresultList/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 srcx onerroralert(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.keysconst 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...ofasync 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控制台面板就能看到红色报错。这也是我在全栈学习过程中每天都要用的工具。看到报错之后不要急着改代码。我的排查顺序是固定的先看报错发生在哪个文件第几行提示信息里通常有明确的关键词。鼠标悬停到报错处看详细堆栈很多浏览器会把调用链一步步列出来。在报错位置附近加上console.log或debugger打印出疑似有问题的变量确认是不是undefined、是不是类型不对。修复一个错误后继续跑往往下一个错误会自己浮现出来。比如最常见的报错之一是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 去解决一个具体的问题。
返回列表