33.JS高级-BOM与DOM在现代开发的应用

405 阅读44分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 从架构的角度来学习我们的BOM、DOM以及事件监听,这么多的方法、属性、事件,都归属于谁,继承于谁?
    • 当通过架构图来进行理解时,也许就一目了然了,原来他们是继承自你啊?怪不得好像好多地方都能见到你的影子
    • 继承者可以使用被继承者的内容,而被继承者是没办法使用继承者的内容,这是因为类继承是单向继承,理清各种内容来自哪里非常重要,以免我们在使用时出现混淆,思路不清晰会直接在代码中体现出来的
  • 这么多的内容,一定不能死记硬背,需要灵活运用文档来辅助我们,那就让我们开始吧!

一、认识BOM

JavaScript有一个非常重要的运行环境就是浏览器,而且浏览器本身又作为一个应用程序需要对其本身进行操作,所以通常浏览器会有对应的对象模型(BOM,Browser Object Model)

  • 我们可以将BOM看成是连接JavaScript脚本与浏览器窗口的桥梁
  • 通过BOM,我们能使用JS对浏览器做出一定的交互,从而实现各种需求。这种做法对于JS来说是非常有特色的,各种各样的"桥梁"让JS能做的事情越来越多

深度优先与广度优先搜索

图33-1 作为桥梁的BOM

  • 所以BOM 归属于浏览器,是由浏览器提供的一组对象和方法,目的是让 JavaScript 可以控制浏览器的行为。它不是 JavaScript 语言标准的一部分,而是浏览器作为 JavaScript 运行环境 所提供的独有特性
  • BOM主要包括以下对象模型,也是我们要学习的重点:


表33-1 BOM主要的对象模型

对象描述
window包括全局属性、方法,控制浏览器窗口相关的属性和方法。
location表示浏览器连接到的 URL 信息,可以获取或修改地址。
history操作浏览器的历史记录,例如前进和后退。
document当前窗口中正在操作的文档对象,表示 HTML 或 XML 内容。
  • 在BOM中,能够看到window对象,他在浏览器中有两个身份:
  • 身份一:全局对象,我们知道ECMAScript其实是有一个全局对象的,这个全局对象在Node中是global,在浏览器中就是window对象
    • 需要清楚,全局对象本质上是一个对整个运行环境的抽象,它管理着全局命名空间,提供了对全局变量、函数和全局环境 API 的统一访问,在不同的环境下有不同的称呼体现,但他所想表达的含义是一致的,因此不必要纠结全局对象到底是window还是global,因为全局对象是他们,而他们只有在属于自己的环境下才是全局对象。可以用一个很好理解的角度去看待,每一部电影里面都有主角,重大的主角都具备"天命所归"的性质,但只有在他们所在的电影里他们才是主角,而"天命所归"在不同电影中所体现的行动与表象都不同,但内核是一致的
    • 如果一定要纠结一个具体的答案的话,在当下得到的答案,在其他地方未必正确,对自身抽象能力有一定基础要求
  • 身份二:浏览器窗口对象,作为浏览器窗口时,提供了对浏览器操作的相关的API

1.1 Window全局对象

  • 在浏览器中,window对象就是之前经常提到的全局对象,也就是我们之前提到过GO对象:
    • 比如在全局通过var声明的变量,会被添加到GO中,也就是会被添加到window上
    • 比如window默认给我们提供了全局的函数和类:setTimeout、Math、Date、Object等
  • 通过var声明的变量,全局提供的类和方法:
// 1. 通过 var 声明的全局变量,会被添加到全局对象 window 上
var myVar = "Hello World";
console.log(window.myVar); // 输出:Hello World

// 2. 通过 window 可以访问全局对象默认提供的函数和类
// setTimeout 是 window 对象上全局可用的方法
window.setTimeout(() => {
  console.log("This message is delayed by 1 second");
}, 1000);

// 3. Math 是 window 对象上默认提供的全局类
console.log(window.Math.sqrt(16)); // 输出:4

// 4. Date 是 window 对象上默认提供的全局类
const currentDate = new window.Date();
console.log(currentDate); // 输出:当前日期和时间

// 5. Object 是全局对象提供的全局类,可以直接使用
const person = new window.Object({ name: "coderwhy", age: 25 });
console.log(person.name); // 输出:coderwhy
  • 这些用法是我们之前讲过的,并且也是作为JavaScript语言本身所拥有的一些特性
    • 那么接下来我们来看一下作为窗口对象,它拥有哪些特性

1.2 Window窗口对象

  • window 对象被称为窗口对象,是因为它代表的是浏览器窗口浏览器的顶级框架,即用户用来查看网页的那部分界面。它包含了浏览器窗口的所有信息,比如窗口大小、窗口位置、视口(用户可以看到的部分)等
    • 所以叫做窗口对象,是因为它管理和控制整个浏览器窗口,包括浏览器显示的内容和窗口本身的特性
    • window 对象提供了很多控制浏览器窗口的功能,例如打开新窗口、关闭窗口、调整窗口大小等。这些功能都是和窗口直接相关的
  • 因此window对象上肩负的重担是非常大的:
    1. 包含大量的属性,localStorage、console、location、history、screenX、scrollX等等(大概60+个属性)
    2. 包含大量的方法,alert、close、scrollTo、open等等(大概40+个方法)
    3. 包含大量的事件,focus、blur、load、hashchange等等(大概30+个事件)
    4. 包含从EventTarget继承过来的方法,addEventListener、removeEventListener、dispatchEvent方法
//来自window的console属性
window.console.log('Hello World!');
console.log('Hello World!');

深度优先与广度优先搜索

图33-2 来自window的console属性

  • 这么多的内容,在本章节中,不会全部具体的进行学习,也几乎没有任何一个课程可以一个个讲解过去,这应该就像一个字典一样,平时有空翻一翻,有个印象,需要的时候就去翻出来使用

  • 那么这些大量的属性、方法、事件在哪里查看呢?

    • 依旧是来自我们的MDN文档:developer.mozilla.org/zh-CN/docs/…
    • 在编程中,知识点非常多,浩如烟海,没有使用导致遗忘是非常正常的事情,我们需要做的事情不是反复的记忆反复的学习,而是牢牢掌握最核心的知识点,其余各类能够快速学会且不足够核心的内容平时多留意位置,等到需要的时候能够第一时间去查看如何使用
  • 查看MDN文档时,我们会发现有很多不同的符号,这里我解释一下是什么意思:

    • 删除符号:表示这个API已经废弃,不推荐继续使用了
    • 点踩符号:表示这个API不属于W3C规范,某些浏览器有实现(所以兼容性的问题)
    • 实验符号:该API是实验性特性,以后可能会修改,并且存在兼容性问题

深度优先与广度优先搜索

图33-3 MDN文档主要标识

  • window是一个实例对象,他由Window类创建而来,而Window类继承自EventTarget,所以虽然window上很多的内容,但并不全部来自自身,而有相当大的一部分来自继承,这里有非常严密的关系
    • 这意味着 window 对象不仅具有操作浏览器窗口的功能(Window类),还具有事件处理的能力(EventTarget)

深度优先与广度优先搜索

图33-4 window的继承路线

//Window继承自EventTarget
class Window extends EventTarget{...}
//window实例对象继承自Window类
const window = new Window()
  • 多种多样的属性、方法、事件层级清晰,在一开始前就已经架构好谁继承于谁,谁归属于谁。而EventTarget位于最上层,在进行学习时,可以先从最通用最上层的开始学习,逐渐细分到所需领域
    • 单从下图中就可以看到拥有繁复内容的window也只是BOM的一个分支,可以对浏览器庞大知识点有一点具象体会,因此不能够一开始就想着都要掌握,这是不切实的
    • 掌握好阅读文档,是从根本解决该问题的方式,编写代码并不是考试,具体内容都可以通过检索能力、阅读能力快速提取使用,我们所需要掌握的是内在的思想体现,API本身只是实现思路的手段

深度优先与广度优先搜索

图33-5 EventTarget继承图

  • 接下来我们来演练一些window中的常见属性、方法和事件
    • 作为归纳总结来说,我们并不需要去记忆他们,在需要的时候,通过MDN文档检索或者AI辅助书写,都会是非常棒的方式,要转变学习方式

1.3 window常见属性

// 常见的 window 对象属性

// 1. window.location - 当前页面的 URL 信息
console.log(window.location.href); // 当前页面的完整 URL

// 2. window.document - 当前页面的文档对象
console.log(window.document.title); // 当前页面的标题

// 3. window.navigator - 浏览器的用户代理信息
console.log(window.navigator.userAgent); // 浏览器的用户代理字符串

// 4. window.innerWidth - 浏览器窗口的视口宽度
console.log(window.innerWidth); // 当前视口的宽度

// 5. window.innerHeight - 浏览器窗口的视口高度
console.log(window.innerHeight); // 当前视口的高度

// 6. window.history - 浏览器历史记录对象
console.log(window.history.length); // 当前会话中的历史记录条数

// 7. window.screen - 用户屏幕的信息
console.log(window.screen.width); // 用户屏幕的宽度
console.log(window.screen.height); // 用户屏幕的高度

// 8. window.localStorage - 本地存储对象
window.localStorage.setItem("username", "Alice"); // 本地存储数据
console.log(window.localStorage.getItem("username")); // 获取本地存储的数据

// 9. window.sessionStorage - 会话存储对象
window.sessionStorage.setItem("sessionId", "12345"); // 会话存储数据
console.log(window.sessionStorage.getItem("sessionId")); // 获取会话存储的数据

// 10. window.console - 控制台对象,用于调试
console.log("This is a log message"); // 输出日志信息到控制台

// 11. window.name - 当前窗口的名称
window.name = "myWindow";
console.log(window.name); // 输出窗口的名称

// 12. window.parent - 获取父窗口的引用(如果存在嵌套框架)
console.log(window.parent === window); // 在顶层窗口中,window.parent 等于 window 本身

// 13. window.top - 获取最顶层窗口的引用
console.log(window.top === window); // 在顶层窗口中,window.top 等于 window 本身

// 14. window.frames - 包含当前窗口中所有框架的集合
console.log(window.frames.length); // 当前窗口中的框架数量

// 15. window.screenX / window.screenY - 当前窗口相对于屏幕的 X 和 Y 坐标
console.log(window.screenX); // 输出窗口的 X 坐标
console.log(window.screenY); // 输出窗口的 Y 坐标


表33-2 window常见属性总结

属性名分类说明
location页面相关属性提供当前页面的 URL 信息
document页面相关属性表示当前页面的文档对象
history页面相关属性表示浏览器的历史记录
navigator浏览器信息提供浏览器的用户代理信息,如设备和浏览器版本
screen浏览器信息提供用户屏幕的信息,如分辨率和颜色深度
innerWidth窗口位置和大小当前浏览器窗口的视口宽度(以像素为单位)
innerHeight窗口位置和大小当前浏览器窗口的视口高度(以像素为单位)
screenX窗口位置和大小浏览器窗口相对于屏幕的 X 坐标
screenY窗口位置和大小浏览器窗口相对于屏幕的 Y 坐标
localStorage存储机制用于持久化存储数据,数据在页面关闭后仍然存在
sessionStorage存储机制用于会话存储数据,数据在页面关闭后会被清除
parent窗口层级属性获取当前窗口的父窗口(如果存在嵌套框架)
top窗口层级属性获取最顶层的窗口对象引用
frames窗口层级属性包含当前窗口中所有框架的集合

1.4 window常见方法

// 常见的 window 对象方法

// 1. window.alert() - 弹出一个警告对话框
window.alert("Hello, World!"); // 显示一个提示框

// 2. window.confirm() - 弹出一个确认对话框
const result = window.confirm("Are you sure?"); // 返回 true 或 false,用户点击 "确认" 或 "取消"

// 3. window.prompt() - 弹出一个输入对话框
const userInput = window.prompt("Enter your name:"); // 返回用户输入的字符串

// 4. window.open() - 打开一个新窗口或新标签页
window.open("https://www.juejin.cn"); // 打开指定 URL 的新页面

// 5. window.close() - 关闭当前窗口
window.close(); // 关闭当前窗口(需要由脚本打开的窗口才有效)

// 6. window.setTimeout() - 设置一个定时器,在指定的时间后执行代码
window.setTimeout(() => {
  console.log("This message is delayed by 2 seconds");
}, 2000); // 延迟 2 秒后输出信息

// 7. window.setInterval() - 设置一个定时器,每隔一定时间重复执行代码
const intervalId = window.setInterval(() => {
  console.log("This message repeats every 1 second");
}, 1000); // 每隔 1 秒输出信息

// 8. window.clearInterval() - 清除通过 setInterval 创建的定时器
window.clearInterval(intervalId); // 停止上面创建的定时器

// 9. window.addEventListener() - 为窗口添加事件监听器
window.addEventListener("resize", () => {
  console.log("Window resized!");
}); // 监听窗口大小变化事件

// 10. window.removeEventListener() - 移除事件监听器
const resizeHandler = () => console.log("Window resized!");
window.addEventListener("resize", resizeHandler);
window.removeEventListener("resize", resizeHandler); // 移除窗口大小变化的监听器

// 11. window.scrollTo() - 滚动到指定位置
window.scrollTo(0, 100); // 滚动到页面的顶部 100 像素处

// 12. window.focus() - 使当前窗口获得焦点
window.focus(); // 使窗口获得焦点

// 13. window.print() - 打印当前页面
window.print(); // 打开打印对话框,用于打印当前页面内容


表33-3 window常见方法总结

方法名说明
alert()弹出一个警告对话框。
confirm()弹出一个确认对话框,返回 true(确认)或 false(取消)。
prompt()弹出一个输入对话框,返回用户输入的字符串。
open()打开一个新窗口或新标签页。
close()关闭当前窗口(仅对由脚本打开的窗口有效)。
setTimeout()设置一个定时器,在指定的时间后执行代码。
setInterval()设置一个定时器,每隔一定时间重复执行代码。
clearInterval()清除通过 setInterval() 创建的定时器。
addEventListener()为窗口添加事件监听器。
removeEventListener()移除事件监听器。
scrollTo()滚动到页面的指定位置。
focus()使当前窗口获得焦点。
print()打印当前页面内容,打开打印对话框。

1.5 window常见事件

  • window的事件都通过继承自EventTarget的addEventListener实例方法完成
    • EventTarget.addEventListener() 方法将指定的监听器注册到 EventTarget上,当该对象触发指定的事件时,指定的回调函数就会被执行
    • 因此addEventListener方法只是一个媒介,具体事件由事件监听器完成
  • 这里值得探讨的是,为什么浏览器会选择这些事件时机交由开发者处理,没一个事件处理的背后
// 常见的 window 对象事件

// 1. load - 页面加载完成事件
window.addEventListener("load", () => {
  console.log("页面加载完成");
});

// 2. unload - 页面卸载事件
window.addEventListener("unload", () => {
  console.log("页面即将卸载");
});

// 3. resize - 窗口大小改变事件
window.addEventListener("resize", () => {
  console.log("窗口大小改变了");
});

// 4. scroll - 窗口滚动事件
window.addEventListener("scroll", () => {
  console.log("页面滚动了");
});

// 5. beforeunload - 页面即将被关闭或刷新前的事件
window.addEventListener("beforeunload", (event) => {
  event.preventDefault(); // 防止页面关闭或刷新
  event.returnValue = ""; // 显示提示信息
});

// 6. focus - 窗口获得焦点事件
window.addEventListener("focus", () => {
  console.log("窗口获得了焦点");
});

// 7. blur - 窗口失去焦点事件
window.addEventListener("blur", () => {
  console.log("窗口失去了焦点");
});

// 8. error - JavaScript 运行错误事件
window.addEventListener("error", (event) => {
  console.error("发生错误:", event.message);
});

// 9. online - 网络连接恢复事件
window.addEventListener("online", () => {
  console.log("网络连接恢复");
});

// 10. offline - 网络连接断开事件
window.addEventListener("offline", () => {
  console.log("网络连接断开");
});


表33-4 window常见事件总结

事件名说明
load页面加载完成时触发
unload页面即将卸载时触发
resize窗口大小改变时触发
scroll页面滚动时触发
beforeunload页面即将被关闭或刷新前触发,可用于防止页面被关闭
focus窗口获得焦点时触发
blur窗口失去焦点时触发
errorJavaScript 运行时发生错误时触发,便于错误处理
online网络连接恢复时触发
offline网络连接断开时触发

1.6 Location对象常见属性

window.location 对象是 BOM(浏览器对象模型) 的一部分,用于表示和操作当前页面的 URL(统一资源定位符) 信息。它提供了与 浏览器地址栏相关的操作能力,从而让 JS 可以用来获取或修改 URL,实现页面跳转、重新加载等功能

  • Location本身翻译为"地址",指浏览器URL地址,而Location对象内的所有属性,都是基于URL本身的一部分组成

  • 这一系列的API都不是重点,因为简单的使用不需要成本,需要掌握的是URL背后映射出来的原理,例如为什么会是这样组成的?

  • 协议提供通信规则、主机名端口提供资源的服务器位置、路径提供资源在服务器上的层次化位置、查询参数为访问资源提供更多灵活性、哈希用于页面内导航,提升用户体验

  • 从本质上来说,每个部分的存在都是为了更好地在互联网上描述、定位和访问各种类型的资源。换个角度来说,URL每多出一部分内容,都是为了更好的定位内容,为了定位互联网上所有的内容

    • 每个部分的背后都有非常深厚的知识延伸,也有非常多的应用扩展,但这些就需要交由大家进行自主探索了,可以从计算机网络中找到答案
    • 单纯获取对应部分内容,已经由API所完成,而利用这些部分能够实现什么事情,则是计算机工程师的能力
// 假设当前页面 URL 为:https://juejin.cn/user/251124329220663

// 1. href - 完整的 URL
console.log(window.location.href); 
// 输出: "https://juejin.cn/user/251124329220663"

// 2. protocol - URL 的协议
console.log(window.location.protocol); 
// 输出: "https:"

// 3. host - 主机名和端口号(没有指定端口时只有主机名)
console.log(window.location.host); 
// 输出: "juejin.cn"

// 4. hostname - 仅主机名,不包含端口号
console.log(window.location.hostname); 
// 输出: "juejin.cn"

// 5. port - 端口号(如果有的话),如果没有设置则输出空字符串
console.log(window.location.port); 
// 输出: ""

// 6. pathname - URL 的路径部分
console.log(window.location.pathname); 
// 输出: "/user/251124329220663"

// 7. search - 查询字符串(以 `?` 开头),如果没有查询参数则输出空字符串
console.log(window.location.search); 
// 输出: ""

// 8. hash - URL 中的哈希部分(以 `#` 开头),如果没有哈希则输出空字符串
console.log(window.location.hash); 
// 输出: ""


表33-5 Location对象常见属性总结

属性名说明
href表示整个 URL 的字符串
protocol表示使用的协议,例如 https:https:
host表示主机名和端口号,例如 juejin.cn:8080(在本案例中没有端口号)
hostname仅包含主机名,例如 juejin.cn
port端口号,如果未设置端口号则为空字符串
pathname表示路径部分,例如 /user/251124329220663
search表示查询字符串部分(? 后面的内容),如果没有查询参数则为空字符串
hash表示 URL 中的锚点(哈希)部分,例如 #section1,如果没有则为空字符串

1.7 Location对象常见方法

  • 我们会发现location其实是URL的一个抽象实现:

深度优先与广度优先搜索

图33-6 URL的组成部分

// 常见的 window.location 对象方法

// 1. location.assign() - 导航到新的 URL,并保存当前页面到历史记录
window.location.assign("https://www.juejin.cn");
// 跳转到指定 URL,并在浏览器历史中记录(可以通过 "后退" 按钮返回)

// 2. location.replace() - 导航到新的 URL,但不保存当前页面
window.location.replace("https://www.baidu.com");
// 跳转到指定 URL,不保留当前页面(无法通过 "后退" 按钮返回)

// 3. location.reload() - 重新加载当前页面
window.location.reload();
// 重新加载当前页面,类似于刷新浏览器


表33-6 Location对象常见方法总结

方法名说明
assign(url)导航到新的 URL,并保存当前页面到历史记录,可以通过“后退”按钮返回。
replace(url)导航到新的 URL,不保存当前页面到历史记录,无法通过“后退”按钮返回。
reload()重新加载当前页面,相当于刷新浏览器。

1.8 history对象常见属性和方法

  • history对象允许我们访问浏览器曾经的会话历史记录,有两个属性和五个方法
    • 属性:会话记录条数(length)、当前保留的会话状态(state)
    • 方法:前进(forward)、返回(back)、加载某一页(go)内容,打开指定地址(pushState)、打开新的地址(replaceState)
  • 这里就好奇了,history是"历史记录"的意思,属性中两个和会话有关,方法中加载会话记录中的阶段都好理解,那为什么方法里面还有打开指定地址和新的地址呢?这和history有什么关系吗?新地址和指定地址有什么关系?
    • 在传统的网页应用中,每次页面的跳转或状态变化都是通过刷新页面来完成的,浏览器自动会将这些页面加载记录到历史记录栈中。而随着单页应用(SPA)的流行,开发者希望在不刷新页面的情况下实现URL 的变化,让用户可以方便地通过浏览器的“前进”和“后退”按钮进行导航
    • pushState()replaceState() 的主要作用是允许 JS 在页面不刷新的情况下,修改浏览器的 URL 并记录到历史记录中,可以理解为性质与其余方法一致(页面不刷新),但这两个方法不在原有历史中选择跳转,而是塞新的内容到历史中
  • 那新地址和指定地址又有什么区别?
    • 指在处理历史记录时的不同方式,也就是两种不同在浏览器的历史记录栈中添加或替换条目的方式
    • 添加新地址会将一个新的条目添加到浏览器的历史记录栈中,这意味调用这个方法之后,浏览器的历史记录会比之前多一个,这种方式模拟了“用户访问了一个新的页面”的行为,因此,浏览器的**“前进”和“后退”按钮**也会受此影响
    • 指定地址不会新增历史条目,而是直接替换掉当前的历史记录条目。这样做的结果是当前页面的 URL 被改变,但历史记录的长度没有发生变化
// 1. history.length - 获取会话历史中的记录条数
console.log(window.history.length); 
// 输出当前浏览器会话的历史记录条数,例如 5

// 2. history.state - 获取当前保留的状态值
console.log(window.history.state); 
// 输出当前状态对象的值,初始值通常为 null

// 3. history.back() - 返回上一页,等价于 history.go(-1)
window.history.back();
// 返回到上一页,等价于点击浏览器的 "后退" 按钮

// 4. history.forward() - 前进到下一页,等价于 history.go(1)
window.history.forward();
// 前进到下一页,等价于点击浏览器的 "前进" 按钮

// 5. history.go() - 加载历史中的某一页,参数是相对于当前页的偏移量
window.history.go(-2);
// 加载历史记录中的某一页,例如 -2 表示向前两页,正数表示向后

// 6. history.pushState() - 添加新的历史记录条目
window.history.pushState({ page: 1 }, "Title 1", "/page1");
// 添加一条新的历史记录条目,并改变 URL 为 "/page1",状态对象为 { page: 1 }

// 7. history.replaceState() - 替换当前历史记录条目
window.history.replaceState({ page: 2 }, "Title 2", "/page2");
// 替换当前历史记录条目,改变 URL 为 "/page2",状态对象为 { page: 2 }


表33-7 history对象常见属性与方法总结

名称类型说明
length属性获取会话中的历史记录条数
state属性获取当前保留的状态值,通常由 pushState()replaceState() 设置
back()方法返回到上一页,等价于 history.go(-1)
forward()方法前进到下一页,等价于 history.go(1)
go(n)方法加载历史中的某一页,n 为相对于当前页的偏移量
pushState(state, title, url)方法添加新的历史记录条目,改变 URL,但不会刷新页面
replaceState(state, title, url)方法替换当前历史记录条目,改变 URL,但不会刷新页面
  • 因此history对象的API实际是在对历史记录栈的操作,所有操作都是围绕这一点进行,从原理角度来说,历史记录其实是一个“堆栈”,浏览器维护着用户访问过的页面历史,当用户点击“前进”和“后退”时,就是在这个堆栈中进行移动
  • 如果觉得不好理解,可以将历史记录栈想象为一个视频,使用history时,就是在对原视频进行剪辑,我们能获取视频的时间长度、回拉时间、快进时间、直接拉到想看的精彩瞬间、拿另一段视频拼接进原视频或者替换原视频的某一部分

二、认识DOM和架构

  • 浏览器是用来展示网页的,而网页中最重要的就是里面各种的标签元素,JavaScript很多时候是需要操作这些元素的

    • JavaScript如何操作元素呢?通过Document Object Model(DOM,文档对象模型)
    • DOM给我们提供了一系列的模型和对象,让我们可以方便的来操作Web页面
  • DOM 是页面内容的编程接口,HTML 文档在加载到浏览器时,会被解析为一棵对象树,即 DOM 树,DOM 提供了访问和操作这棵树的接口,开发者可以通过该接口修改页面的结构、内容和样式

深度优先与广度优先搜索

图33-7 DOM树架构

  • HTML代码是能够直接交给浏览器处理的,在没有任何JS代码的情况下依旧能够执行,因此单从HTML展示到浏览器界面中是可以没有JS参与的。但JS现在需要做的事情越来越多了,由此延伸出"前端渲染"的概念
    • 以前的做法是在服务器将HTML与数据结合,直接返回完整HTML页面给客户端进行展示,因为浏览器可以直接处理HTML,这种做法被称为服务端渲染
    • 现在的主流做法则是从服务器中请求数据,在前端中使用JS将这些数据转为HTML代码,再交给浏览器进行执行,也被称为前端渲染
  • 但每种做法都有其独特的优势所在,现在用得少不意味着不使用,只是比例较低,这是由需求决定的,例如首页就可以直接在服务器中渲染完成直接发送完整HTML给客户端,这样首屏加载就快,用户体验也好,结合使用的做法被称为同构渲染

