尧图精选

JavaScript知识补充:从基础语法到异步编程的实用指南

🕒 发布时间:2026/9/9 9:38:07 📁 来源:尧图网络
JavaScript这门语言说实话很多人都会写但真正写明白的人不多。尤其是这些年从前端框架到后端Node.js再到小程序、桌面应用JavaScript的身影无处不在但正因为它的灵活和松散很多人在实际开发中会频繁踩坑——明明代码能跑却不知道为什么能跑出了问题也不知道该从哪里排查。这篇内容我想从知识补充的角度把我这些年积累的经验做一次系统梳理覆盖基础语法、异步编程、数组操作、常见报错、环境配置这些高频场景。适合刚学完语法但没真正上手的人也适合写了两三年代码但想查漏补缺的朋友。1. 先搞清楚JavaScript到底是什么1.1 它的三重身份很多初学者对JavaScript的理解就是网页脚本语言这个认识没错但不够完整。实际上JavaScript这些年已经变成了一个全栈语言至少有三重身份需要分清楚。第一重身份是浏览器端的脚本语言。这个大家最熟悉它负责页面上的交互逻辑——点击按钮弹窗、表单验证、数据请求、DOM操作、动画效果全都靠它。在这个场景下JavaScript的运行环境是浏览器它能访问window、document这些对象操作页面的HTML和CSS。第二重身份是服务端语言。自从Node.js出现之后JavaScript就跑到了服务器上可以处理HTTP请求、读写数据库、操作文件系统。这个场景下运行环境是Node.js它提供fs、http、path这些模块没有window和document。第三重身份是嵌入式脚本语言。很多桌面应用、移动应用、工具软件都内置了JavaScript引擎用JavaScript作为自动化脚本或扩展语言。比如Chrome的扩展、Adobe的脚本、各种游戏中嵌入的Mod系统都支持用JavaScript来写逻辑。理解这三重身份非常重要因为你在不同环境里写JavaScript能用的API是不一样的。有时候你写了一段代码在浏览器里跑得好好的拿到Node.js里直接报错原因就是它调用了浏览器专属的对象。反之也一样。1.2 为什么要做知识补充而不是系统学习我见过不少开发者培训机构的课程学完或者大学里《Web开发》这门课上完就觉得自己会JavaScript了。但一到实际开发问题就来了变量提升到底是什么this为什么指向不定闭包有什么实用价值和到底该用哪个回调地狱怎么解决这些东西你在教程里都能看到定义但很少能看到为什么。语法是学会了原理没通写代码就像背课文——能默写不会造句。所以我更愿意把这次内容定位成知识补充目的就是在你已经有的JavaScript知识框架上把那些模糊的节点重新讲透把那些平时容易忽略但实际很重要的点补上。这样你回头看自己写的旧代码会发现很多可以优化的地方。我保证这趟补课下来你至少能少踩一半的坑。2. 基础语法查漏补缺类型、作用域、闭包2.1 类型系统的几个坑JavaScript的类型系统用两个字来形容就是松散。它不强制你声明变量类型运行时自动推断这让代码写起来很省事但也埋了不少隐患。第一个坑是typeof null返回object。这是个历史遗留bug从语言诞生那天起就存在因为JavaScript的早期版本里值是用类型标签实际值的方式存储的null的标签是0正好和对象类型的标签一样。虽然这是个bug但你没法修因为修了会影响全世界所有依赖这个行为的老代码。实际开发中如果你想判断一个变量是不是真正的对象需要这样写function isPlainObject(value) { return Object.prototype.toString.call(value) [object Object]; }第二个坑是隐式类型转换。5 - 3结果是2因为减法操作符会强制把字符串转成数字但5 3结果是53因为加法操作符在有字符串参与时会被当成字符串拼接。这种不一致很容易导致bug。我的经验是在涉及数字运算时一律用Number()、parseInt()或parseFloat()显式转换不要依赖隐式转换。第三个坑是NaN。NaN表示不是一个数字但它的类型却是number而且NaN NaN的结果是false。判断一个值是不是NaN要用Number.isNaN()方法不要用全局的isNaN()因为后者会把非数字的字符串如abc也当作NaN处理。2.2 作用域var与let/const的本质区别作用域决定了变量在哪些地方可以访问。JavaScript里有三种作用域全局作用域、函数作用域、块级作用域。var声明的是函数作用域变量——它在函数内任意位置都能访问和你在哪里声明无关。这种机制叫变量提升JavaScript引擎在运行代码之前会把所有var声明提升到函数顶部。比如function test() { console.log(a); // 输出undefined不是报错 var a 10; }这里输出undefined是因为var a被提升了但赋值操作a 10还留在原来的位置。这个行为对新手来说非常反直觉也是很多隐蔽bug的来源。let和const是ES6引入的块级作用域变量。它们也有提升但不会初始化所以在声明之前访问会报错这个阶段叫暂时性死区。它们的规则更严格也能避免很多问题。我的建议很简单写新代码一律使用const只有当你确定这个变量的值需要重新赋值时才用let。var就让它留在旧代码里吧。2.3 闭包除了面试题它还能干嘛闭包这个概念每个学JavaScript的人都背过定义——函数能够记住并访问它定义时的作用域。但很多人背完就忘因为不知道它有什么用。实际开发中闭包有两个非常实用的场景。第一个场景是创建私有变量。JavaScript语言本身没有private关键字但你可以用闭包模拟function createCounter() { let count 0; return { increment: () count, decrement: () --count, getCount: () count }; } const counter createCounter(); counter.increment(); counter.increment(); console.log(counter.getCount()); // 2 console.log(counter.count); // undefined这里的count变量对外部完全不可见只能用返回的三个方法来操作。这在封装库、状态管理时非常有用。第二个场景是延迟执行时保存现场。比如给一组按钮绑定点击事件循环变量会泄漏的问题你会不会处理用let可以解决但如果你必须用var就得靠闭包for (var i 0; i 5; i) { (function(index) { button[index].onclick function() { console.log(index); }; })(i); }2.4 this指向四个规则记住就够this是JavaScript里被讨论最多的关键字之一因为它的指向是动态的取决于函数怎么被调用。总结下来只有四个规则第一种普通函数调用this指向全局对象浏览器里是windowNode.js里是global。严格模式下this是undefined。第二种作为对象的方法调用this指向调用它的对象。第三种通过call()、apply()、bind()调用this指向你传入的第一个参数。第四种作为构造函数用new调用this指向新创建的对象。箭头函数就简单多了它没有自己的this你写在哪个作用域就用哪个作用域的this。这一条让箭头函数在回调场景里特别受宠不用担心this被意外改写。我见过一个高频场景的bug在Vue或React组件里普通函数回调里的this变成了undefined解法就是改用箭头函数或者提前const self this。理解了上面的规则这种bug一眼就能看穿。3. 异步编程从回调地狱到async/await3.1 为什么JavaScript需要异步JavaScript是单线程语言也就是说同一时间只能执行一段代码。如果所有操作都是排队执行那么遇到网络请求、文件读写、定时器这些耗时操作时页面就会卡住不动用户体验极差。异步编程的思路就是先不等着耗时操作完成而是先继续执行后面的代码等耗时操作完成了再回过头来处理它的结果。这就是回调函数存在的意义——它是完成之后要做的事。但问题也来了。当一个异步操作的结果需要用另一个异步操作来处理而那个异步操作的结果又需要再处理时代码就会出现多层嵌套getUser(id, function(user) { getPosts(user.id, function(posts) { getComments(posts[0].id, function(comments) { console.log(comments); }); }); });这种代码就是传说中的回调地狱。它的问题不只是丑更重要的是错误处理非常困难——你必须每一层都写错误回调稍有不慎就漏掉了。3.2 Promise让异步代码更优雅Promise是ES6引入的解决方案它本质上是一个状态容器有三个状态pending进行中、fulfilled已成功、rejected已失败。状态一变就不可逆也就是说Promise只能成功或失败一次。用Promise重写上面的回调地狱getUser(id) .then(user getPosts(user.id)) .then(posts getComments(posts[0].id)) .then(comments console.log(comments)) .catch(error console.error(error));代码从嵌套变成了链式调用逻辑清晰多了错误处理也集中到了最后面的catch里。但then链有一个我踩过好几次的坑回调函数里如果没有returnPromise就会认为这个阶段的结果是undefined下一个then拿到的就是undefined。写的时候要特别留意。Promise.all和Promise.race也是高频API。前者接收一组Promise等全部成功后就返回所有结果组成的数组后者只看谁先完成用最先完成的结果。加请求并发控制、做超时熔断时经常用到。3.3 async/await写起来像同步代码async/await是ES2017引入的语法糖它让异步代码的写法看起来和同步代码差不多大幅度提升了可读性。async function loadData() { try { const user await getUser(id); const posts await getPosts(user.id); const comments await getComments(posts[0].id); console.log(comments); } catch (error) { console.error(error); } }await关键字会暂停当前函数的执行等待Promise状态变化然后拿到结果。它只能用在async函数内部。这里有几个实际经验值得分享。第一await不要滥用。如果你有多个互相没有依赖关系的异步操作用Promise.all并行执行不要一个一个await排队节省的不仅仅是代码行数更是真实的执行时间。// 错误示范串行执行耗时是两者之和 const user await getUser(id); const config await getConfig(); // 正确示范并行执行耗时是两者中较长的 const [user, config] await Promise.all([getUser(id), getConfig()]);第二await只能在async函数中使用。如果你在一个普通函数里写了await直接报语法错误。有些场景下比如在事件回调里你不能直接写async那就抽一个async函数包一层。4. 数组高阶方法日常开发的主战场4.1 map、forEach和for循环的区别数组操作是JavaScript开发中最频繁的任务之一。map、forEach、filter、reduce这几个高阶方法用好了能极大精简代码。map用于映射——把一个数组变成另一个等长的新数组每个元素经过映射函数处理后作为新数组对应位置的值。注意map返回的是新数组不会修改原数组。forEach只负责遍历不返回新数组。它的作用是对每个元素执行一次回调函数常用于日志输出、外部API调用这些副作用操作。区别理解起来很简单但用法上有个容易忽略的坑在map的回调里如果函数体没有return返回的数组每个位置就都是undefined。很多人写const result arr.map(item { doSomething(item); })拿到的是全是undefined的数组排查半天才发现是少写了return。4.2 filter和reduce真香组合filter用于筛选返回一个新数组只包含回调函数返回true的元素const numbers [1, 2, 3, 4, 5, 6]; const evenNumbers numbers.filter(n n % 2 0); // [2, 4, 6]reduce是这几个里最灵活但初次接触最难懂的。它把数组归约成一个单一的值可以求总和、求最大值、分组、去重、累加对象几乎什么都能做const numbers [1, 2, 3, 4, 5]; const sum numbers.reduce((acc, cur) acc cur, 0); // 15reduce回调有四个参数累加器acc、当前值cur、当前索引、原数组。初始值可以传也可以不传不传时用数组第一个元素作为初始值从第二个元素开始遍历。我建议总是显式传入初始值因为当数组为空时不传初始值会报错。实际开发中我用reduce做按属性分组的场景特别多const users [ { name: 张三, role: admin }, { name: 李四, role: user }, { name: 王五, role: admin } ]; const groupedByRole users.reduce((result, user) { (result[user.role] result[user.role] || []).push(user); return result; }, {});4.3 解构与展开运算符每天都要用的便捷技巧对象解构和数组解构让取值变得非常方便。从接口返回的数据里取字段用法是这样的const user { id: 1, name: 张三, email: zhangsanexample.com }; const { name, email } user;再配合重命名和默认值简直不要太爽const { name: userName, email 未设置, ...restOfUser } user;展开运算符...可以用于复制数组和对象、合并、传参。它和Object.assign在对象拷贝上的区别是展开运算符只处理可枚举属性且是浅拷贝。深拷贝的坑在嵌套对象时特别明显——修改拷贝出来的对象的嵌套属性原对象也会变。真需要深拷贝可以JSON.parse(JSON.stringify(obj))但注意这种方法会丢失undefined、function、Symbol这些类型的值。5. 那些高频报错和void(0)的真相5.1 最常见的运行时报错每次看到新人在群里发JavaScript 运行时报错的截图我基本能猜到几种情况。这里挑三个最高频的分享排查思路。第一个是Uncaught TypeError: Cannot read property xxx of undefined。这是一哥级别的报错——代码尝试访问一个undefined值的属性。场景通常是接口返回的数据结构和你预期不一致比如data.items是undefined你直接data.items.map()就崩了。排查方法是先打日志确认数据长什么样再用可选链操作符?.容错const items response.data?.items ?? [];第二个是ReferenceError: xxx is not defined。这个报错说明你访问的变量压根没声明过或声明在其他作用域里。排查时往前找变量声明看看是不是拼写错了或者变量被包在某个块级作用域里。第三个是SyntaxError: Unexpected token。这是代码被当成字符串传给某个API或者少写了一个括号/引号。用编辑器自带的语法检查或者eslint工具基本可以提前拦截。5.2 javascript:void(0)到底是什么很多老代码里会有hrefjavascript:void(0)这种写法尤其是在外链标签里。它的作用就是点了链接但什么都不做。void操作符会对后面的表达式求值但始终返回undefined所以void(0)就是返回undefined。当你写hrefjavascript:void(0)时点击这个链接不会跳转也不会发生什么。为什么要这么写早期Web开发中人们会用a href#来阻止页面跳转但#会导致URL变化浏览器会在历史记录里加一条点回退键很烦。用javascript:void(0)就没有这个问题。不过现在前端工程化程度高了我自己的实践是能不用就别用。需要点击行为的元素直接用button更语义化如果非要用a就href指向一个真实地址在JS里e.preventDefault()拦截默认行为。这样既保证了无JS环境的降级可用也避免了很多伪链接带来的隐患。5.3 A JavaScript error occurred in the main process是怎么回事这个报错常见于Electron桌面应用。热搜词里出现了a javascript error occurred in the main process这其实是Electron主进程抛出了未捕获的JavaScript异常。出现这个弹窗时说明主进程的代码本身就崩了而且你没有做全局错误捕获。排查思路分三步。第一步看弹窗里的详细信息通常包含完整的堆栈跟踪这会直接定位到出错的文件和行号。第二步检查主进程入口文件里的初始化逻辑比如app.whenReady()里的操作这里最容易出问题。第三步给主进程加上全局兜底process.on(uncaughtException, (error) { console.error(Uncaught Exception:, error); }); process.on(unhandledRejection, (reason) { console.error(Unhandled Rejection:, reason); });加完之后即使出问题也不会弹窗了错误会打到日志里方便排查。5.4 再说说javascript:void(o)和document写cookie的隐患javascript:void(o)的报错本质上就是void(0)相关代码在某个执行环境中把名字解析错了——o被当成一个变量名实际没这个变量于是报ReferenceError: o is not defined。如果是第三方代码里的问题试试换形式用事件处理替代javascript:伪协议。还有一种特别危险的写法是javascript:void(document.titledocument.cookie)。这是典型的XSS攻击姿势——通过javascript:伪协议执行任意脚本读取document.cookie。如果有人把这种URL发给你的用户用户点击之后cookie就被盗了。开发时务必注意没有经过严格校验的用户输入不能直接拼接到javascript:协议或href属性里。这属于严重的安全隐患写代码时必须坚决杜绝这种模式。6. 环境配置与跨语言、跨平台实战6.1 macOS上搭建JavaScript开发环境在macOS上搭建JavaScript开发环境看起来很简单但有几处细节容易卡住。我的推荐组合是Node.js npm或pnpm VS Code。安装Node.js最推荐的方式不是去官网下载安装包而是用nvmNode Version Manager做版本管理。因为不同项目可能依赖不同的Node版本用nvm可以随时切换避免这个项目在我的机器上跑不起来的尴尬。# 安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 安装并切换Node版本 nvm install 20 nvm use 20 # 验证 node -v npm -v装完之后你可能还需要配置npm的镜像源根据你的网络情况选择并且在~/.zshrc里设置环境变量。配置完之后新开终端输入node -v能输出版本号基本就说明环境OK了。VS Code里我建议装几个必装插件ESLint、Prettier、JavaScript (ES6) code snippets。这三个配合起来写代码时语法错误实时标红保存时自动格式化能帮你省掉大量排查低级错误的时间。6.2 OC和JavaScript互相调用热搜词里有oc和javascript互相调用这是一个移动端H5开发里的经典问题。OC是Objective-C的简称也就是iOS原生开发语言。在iOS的WKWebView里JavaScript和OC是可以互相通信的。先说JavaScript调用OC。在WKWebView中你可以通过WKScriptMessageHandler注册一个处理器然后在JavaScript端用window.webkit.messageHandlers.方法名.postMessage(参数)来调用原生代码。比如window.webkit.messageHandlers.showAlert.postMessage({ message: Hello from JS });原生端需要实现WKScriptMessageHandler协议在userContentController:didReceiveScriptMessage:回调里拿到消息然后执行对应的原生逻辑。反过来OC调用JavaScript就简单一些用evaluateJavaScript(_:completionHandler:)方法直接执行一段JavaScript字符串。通常是原生要往H5页面传数据比如登录状态、用户信息就拼一个JSON字符串调callback函数。这两条路互通的思路是WKWebView做了JavaScript和原生通信的翻译官。有一个坑要提醒一下postMessage传的数据必须能被序列化成JSON格式函数、undefined这些值都会被丢失。6.3 在Axure里嵌入JavaScript脚本Axure是一个原型设计工具热词里提到了如何在axure嵌入javascript脚本。很多人以为Axure只能拖拽组件但其实它支持在交互事件里写JavaScript。最常见的做法是用交互面板里的设置文本或其他动作中的JavaScript选项或者直接在上面板中执行。如果你需要更灵活的脚本控制可以用Axure的OnLoad交互在页面加载时执行初始化脚本。举个实际场景你想在原型里实现一个动态计算比如根据输入框里的单价和数量自动计算总价。用Axure自带的公式编辑器也能做但写JavaScript更灵活可以直接操作所有元素。在Axure 10里你可以右键组件选择交互添加用例然后选择开发/JavaScript填入脚本var price parseFloat($([data-labelprice]).val()) || 0; var qty parseFloat($([data-labelqty]).val()) || 0; $([data-labeltotal]).text(price * qty);这里用的$是Axure内置的jQuery能直接选择页面上的组件。这个技巧在原型评审时非常能加分因为你展示的不再是静态图而是带真实计算逻辑的可交互原型。7. 学习资源推荐与进阶路线7.1 值得反复读的三本书JavaScript的知识点太散靠官方文档从头啃很容易迷失。书本类的经典资料我推荐三本。第一本是《你不知道的JavaScript上中下》。这套书针对JavaScript里最让人困惑的部分做了深度剖析比如作用域、闭包、this、原型、异步。我每次重读都有新收获很多原来如此的时刻都是这本书给的。第二本是《JavaScript高级程序设计》也就是大家叫的红宝书。书很厚但内容非常全面从基础到DOM、事件、网络请求几乎覆盖了前端开发的全部知识点。适合当工具书放案头遇到问题随时翻。第三本是《JavaScript语言精粹》。这本书很薄但字字珠玑它讲的是好的部分——写作JavaScript时应该优先用哪些语言特性避开哪些不好的部分。刚入门就读这本书可能体会不深建议有半年经验后再读。另外MDNMozilla Developer Network是免费的在线权威文档遇到不确定的API优先去查它不要只依赖百度或搜索引擎的零碎结果。7.2 从基础到实战的路径建议学JavaScript最忌讳只看不写。按照我自己的经验比较有效的路径是这样的第一阶段先掌握基础语法变量、类型、运算符、控制流、函数、数组、对象。第二阶段把ES6核心特性过一遍解构、展开、模板字符串、箭头函数、类、模块化。第三阶段理解DOM和事件机制能自己做一个简单的待办事项网页。第四阶段进入异步编程理解事件循环、Promise、async/await。这时候可以把之前写的项目改成异步版本。第五阶段选一个框架深入——Vue或React都行然后结合Node.js把前后端跑通。一到三阶段大约两周可以搞定四阶段可能需要一个月慢慢消化五阶段就是长期工程了。别急着跳级基础没打好后面写框架代码时处处都是拦路虎。8. 几个我踩过的坑和最终建议最后分享几个实际开发中的经验和收尾心得。第一写JavaScript一定要开严格模式。ES5之后就推荐use strict它能避免很多静默失败——比如给只读属性赋值、使用未声明变量这些在严格模式下都会变成报错能帮你把bug消灭在萌芽状态。现在的前端工程化工具基本都默认开启严格模式但手动写的独立JS文件别忘了加上。第二用代替。会做隐式类型转换1 1是true这往往不是你想要的结果。要求类型一致才相等规则简单得多。我从开始写JavaScript到现在几乎没用过也几乎没有错过相关的bug。第三给自己定一个小项目练手的规矩。我刚学JavaScript的时候就是不停写小项目——计算器、待办清单、倒计时器、贪吃蛇、网页小游戏。每写一个项目就会用到一批新API遇到一批新报错解决一批新问题。这个成长速度比单纯看书快得多。第四不要怕读报错信息。很多新人一看到英文报错就截图问别人其实报错信息本身已经告诉了你问题在哪一行、什么类型的错误。先自己读一遍尝试翻译一遍再动手改。这个过程训练多了你排查bug的能力会飞速提升。JavaScript是一门越用越觉得有意思的语言。它不够完美有很多历史包袱和设计缺陷但正是这种灵活和宽容让它在这个时代几乎无处不在。希望这篇内容能帮你把那些模糊的知识点补扎实。下一步的建议很简单——去写一个以前没写过的小项目遇到问题再回来翻这篇。写代码的能力不是看出来的是自己踩坑踩出来的。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →