简介:《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); // undefined:var 声明被提升,赋值没有 var a = 1; // console.log(b); // ReferenceError:b 在声明前不可访问 let b = 2; const c = 3; // c = 4; // TypeError:const 绑定不能被重新赋值 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会返回"object",NaN也不等于自身。数组用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 找单个元素,找不到返回null;querySelector返回第一个匹配元素,querySelectorAll返回静态NodeList。先判断title是否存在,是为了避免空引用报错。textContent写入纯文本,不会解析标签;innerHTML会解析 HTML,用户输入拼进去就有注入风险。参数说明:classList.add添加类名,不会覆盖原有类;Array.from把类数组转成真数组,后面才能用map、filter。
| 方法 | 返回 | 找不到时 | 适合场景 |
|---|---|---|---|
| getElementById | 单个元素 | null | id 唯一且确定 |
| 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 lang="zh-CN"> <body> <input id="todoInput" placeholder="输入待办" /> <button id="addBtn">添加</button> <ul id="todoList"></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 class="todo-text"></span><button class="delete-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。解决:链式调用末尾统一加catch,async函数内部用try/catch包住await,finally里恢复按钮状态和加载提示。现象:循环里
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 链,都强制走一遍这段冒烟脚本,再手动点一次真实按钮。希望帮到你。
本文还有配套的精品资源,点击获取