深度优先与广度优先搜索

图33-8 前端渲染路径

  • 相对于以前,多出一个JS的环节,因此给了JS参与的机会,那么JS是如何将数据转为HTML呢?或者说是如何参与进HTML的搭建之中的呢?

    • 浏览器将 HTML 文档分解为不同的标签、属性、文本节点等,逐步将这些元素组织为一个 DOM 树
    • 而如果采用"前端渲染"的话,览器会先请求一个空白的 HTML 模板,这个模板通常只有一些基本的页面结构和对 JavaScript 的引用。例如,一个简单的模板可能只有一个空的 <div id="app"></div>,以及指向前端 JavaScript 文件的 <script> 标签
  • 这个最初的div标签很重要,通常以这为主干,结合模块化构建出完整的HTML,通过这最初的div,往里面添加标签、属性、文本节点等创建元素做法,再不断进行拆分重组优化构建,形成一个大型项目,每一次添加都可以将JS请求而来的数据利用DOM树所提供的接口,进行结合成HTML

    • 在原生JS的时候,我们会手动操作这每一个枝干,将内容结合为HTML传递给DOM节点
    • 而现在可以利用框架,将DOM操作封装起来不去关心,而专注于业务开发
  • 在DOM操作中,高频部分主要有Node、Document、Element这三部分,也是我们讲解的重点

2.1 Node节点

  • 所有的DOM节点类型都继承自Node接口,具体内容可以查看:developer.mozilla.org/zh-CN/docs/…
  • 在Node中,有几个非常重要的属性:分别为节点名称、类型、值和子节点。这几个属性构建了整个节点网络,就像是一个组织,最主要为每个人的信息以及下线


表33-8 Node节点主要的属性名

属性名说明用途
nodeName节点的名称返回节点的名称,例如元素节点返回标签名称(大写字母),文本节点返回 #text
nodeType节点的类型用于区分节点类型,1 代表元素节点,3 代表文本节点等
nodeValue节点的值返回或设置节点的值,文本节点返回其文本内容,元素节点返回 null
childNodes当前节点的所有子节点(NodeList)包含当前节点的所有直接子节点,返回的是一个类似数组的 NodeList
// 创建一个简单的 HTML 结构
const divElement = document.createElement("div");
divElement.id = "box";
divElement.innerHTML = `
  <span class="content">Hello World!</span>
  <p>Paragraph content.</p>
`;
document.body.appendChild(divElement);

// 获取 div 节点
const node = document.querySelector("#box");

// 1. 使用 nodeName 获取节点的名称
console.log(node.nodeName); // 输出: "DIV" (元素节点返回标签名称,大写)

// 2. 使用 nodeType 获取节点的类型
console.log(node.nodeType); // 输出: 1 (元素节点)

// 3. 使用 nodeValue 获取节点的值
// 对于元素节点,nodeValue 通常为 null
console.log(node.nodeValue); // 输出: null

// 对于文本节点,nodeValue 返回文本内容
const textNode = document.createTextNode("This is a text node.");
console.log(textNode.nodeValue); // 输出: "This is a text node."

// 4. 使用 childNodes 获取所有子节点
// childNodes 返回当前节点的所有直接子节点(包括文本节点和元素节点)
const children = node.childNodes;
children.forEach((child) => {
  console.log(child.nodeName); // 可能输出: "#text" (文本节点), "SPAN", "P"
  console.log(child.nodeType); // 输出: 3 (文本节点) 或 1 (元素节点)
});

  • 节点类型分好几种,主要为:元素节点、文本节点、注释节点、属性节点,具体对应的返回数字可以看MDN文档:Node:nodeType 属性 - Web API | MDN (mozilla.org)
    • nodeValue 的设计初衷是为那些可以直接存储值的节点类型提供一个获取或设置值的接口
    • 对于 文本节点注释节点,它们本质上是存储内容的,因此 nodeValue 返回它们的文本
    • 对于 元素节点,它们是用来描述页面结构的,不直接存储值,nodeValue 自然也没有可用的值,因此返回 null
  • 在早期的 DOM 规范中,属性节点有一个 nodeValue,可以用来获取属性的值。不过现代的 DOM 已不直接使用属性节点来操作,更多是通过 getAttribute()setAttribute() 方法来访问或修改属性
  • 我们以一个HTML结构来举例,看各种节点的不同表现具体为什么
<!-- HTML 文档的开始 -->
<!DOCTYPE html> <!-- 文档类型声明节点 -->

<html>
  <head>
    <title>Node Types Example</title>
  </head>
  <body>
    <!-- 元素节点(Element Node) -->
    <div id="container">
      
      <!-- 属性节点(Attribute Node) -->
      <!-- 这个 `id="container"` 就是一个属性节点,指定了元素的属性信息 -->

      <!-- 文本节点(Text Node) -->
      <p>Hello, World!</p>
      <!-- `Hello, World!` 就是一个文本节点,表示元素中的文本内容 -->
      
      <!-- 注释节点(Comment Node) -->
      <!-- This is a comment -->
      <!-- 这个就是一个注释节点,提供对代码的描述或注释 -->

      <!-- 文本节点与元素节点的结合 -->
      <span>This is a <strong>bold</strong> text.</span>
      <!-- `This is a` 是一个文本节点 -->
      <!-- `<strong>bold</strong>` 是一个元素节点 -->

    </div>
  </body>
</html>

2.2 Document

  • 每一个页面(HTML)都会被浏览器解析为一个document,这不是由我们手动创建的,而是自动生成。而document继承自EventTarget,所以可以使用来自EventTarget的各种监听器

深度优先与广度优先搜索

图33-9 继承自EventTarget的document

//使用addEventListener监听整个页面的点击操作,一点击则触发回调函数
document.addEventListener("click", () => {
  console.log("document被点击")
})
  • 如果想要选择页面的某一部分,则需要利用来自Document本身的querySelector实例方法,这里需要注意,Document是一个接口或类,表示整个文档,而document则是Document 的实例,表示当前加载的文档。类的概念在之前已经学得很明白了,这里不再赘述
    • 通过querySelector选择HTML文档中的其中一个节点,结合来自EventTarget的addEventListener,实现了先选中所需部分后对其进行操作
//ID选择器box(单选)和类选择器content(多选)
//来自:<div id="box"></div>
const divEl = document.querySelector("#box")
//来自:<span class="content"></span>
const spanEl = document.querySelector(".content")

divEl.addEventListener("click", () => {
  console.log("div元素被点击")
})

