
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Wed前端系列个人专栏: 【别传】Web前端开发
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- [一、 初识 JavaScript 与运行机制](#一、 初识 JavaScript 与运行机制)
-
- [1.1 什么是 JavaScript](#1.1 什么是 JavaScript)
- [1.2 网页三剑客的协同关系](#1.2 网页三剑客的协同关系)
- [1.3 JavaScript 的内部运行过程](#1.3 JavaScript 的内部运行过程)
- [1.4 JavaScript 的三大核心组成部分](#1.4 JavaScript 的三大核心组成部分)
- [二、 JS 语法基础与开发前置知识](#二、 JS 语法基础与开发前置知识)
-
- [2.1 书写形式与嵌入规范](#2.1 书写形式与嵌入规范)
-
- [2.1.1 行内式](#2.1.1 行内式)
- [2.1.2 内嵌式](#2.1.2 内嵌式)
- [2.1.3 外部式](#2.1.3 外部式)
- [2.2 注释规范与快捷键](#2.2 注释规范与快捷键)
- [2.3 基础输入与输出(日志调试的核心)](#2.3 基础输入与输出(日志调试的核心))
- [三、 变量与动态类型系统](#三、 变量与动态类型系统)
-
- [3.1 变量的声明、初始化与使用](#3.1 变量的声明、初始化与使用)
-
- [3.1.1 基本用法](#3.1.1 基本用法)
- [3.1.2 变量的读取与修改](#3.1.2 变量的读取与修改)
- [3.1.3 交互式变量案例](#3.1.3 交互式变量案例)
- [3.2 动态类型系统的核心特征](#3.2 动态类型系统的核心特征)
- [四、 核心基本数据类型详解](#四、 核心基本数据类型详解)
-
- [4.1 number 数字类型](#4.1 number 数字类型)
-
- [4.1.1 进制表示法](#4.1.1 进制表示法)
- [4.1.2 特殊的数值常量](#4.1.2 特殊的数值常量)
- [4.2 string 字符串类型](#4.2 string 字符串类型)
-
- [4.2.1 引号嵌套与转义规则](#4.2.1 引号嵌套与转义规则)
- [4.2.2 长度获取与字符串拼接](#4.2.2 长度获取与字符串拼接)
- [4.3 boolean 布尔类型与非科学运算](#4.3 boolean 布尔类型与非科学运算)
- [4.4 undefined 与 null 的深度辨析](#4.4 undefined 与 null 的深度辨析)
- [五、 运算符与控制流控制](#五、 运算符与控制流控制)
-
- [5.1 比较运算符中的隐式转换坑点](#5.1 比较运算符中的隐式转换坑点)
- [5.2 分支控制结构](#5.2 分支控制结构)
-
- [5.2.1 if 语句多分支规范](#5.2.1 if 语句多分支规范)
- [5.2.2 闰年判定算法实现](#5.2.2 闰年判定算法实现)
- [5.2.3 三元表达式与 switch 分支](#5.2.3 三元表达式与 switch 分支)
- [5.3 循环控制:while、continue 与 break](#5.3 循环控制:while、continue 与 break)
- [六、 迈向 WebAPI:DOM 基础与元素获取](#六、 迈向 WebAPI:DOM 基础与元素获取)
-
- [6.1 什么是 WebAPI](#6.1 什么是 WebAPI)
- [6.2 DOM 树的核心概念](#6.2 DOM 树的核心概念)
- [6.3 现代化全能元素选择器:querySelector](#6.3 现代化全能元素选择器:querySelector)
-
- [6.3.1 选择首个匹配元素(querySelector)](#6.3.1 选择首个匹配元素(querySelector))
- [6.3.2 选择所有匹配元素(querySelectorAll)](#6.3.2 选择所有匹配元素(querySelectorAll))
- [七、 事件初识与核心三要素](#七、 事件初识与核心三要素)
-
- [7.1 什么是事件与浏览器哨兵机制](#7.1 什么是事件与浏览器哨兵机制)
- [7.2 事件的核心三要素](#7.2 事件的核心三要素)
- [7.3 事件绑定(注册)代码规范](#7.3 事件绑定(注册)代码规范)
- [八、 DOM 元素与节点的高级操作指南](#八、 DOM 元素与节点的高级操作指南)
-
- [8.1 元素内容的读写操纵:innerText vs innerHTML](#8.1 元素内容的读写操纵:innerText vs innerHTML)
-
- [8.1.1 innerText](#8.1.1 innerText)
- [8.1.2 innerHTML](#8.1.2 innerHTML)
- [8.2 元素标准属性的操控](#8.2 元素标准属性的操控)
- [8.3 表单元素属性(Input 状态控制)](#8.3 表单元素属性(Input 状态控制))
-
- [8.3.1 经典案例:播放与暂停状态切换按钮](#8.3.1 经典案例:播放与暂停状态切换按钮)
- [8.3.2 经典案例:点击实现文本框数值递增计数器](#8.3.2 经典案例:点击实现文本框数值递增计数器)
- [8.3.3 进阶高阶案例:表格/列表的"全选与取消全选"算法实现](#8.3.3 进阶高阶案例:表格/列表的“全选与取消全选”算法实现)
- [8.4 元素样式属性的精准修改](#8.4 元素样式属性的精准修改)
-
- [8.4.1 style 行内样式操作](#8.4.1 style 行内样式操作)
- [8.4.2 className 类名样式操作](#8.4.2 className 类名样式操作)
- [8.5 节点的动态增删改查机制](#8.5 节点的动态增删改查机制)
-
- [8.5.1 新增节点两步走](#8.5.1 新增节点两步走)
- [8.5.2 插入节点的高级控制](#8.5.2 插入节点的高级控制)
- [8.5.3 节点的安全移除(removeChild)](#8.5.3 节点的安全移除(removeChild))
概要&序論
Hello大家好,我是此方。本文深度解构 JavaScript(JS)的核心语法基础与 WebAPI 机制。
- 阐述 JS 解释型语言特征、浏览器内核/引擎运行期的数据流向与三大核心组成部分;
- 梳理行内、内嵌、外部式书写规范,以及动态类型系统的底层运作与转换机制;
- 详解
number、string、boolean、undefined与null等基本数据类型的边界常量与深度辨析;- 剖析全等与等值比较中的隐式转换坑点,以及分支、循环等控制流的规整度判定;
- 揭示 WebAPI 核心,掌握利用全能选择器进行 DOM 树节点与元素的精细化控制;
- 解构事件三要素的哨兵机制,并针对表单、全选复选框、级联样式切换以及节点动态增删改查实现多套核心算法。
好的,我们直接开始。
一、 初识 JavaScript 与运行机制
1.1 什么是 JavaScript
JavaScript(简称 JS)是当今世界上最流行、应用最广泛的脚本编程语言之一。它是一种解释型语言,代码不需要预先编译,而是通过解释器逐行读取并运行。
javascript
// JS 的经典问候程序
console.log("Hello, JavaScript!");
最初,JavaScript 由布兰登·艾奇(Brendan Eich)在 1995 年利用短短 10 天时间设计完成。由于早期设计周期极短,语言的一些底层细节在初期考虑得不够严谨,导致在诞生后的很长一段时间内,复杂的 JS 程序往往显得混乱不堪。最初在网景(Netscape)公司内部它被命名为 LiveScript,后因 Java 语言大火,为了借助其热度进行推广,商业上将其改名为 JavaScript。事实上,Java 和 JavaScript 之间的语法风格与设计理念相去甚远,它们之间的关系就像雷锋和雷峰塔、印度和印度尼西亚、周杰和周杰伦、或者黑客和博客一样,毫无实质性血缘关系。
随着时间的推移与 ECMAScript 标准的不断更迭,现代 JavaScript 早已蜕变为一门全栈式的通用编程语言。它所涉及的领域极其广泛:
- 网页开发:在前端通过复杂的特效与丰富灵活的用户交互控制页面的灵魂。
- 网页游戏开发:借助 Canvas 或 WebGL 开发高性能的在线互动游戏。
- 服务器开发:依托 Node.js 运行环境,JS 得以在后端大放异彩,处理高并发网络服务。
- 桌面程序开发:利用 Electron 框架,诸如 VSCode 等大厂主流桌面应用得以高效构建。
- 手机 App 开发:通过各类跨平台移动开发框架,实现一套代码多端运行。
1.2 网页三剑客的协同关系
在传统 Web 前端开发中,HTML、CSS 和 JavaScript 共同构成了一个完整网页的核心。它们各司其职,形成了紧密的协同关系:
- HTML(结构):网页的结构与骨架。它负责定义页面上有哪些文本、图片、输入框或按钮等基础内容。
- CSS(表现):网页的表现与皮肤。它负责控制这些骨架元素的颜色、尺寸、布局、边距以及整体视觉美化。
- JavaScript(行为):网页的行为与魂魄。它负责赋予静态页面动态交互的能力,比如点击按钮弹窗、异步拉取网络数据、以及复杂的动画控制。
1.3 JavaScript 的内部运行过程
理解 JavaScript 代码是如何在计算机中运行的,对于编写高性能的程序至关重要。其核心步骤主要遵循以下数据流向:
- 外存存储 :编写好的
.js或.html代码以文本形式保存在硬盘(外存)上。 - 加载内存 :双击 HTML 文件或启动网页时,浏览器(应用程序)会读取该文件,并把文件内容加载到系统的运行内存中(数据流向:硬盘 ⇒ \Rightarrow ⇒ 内存)。
- 指令翻译(JS 引擎) :浏览器内部包含两个核心引擎------渲染引擎 (负责解析 HTML 和 CSS,俗称浏览器内核)与 JS 引擎(即 JS 解释器,如 Chrome 内置的著名高性能 V8 引擎)。JS 引擎逐行读取内存中的 JS 源码,将其解析并翻译成计算机底层能够识别的二进制机器指令。
- CPU 执行 :翻译得到的二进制指令被加载到 CPU(中央处理器)中进行最终的运算与执行(数据流向:内存 ⇒ \Rightarrow ⇒ CPU)。
1.4 JavaScript 的三大核心组成部分
在浏览器端运行的现代 JavaScript,其完整的体系结构主要由以下三部分组成:
- ECMAScript(ES):JavaScript 的基础语法部分。它规定了变量声明、数据类型、运算符、条件分支、循环结构、函数、对象等纯逻辑层面的"标准"。这是一套所有 JS 引擎都必须严格遵守的铁律,属于基础内功。
- DOM(文档对象模型):提供了操作页面结构的 API。利用 DOM,程序员可以随心所欲地增删改查网页中的 HTML 标签、修改文本、更改属性等。
- BOM(浏览器对象模型):提供了操作浏览器窗口的 API。允许程序员操控浏览器窗口的弹窗、获取屏幕分辨率、控制页面重定向、读取浏览器历史记录等。
需要说明的是,如果脱离了浏览器端(例如在 Node.js 服务端运行),JavaScript 将主要依赖 Node.js 提供的各类底层 API(如文件系统 fs、网络模块 http 等),此时就不再包含 DOM 和 BOM 部分。
二、 JS 语法基础与开发前置知识
2.1 书写形式与嵌入规范
在 HTML 页面中引入并书写 JavaScript 代码,主要有以下三种规范形式:
2.1.1 行内式
直接将少量的 JS 代码嵌入到 HTML 元素的事件属性内部。
html
<input type="button" value="点我一下" onclick="alert('haha')">
规范注意:HTML 标准中属性值推荐使用双引号,为了防止混淆,JS 内部的字符串常量推荐使用单引号包裹。行内式由于可读性差、代码耦合度高,在实际生产开发中极少推荐使用。
2.1.2 内嵌式
将 JS 代码集中书写在 HTML 文件的 <script> 标签内部。
html
<script>
alert("haha");
</script>
这种形式适合用于编写一些中等体量的实验性代码或单页面演示 Demo。
2.1.3 外部式
将 JS 代码剥离到一个纯粹的 .js 后缀独立文件中,再通过 HTML 的 <script> 标签的 src 属性进行异步或同步引入。
html
<script src="hello.js"></script>
核心避坑指南 :一旦 <script> 标签指定了 src 属性引入了外部文件,那么该标签中间就绝对不能再编写任何自定义的内联 JS 代码了。即使写了,浏览器也只会加载外部文件而完全忽略内部的代码。
2.2 注释规范与快捷键
代码注释是提高程序可读性和便于团队维护的终极利器。JavaScript 支持以下两种注释方式:
- 单行注释 :使用
//符号,其右侧当前行的所有内容都会被解释器忽略。通常建议在代码调试或简短说明时使用。 - 多行注释 :使用
/*开头,并以*/结尾。可以跨越多行书写。
javascript
// 我是单行注释,建议使用
/* 我是多行注释
我是多行注释
注意:多行注释绝对不能发生嵌套,否则会导致编译/解析报错!
*/
在主流编辑器(如 VSCode)中,可以使用快捷键 Ctrl + / 快速切换单行注释状态。
2.3 基础输入与输出(日志调试的核心)
JS 与外界或程序员沟通的基础手段有以下三种:
- 输入框(prompt):弹出一个带有输入提示的交互框,用于接收用户输入的字符串。
javascript
// 弹出一个输入框
prompt("请输入您的姓名:");
- 警示框(alert):弹出一个同步阻塞式的对话框,向用户显示一些警告或提示信息。
javascript
// 弹出一个输出框
alert("客户操作提示");
- 控制台日志(console.log):在浏览器的开发者工具控制台中打印输出一条日志。
javascript
// 向控制台输出日志
console.log("这是一条供程序员调试的日志");
开发技巧 :在 VSCode 中,直接输入字母 log 然后按下 Tab 键,即可触发代码片段自动生成完整的 console.log();。运行后需要按下键盘的 F12 打开浏览器的开发者工具,切换到 Console 标签页查看。
日志是程序员调试程序的最核心武器。如果程序运行不符合预期,通过打印日志能精准捕获每一时刻变量的真实状态。相比于医生需要看 CT 和血常规报告,程序员在面对未知 Bug 时的终极绝招往往是:"多打几条日志看看到底哪一步歪了,实在不行重启试试"。
三、 变量与动态类型系统
3.1 变量的声明、初始化与使用
在 JavaScript 中,变量是用来存储数据的内存容器(盒子)。
3.1.1 基本用法
最传统的声明变量方式是使用 var 关键字(现代标准推荐使用更严谨的 let 关键字控制块级作用域)。
javascript
// 声明并初始化变量
var name = 'zhangsan';
var age = 20;
var/let是 JS 内部的关键字,用来向系统申请一段内存空间。=是赋值运算符,其含义是将右侧的数据装入左侧定义的变量盒子中。- 变量名两侧与赋值号之间建议留出一个空格,以符合行业代码美化规范。每一行语句结尾推荐显式加上英文分号
;。
3.1.2 变量的读取与修改
javascript
console.log(age); // 读取变量盒子里的内容并打印:20
age = 30; // 修改变量内容,旧值被覆盖
console.log(age); // 再次读取,结果为 30
3.1.3 交互式变量案例
javascript
// 提示用户输入信息并赋值给变量
var userName = prompt("请输入姓名:");
var userAge = prompt("请输入年龄:");
var userScore = prompt("请输入分数");
// 使用 + 进行多段字符串拼接,\n 表示换行控制
alert("您的姓名是:" + userName + "\n" +
"您的年龄是:" + userAge + "\n" +
"您的分数是:" + userScore + "\n");
3.2 动态类型系统的核心特征
JavaScript 是一门典型的动态类型语言。这一特性使它与 C++、Java、Go 等静态类型语言有着本质的区别:
- 运行期决定类型 :在变量最初创建时,不需要像 C++ 那样显式指定
int或string。变量的具体类型是在程序实际运行到赋值语句(等号右侧)那一刻才被最终确定的。 - 类型可随时流转:随着程序的深入执行,同一个变量可以被重新赋值为其他完全不同类型的数据,其数据类型也会自动随之改变。
javascript
var demoVariable = 10; // 此时 demoVariable 是 number 数字类型
demoVariable = "hehe"; // 随着程序运行,变量类型自动变成了 string 字符串类型
在静态类型语言中,这种操作在编译阶段就会直接引发严重的类型不匹配报错。动态类型赋予了 JS 极高的灵活性,但同时也隐含了运行期类型混乱的风险,因此现代大型项目往往会拥抱 TypeScript 从而增强类型安全。
四、 核心基本数据类型详解
JavaScript 中内置了几种最基本的数据类型,主要包括:number(数字)、string(字符串)、boolean(布尔)、undefined(未定义)以及 null(空值)。
4.1 number 数字类型
4.1.1 进制表示法
JS 的数字类型极其特殊,它不细分 int 整数或 float / double 浮点数,所有的数字统一都是"数字类型",底层一律采用双精度浮点数(64位)进行存储。
在代码中,我们可以使用多种机制来表示不同进制的数字:
javascript
var numOctal = 07; // 八进制整数,以数字 0 开头
var numHex = 0xa; // 十六进制整数,以 0x 或 0X 开头
var numBin = 0b10; // 二进制整数,以 0b 或 0B 开头
4.1.2 特殊的数值常量
数字类型中包含三个特殊的边界数值:
Infinity:正无穷大。代表数值已经超过了 JavaScript 能够表示的最大上限(Number.MAX_VALUE)。-Infinity:负无穷大。数值小于 JS 所能表达的极限负值。NaN(Not a Number):非数字。表示一个本来应该返回数字的算术运算在执行时发生了非法操作。
javascript
var maxVal = Number.MAX_VALUE;
console.log(maxVal * 2); // 越界,输出:Infinity
console.log(-maxVal * 2); // 越界,输出:-Infinity
console.log('hehe' - 10); // 字符串减数字无法计算,输出:NaN
特殊坑点提醒 :如果是 'hehe' + 10,执行结果绝对不是 NaN。因为 + 运算符在遇到字符串时,会触发隐式类型转换 ,自动将数字 10 转成字符串 '10',然后进行字符串首尾拼接,最终输出 'hehe10'。
我们可以使用内置的全局函数 isNaN() 来准确判定一个变量或表达式是否为"非数字值":
javascript
console.log(isNaN(10)); // 10是数字,返回 false
console.log(isNaN('hehe' - 10)); // 结果是 NaN,返回 true
4.2 string 字符串类型
4.2.1 引号嵌套与转义规则
字符串是用单引号 '' 或双引号 "" 包裹起来的任意文本。如果字符串内容本身就带有引号,必须采取配套或转义的手段:
javascript
// 错误写法:var msg = "My name is "zhangsan""; -> 报错引发语法异常
var msg1 = "My name is \"zhangsan\""; // 正确:使用反斜杠 \ 转移内部双引号
var msg2 = "My name is 'zhangsan'"; // 正确:外部双引号,内部单引号
var msg3 = 'My name is "zhangsan"'; // 正确:外部单引号,内部双引号
4.2.2 长度获取与字符串拼接
利用字符串对象的 .length 属性可以获取该字符串包含的字符总总数(注意:无论中文字符还是英文字母,皆作为一个字符计算):
javascript
var str1 = 'hehe';
console.log(str1.length); // 输出 4
var str2 = '哈哈';
console.log(str2.length); // 输出 2
使用 + 可以拼接任意两段数据:
javascript
console.log('my name is ' + 'zhangsan'); // "my name is zhangsan"
console.log('my score is ' + 100); // "my score is 100" (隐式转换)
console.log(100 + 100); // 200 (纯数字相加)
console.log('100' + 100); // "100100" (一侧为字符串,发生拼接)
4.3 boolean 布尔类型与非科学运算
布尔类型只有两个字面值:true(真)和 false(假)。它经常作为各种控制流语句(if、while)的条件判断。
需要警惕的是,在 JavaScript 早期历史中,布尔类型在参与纯数学算术运算时,会被默认当成数字 1 和 0 来进行计算:
javascript
console.log(true + 1); // 输出 2
console.log(false + 1); // 输出 1
这种设计在严谨的系统开发中是非常不科学且危险的,实际项目编写时应绝对杜绝让布尔值直接参与算术加减的操作。
4.4 undefined 与 null 的深度辨析
这两个类型经常导致初学者混淆,因为它们都带有某种"空缺"的语义,但它们的侧重点有着本质的区别:
undefined:未定义类型 。如果一个变量仅被声明了,但从未被赋予过初始值 ,系统默认分配给它的就是undefined。这代表着"连装东西的盒子都还没有建好,或者根本不存在这个标识符"。null:空值类型 。代表变量盒子已经准备妥当了,但目前程序员显式地往里面塞入了一个空标识。它往往用来表示一个对象的占位符。
javascript
var a;
console.log(a); // 声明未初始化,输出:undefined
console.log(a + "10"); // 发生拼接,输出:"undefined10"
console.log(a + 10); // 无法与数字相加,输出:NaN
var b = null;
console.log(b + 10); // null 参与算术运算被视为 0,输出:10
console.log(b + "10"); // 发生拼接,输出:"null10"
五、 运算符与控制流控制
5.1 比较运算符中的隐式转换坑点
JS 的算术、赋值、位运算和逻辑运算符与主流语言大同小异。但在比较运算符中,存在一个关于全等和相等的历史经典设计:
==(等值比较):会首先尝试将两边的操作数进行隐式类型转换,然后再去判断它们的值是否相等。===(全等比较):严格判定。不进行任何隐式类型转换,只有当两边的数据类型 与具体值 全部完全一模一样时,才返回true。
javascript
console.log('10' == 10); // 隐式将字符串转为数字,返回:true
console.log('10' === 10); // 类型不一致(string vs number),返回:false
console.log('10' !== 10); // 全等不成立,返回:true
在大厂代码开发规范中,为了规避隐式转换带来的不可控 Bug,推荐全部统一使用 === 和 !==。
5.2 分支控制结构
5.2.1 if 语句多分支规范
javascript
var num = 10;
// 奇偶性判定核心逻辑(考虑负数,不推荐直接 == 1)
if (num % 2 === 0) {
console.log("num 是偶数");
} else {
console.log("num 是奇数");
}
// 多分支逻辑
if (num > 0) {
console.log("num 是正数");
} else if (num < 0) {
console.log("num 是负数");
} else {
console.log("num 是 0");
}
5.2.2 闰年判定算法实现
javascript
var year = 2000;
if (year % 100 === 0) {
// 世纪闰年判定
if (year % 400 === 0) {
console.log("是闰年");
} else {
console.log("不是闰年");
}
} else {
// 普通闰年判定
if (year % 4 === 0) {
console.log("是闰年");
} else {
console.log("不是闰年");
}
}
5.2.3 三元表达式与 switch 分支
三元表达式是简单的 if-else 的缩写语法,格式为 条件 ? 表达式1 : 表达式2。注意它的运算符优先级非常低。
对于确定性多分支,switch-case 结构具有更高的执行效率和可读性:
javascript
var day = prompt("请输入今天星期几(1-7): ");
// prompt 接收到的是字符串,必须通过 parseInt 转化为整数进行精确匹配
switch (parseInt(day)) {
case 1:
console.log("星期一");
break;
case 2:
console.log("星期二");
break;
case 3:
console.log("星期三");
break;
case 4:
console.log("星期四");
break;
case 5:
console.log("星期五");
break;
case 6:
console.log("星期六");
break;
case 7:
console.log("星期日");
break;
default:
console.log("您的输入有误,请输入1-7之间的数字");
}
5.3 循环控制:while、continue 与 break
while循环在循环条件为真时,重复执行内部代码块。continue用来立即跳出本次循环,直接进入下一次的条件判定。break用来立即彻底终止并强行跳出整个循环体。
javascript
// 示例1:打印 1-10 的数字
var loopNum = 1;
while (loopNum <= 10) {
console.log(loopNum);
loopNum++;
}
// 示例2:寻找 100-200 之间所有 3 的倍数(展示 continue)
var current = 100;
while (current <= 200) {
if (current % 3 !== 0) {
current++;
continue; // 不是3的倍数,扔掉,提前结束本次循环
}
console.log("找到3的倍数:" + current);
current++;
}
六、 迈向 WebAPI:DOM 基础与元素获取
6.1 什么是 WebAPI
只掌握 ECMAScript 基础语法,就像练武扎好了马步,具备了基础的逻辑思维,但还无法直接写出具有精美动态交互的现代化网页。这就需要引入 WebAPI 。
WebAPI 是由 W3C 组织制定的规范标准。它本质上就是浏览器提供给 JS 开发人员的一套现成的工具箱(包含大量的全局对象与函数),让我们拿来即用。WebAPI 主要由两大核心构成:
- DOM API:专门用来控制、增删改查页面的结构与内容。
- BOM API:专门用来控制和感知浏览器窗口的行为。
6.2 DOM 树的核心概念
DOM 的全称为 Document Object Model(文档对象模型) 。整个 HTML 页面在浏览器内核解析时,会被映射为一棵树状数据结构,称之为 DOM 树 。
在这棵树中,我们需要牢记以下三个最核心的骨干概念:
- 文档(document) :一个完整的 HTML 网页就是一个独立的文档,在 JS 代码中统一由全局内置对象
document实例进行表示。 - 元素(element) :网页中所有被各类标签包裹的内容(如
<div>、<a>、<p>)都称为元素。 - 节点(node):网页中更广泛的内容基类。包括标签节点(元素节点)、注释节点、文本节点(单纯的换行、空格和文字)、属性节点等。在 DOM 模型中,所有这些概念最终都会对应成 JS 代码中的一个个对象,通过操作这些对象,我们就能实时操纵页面。
6.3 现代化全能元素选择器:querySelector
在过去的 DOM 开发中,我们需要使用 getElementById、getElementsByClassName 等一长串繁琐的方法来抓取标签。现代 HTML5 标准推出了极其强大的元素捕获 API,它们完全复用了 CSS 选择器的语法规则:
6.3.1 选择首个匹配元素(querySelector)
javascript
// 1. 获取页面中匹配该类选择器的【第一个】Element对象
var elemBox = document.querySelector('.box');
console.log(elemBox);
// 2. 获取匹配该ID选择器的元素对象
var elemId = document.querySelector('#id');
console.log(elemId);
// 3. 后代选择器连写:抓取 h3 内部 span 内部的 input 标签
var elemInput = document.querySelector('h3 span input');
console.log(elemInput);
核心警示 :因为 querySelector 传入的参数是完全标准的 CSS 选择器字符串,所以如果是类名,前面必须 带上点号 .;如果是 ID,前面必须 带上井号 #。
6.3.2 选择所有匹配元素(querySelectorAll)
javascript
// 获取页面中所有的 <div> 标签
var allDivs = document.querySelectorAll('div');
// 返回的是一个伪数组(NodeList),包含了所有查找到的 div 对象
console.log(allDivs);
七、 事件初识与核心三要素
7.1 什么是事件与浏览器哨兵机制
JavaScript 想要构建高度交互的动态页面,就必须实时感知到用户的操作行为。用户在网页上触发的点击、选择、文本修改、鼠标悬停等动作,在浏览器底层都会产生一个事件 。
我们可以把浏览器想象成一个在前线帮我们冲锋侦查的"哨兵"。一旦前线有敌情(用户触发了某个特定的点击动作),哨兵就会点燃烽火台上的狼烟(触发事件),我们在后方大本营编写的代码就可以根据具体的狼烟类型,来决定下一步的对敌反击策略。
7.2 事件的核心三要素
每次注册或处理一个完整的事件,程序员必须清晰地定位并明确以下三要素:
- 事件源:到底是页面上的哪一个 HTML 元素标签触发了这个动作。
- 事件类型 :具体触发了什么行为。是点击(
click)、双击、鼠标移入,还是表单内容被修改(change)。 - 事件处理程序 :当事件发生后,我们要怎么应对。它在代码层面上往往对应一个回调函数。
7.3 事件绑定(注册)代码规范
html
<button id="btn">点我一下</button>
<script>
// 第一步:获取事件源元素
var myBtn = document.getElementById('btn');
// 第二步:通过事件源的 onclick 属性绑定事件类型,并指定一个匿名回调函数作为处理程序
myBtn.onclick = function () {
alert("hello world"); // 当用户真正点击按钮时,该代码由浏览器自动调用执行
};
</script>
重要机制理解 :这里绑定的匿名函数被称为回调函数(Callback Function) 。这个函数绝对不需要我们程序员在代码中主动写 myBtn.onclick() 去调用它。我们要做的是把它完全移交给浏览器,浏览器会在合适的时机(用户真正用鼠标按下了该按钮的那一瞬间)自动帮我们执行。
八、 DOM 元素与节点的高级操作指南
8.1 元素内容的读写操纵:innerText vs innerHTML
在获取到某个 DOM 元素对象后,如果想要修改或读取该标签内部包含的内容,主要有两种手段,但它们在对 HTML 标签的识别和源码格式保留上有着巨大分野:
8.1.1 innerText
- 读取操作:只会提取当前节点及其所有后代节点的纯文本内容,完全忽略并过滤掉任何 HTML 结构标签。
- 写入操作:不能识别 HTML 标签 。如果你尝试将带有标签的字符串赋值给它,它会把所有的尖括号
<span>当作纯文本、原封不动地渲染在页面上。此外,它属于非 W3C 标准(由老一代 IE 浏览器发起)。
javascript
var divBox = document.querySelector('div');
// 读操作
console.log(divBox.innerText);
// 写操作:界面上会直接显示一串带有尖括号的纯文本
divBox.innerText = 'hello js <span>hello js</span>';
8.1.2 innerHTML
- 读取操作:不仅获取纯文本,还会将该节点内部所有的 HTML 结构、换行和空格原汁原味地保留并读取出来。
- 写入操作:能够完美识别并解析 HTML 标签。它属于官方 W3C 标准,在日常前端开发中其应用场景比 innerText 更加广泛。
javascript
var divBox = document.querySelector('div');
// 读操作:连同内部的标签结构一起输出
console.log(divBox.innerHTML);
// 写操作:浏览器会把 <span> 标签解析成真正的底层 DOM 节点并渲染
divBox.innerHTML = '<span>hello js</span>';
8.2 元素标准属性的操控
我们可以直接通过 JS 暴露的 Element 对象属性,来实时增删改查 HTML 标签的各属性值,这会直接同步反映到网页的显隐状态上。
html
<img src="rose.jpg" alt="这是一朵花" title="玫瑰花">
<script>
var myImg = document.querySelector('img');
// 1. 读取当前图片的各属性值
console.log(myImg.src); // 打印绝对/相对路径
console.log(myImg.title); // 打印 "玫瑰花"
console.log(myImg.alt); // 打印 "这是一朵花"
// 2. 点击图片实现双状态来回切换核心逻辑
myImg.onclick = function () {
// 通过判定字符串中是否包含某图片关键字来实现状态分流
if (myImg.src.lastIndexOf('rose.jpg') !== -1) {
myImg.src = './rose2.png'; // 切换到第二张图
} else {
myImg.src = './rose.jpg'; // 切回第一张图
}
};
</script>
8.3 表单元素属性(Input 状态控制)
表单标签(尤其是 <input> 标签)拥有一些非常特殊的关键属性。这些属性的状态无法通过 innerHTML 控制,必须通过专属的布尔值或 value 属性进行精准更改:
value:控制和代表输入框当前输入的文本内容,或者普通按钮上显示的文字。disabled:布尔值。控制表单是否被置灰禁用(true禁用,false启用)。checked:布尔值。控制单选/复选框的勾选状态。type:控制输入框的类型(如在text文本和password密码密文类型之间切换)。
8.3.1 经典案例:播放与暂停状态切换按钮
html
<input type="button" value="播放">
<script>
var playBtn = document.querySelector('input');
playBtn.onclick = function () {
if (playBtn.value === '播放') {
playBtn.value = '暂停';
} else {
playBtn.value = '播放';
}
};
</script>
8.3.2 经典案例:点击实现文本框数值递增计数器
html
<input type="text" id="text" value="0">
<input type="button" id="btn" value="点我+1">
<script>
var textInput = document.querySelector('#text');
var addBtn = document.querySelector('#btn');
addBtn.onclick = function () {
// 注意:从表单 value 获取到的永远是字符串类型!
// 必须在前面加上单目运算符 + 或者使用 parseInt() 将其强制转换为真正的 number 数字
var currentNum = +textInput.value;
currentNum++;
textInput.value = currentNum; // 将计算后的数字写回输入框,同步更新界面显示
};
</script>
8.3.3 进阶高阶案例:表格/列表的"全选与取消全选"算法实现
这是一个经典的电商或后台管理系统交互。核心逻辑包含两部分:
- 点击全选框,所有下属子复选框的选中状态自动跟着全选框保持一致。
- 每一个子复选框在点击时,都要实时触发一次"全反例扫描":只要发现有一个子复选框没有被勾选,全选框立即取消勾选;只有当所有子框全部被选中时,全选框才自动亮起。
html
<input type="checkbox" id="all"> 我全都要 <br>
<input type="checkbox" class="girl"> 貂蝉 <br>
<input type="checkbox" class="girl"> 小乔 <br>
<input type="checkbox" class="girl"> 安琪拉 <br>
<input type="checkbox" class="girl"> 妲己 <br>
<script>
// 1. 获取全选大框以及所有的子复选框列表
var allSelect = document.querySelector('#all');
var girlSelects = document.querySelectorAll('.girl');
// 2. 联动一:给全选框绑定点击事件
allSelect.onclick = function () {
// 遍历所有子框,让它们的 checked 状态直接等于当前全选大框的 checked 状态
for (var i = 0; i < girlSelects.length; i++) {
girlSelects[i].checked = allSelect.checked;
}
};
// 3. 联动二:为每一个子复选框循环绑定各自的独立点击事件
for (var i = 0; i < girlSelects.length; i++) {
girlSelects[i].onclick = function () {
// 每次点击子框,都调用 checkGirls 函数执行全量反例检索,并把结果直接回写给全选大框
allSelect.checked = checkGirls(girlSelects);
};
}
// 4. 扫描核心算法函数
function checkGirls(selectList) {
for (var i = 0; i < selectList.length; i++) {
if (!selectList[i].checked) {
// 只要在列表中发现任何一个子框是没选中的(找到了反例),全选不成立,立刻提前返回 false
return false;
}
}
// 如果循环走完了,都没有触发上面的 if,说明完全没有反例,判定全部选中,返回 true
return true;
}
</script>
8.4 元素样式属性的精准修改
8.4.1 style 行内样式操作
直接修改行内样式,其底层原理是通过 JS 在 HTML 标签上强行注入 style="..." 属性。这种方式优先级最高,适合于修改少量、特定的单一样式。
驼峰命名转换法则 :在 CSS 中所有带连字符的样式属性(如 font-size、background-color),在 JS 中必须全部转换为驼峰命名法 (对应的简写名称为 fontSize、backgroundColor)。
html
<div style="font-size: 20px; font-weight: 700;">点击文字放大字体</div>
<script>
var fontDiv = document.querySelector('div');
fontDiv.onclick = function () {
// this 关键字在事件回调函数中指向当前触发该事件的 DOM 元素本身
// 使用 parseInt 剥离出纯数字(如 "20px" -> 20)
var curFontSize = parseInt(this.style.fontSize);
curFontSize += 10;
this.style.fontSize = curFontSize + "px"; // 拼接 px 单位后写回
};
</script>
8.4.2 className 类名样式操作
如果要同时修改或切换的视觉样式非常之多(例如一键开启夜间模式),如果再用上面的驼峰单写,代码会变得极度臃肿。此时最好的解决方案是在 CSS 文件中预先写好一个控制类名,通过 JS 修改元素的 className 来实现一键批量样式切换。
规范注意 :因为在 JavaScript 语言中 class 是原生的保留字,所以在 DOM API 中将其改名为 className。
html
<div class="container light">
这是一大段话,点击页面可以切换日间/夜间模式。<br>
</div>
<style>
html, body { width: 100%; height: 100%; }
.container { width: 100%; height: 100%; }
/* 日间日盲底色 */
.light { background-color: #f3f3f3; color: #333; }
/* 夜间黑暗底色 */
.dark { background-color: #333; color: #f3f3f3; }
</style>
<script>
var modeDiv = document.querySelector('div');
modeDiv.onclick = function () {
console.log(modeDiv.className);
// 通过检测当前类名中是否包含 light 关键字来实现昼夜流转
if (modeDiv.className.indexOf('light') !== -1) {
modeDiv.className = 'container dark'; // 批量替换为黑夜样式
} else {
modeDiv.className = 'container light'; // 还原为白天样式
}
};
</script>
8.5 节点的动态增删改查机制
操作 DOM 树节点的增加,必须严格遵循完备的"两步走机制"。通俗地讲,第一步相当于"把孩子生出来"(创建节点),第二步相当于"给孩子上户口挂籍"(将节点切实插入到 DOM 树中)。如果漏掉第二步,新创建的标签只会在内存中悬空,无法在网页上被用户看到。
8.5.1 新增节点两步走
html
<div class="container"></div>
<script>
// 步骤一:创建全新的元素节点对象
var newDiv = document.createElement('div');
newDiv.id = 'mydiv';
newDiv.className = 'box';
newDiv.innerHTML = '我是新创建的动态文本';
console.log(newDiv); // 此时在控制台可以看到对象,但页面上空空如也
// 步骤二:获取准备挂载的父节点容器,将其作为最后一个子节点强行插入
var parentContainer = document.querySelector('.container');
parentContainer.appendChild(newDiv); // 执行后页面上立刻展现新元素!
</script>
8.5.2 插入节点的高级控制
除了追加到末尾的 appendChild,DOM 还提供了 insertBefore 允许我们将新节点精准插到某一个指定引用节点的前面:
html
<div class="list-container">
<div>11</div>
<div>22</div>
</div>
<script>
var insertDiv = document.createElement('div');
insertDiv.innerHTML = '我是插队的新节点';
var parentList = document.querySelector('.list-container');
// 获取当前父容器下现有的子元素节点数组列表
// 把它精准插入到当前的 0 号子节点(即 11)的前面
parentList.insertBefore(insertDiv, parentList.children[0]);
</script>
高级插入避坑特性:
- 单一实体移动特性 :如果在代码中针对同一个已经存在的节点对象连续调用了两次插入方法(例如先插到0号,再插到2号),它绝对不会克隆复制出两个元素。它的底层表现是"移动",即最后一次生效,会将旧位置的节点直接拔除并瞬移到新位置。
- 动态内容同步连动:一旦一个节点已经被成功挂载上 DOM 树,在代码里只要继续修改这个节点对象的属性或 innerHTML,网页前端会立刻实时联动更新。
8.5.3 节点的安全移除(removeChild)
javascript
// 语法格式:被删除的旧节点 = 父节点.removeChild(待删除的目标子节点);
var oldChild = parentNode.removeChild(childNode);
内存特性深度解析 :当调用 removeChild 从页面结构中把某个标签砍掉后,该节点只是在前端的 DOM 树中被移除了。在计算机底层,它依然完整地驻留在内存中 。这意味着,如果你在后续的代码中由于某种交互又需要它了,可以随时重新调用 appendChild 将其重新插入挂载到 DOM 树的任何其他新位置。如果强行传入一个根本不是其亲生子节点的参数,方法会直接抛出严重的运行期异常。
好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!