首页 / 资讯中心 / 文章详情

JavaScript核心教程:作用域、DOM事件委托与异步编程实战

JavaScript核心教程:作用域、DOM事件委托与异步编程实战 ★ FEATURED ARTICLE
简介《JavaScript编程语言入门及核心功能详解》是一份面向零基础及有一定编程经验学习者的JavaScript入门教程围绕语言基础与Web前端核心能力展开帮助读者建立从语法到页面交互、异步处理的完整知识框架。全文以单个docx文档交付压缩包大小约19KB共1个文件便于直接阅读和本地检索。内容涵盖变量声明、数据类型、运算符、条件与循环等基础语法并延伸到函数定义与调用、对象键值结构、DOM元素获取与内容修改、事件监听以及回调、Promise和async/await等异步编程要点配有示例代码与解释。已有88人学习适合按章节动手实践对照示例理解var、let、const的作用域差异、引用类型的使用方式与异步任务的组织方法也可作为课堂辅导或自查清单使用。1. 从控制台报错倒推这份 JavaScript 教程到底补哪块能力很多人第一次在浏览器控制台里看到Cannot read properties of null或者按钮点了没反应、接口数据明明回来了页面却不更新才回头补 JavaScript 编程语言入门和核心功能。这份教程把变量声明、数据类型、运算符、控制结构、函数、对象、DOM 操作和异步编程按一条可练习的线串起来不是只列语法点。它适合刚接触前端的人也适合写过后端、想把浏览器侧交互和异步流程补齐的从业者。拿到资源后别急着从头看到尾先按示例敲一遍再对照控制台输出改参数理解会快很多。真正要盯住的是三件事变量作用域有没有踩坑、DOM 节点拿到时页面是否已经就绪、异步任务的顺序是否符合预期。2. 变量、类型与控制结构先把 JavaScript 的运行时脾气摸清教程前半段看起来是基础语法实际决定了后面排查 DOM 和异步问题的速度。变量提升、引用类型共享、隐式转换这些问题平时不显眼一旦出现在业务代码里就会变成“明明赋值了却没生效”“改了副本原对象也变了”之类的玄学。先在一段独立脚本里把这些行为跑出来比直接背结论可靠。2.1 var、let、const 的作用域对照与变量提升先跑下面这段代码重点看报错位置不要只看正常输出。// 作用域与提升演示 console.log(a); // undefinedvar 声明被提升赋值没有 var a 1; // console.log(b); // ReferenceErrorb 在声明前不可访问 let b 2; const c 3; // c 4; // TypeErrorconst 绑定不能被重新赋值 function scopeDemo() { if (true) { var fnVar 函数级作用域; let blockLet 块级作用域; const blockConst 块级常量; } console.log(fnVar); // 可以访问 // console.log(blockLet); // ReferenceError } scopeDemo();逻辑说明var的声明会被提升到函数顶部初始值是undefined所以第一行不会报错let和const也会提升但存在暂时性死区声明前访问直接抛ReferenceError。const约束的是绑定不能换不是对象内容不能改后面对象那一章还会用到这一点。参数怎么改把var a 1改成let a 1第一行立刻报错用这个对比记住提升差异最直接。声明方式作用域变量提升重复声明声明前访问重新赋值var函数级有初始 undefined允许得到 undefined允许let块级有但有暂时性死区同作用域不允许抛 ReferenceError允许const块级有但有暂时性死区同作用域不允许抛 ReferenceError不允许换绑定注意在循环里用var声明循环变量再在回调里引用它很容易拿到同一个最终值。改成let后每次迭代都会生成新的绑定这个坑在后面异步回调里还会遇到。2.2 原始类型与引用类型赋值、浅拷贝和意外共享原始类型复制的是值引用类型复制的是地址这句话要落到代码里才有感觉。// 原始类型复制值引用类型复制地址 let score 80; let copyScore score; copyScore 90; console.log(score); // 80 const student { name: A同学, tags: [js] }; const sameRef student; sameRef.name B同学; console.log(student.name); // B同学两个变量指向同一个对象 const shallow { ...student, tags: [...student.tags] }; shallow.tags.push(dom); console.log(student.tags); // [js]原对象没有被污染逻辑说明sameRef和student指向同一个堆内存对象改任何一个都会影响另一个。展开运算符做的是浅拷贝第一层属性复制了嵌套对象仍然共享所以示例里额外把tags也展开了一层。参数说明...student只复制可枚举的自有属性方法、原型链上的内容不会带过来如果对象里嵌套很深常见做法是手写拷贝函数或使用结构化克隆但这已经超出入门示例的范围先保证知道浅拷贝的边界。判断类型时typeof null会返回objectNaN也不等于自身。数组用Array.isArray判断更稳数字先用Number.isNaN再参与计算。教程里的原始类型清单包括字符串、数字、布尔值、空值、未定义和符号引用类型包括对象、数组和函数这些分类不需要死背但要知道为什么const声明的对象还能改属性。2.3 控制结构用成绩分级器跑通 if、for、while 与跳转控制结构单独看没有难度放进一个小函数里就能看到类型校验和循环跳转怎么配合。// 成绩分级if/else、for、continue function grade(score) { if (typeof score ! number || Number.isNaN(score)) { return 无效分数; } if (score 90) return A; if (score 80) return B; if (score 60) return C; return D; } const scores [95, 82, 58, 76, 100]; for (let i 0; i scores.length; i) { if (scores[i] 60) { console.log(第 ${i 1} 个分数不及格跳过统计); continue; } console.log(第 ${i 1} 个分数等级${grade(scores[i])}); }逻辑说明continue只跳过当前这一轮后面的循环继续break会直接结束整个循环return结束函数。这里先做类型校验是为了避免字符串比较出现意外结果。参数说明Number.isNaN只对真正的NaN返回真比全局isNaN少一些隐式转换scores[i]作为参数传给grade每次循环独立计算。while更适合次数不确定的场景比如轮询一个状态直到成功或超时。实际写法里要给循环加退出条件否则页面会卡死。教程里还提到do...while它至少执行一次适合先弹一次确认框再判断是否继续的交互。2.4 运算符与隐式转换比较时把类型收紧运算符里最容易翻车的是和。前者会做隐式转换后者遇到字符串就变成拼接。// 与 的差异以及加法运算符的字符串拼接 console.log(1 1); // true发生隐式转换 console.log(1 1); // false类型不同 const total 10 20 元; console.log(total); // 30元从左到右先算数值再拼接 const price 10; console.log(Number(price) 5); // 15 console.log(parseInt(10px, 10)); // 10逻辑说明业务里的相等判断优先用需要数字时显式转换不要把希望寄托在隐式规则上。parseInt会从开头解析到第一个非数字字符适合带单位的字符串Number要求整个字符串都能转成数字否则得到NaN。参数说明parseInt第二个参数传10明确按十进制解析避免旧代码里前导零被当成八进制。三元运算符适合短分支嵌套超过两层就拆成if。逻辑运算符和||会返回操作数本身不一定是布尔值这一点在设置默认值时很常用但要知道空字符串、0、null都会触发默认值必要时用空值合并运算符单独判断。3. 函数与对象把一等公民和键值对用到业务里函数在 JavaScript 里是一等公民能赋值、能传参、能返回对象是键值对的集合数组和函数也是对象。教程把这两块放在一起讲是为了后面写事件回调和数据模型时不用来回翻语法。真正要练的是this指向、参数默认值、闭包保存状态以及对象方法调用时上下文有没有丢。3.1 函数声明、函数表达式与箭头函数的 this 差异三种写法都能调用但this行为不同放在对象方法里最明显。// 三种函数写法与 this function normalFn(name) { return 普通函数${name}; } const exprFn function (name) { return 函数表达式${name}; }; const arrowFn (name) 箭头函数${name}; const counter { count: 0, incNormal() { this.count 1; return this.count; }, incArrow: () { // 箭头函数没有自己的 this这里拿不到 counter return this.count; } }; console.log(normalFn(A同学)); console.log(exprFn(B同学)); console.log(arrowFn(C同学)); console.log(counter.incNormal()); // 1 console.log(counter.incArrow()); // undefined 或 NaN取决于外层 this逻辑说明普通函数和方法里的this由调用方式决定counter.incNormal()调用时this指向counter箭头函数没有自己的this会沿着定义位置向外找所以不适合直接作为依赖this的对象方法。参数说明name是普通形参箭头函数只有一条表达式时可以省略大括号和return但返回对象字面量时要加圆括号。常见做法是需要动态this的方法用普通函数回调和纯计算用箭头函数。事件监听里如果直接在回调中使用this普通函数的this指向绑定元素箭头函数则指向外层这个差异在第 4 章的事件委托里还会出现。3.2 默认参数、剩余参数与回调把函数当参数传函数作为参数传递是后面 Promise 和事件回调的基础。先看一个订单计算的小例子。// 默认参数、剩余参数、回调 function createOrder(user, items [], ...coupons) { const total items.reduce((sum, item) sum item.price, 0); const discount coupons.reduce((sum, c) sum c.amount, 0); return { user, items, payable: Math.max(total - discount, 0) }; } const order createOrder( A同学, [{ name: 键盘, price: 200 }, { name: 鼠标, price: 100 }], { amount: 20 }, { amount: 10 } ); console.log(order.payable); // 270逻辑说明items []是默认参数调用时不传或传undefined才会生效传null不会走默认值。...coupons是剩余参数把后面所有实参收集成数组所以可以传多个优惠对象。reduce的回调接收累计值和当前项第二个参数是初始值。参数说明Math.max(total - discount, 0)防止优惠大于总价时出现负数这是业务代码里很常见的兜底。回调函数本质上就是“把一个函数交给另一个函数在合适的时候调用”。数组的map、filter、reduce都接收回调事件监听也接收回调。写回调时要注意参数顺序reduce是累计值在前、当前值在后forEach是当前值在前、索引在后记混了不会报错但结果会悄悄错。3.3 对象字面量、方法与 this把键值对变成可维护模型对象字面量不只是存数据方法里的this和对象复制也要一起看。// 对象方法、this、解构 const user { name: A同学, age: 30, greet() { console.log(Hello, ${this.name}); }, profile: { city: 城市A } }; user.greet(); const { name, age } user; console.log(name, age); const summary { ...user, age: 31 }; console.log(summary.age); // 31 console.log(user.age); // 30原对象不变逻辑说明greet是对象方法调用时this指向user如果把user.greet单独赋值给一个变量再调用this就丢了这是后面事件回调里最常见的翻车点之一。解构赋值按属性名取值不复制对象本身扩展运算符做浅拷贝修改第一层属性不会影响原对象但嵌套的profile仍然共享。参数说明对象方法可以简写为greet() {}不需要写function。如果方法内部要用到对象自身优先用普通函数方法不要随手改成箭头函数。实际项目里对象可能来自接口字段缺失时可以用可选链读取但入门阶段先把this和浅拷贝弄清楚更重要。3.4 闭包做表单校验函数返回函数闭包是函数记住并访问定义时作用域的能力。用它做校验器工厂可以把规则和校验逻辑分开。// 闭包校验器工厂 function createValidator(rules) { return function (value) { for (const rule of rules) { if (!rule.test(value)) { return rule.message; } } return ; }; } const validateUsername createValidator([ { test: v v.length 3, message: 用户名至少 3 个字符 }, { test: v /^[a-zA-Z0-9_]$/.test(v), message: 只能包含字母、数字、下划线 } ]); console.log(validateUsername(ab)); // 用户名至少 3 个字符 console.log(validateUsername(user_01)); // 空字符串表示通过逻辑说明createValidator返回的函数一直保留着rules的引用所以调用validateUsername时不用再传规则数组。for...of按顺序检查遇到第一个不通过的就返回提示。参数说明rules是数组每项包含test函数和message字符串正则里的^和$表示整串匹配表示至少一个字符。闭包也会带来内存占用如果返回的函数一直被引用外层变量就不会释放。入门示例里不用刻意处理但要知道“函数返回函数”不是魔法它只是保留了作用域。常见做法是把校验器放在模块顶层创建一次表单提交时重复调用而不是每次输入都重新创建一遍规则。4. DOM 操作从 getElementById 到事件委托的完整链路DOM 操作是教程里最容易“看懂了但写不出来”的部分。页面元素什么时候能拿到、改内容用哪个属性、事件绑在父级还是子级这三个问题决定代码能不能稳定跑。先把选择元素、改文本、绑事件跑通再写一个待办清单基本就能覆盖日常交互。4.1 选元素与改内容getElementById、querySelector 与 textContent先确认脚本执行时元素已经存在再谈修改。// 选元素、改文本、改样式 const title document.getElementById(title); const list document.querySelector(.todo-list); if (title) { title.textContent 待办清单; title.classList.add(is-active); } // 批量选择返回 NodeList不是数组需要转换才能用 map const items Array.from(document.querySelectorAll(.todo-item)); console.log(items.length);逻辑说明getElementById按 id 找单个元素找不到返回nullquerySelector返回第一个匹配元素querySelectorAll返回静态NodeList。先判断title是否存在是为了避免空引用报错。textContent写入纯文本不会解析标签innerHTML会解析 HTML用户输入拼进去就有注入风险。参数说明classList.add添加类名不会覆盖原有类Array.from把类数组转成真数组后面才能用map、filter。方法返回找不到时适合场景getElementById单个元素nullid 唯一且确定querySelector第一个匹配元素null需要 CSS 选择器querySelectorAll静态 NodeList空 NodeList批量操作提示脚本放在body末尾或者包在DOMContentLoaded回调里能避开大部分“元素还没渲染就执行”的问题。不要用固定延时去赌页面加载速度。4.2 事件监听与事件委托列表增删不绑 N 个监听器列表项是动态生成的如果每生成一项就绑一个删除事件代码会越来越难维护。事件委托把监听器绑在父容器上利用冒泡统一处理。// 事件委托父容器监听判断真实目标 const list document.querySelector(#todoList); list.addEventListener(click, function (event) { const target event.target; if (target.matches(.delete-btn)) { const item target.closest(.todo-item); if (item) item.remove(); } });逻辑说明点击删除按钮时事件从按钮冒泡到#todoList父容器的监听器被触发。event.target是实际点击的元素matches判断它是不是删除按钮closest向上找最近的列表项然后移除。参数说明addEventListener第三个参数不传时默认在冒泡阶段触发closest会一直向上查找到根节点找不到返回null所以后面要判断。事件委托的好处是动态新增的列表项不用重新绑定内存占用也更低。要注意this在普通函数回调里指向绑定监听的元素也就是父容器如果改成箭头函数this会指向外层拿不到父容器。实际写的时候直接用event.target和closest更稳不必依赖this。4.3 待办清单最小实现DOM 操作与状态同步把选择元素、创建节点、事件委托串起来就是一个能跑的最小交互。!doctype html html langzh-CN body input idtodoInput placeholder输入待办 / button idaddBtn添加/button ul idtodoList/ul script const input document.getElementById(todoInput); const addBtn document.getElementById(addBtn); const list document.getElementById(todoList); function renderItem(text) { const li document.createElement(li); li.className todo-item; li.innerHTML span classtodo-text/spanbutton classdelete-btn删除/button; // 用户输入用 textContent 写入避免被当成 HTML 解析 li.querySelector(.todo-text).textContent text; return li; } addBtn.addEventListener(click, () { const text input.value.trim(); if (!text) return; list.appendChild(renderItem(text)); input.value ; }); list.addEventListener(click, (event) { if (event.target.matches(.delete-btn)) { event.target.closest(.todo-item).remove(); } }); /script /body /html逻辑说明renderItem先用固定的innerHTML搭结构再用textContent填用户输入这样既省去多次createElement又不会把输入当标签执行。添加按钮读取输入、去空格、判空然后追加节点并清空输入框。删除走事件委托动态生成的按钮也能响应。参数说明trim()去掉首尾空格避免只输入空格也能添加appendChild把新节点放到列表末尾remove()直接移除当前节点。这段代码可以直接保存为.html文件在浏览器打开不需要构建工具。要改样式就在style里加类名要加“完成”状态就在li上加classList.toggle(done)。常见做法是把数据存在数组里渲染时根据数组生成列表但入门阶段先用 DOM 节点本身承载状态等异步那一章再考虑数据驱动。5. 异步编程从事件循环到五个翻车现场的排查顺序异步是 JavaScript 单线程模型下的核心能力。教程里提到回调、Promise 和async/await真正用起来容易乱的是执行顺序和错误捕获。先跑输出顺序题再看 Promise 链最后把常见翻车现场按“现象、原因、解决”排一遍排查时就不容易东一榔头西一棒子。5.1 单线程与事件循环先看清同步、微任务、宏任务的输出顺序下面这段代码建议直接粘到控制台先猜输出再运行。// 输出顺序题 console.log(1 同步开始); setTimeout(() { console.log(4 宏任务); }, 0); Promise.resolve().then(() { console.log(3 微任务); }); console.log(2 同步结束);逻辑说明同步代码先执行输出 1 和 2当前调用栈清空后先清微任务队列输出 3最后执行宏任务输出 4。setTimeout的0不是立即执行只是尽快排到宏任务队列。参数说明Promise.resolve()创建一个已完成的 Promise.then回调进入微任务setTimeout第二个参数是延迟毫秒数实际执行时间还受主线程忙碌程度影响。任务类型常见来源执行时机同步任务普通函数调用、赋值立即按调用栈微任务Promise.then、queueMicrotask当前同步代码结束后宏任务setTimeout、事件回调微任务清空后注意在微任务里再产生微任务会一直加到当前微任务队列清空为止在微任务里产生宏任务要等下一轮事件循环。排查顺序问题时先分清任务类型不要只看代码位置。5.2 Promise 链与 async/await把回调拆成可读流程Promise 有三种状态等待、成功、失败。状态一旦确定就不会再变链式调用每次then都返回一个新的 Promise。// Promise 状态与链式错误捕获 function delay(ms) { return new Promise((resolve) { setTimeout(() resolve(等待 ${ms}ms), ms); }); } delay(100) .then((msg) { console.log(msg); return delay(50); }) .then((msg) { console.log(msg); }) .catch((err) { console.error(失败, err); }) .finally(() { console.log(结束); });逻辑说明delay返回一个 Promise定时器结束后调用resolve把状态改成成功。第一个then返回新的delay(50)所以下一个then会等它完成这就是链式调用解决回调嵌套的方式。catch捕获链上任意一环的失败finally不管成功失败都执行。参数说明resolve传出的值会进入下一个then的参数reject传出的错误会进入catch。async/await是 Promise 的语法糖写起来像同步代码但不要忘了错误捕获。// async/await 串行与并发 async function loadData() { try { const user await delay(50); const orders await delay(30); console.log(user, orders); } catch (err) { console.error(err); } } async function loadAll() { const results await Promise.all([delay(50), delay(30)]); console.log(results); } loadData(); loadAll();逻辑说明loadData里两个await是串行总耗时约 80 毫秒loadAll用Promise.all并发总耗时约 50 毫秒。try/catch捕获await后面的失败Promise.all只要有一个失败就整体进入catch。参数说明Promise.all接收 Promise 数组返回结果数组顺序和传入顺序一致如果希望拿到全部结果而不被单个失败打断用Promise.allSettled。超时控制常用Promise.race谁先完成就用谁的结果。function withTimeout(promise, ms) { const timeout new Promise((_, reject) { setTimeout(() reject(new Error(请求超时)), ms); }); return Promise.race([promise, timeout]); }逻辑说明withTimeout把业务 Promise 和超时 Promise 放进race业务先完成就返回业务结果超时先到就抛错。参数说明ms是超时阈值要根据接口实际情况设置reject的第一个参数是错误对象不要传字符串否则堆栈信息会丢。5.3 五个高频翻车现场现象、原因、解决下面五条是教程示例跑通后仍容易在真实页面里遇到的问题按现象、原因、解决记排查时直接对号入座。现象控制台报Cannot read properties of null页面完全没反应。原因脚本在目标元素之前执行getElementById返回null后面读属性就炸。解决把script放到body末尾或者用document.addEventListener(DOMContentLoaded, () { ... })包住初始化逻辑拿元素后先判空再操作。现象循环里绑定的回调都输出最后一个索引。原因用var声明循环变量所有回调共享同一个函数级作用域。解决把var改成let每次迭代生成独立绑定或者用立即执行函数把当前索引传进去。这个坑在定时器和事件监听里最常见。现象对象方法作为回调传给setTimeout或事件监听后this变成undefined或全局对象。原因函数被单独取出后调用丢失了原来的调用上下文。解决用bind绑定或者在回调里用箭头函数包一层显式调用对象方法不要指望方法自己记住this。现象Promise 链中间某一步失败控制台出现未捕获拒绝页面没有任何提示。原因只写了then没写catch或者async函数里没包try/catch。解决链式调用末尾统一加catchasync函数内部用try/catch包住awaitfinally里恢复按钮状态和加载提示。现象循环里await十个请求页面等了十秒才出结果。原因await在for循环里逐个等待变成了串行。解决互不依赖的请求用Promise.all并发需要控制并发数就分批或者用for...of配合队列。只有后一个请求依赖前一个结果时才保留串行。6. 上线前十分钟用冒烟脚本验证 DOM 与异步示例是否真跑通教程示例最容易产生“看懂了”的错觉改完 DOM 事件和 Promise 链后我一般会跑一段最小冒烟检查不依赖测试框架直接看页面状态和耗时。// 冒烟检查DOM 节点、异步顺序、并发耗时 async function smokeTest() { const results []; const list document.querySelector(#todoList); results.push([todoList 存在, Boolean(list)]); const order []; order.push(sync); await Promise.resolve().then(() order.push(micro)); setTimeout(() order.push(macro), 0); results.push([微任务先入队, order[1] micro]); const start Date.now(); await Promise.all([ new Promise(r setTimeout(r, 50)), new Promise(r setTimeout(r, 50)) ]); results.push([并发耗时小于 120ms, Date.now() - start 120]); console.table(results); } smokeTest();逻辑说明第一项检查待办列表容器是否存在第二项验证微任务在同步代码后执行第三项用两个 50 毫秒定时器验证并发没有退化成串行。参数说明选择器按实际页面改耗时阈值按机器性能放宽不要把 120 毫秒当成绝对标准它只是用来发现明显的串行问题。检查项预期失败先看关键 DOM 节点非 null脚本位置、选择器拼写微任务顺序micro 在 sync 后Promise 是否漏写 then并发耗时明显小于串行总和是否在循环里逐个 await错误捕获有 catch 或 try/catch链式调用末尾是否兜底血泪经验是控制台没红字不等于流程对页面状态、按钮禁用和接口耗时都要看。从那以后我每次改完事件委托和 Promise 链都强制走一遍这段冒烟脚本再手动点一次真实按钮。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站