spanEl.addEventListener("click", () => {
  console.log("span元素被点击")
})
  • document代表整个文档,因为文档由很多内容组成,所以document想要对应的话,内部会存在很多内容,包括了以下几大类:

    1. 页面结构相关内容
    2. 节点查询方法
    3. 创建和操作节点的方法
    4. 与浏览器交互相关的内容
    5. 文档元数据
    6. 文档节点的访问与集合
    7. 事件处理
    8. 文档导航
  • 这些内容,返回的是文档中的一部分,这些都是知识,但不属于活知识,因为我们还不会运用他们

    • 作为知识本身,他们是不存在变化的,需要时直接查文档看如何获取
    • 具备难度的在于如何使用这些知识,都能运用在哪些地方,这些是没办法速成的,需要从项目中进行一次次的试错和思考才能掌握
  • 例如:假如我拿到了这个文本节点,我能够做什么?

    • 也许可以进行判断看是否是需要的内容,从而进行下一步操作,例如跳转到其他页面,将内容与跳转URL使用键值对进行映射,当出现文字关键字可以直接附加超链接进行跳转,这一点在MDN文档其实能经常看见,较为清晰的一种封装思路
    • 也能够对其元素添加监听器,当鼠标滑过时附加各种动画
    • 亦或者当失去网络时,进行文字变换,提示用户当前网络已断开...
// 1. 页面结构相关内容
console.log(document.documentElement);  // 返回 <html> 根元素
console.log(document.head);             // 返回 <head> 元素
console.log(document.body);             // 返回 <body> 元素
document.title = "New Title";           // 获取或设置文档的标题

// 2. 节点查询方法
let byId = document.getElementById("myId");              // 通过 ID 获取元素
let byClass = document.getElementsByClassName("myClass");// 通过类名获取元素集合
let byTag = document.getElementsByTagName("div");        // 通过标签名获取元素集合
let byQuery = document.querySelector(".myClass");        // 返回匹配选择器的第一个元素
let byQueryAll = document.querySelectorAll("div");       // 返回匹配选择器的所有元素

// 3. 创建和操作节点的方法
let newDiv = document.createElement("div");              // 创建一个 <div> 元素
let textNode = document.createTextNode("Hello World");   // 创建文本节点
let fragment = document.createDocumentFragment();        // 创建文档片段

// 4. 与浏览器交互相关内容
console.log(document.URL);               // 返回当前页面的 URL
console.log(document.referrer);          // 返回来源 URL
document.cookie = "name=value";          // 获取或设置 cookie 信息
console.log(document.domain);            // 获取文档域名
console.log(document.location.href);     // 获取或设置当前 URL

// 5. 文档元数据
console.log(document.characterSet);      // 返回文档字符编码
console.log(document.lastModified);      // 返回文档上次修改日期
console.log(document.doctype);           // 返回文档类型声明

// 6. 文档节点访问与集合
console.log(document.childNodes);        // 返回所有子节点
console.log(document.firstChild);        // 返回第一个子节点
console.log(document.lastChild);         // 返回最后一个子节点
console.log(document.children);          // 返回直接子元素集合
console.log(document.forms);             // 返回所有表单
console.log(document.images);            // 返回所有图像
console.log(document.links);             // 返回所有超链接

// 7. 事件处理
document.addEventListener("DOMContentLoaded", () => {
  console.log("Document is ready!");     // 添加事件监听器
});
document.removeEventListener("DOMContentLoaded", () => {}); // 移除事件监听器
console.log(document.readyState);        // 返回文档加载状态

// 8. 文档导航
console.log(document.anchors);           // 返回所有具有 name 属性的 <a> 标签
console.log(document.scripts);           // 返回所有 <script> 标签
console.log(document.links);             // 返回所有包含 href 的链接

2.3 Element

  • Element 是 HTML 文档中的基本构建块,包含了大量用于访问和修改元素的属性和方法
    • 通过 Element 的属性,可以获取元素的各种信息,例如类名、ID、文本内容等
    • 通过 Element 的方法,可以动态操作元素的属性、类名、子节点、位置等,从而实现复杂的页面交互
  • 需要注意,使用element元素时,需要先使用querySelector获取到具体的元素或者获取到创建的元素,再对该元素进行操作
    • 将获取的元素替换掉下方案例中的element,例如element.className替换为:具体元素.className
// -------------------- Element 常见属性 --------------------

// 获取或设置元素的 `id` 属性
let elementId = element.id; // 获取元素 ID
element.id = "newId"; // 设置元素 ID

// 获取或设置元素的 `class` 属性
let elementClass = element.className; // 获取元素类名
element.className = "newClass"; // 设置元素类名

// 获取或设置元素的文本内容
let textContent = element.textContent; // 获取文本内容
element.textContent = "New Text"; // 设置文本内容

// 获取或设置元素的 HTML 内容
let innerHtml = element.innerHTML; // 获取 HTML 内容
element.innerHTML = "<p>New Paragraph</p>"; // 设置 HTML 内容

// 获取或设置元素的外部 HTML 包含元素自身
let outerHtml = element.outerHTML; // 获取外部 HTML
element.outerHTML = "<div>New Div</div>"; // 替换整个元素

// 获取元素的子元素集合
let childElements = element.children; // 返回元素的直接子元素 (HTMLCollection)

// 获取元素的直接父元素
let parentElement = element.parentElement; // 返回直接父元素 (如果存在)

// -------------------- Element 常见方法 --------------------

// 设置元素的属性
element.setAttribute("data-info", "someValue"); // 设置自定义属性

// 获取元素的属性值
let attributeValue = element.getAttribute("data-info"); // 获取自定义属性值

// 移除元素的属性
element.removeAttribute("data-info"); // 移除属性

// 检查元素是否具有指定的属性
let hasAttribute = element.hasAttribute("data-info"); // 返回 true 或 false

// 添加 CSS 类
element.classList.add("newClass"); // 添加一个类

// 移除 CSS 类
element.classList.remove("oldClass"); // 移除一个类

// 切换 CSS 类
element.classList.toggle("active"); // 如果类存在则移除,否则添加

// 检查元素是否包含某个 CSS 类
let containsClass = element.classList.contains("active"); // 返回 true 或 false

// 获取元素的相对窗口的位置和大小
let rect = element.getBoundingClientRect(); // 返回元素的边界信息 (如宽度、高度、位置等)

// 插入一个子元素
let newChild = document.createElement("span");
element.appendChild(newChild); // 在元素末尾插入子元素

// 在特定位置插入一个新元素
let anotherChild = document.createElement("div");
element.insertBefore(anotherChild, element.firstChild); // 插入为第一个子元素

// 删除某个子元素
element.removeChild(newChild); // 从父元素中移除一个子元素

三、认识事件监听

  • 前面我们讲到了JavaScript脚本和浏览器之间交互时,浏览器给我们提供的BOM、DOM等一些对象模型
    • 事实上还有一种需要和浏览器经常交互的事情就是事件监听,浏览器在某个时刻可能会发生一些事件,比如鼠标点击、移动、滚动、获取、失去焦点、输入内容等等一系列的事件
  • 我们需要以某种方式(代码)来对其进行响应,进行一些事件的处理,在Web当中,事件在浏览器窗口中被触发,并且通过绑定到某些元素上或者浏览器窗口本身,那么我们就可以给这些元素或者window窗口来绑定事件的处理程序,来对事件进行监听
  • 在window常见事件中,我们已经能学习了如何在多种事件时机中进行回调处理,那么总共有多少种监听方式?
    • 事件监听方式一:在script中直接监听
    • 事件监听方式二:通过元素的on来监听事件
    • 事件监听方式三:通过前面所学的EventTarget中的addEventListener来监听,这也是最常见的方式
//事件监听方式一:在script中直接监听
<div class="box" onclick="console.log('div元素被点击')"></div>

//事件监听方式二:通过元素的on来监听事件
<div class="box" onclick="divClick()"></div>
//JS中实现函数方法
const divEl = document.querySelector(".box")
divEl.onclick = function() {
  console.log("div元素被点击3")
}
  • 其实事件监听方式一和二都是利用了on元素,不同之处在于方式一直接内嵌在HTML文件之中,类似CSS的行内样式做法,相对方式一而言,更推荐方式二,因为不会导致太多逻辑内容参合扭曲html结构清晰度
    • 但方式二的on形式也有缺陷,那就是不能重复,如果连续使用,后者内容会覆盖前者,不能在多个函数中进行响应
divEl.onclick = function() {
  console.log("div元素被点击1")
}
//divEl第二个onclick会覆盖第一个onclick
divEl.onclick = function() {
  console.log("div元素被点击2")
}
//divEl第三个onclick会覆盖第二个onclick
divEl.onclick = function() {
  console.log("div元素被点击3")
}
  • 而方式三则是我们最熟悉的addEventListener,DOM2时期推出,在方式二的基础上,进一步集成明确,且支持多函数同时响应,不会出现覆盖问题
const divEl = document.querySelector(".box")
divEl.addEventListener("click", () => {
  console.log("div元素被点击4")
})
divEl.addEventListener("click", () => {
  console.log("div元素被点击5")
})
divEl.addEventListener("click", () => {
  console.log("div元素被点击6")
})

3.1 事件流的由来

  • 事实上对于事件有一个概念叫做事件流,为什么会产生事件流呢?
    • 我们可以想到一个问题:当我们在浏览器上对着一个元素点击时,点击的不仅仅是这个元素本身
    • 这是因为我们的HTML元素是存在父子元素叠加层级的
    • 比如一个span元素是放在div元素上的,div元素是放在body元素上的,body元素是放在html元素上的。当我们对某个元素执行操作(例如点击)时,我们不仅仅是对这个元素本身进行操作,还影响到它的所有父元素
    • 在概念上是处于部分重叠状态,而一旦点击或者以其他方式接触到该重叠部分,会同时触发重叠处的所有内容
  • 那么这种情况好不好呢?客观来说是不太好的,也许我们有这类需求,但触发因素并不明朗,我们从代码层面只对一层元素做操作,却同时在多层元素生效
    • 就算我们想同时对多个元素进行生效,也至少需要从代码中体现出来,不然隐藏的特性在未来相当于埋下一个隐患
  • 同时触发多个元素,也是有执行顺序的,大家觉得是里层元素先被触发,还是外层元素先被触发?
    • 答案是都可以,取决于我们怎么做
    • 这个触发流程就被称为事件流,具体是说:指在网页中,事件从页面的顶层元素向目标元素或从目标元素向顶层元素传播的过程,描述网页中某个事件被触发后,事件在元素层级之间传播的顺序和方式
    • 为了合理地控制和管理这些层级元素之间的交互,事件流模型应运而生

3.2 事件冒泡和事件捕获

  • 我们说同时触发多个元素,里层或者外层先触发都是有可能的,这是怎么回事?
  • 这主要涉及到我们的事件冒泡事件捕获概念
    • 最里层依次向外触发,被称为事件冒泡
    • 最外层向最里层触发,被称为事件捕获
    • 默认情况下,是处于事件冒泡阶段,也就是里层先触发,之所以叫做冒泡,是因为它的传播方式类似于水中气泡从底部逐渐向上冒出的过程,这个气泡会穿过一层层的水面(也就是嵌套的父元素),最终到达水面(最外层元素)
  • 事件捕获之所以称为捕获,是因为这像“从外部到内部逐层捕捉目标”的行为,就像一个网在捕捉一只在内部的小鸟,网从外到内逐层靠近,直到最终捕获到目标
  • 我们使用一个案例说明其中的区别:
    • 捕获阶段:事件从 window -> document -> html -> body -> div -> span
    • 目标阶段:事件在目标元素 <span> 上触发
    • 冒泡阶段:事件从 <span> -> div -> body -> html -> document -> window
<html>
  <body>
    <div>
      <span id="mySpan">Click Me!</span>
    </div>
  </body>
</html>
  • 默认情况为事件冒泡,不需要我们进行任何额外操作,那我们要如何进入事件捕获状态?
    • 开启方式来自addEventListener方法的第三参数,true为捕获,false为冒泡,不填写则默认为false
const div = document.querySelector("div");
const span = document.querySelector("#mySpan");

// 捕获阶段监听
document.body.addEventListener("click", () => {
  console.log("Body capturing phase");
}, true); // `true` 表示在捕获阶段处理

// 冒泡阶段监听
div.addEventListener("click", () => {
  console.log("Div bubbling phase");
}, false); // `false` 表示在冒泡阶段处理

span.addEventListener("click", () => {
  console.log("Span bubbling phase");
}, false); // `false` 表示在冒泡阶段处理
  • 为什么会出现这种不统一的情况,导致产生两种不太的处理流呢?
    • 这是因为早期浏览器开发时,不管是IE还是Netscape公司都发现了这个问题,这两个浏览器厂商在竞争的过程中,分别提出了不同的事件处理模型,每家公司都想以自己的方式来实现更好的事件处理,结果导致了我们今天看到的两种事件传播机制
    • IE采用了事件冒泡的方式,Netscape采用了事件捕获的方式
  • 由于不同厂商的事件处理模型不同,导致在不同的浏览器中处理相同的事件会有不同的行为,这样的不统一让开发者在实现跨浏览器兼容的 JavaScript 代码时非常困难。因此,为了解决这种混乱的局面,W3C(万维网联盟)制定了一个统一的事件处理模型,并将这两种事件传播方式结合在了一起,形成了今天的事件流模型
    • 事件捕获阶段:事件从最外层(例如 windowdocument)逐层向下传播到目标元素
    • 目标阶段:事件到达目标元素,并在目标元素上触发。
    • 事件冒泡阶段:事件从目标元素逐层向上传播,直到最外层(例如 documentwindow
  • 统一的做法让我们不需要在根据不同浏览器去进行被迫适配,而是直接在代码中就能够主动选择所需事件流模型
    • 同时必须要说,不管是事件捕获还是事件冒泡,都有其独特的优势,需要根据需求决定。两种处理方式的割裂原因,浏览器厂商竞争是一个因素,但只有实力匹敌才构成竞争,因此我们单从逻辑角度来说,两者都能被万维网纳入规范,而非其中一方被淘汰,就能够说明问题了


表33-9 事件冒泡与事件捕获对比

事件类型优点缺点
事件捕获让开发者可以在事件真正触发前进行预处理,适合提前拦截事件或特殊处理场景传播顺序不符合用户对事件的直观理解,因此在实践中较少使用
事件冒泡符合用户的使用习惯,目标元素先响应,适合对父级元素一致处理的场景,并支持行为代理如果需要提前阻止事件,冒泡只能在事件到达目标元素后处理,不如捕获阶段及时

3.3 事件对象event常见属性与方法

  • 当我们点击某个元素时,在对元素进行回调施加影响时,元素本身需不需要返回一些元素自身信息给我们,好针对性做出一些操作呢?
    • 在我们事件发生时,也会有很多信息会随着事件的发生而一起发生,用中国的古话说:牵一发而动全身
    • 这些信息一旦产生,就很有可能会用上,由于我们是对事件进行操作的,而这些信息和事件本身具备较为紧密的联系
  • 浏览器是有将这些信息给我们的,将这些信息全都封装到一个event对象中,并且在我们触发事件时,将该event对象以回调参数的形式传递给我们,在event对象中,有类型、元素、位置等一大堆信息
  • 具体详细的信息可以查看MDN文档:Event - Web API | MDN (mozilla.org),我们这里主要演示常见属性与方法
  • 常见属性有:事件类型和目标元素鼠标事件相关属性键盘事件相关属性修饰键状态
const spanEl = document.querySelector(".span")

spanEl.addEventListener("click", (event) => {
  // ---------- 1. 事件类型和目标元素 ----------
  // 事件的类型,例如 "click"、"keydown"
  console.log("事件类型:", event.type); // 输出事件类型

  // 触发事件的目标元素
  console.log("触发事件的元素:", event.target); // 输出触发事件的元素,例如 <button>

  // 当前绑定事件处理程序的元素
  console.log("绑定事件的元素:", event.currentTarget); // 输出当前绑定事件的元素,例如 <button>

  // ---------- 2. 鼠标事件相关属性 ----------
  // 鼠标事件的坐标位置 (相对于视口)
  console.log("鼠标位置 - 相对于视口 (clientX, clientY):", event.clientX, event.clientY); // 输出鼠标点击的 X、Y 坐标

  // 页面滚动后的鼠标位置 (相对于整个页面)
  console.log("鼠标位置 - 相对于整个页面 (pageX, pageY):", event.pageX, event.pageY); // 输出鼠标点击的 X、Y 坐标(包括页面滚动偏移)

  // ---------- 3. 键盘事件相关属性 ----------
  // 按下的键 (对于键盘事件)
  // 需要事件类型为键盘事件 (如 'keydown', 'keyup'),在此处以例子展示
  console.log("按下的键:", event.key); // 输出按下的键,例如 "Enter"

  // ---------- 4. 修饰键状态 ----------
  // 是否按住了 "Alt" 键
  console.log("Alt 键是否被按下:", event.altKey); // 返回 true 或 false

  // 是否按住了 "Shift" 键
  console.log("Shift 键是否被按下:", event.shiftKey); // 返回 true 或 false

  // 是否按住了 "Ctrl" 键
  console.log("Ctrl 键是否被按下:", event.ctrlKey); // 返回 true 或 false

  // 鼠标或键盘是否按下了 "Meta" 键 (Windows 键或 Command 键)
  console.log("Meta 键是否被按下:", event.metaKey); // 返回 true 或 false
})
  • 常见的对象方法有两个:
    1. preventDefault:取消事件的默认行为
    2. stopPropagation:阻止事件的进一步传递
  • 在事件冒泡和事件捕获中,我们探讨过一个问题,那就是同时触发到底好不好,在那时候刻意忽略了如果我只想要触发一个呢?其余无关内容,不应该在作用范围
    • 在决定了从最外层还是最里层触发后,我们可以通过事件对象event的方法stopPropagation来阻止进一步捕获或者冒泡
    • 从操作来说,这只是一个API的调用,很简单,关键在于,我们需要清楚,为什么要阻止进一步捕获or冒泡?这真的是我们想要的吗?在需求和做法之前衡量取舍,是拉开程序员之间能力差距的重要部分
  • preventDefault()stopPropagation() 这两个方法之所以是事件对象的常见方法,是因为它们在处理用户交互时最为核心和常用。注意,他们是交互的核心,因此一旦在处理用户交互时,这两个方法背后的原因应该第一时间想起
document.getElementById("exampleButton").addEventListener("click", function(event) {
  // ---------- 1. 阻止事件的默认行为 ----------
  // 阻止事件的默认行为,例如阻止链接跳转或表单提交
  event.preventDefault();
  console.log("默认行为已阻止");

  // ---------- 2. 事件传播控制 ----------
  // 阻止事件冒泡,使事件不会继续传递到父元素
  event.stopPropagation();
  console.log("事件冒泡已停止");

  // 阻止事件在当前元素上的其他监听器触发,并停止事件冒泡
  event.stopImmediatePropagation();
  console.log("事件立即停止传播,其他监听器不会触发");
});

后续预告

  • 在下一章节,我们会开始编写几个工具函数来锻炼巩固我们前面所学JS的能力
  • 一共有四个工具函数:防抖函数、节流函数、自定义深拷贝、事件总线
    • 如何区分防抖和节流之间的区别?
    • 如何编写出一个合格的工具函数,在工具函数中,我们都需要注意哪些细节?有哪些边界判断是必要的?在工具函数中如何使用文档注释来进一步提升使用便捷度?如何一步步优化属于我们的工具函数?
    • 当手写了这些工具函数之后?带给我们印象最深的会是哪些呢?如何保证编写过程中,思维逻辑的清晰
  • 让我们下一章节见