DOM 操作
浏览器环境 , 规格
JavaScript 语言最初是为 Web 浏览器创建的。此后,它已经发展成为一种具有多种用途和平台的语言。
平台可以是一个浏览器,一个 Web 服务器,或其他 主机(host),甚至可以是一个“智能”咖啡机,如果它能运行 JavaScript 的话。它们每个都提供了特定于平台的功能。JavaScript 规范将其称为 主机环境。
主机环境提供了自己的对象和语言核心以外的函数。Web 浏览器提供了一种控制网页的方法。Node.JS 提供了服务器端功能,等等。
下面是 JavaScript 在浏览器中运行时的鸟瞰示意图:
有一个叫做 window
的“根”对象。它有两个角色:
- 首先,它是 JavaScript 代码的全局对象,如 全局对象 一章所述。
- 其次,它代表“浏览器窗口”,并提供了控制它的方法。
例如,在这里我们将它用作全局对象:
function sayHi() {
alert("Hello");
}
// 全局函数是全局对象的方法:
window.sayHi();
在这里,我们将它用作浏览器窗口,以查看窗口高度:
在这里,我们将它用作浏览器窗口,以查看窗口高度:
alert(window.innerHeight); // 内部窗口高度
还有更多窗口特定的方法和属性,我们稍后会介绍它们。
文档对象模型 DOM
DOM: Document Object Mode 文档对像模型
是HTML和XML文档的编程接口, 定义了访问和操作HTML和XML文档的标准方法 。
DOM 以树形目录结构表达HTML和XML文档 的 , 每一个节点就是一个DOM元素
例如:
// 将背景颜色修改为红色
document.body.style.background = "red";
// 在 1 秒后将其修改回来
setTimeout(() => document.body.style.background = "", 1000); // 1000ms = 1s
DOM 不仅仅用于浏览器
DOM 规范解释了文档的结构,并提供了操作文档的对象。有的非浏览器设备也使用 DOM。
例如,下载 HTML 文件并对其进行处理的服务器端脚本也可以使用 DOM。但它们可能仅支持部分规范中的内容。
用于样式的 CSSOM
另外也有一份针对 CSS 规则和样式表的、单独的规范 CSS Object Model (CSSOM),这份规范解释了如何将 CSS 表示为对象,以及如何读写这些对象。
当我们修改文档的样式规则时,CSSOM 与 DOM 是一起使用的。但实际上,很少需要 CSSOM,因为我们很少需要从 JavaScript 中修改 CSS 规则(我们通常只是添加/移除一些 CSS 类,而不是直接修改其中的 CSS 规则),但这也是可行的。
浏览器对象模型 BOM
浏览器对象模型(Browser Object Model),简称 BOM,表示由浏览器(主机环境)提供的用于处理文档(document)之外的所有内容的其他对象。
例如:
- navigator 对象提供了有关浏览器和操作系统的背景信息。navigator 有许多属性,但是最广为人知的两个属性是:
navigator.userAgent
— 关于当前浏览器,navigator.platform
— 关于平台(可以帮助区分 Windows/Linux/Mac 等)。 - location 对象允许我们读取当前 URL,并且可以将浏览器重定向到新的 URL。
这是我们可以如何使用 location
对象的方法:
alert(location.href); // 显示当前 URL
if (confirm("Go to Wikipedia?")) {
location.href = "https://wikipedia.org"; // 将浏览器重定向到另一个 URL
}
函数 alert/confirm/prompt
也是 BOM 的一部分:它们与文档(document)没有直接关系,但它代表了与用户通信的纯浏览器方法。
规范
BOM 是通用 HTML 规范 的一部分。
是的,你没听错。在 https://html.spec.whatwg.org 中的 HTML 规范不仅是关于“HTML 语言”(标签,特性)的,还涵盖了一堆对象、方法和浏览器特定的 DOM 扩展。这就是“广义的 HTML”。此外,某些部分也有其他的规范,它们被列在 https://spec.whatwg.org 中。
DOM树
HTML 文档的主干是标签(tag)。
根据文档对象模型(DOM),每个 HTML 标签都是一个对象。嵌套的标签是闭合标签的“子标签(children)”。标签内的文本也是一个对象。
所有这些对象都可以通过 JavaScript 来访问,我们可以使用它们来修改页面。
例如,document.body
是表示 <body>
标签的对象。
运行这段代码会使 <body>
保持 3 秒红色状态:
document.body.style.background = 'red'; // 将背景设置为红色
setTimeout(() => document.body.style.background = '', 3000); // 恢复回去
在这,我们使用了 style.background
来修改 document.body
的背景颜色,但是还有很多其他的属性,例如:
innerHTML
— 节点的 HTML 内容。offsetWidth
— 节点宽度(以像素度量)- ……等。
document -> html/xml -> head/body -> ….
搜索: getElement querySelector
当元素彼此靠得近时,DOM 导航属性(navigation property)非常有用。如果不是,那该怎么办?如何去获取页面上的任意元素?
还有其他搜索方法。
教程 : https://zh.javascript.info/searching-elements-dom
document.getElementById 【 或者只使用 id】
如果一个元素有 id
特性(attribute),那我们就可以使用 document.getElementById(id)
方法获取该元素,无论它在哪里。
例如:
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
// 获取该元素
let elem = document.getElementById('elem'); // 查找id
// 将该元素背景改为红色
elem.style.background = 'red'; // 设置样式
</script>
此外,还有一个通过 id
命名的全局变量,它引用了元素: (建议不要使用)
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
// elem 是对带有 id="elem" 的 DOM 元素的引用
elem.style.background = 'red'; // 也就是 window.elem.style.background = "red";
// id="elem-content" 内有连字符,所以它不能成为一个变量
// ...但是我们可以通过使用方括号 window['elem-content'] 来访问它
</script>
请不要使用以 id 命名的全局变量来访问元素
在规范中 对此行为进行了描述,所以它是一种标准。但这是注意考虑到兼容性才支持的。
浏览器尝试通过混合 JavaScript 和 DOM 的命名空间来帮助我们。对于内联到 HTML 中的简单脚本来说,这还行,但是通常来说,这不是一件好事。因为这可能会造成命名冲突。另外,当人们阅读 JavaScript 代码且看不到对应的 HTML 时,变量的来源就会不明显。
在本教程中,我们只会在元素来源非常明显时,为了简洁起见,才会使用 id
直接引用对应的元素。
在实际开发中,document.getElementById
是首选方法。
**id**
必须是唯一的
id
必须是唯一的。在文档中,只能有一个元素带有给定的 id
。
如果有多个元素都带有同一个 id
,那么使用它的方法的行为是不可预测的,例如 document.getElementById
可能会随机返回其中一个元素。因此,请遵守规则,保持 id
的唯一性。
querySelectorAll 【获取所有选择器的内容】
到目前为止,最通用的方法是 elem.querySelectorAll(css)
,它返回 elem
中与给定 CSS 选择器匹配的所有元素。
在这里,我们查找所有为最后一个子元素的 <li>
元素:
<body>
<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
</body>
<script>
let elements = document.querySelectorAll("ul > li:last-child"); // ul 下面的 最后的li 元素
for (let elem of elements) console.log(elem.innerHTML); // test , passed ;
</script>
也可以使用伪类
CSS 选择器的伪类,例如 :hover
和 :active
也都是被支持的。例如,document.querySelectorAll(':hover')
将会返回鼠标指针现在已经结束的元素的集合(按嵌套顺序:从最外层 <html>
到嵌套最多的元素)。
querySelector 【获取第一个选择器】
elem.querySelector(css)
调用会返回给定 CSS 选择器的第一个元素。
换句话说,结果与 elem.querySelectorAll(css)[0]
相同,但是后者会查找 所有 元素,并从中选取一个,而 elem.querySelector
只会查找一个。因此它在速度上更快,并且写起来更短。
matches 【判断获取到匹配的选择器】
之前的方法是搜索 DOM。
elem.matches(css) 不会查找任何内容,它只会检查 elem
是否与给定的 CSS 选择器匹配。它返回 true
或 false
。
<body>
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
</body>
<script>
for (let elem of document.querySelectorAll("a")) {
if (elem.matches("a[href$='zip']")) console.log("The archive reference: " + elem.href);
}
</script>
closest 【查找标签的父级】
元素的祖先(ancestor)是:父级,父级的父级,它的父级等。祖先们一起组成了从元素到顶端的父级链。
elem.closest(css)
方法会查找与 CSS 选择器匹配的最近的祖先。elem
自己也会被搜索。
换句话说,方法 closest
在元素中得到了提升,并检查每个父级。如果它与选择器匹配,则停止搜索并返回该祖先。
<body>
<h1>Contents</h1>
<div class="contents">
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 1</li>
</ul>
</div>
</body>
<script>
let chapter = document.querySelector('.chapter'); // 选择 li
// 使用 closest 方法 elem.closest(css) 方法会查找与 CSS 选择器匹配的最近的祖先 ,
// 如果它与选择器匹配,则停止搜索并返回该祖先。
console.log(chapter.closest(".book")); // ul
console.log(chapter.closest(".contents")); // div
console.log(chapter.closest("h1")); // null 因为 h1 不是它的父级或者祖级
</script>
getElementsBy* 【匹配其他标签】
还有其他通过标签,类等查找节点的方法。
如今,它们大多已经成为了历史,因为 querySelector
功能更强大,写起来更短。
因此,这里我们介绍它们只是为了完整起见,而你仍然可以在旧脚本中找到这些方法。
elem.getElementsByTagName(tag)
查找具有给定标签的元素,并返回它们的集合。tag
参数也可以是对于“任何标签”的星号"*"
。elem.getElementsByClassName(className)
返回具有给定CSS类的元素。document.getElementsByName(name)
返回在文档范围内具有给定name
特性的元素。很少使用。
例如:
// 获取文档中的所有 div
let divs = document.getElementsByTagName('div');
让我们查找 table 中的所有 input
标签:
<body>
<table class="table">
<tr>
<td>
<label for=""><input type="radio" name="age" value="young" checked="checked"></label> less than 18
<label for=""><input type="radio" name="age" value="mature" ></label> from 18 to 50
<label for=""><input type="radio" name="age" value="senior" ></label> more than 60
</td>
<td></td>
</tr>
</table>
</body>
<script>
let inputs = document.getElementsByTagName("input");
for (let elem of inputs) {
console.log(elem.value + ":" + elem.checked); // young : true
}
</script>
不要忘记字母
**"s"**
!新手开发者有时会忘记字符
"s"
。也就是说,他们会调用getElementByTagName
而不是getElement**s**ByTagName
。
getElementById
中没有字母"s"
,是因为它只返回单个元素。但是getElementsByTagName
返回的是元素的集合,所以里面有"s"
。它返回的是一个集合,不是一个元素!
新手的另一个普遍的错误是写:
这是行不通的,因为它需要的是一个 input 的 集合,并将值赋(assign)给它,而不是赋值给其中的一个元素。
我们应该遍历集合或通过对应的索引来获取元素,然后赋值,如下所示:
// 行不通
document.getElementsByTagName('input').value = 5;
// 应该可以运行(如果有 input)
document.getElementsByTagName('input')[0].value = 5;
查找 .article
元素:
<form name="my-form">
<div class="article">Article</div>
<div class="long article">Long article</div>
</form>
<script>
// 按 name 特性查找
let form = document.getElementsByName('my-form')[0];
// 在 form 中按 class 查找
let articles = form.getElementsByClassName('article'); // 返回的是数组
alert(articles.length); // 2, found two elements with class "article"
</script>
实时的集合
所有的 "getElementsBy*"
方法都会返回一个 实时的(live) 集合。这样的集合始终反映的是文档的当前状态,并且在文档发生更改时会“自动更新”。
在下面的例子中,有两个脚本。
- 第一个创建了对
<div>
的集合的引用。截至目前,它的长度是1
。 - 第二个脚本在浏览器再遇到一个
<div>
时运行,所以它的长度是2
。
<div>First div</div>
<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 2
</script>
相反,querySelectorAll
返回的是一个 静态的 集合。就像元素的固定数组 。
如果我们使用它,那么两个脚本都会输出 1
:
<div>First div</div>
<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 1
</script>
现在我们可以很容易地看到不同之处。在文档中出现新的 div
后,静态集合并没有增加。
小结
有 6 种主要的方法,可以在 DOM 中搜索元素节点:
方法名 | 搜索方式 | 可以在元素上调用? | 实时的? |
---|---|---|---|
querySelector |
CSS-selector | ✔ | - |
querySelectorAll |
CSS-selector | ✔ | - |
getElementById |
id |
- | - |
getElementsByName |
name |
- | ✔ |
getElementsByTagName |
tag or '*' |
✔ | ✔ |
getElementsByClassName |
class | ✔ | ✔ |
目前为止,最常用的是 querySelector
和 querySelectorAll
,但是 getElement(s)By*
可能会偶尔有用,或者可以在旧脚本中找到。
此外:
elem.matches(css)
用于检查elem
与给定的 CSS 选择器是否匹配。elem.closest(css)
用于查找与给定 CSS 选择器相匹配的最近的祖先。elem
本身也会被检查。
让我们在这里提一下另一种用来检查子级与父级之间关系的方法,因为它有时很有用:
- 如果
elemB
在elemA
内(elemA
的后代)或者elemA==elemB
,elemA.contains(elemB)
将返回 true。
节点属性 : type tag 和 content
不同的 DOM 节点可能有不同的属性。例如,标签 <a>
相对应的元素节点具有链接相关的(link-related)属性,标签 <input>
相对应的元素节点具有与输入相关的属性,等。文本节点与元素节点不同。但是所有这些标签对应的 DOM 节点之间也存在共有的属性和方法,因为所有类型的 DOM 节点都形成了一个单一层次的结构(single hierarchy)。
每个 DOM 节点都属于相应的内建类。
类如下所示:
- EventTarget — 是根的“抽象(abstract)”类。该类的对象从未被创建。它作为一个基础,以便让所有 DOM 节点都支持所谓的“事件(event)”,我们会在之后学习它。
- Node — 也是一个“抽象”类,充当 DOM 节点的基础。它提供了树的核心功能:
parentNode
,nextSibling
,childNodes
等(它们都是 getter)。Node
类的对象从未被创建。但是有一些继承自它的具体的节点类,例如:文本节点的Text
,元素节点的Element
,以及更多异域(exotic)类,例如注释节点的Comment
。 - Element — 是 DOM 元素的基本类。它提供了元素级的导航(navigation),例如
nextElementSibling
,children
,以及像getElementsByTagName
和querySelector
这样的搜索方法。浏览器中不仅有 HTML,还会有 XML 和 SVG。Element
类充当更多特定类的基本类:SVGElement
,XMLElement
和HTMLElement
。 - HTMLElement
— 最终是所有 HTML 元素的基本类。各种 HTML 元素均继承自它:- HTMLInputElement —
<input>
元素的类, - HTMLBodyElement —
<body>
元素的类, - HTMLAnchorElement —
<a>
元素的类, - ……等,每个标签都有自己的类,这些类可以提供特定的属性和方法。
- HTMLInputElement —
例如,我们考虑一下 <input>
元素的 DOM 对象。它属于 HTMLInputElement 类。
它获取属性和方法,并将其作为下列类(按继承顺序列出)的叠加:
HTMLInputElement
— 该类提供特定于输入的属性,HTMLElement
— 它提供了通用(common)的 HTML 元素方法(以及 getter 和 setter)Element
— 提供通用(generic)元素方法,Node
— 提供通用 DOM 节点属性,EventTarget
— 为事件(包括事件本身)提供支持,- ……最后,它继承自
Object
,因为像hasOwnProperty
这样的“普通对象”方法也是可用的。
我们可以通过回调来查看 DOM 节点类名,因为对象通常都具有 constructor
属性。它引用类的 constructor,constructor.name
就是它的名称:
console.log(document.body.constructor.name); // HTMLBodyElemnet
……或者我们可以对其使用 toString
方法:
console.log(document.body); // [object HTMLBodyElement]
nodeType 属性
nodeType
属性提供了另一种“过时的”用来获取 DOM 节点类型的方法。
它有一个数值型值(numeric value):
- 对于元素节点
elem.nodeType == 1
, - 对于文本节点
elem.nodeType == 3
, - 对于 document 对象
elem.nodeType == 9
, - 在 规范 中还有一些其他值。
例如:
<body>
<script>
let elem = document.body;
// 让我们检查一下它是什么?
alert(elem.nodeType); // 1 => element
// 第一个子节点是
alert(elem.firstChild.nodeType); // 3 => text
// 对于 document 对象,类型是 9
alert( document.nodeType ); // 9
</script>
</body>
在现代脚本中,我们可以使用 instanceof
和其他基于类的检查方法来查看节点类型,但有时 nodeType
可能更简单。我们只能读取 nodeType
而不能修改它。
标签: nodeName 和 tagName
给定一个 DOM 节点,我们可以从 nodeName
或者 tagName
属性中读取它的标签名:
例如:
alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY
tagName 和 nodeName 之间有什么不同吗?
当然,差异就体现在它们的名字上,但确实有些微妙。
tagName
属性仅适用于Element
节点。nodeName
是为任意
定义的:- 对于元素,它的意义与
tagName
相同。 - 对于其他节点类型(text,comment 等),它拥有一个对应节点类型的字符串。
换句话说,tagName
仅受元素节点支持(因为它起源于 Element
类),而 nodeName
则可以说明其他节点类型。
例如,我们比较一下 document
的 tagName
和 nodeName
,以及一个注释节点:
<body><!-- comment -->
<script>
// for comment
alert( document.body.firstChild.tagName ); // undefined(不是一个元素)
alert( document.body.firstChild.nodeName ); // #comment
// for document
alert( document.tagName ); // undefined(不是一个元素)
alert( document.nodeName ); // #document
</script>
</body>
如果我们只处理元素,那么 tagName
和 nodeName
这两种方法,我们都可以使用,没有区别。
innerHTML 内容
innerHTML 属性允许将元素中的 HTML 获取为字符串形式。
我们也可以修改它。因此,它是更改页面最有效的方法之一。
下面这个示例显示了 document.body
中的内容,然后将其完全替换:
<body>
<p>A paragraph</p>
<div>A div</div>
<script>
alert( document.body.innerHTML ); // 读取当前内容
document.body.innerHTML = 'The new BODY!'; // 替换它
</script>
</body>
我们可以尝试插入无效的 HTML,浏览器会修复我们的错误:
<body>
<script>
document.body.innerHTML = '<b>test'; // 忘记闭合标签
alert( document.body.innerHTML ); // <b>test</b>(被修复了)
</script>
</body>
脚本不会执行
如果 innerHTML
将一个 <script>
标签插入到 document 中 — 它会成为 HTML 的一部分,但是不会执行。
innerHTML+=” 会进行完全重写
我们可以使用 elem.innerHTML+="more html"
将 HTML 附加到元素上。
就像这样:
chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "How goes?";
但我们必须非常谨慎地使用它,因为我们所做的 不是 附加内容,而且完全地重写。
从技术上来说,下面这两行代码的作用相同:
elem.innerHTML += "...";
// 进行写入的一种更简短的方式:
elem.innerHTML = elem.innerHTML + "..."
换句话说,innerHTML+=
做了以下工作:
- 移除旧的内容。
- 然后写入新的
innerHTML
(新旧结合)。
总结
每个 DOM 节点都属于一个特定的类。这些类形成层次结构(hierarchy)。完整的属性和方法集是继承的结果。
主要的 DOM 节点属性有:
nodeType
我们可以使用它来查看节点是文本节点还是元素节点。它具有一个数值型值(numeric value):1
表示元素,3
表示文本节点,其他一些则代表其他节点类型。只读。nodeName/tagName
用于元素名,标签名(除了 XML 模式,都要大写)。对于非元素节点,nodeName
描述了它是什么。只读。innerHTML
元素的 HTML 内容。可以被修改。outerHTML
元素的完整 HTML。对elem.outerHTML
的写入操作不会触及elem
本身。而是在外部上下文中将其替换为新的 HTML。nodeValue/data
非元素节点(文本、注释)的内容。两者几乎一样,我们通常使用data
。可以被修改。textContent
元素内的文本:HTML 减去所有<tags>
。写入文本会将文本放入元素内,所有特殊字符和标签均被视为文本。可以安全地插入用户生成的文本,并防止不必要的 HTML 插入。hidden
当被设置为true
时,执行与 CSSdisplay:none
相同的事。
DOM 节点还具有其他属性,具体有哪些属性则取决于它们的类。例如,<input>
元素(HTMLInputElement
)支持 value
,type
,而 <a>
元素(HTMLAnchorElement
)则支持 href
等。大多数标准 HTML 特性(attribute)都具有相应的 DOM 属性。
然而,但是 HTML 特性(attribute)和 DOM 属性(property)并不总是相同的,我们将在下一章中看到。
DOM 属性
当浏览器加载页面时,它会“读取”(或者称之为:“解析”)HTML 并从中生成 DOM 对象。对于元素节点,大多数标准的 HTML 特性(attributes)会自动变成 DOM 对象的属性(properties)。(译注:attribute 和 property 两词意思相近,为作区分,全文将 attribute 译为“特性”,property 译为“属性”,请读者注意区分。)
例如,如果标签是 <body id="page">
,那么 DOM 对象就会有 body.id="page"
。
但特性—属性映射并不是一一对应的!在本章,我们将带领你一起分清楚这两个概念,了解如何使用它们,了解它们何时相同何时不同。
属性
我们已经见过了内建 DOM 属性。它们数量庞大。但是从技术上讲,没有人会限制我们,如果我们觉得这些 DOM 还不够,我们可以添加我们自己的。
例如,让我们在 document.body
中创建一个新的属性:
document.body.myData = {
name: 'Caesar',
title: 'Imperator'
};
alert(document.body.myData.title); // Imperator
我们也可以像下面这样添加一个方法:
document.body.sayTagName = function() {
alert(this.tagName);
};
document.body.sayTagName(); // BODY(这个方法中的 "this" 的值是 document.body)
我们还可以修改内建属性的原型,例如修改 Element.prototype
为所有元素添加一个新方法:
Element.prototype.sayHi = function() {
alert(`Hello, I'm ${this.tagName}`);
};
document.documentElement.sayHi(); // Hello, I'm HTML
document.body.sayHi(); // Hello, I'm BODY
所以,DOM 属性和方法的行为就像常规的 Javascript 对象一样:
- 它们可以有很多值。
- 它们是大小写敏感的(要写成
elem.nodeType
,而不是elem.NoDeTyPe
)。
HTML 特性
在 HTML 中,标签可能拥有特性(attributes)。当浏览器解析 HTML 文本,并根据标签创建 DOM 对象时,浏览器会辨别 标准的 特性并以此创建 DOM 属性。
所以,当一个元素有 id
或其他 标准的 特性,那么就会生成对应的 DOM 属性。但是非 标准的 特性则不会。
例如:
<body id="test" something="non-standard">
<script>
alert(document.body.id); // test
// 非标准的特性没有获得对应的属性
alert(document.body.something); // undefined
</script>
</body>
所以,如果一个特性不是标准的,那么就没有相对应的 DOM 属性。那我们有什么方法来访问这些特性吗?
当然。所有特性都可以通过使用以下方法进行访问:
elem.hasAttribute(name)
— 检查特性是否存在。elem.getAttribute(name)
— 获取这个特性值。elem.setAttribute(name, value)
— 设置这个特性值。elem.removeAttribute(name)
— 移除这个特性。
这些方法操作的实际上是 HTML 中的内容。
我们也可以使用 elem.attributes
读取所有特性:属于内建 Attr 类的对象的集合,具有 name
和 value
属性。
下面是一个读取非标准的特性的示例:
<body something="non-standard">
<script>
alert(document.body.getAttribute('something')); // 非标准的
</script>
</body>
HTML 特性有以下几个特征:
- 它们的名字是大小写不敏感的(
id
与ID
相同)。 - 它们的值总是字符串类型的。
下面是一个使用特性的扩展示例:
<body>
<div id="elem" about="Elephant"></div>
<script>
alert( elem.getAttribute('About') ); // (1) 'Elephant',读取
elem.setAttribute('Test', 123); // (2) 写入
alert( elem.outerHTML ); // (3) 查看特性是否在 HTML 中(在)
for (let attr of elem.attributes) { // (4) 列出所有
alert( `${attr.name} = ${attr.value}` );
}
</script>
</body>
请注意:
getAttribute('About')
— 这里的第一个字母是大写的,但是在 HTML 中,它们都是小写的。但这没有影响:特性的名称是大小写不敏感的。- 我们可以将任何东西赋值给特性,但是这些东西会变成字符串类型的。所以这里我们的值为
"123"
。 - 所有特性,包括我们设置的那个特性,在
outerHTML
中都是可见的。 attributes
集合是可迭代对象,该对象将所有元素的特性(标准和非标准的)作为name
和value
属性存储在对象中。
属性特性同步
当一个标准的特性被改变,对应的属性也会自动更新,(除了几个特例)反之亦然。
在下面这个示例中,id
被修改为特性,我们可以看到对应的属性也发生了变化。然后反过来也是同样的效果:
<input>
<script>
let input = document.querySelector('input');
// 特性 => 属性
input.setAttribute('id', 'id'); // 改变了id内容
alert(input.id); // id(被更新了)
// 属性 => 特性
input.id = 'newId'; // 改变了id内容
alert(input.getAttribute('id')); // newId(被更新了)
</script>
但这里也有些例外,例如 input.value
只能从特性同步到属性,反过来则不行:
<input>
<script>
let input = document.querySelector('input');
// 特性 => 属性
input.setAttribute('value', 'text'); // 更改value的值
alert(input.value); // text
// 这个操作无效,属性 => 特性
input.value = 'newValue'; // 因为没有 input.value 这个特性,不能改变
alert(input.getAttribute('value')); // text(没有被更新!)
</script>
在上面这个例子中:
- 改变特性值
value
会更新属性。 - 但是属性的更改不会影响特性。
这个“功能”在实际中会派上用场,因为用户行为可能会导致 value
的更改,然后在这些操作之后,如果我们想从 HTML 中恢复“原始”值,那么该值就在特性中。
DOM 属性是多类的
DOM 属性不总是字符串类型的。例如,input.checked
属性(对于 checkbox 的)是布尔型的。
<input id="input" type="checkbox" checked />
<script>
alert(input.getAttribute('checked')); // 特性值是:空字符串
alert(input.checked); // 属性值是:true
</script>
还有其他的例子。style
特性是字符串类型的,但 style
属性是一个对象:
<div id="div" style="color:red;font-size:120%">Hello</div>
<script>
// 字符串
alert(div.getAttribute('style')); // color:red;font-size:120%
// 对象
alert(div.style); // [object CSSStyleDeclaration]
alert(div.style.color); // red
</script>
尽管大多数 DOM 属性都是字符串类型的。
非标准的特性, dataset
当编写 HTML 时,我们会用到很多标准的特性。但是非标准的,自定义的呢?首先,让我们看看它们是否有用?用来做什么?
有时,非标准的特性常常用于将自定义的数据从 HTML 传递到 JavaScript,或者用于为 JavaScript “标记” HTML 元素。
像这样:
<!-- 标记这个 div 以在这显示 "name" -->
<div show-info="name"></div>
<!-- 标记这个 div 以在这显示 "age" -->
<div show-info="age"></div>
<script>
// 这段代码找到带有标记的元素,并显示需要的内容
let user = {
name: "Pete",
age: 25
};
for(let div of document.querySelectorAll('[show-info]')) {
// 在字段中插入相应的信息
let field = div.getAttribute('show-info');
div.innerHTML = user[field]; // 首先 "name" 变为 Pete,然后 "age" 变为 25
}
</script>
总结
- 特性(attribute)— 写在 HTML 中的内容。
- 属性(property)— DOM 对象中的内容。
简略的对比:
属性 | 特性 | |
---|---|---|
类型 | 任何值,标准的属性具有规范中描述的类型 | 字符串 |
名字 | 名字(name)是大小写敏感的 | 名字(name)是大小写不敏感的 |
操作特性的方法:
elem.hasAttribute(name)
— 检查是否存在这个特性。elem.getAttribute(name)
— 获取这个特性值。elem.setAttribute(name, value)
— 设置这个特性值。elem.removeAttribute(name)
— 移除这个特性。elem.attributes
— 所有特性的集合。
在大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且我们真的需要特性时,才使用特性,例如:
- 我们需要一个非标准的特性。但是如果它以
data-
开头,那么我们应该使用dataset
。 - 我们想要读取 HTML 中“所写的”值。对应的 DOM 属性可能不同,例如
href
属性一直是一个 完整的 URL,但是我们想要的是“原始的”值。
DOM修改文档
DOM 修改是创建“实时”页面的关键。
在这里,我们将会看到如何“即时”创建新元素并修改现有页面内容。
例子:展示一条消息
让我们使用一个示例进行演示。我们将在页面上添加一条比 alert
更好看的消息。
它的外观如下:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>
这是一个 HTML 示例。现在,让我们使用 JavaScript 创建一个相同的 div
(假设样式已经在 HTML/CSS 文件中)。
创建一个元素
要创建 DOM 节点,这里有两种方法:
document.createElement(tag)
用给定的标签创建一个新 元素节点(element node):let div = document.createElement('div');
document.createTextNode(text)
用给定的文本创建一个 文本节点:let textNode = document.createTextNode('Here I am');
大多数情况下,我们需要为此消息创建像 div
这样的元素节点。
创建一条消息
创建一个消息 div
分为 3 个步骤:
// 1. 创建 <div> 元素
let div = document.createElement('div');
// 2. 将元素的类设置为 "alert"
div.className = "alert";
// 3. 填充消息内容
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
我们已经创建了该元素。但到目前为止,它还只是在一个名为 div
的变量中,尚未在页面中。所以我们无法在页面上看到它。
插入方法
为了让 div
显示出来,我们需要将其插入到 document
中的某处。例如,into <body>
element, referenced by document.body
.
对此有一个特殊的方法 append
:document.body.append(div)
。
这是完整代码:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
document.body.append(div);
</script>
在这个例子中,我们对 document.body
调用了 append
方法。不过我们可以在其他任何元素上调用 append
方法,以将另外一个元素放入到里面。例如,通过调用 div.append(anotherElement)
,我们便可以在 <div>
末尾添加一些内容。
这里是更多的元素插入方法,指明了不同的插入位置:
node.append(...nodes or strings)
—— 在node
末尾 插入节点或字符串,node.prepend(...nodes or strings)
—— 在node
开头 插入节点或字符串,node.before(...nodes or strings)
—— 在node
前面 插入节点或字符串,node.after(...nodes or strings)
—— 在node
后面 插入节点或字符串,node.replaceWith(...nodes or strings)
—— 将node
替换为给定的节点或字符串。
这些方法的参数可以是一个要插入的任意的 DOM 节点列表,或者文本字符串(会被自动转换成文本节点)。
让我们在实际应用中看一看。
下面是使用这些方法将列表项添加到列表中,以及将文本添加到列表前面和后面的示例:
<ol id="ol">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
ol.before('before'); // 将字符串 "before" 插入到 <ol> 前面
ol.after('after'); // 将字符串 "after" 插入到 <ol> 后面
let liFirst = document.createElement('li');
liFirst.innerHTML = 'prepend';
ol.prepend(liFirst); // 将 liFirst 插入到 <ol> 的最开始
let liLast = document.createElement('li');
liLast.innerHTML = 'append';
ol.append(liLast); // 将 liLast 插入到 <ol> 的最末尾
</script>
节点移除
想要移除一个节点,可以使用 node.remove()
。
让我们的消息在一秒后消失:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
document.body.append(div);
setTimeout(() => div.remove(), 1000);
</script>
请注意:如果我们要将一个元素 移动 到另一个地方,则无需将其从原来的位置中删除。
所有插入方法都会自动从旧位置删除该节点。
例如,让我们进行元素交换:
<div id="first">First</div>
<div id="second">Second</div>
<script>
// 无需调用 remove
second.after(first); // 获取 #second,并在其后面插入 #first
</script>
克隆节点
如何再插入一条类似的消息?
我们可以创建一个函数,并将代码放在其中。但是另一种方法是 克隆 现有的 div
,并修改其中的文本(如果需要)。
当我们有一个很大的元素时,克隆的方式可能更快更简单。
调用 elem.cloneNode(true)
来创建元素的一个“深”克隆 — 具有所有特性(attribute)和子元素。如果我们调用 elem.cloneNode(false)
,那克隆就不包括子元素。
一个拷贝消息的示例:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert" id="div">
<strong>Hi there!</strong> You've read an important message.
</div>
<script>
let div2 = div.cloneNode(true); // 克隆消息
div2.querySelector('strong').innerHTML = 'Bye there!'; // 修改克隆
div.after(div2); // 在已有的 div 后显示克隆
</script>
DocumentFragment
DocumentFragment
是一个特殊的 DOM 节点,用作来传递节点列表的包装器(wrapper)。
我们可以向其附加其他节点,但是当我们将其插入某个位置时,则会插入其内容。
例如,下面这段代码中的 getListContent
会生成带有 <li>
列表项的片段,然后将其插入到 <ul>
中:
<ul id="ul"></ul>
<script>
function getListContent() {
let fragment = new DocumentFragment(); // `DocumentFragment` 是一个特殊的 DOM 节点,用作来传递节点列表的包装器(wrapper)
for(let i=1; i<=3; i++) {
let li = document.createElement('li');
li.append(i);
fragment.append(li);
}
return fragment;
}
ul.append(getListContent()); // (*)
</script>
请注意,在最后一行 (*)
我们附加了 DocumentFragment
,但是它和 ul
“融为一体(blends in)”了,所以最终的文档结构应该是:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
DocumentFragment
很少被显式使用。如果可以改为返回一个节点数组,那为什么还要附加到特殊类型的节点上呢?重写示例:
<ul id="ul"></ul>
<script>
function getListContent() {
let result = [];
for(let i=1; i<=3; i++) {
let li = document.createElement('li');
li.append(i);
result.push(li);
}
return result;
}
ul.append(...getListContent()); // append + "..." operator = friends!
</script>
我们之所以提到 DocumentFragment
,主要是因为它上面有一些概念,例如 template 元素,我们将在以后讨论。
总结
- 创建新节点的方法:
document.createElement(tag)
— 用给定的标签创建一个元素节点,document.createTextNode(value)
— 创建一个文本节点(很少使用),elem.cloneNode(deep)
— 克隆元素,如果deep==true
则与其后代一起克隆。
- 插入和移除节点的方法:
node.append(...nodes or strings)
— 在node
末尾插入,node.prepend(...nodes or strings)
— 在node
开头插入,node.before(...nodes or strings)
— 在node
之前插入,node.after(...nodes or strings)
— 在node
之后插入,node.replaceWith(...nodes or strings)
— 替换node
。node.remove()
— 移除node
。
文本字符串被“作为文本”插入。
- 这里还有“旧式”的方法:
parent.appendChild(node)
parent.insertBefore(node, nextSibling)
parent.removeChild(node)
parent.replaceChild(newElem, node)
这些方法都返回 node
。
- 在
html
中给定一些 HTML,elem.insertAdjacentHTML(where, html)
会根据where
的值来插入它:"beforebegin"
— 将html
插入到elem
前面,"afterbegin"
— 将html
插入到elem
的开头,"beforeend"
— 将html
插入到elem
的末尾,"afterend"
— 将html
插入到elem
后面。
另外,还有类似的方法,elem.insertAdjacentText
和 elem.insertAdjacentElement
,它们会插入文本字符串和元素,但很少使用。
- 要在页面加载完成之前将 HTML 附加到页面:
document.write(html)
页面加载完成后,这样的调用将会擦除文档。多见于旧脚本。
样式和类
在我们讨论 JavaScript 处理样式和类的方法之前 — 有一个重要的规则。希望它足够明显,但是我们仍然必须提到它。
通常有两种设置元素样式的方式:
- 在 CSS 中创建一个类,并添加它:
<div class="...">
- 将属性直接写入
style
:<div style="...">
。
JavaScript 既可以修改类,也可以修改 style
属性。
相较于将样式写入 style
属性,我们应该首选通过 CSS 类的方式来添加样式。仅当类“无法处理”时,才应选择使用 style
属性的方式。
例如,如果我们动态地计算元素的坐标,并希望通过 JavaScript 来设置它们,那么使用 style
是可以接受的,如下所示:
let top = /* 复杂的计算 */;
let left = /* 复杂的计算 */;
elem.style.left = left; // 例如 '123px',在运行时计算出的
elem.style.top = top; // 例如 '456px'
className 和 classList
更改类是脚本中最常见的操作之一。
在很久以前,JavaScript 中有一个限制:像 "class"
这样的保留字不能用作对象的属性。这一限制现在已经不存在了,但当时就不能存在像 elem.class
这样的 "class"
属性。
因此,对于类,引入了看起来类似的属性 "className"
:elem.className
对应于 "class"
特性(attribute)。
例如:
<body class="main page">
<script>
alert(document.body.className); // main page
</script>
</body>
如果我们对 elem.className
进行赋值,它将替换类中的整个字符串。有时,这正是我们所需要的,但通常我们希望添加/删除单个类。
这里还有另一个属性:elem.classList
。
elem.classList
是一个特殊的对象,它具有 add/remove/toggle
单个类的方法。
- add() 添加类名
- remove() 删除类名
- toggle() 有类名就删除,没有类名就添加
- contains() 检查是否存在类名,有就返回 true , 没有就返回false
例如:
<body class="main page">
<script>
// 添加一个 class
document.body.classList.add('article');
alert(document.body.className); // main page article
</script>
</body>
因此,我们既可以使用 className
对完整的类字符串进行操作,也可以使用使用 classList
对单个类进行操作。我们选择什么取决于我们的需求。
classList
的方法:
elem.classList.add/remove(class)
— 添加/移除类。elem.classList.toggle(class)
— 如果类不存在就添加类,存在就移除它。elem.classList.contains(class)
— 检查给定类,返回true/false
。
此外,classList
是可迭代的,因此,我们可以像下面这样列出所有类:
<body class="main page">
<script>
for (let name of document.body.classList) {
alert(name); // main,然后是 page
}
</script>
</body>
元素样式
elem.style
属性是一个对象,它对应于 "style"
特性(attribute)中所写的内容。elem.style.width="100px"
的效果等价于我们在 style
特性中有一个 width:100px
字符串。
对于多词(multi-word)属性,使用驼峰式 camelCase:
background-color => elem.style.backgroundColor
z-index => elem.style.zIndex
border-left-width => elem.style.borderLeftWidth
document.body.style.backgroundColor = prompt('background color?', 'green');
前缀属性
像 -moz-border-radius
和 -webkit-border-radius
这样的浏览器前缀属性,也遵循同样的规则:连字符 -
表示大写。
例如:
button.style.MozBorderRadius = '5px';
button.style.WebkitBorderRadius = '5px';
重置样式属性
有时我们想要分配一个样式属性,稍后移除它。
例如,为了隐藏一个元素,我们可以设置 elem.style.display = "none"
。
然后,稍后我们可能想要移除 style.display
,就像它没有被设置一样。这里不应该使用 delete elem.style.display
,而应该使用 elem.style.display = ""
将其赋值为空。
// 如果我们运行这段代码,<body> 将会闪烁
document.body.style.display = "none"; // 隐藏
setTimeout(() => document.body.style.display = "", 1000); // 恢复正常
如果我们将 display
设置为空字符串,那么浏览器通常会应用 CSS 类以及内置样式,就好像根本没有这样的 style
属性一样。
用 **style.cssText**
进行完全的重写, 完全重写,也就是连之前存在的样式会被覆盖
通常,我们使用 style.*
来对各个样式属性进行赋值。我们不能像这样的 div.style="color: red; width: 100px"
设置完整的属性,因为 div.style
是一个对象,并且它是只读的。
想要以字符串的形式设置完整的样式,可以使用特殊属性 style.cssText
:
<div id="div">Button</div>
<script>
// 我们可以在这里设置特殊的样式标记,例如 "important"
div.style.cssText=`color: red !important;
background-color: yellow;
width: 100px;
text-align: center;
`;
alert(div.style.cssText);
</script>
我们很少使用这个属性,因为这样的赋值会删除所有现有样式:它不是进行添加,而是替换它们。有时可能会删除所需的内容。但是,当我们知道我们不会删除现有样式时,可以安全地将其用于新元素。
可以通过设置一个特性(attribute)来实现同样的效果:div.setAttribute('style', 'color: red...')
。
注意单位
不要忘记将 CSS 单位添加到值上。
例如,我们不应该将 elem.style.top
设置为 10
,而应将其设置为 10px
。否则设置会无效:
<body>
<script>
// 无效!
document.body.style.margin = 20;
alert(document.body.style.margin); // ''(空字符串,赋值被忽略了)
// 现在添加了 CSS 单位(px)— 生效了
document.body.style.margin = '20px';
alert(document.body.style.margin); // 20px
alert(document.body.style.marginTop); // 20px
alert(document.body.style.marginLeft); // 20px
</script>
</body>
请注意:浏览器在最后几行代码中对属性 style.margin
进行了“解包”,并从中推断出 style.marginLeft
和 style.marginTop
。
总结
要管理 class,有两个 DOM 属性:
className
— 字符串值,可以很好地管理整个类的集合。classList
— 具有add/remove/toggle/contains
方法的对象,可以很好地支持单个类。
要改变样式:
style
属性是具有驼峰(camelCased)样式的对象。对其进行读取和修改与修改"style"
特性(attribute)中的各个属性具有相同的效果。要了解如何应用important
和其他特殊内容 — 在 MDN 中有一个方法列表。style.cssText
属性对应于整个"style"
特性(attribute),即完整的样式字符串。
要读取已解析的(resolved)样式(对于所有类,在应用所有 CSS 并计算最终值之后):
getComputedStyle(elem, [pseudo])
返回与style
对象类似的,且包含了所有类的对象。只读。
元素大小的滚动
示例
作为演示属性的示例元素,我们将使用下面给出的元素:
<div id="example">
...Text...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F;
padding: 20px;
overflow: auto;
}
</style>
它有边框(border),内边距(padding)和滚动(scrolling)等全套功能。但没有外边距(margin),因为它们不是元素本身的一部分,并且它们没什么特殊的属性。
这个元素看起来就像这样:
Window 大小和移动
我们如何找到浏览器窗口(window)的宽度和高度呢?我们如何获得文档(document)的包括滚动部分在内的完整宽度和高度呢?我们如何使用 JavaScript 滚动页面?
对于此类信息,我们可以使用与 <html>
标签相对应的根文档元素 document.documentElement
。但是还有其他方法和特性需要考虑。
窗口的 width / heigth
为了获取窗口(window)的宽度和高度,我们可以使用 document.documentElement
的 clientWidth/clientHeight
:
不是
**window.innerWidth/innerHeight**
浏览器也支持像
window.innerWidth/innerHeight
这样的属性。它们看起来像我们想要的,那为什么不使用它们呢?如果这里存在一个滚动条,并且滚动条占用了一些空间,那么
clientWidth/clientHeight
会提供没有滚动条(减去它)的 width/height。换句话说,它们返回的是可用于内容的文档的可见部分的 width/height。
window.innerWidth/innerHeight
包括了滚动条。如果这里有一个滚动条,它占用了一些空间,那么这两行代码会显示不同的值:
在大多数情况下,我们需要 可用 的窗口宽度以绘制或放置某些东西。也就是说,在滚动条内(如果有)。所以,我们应该使用
documentElement.clientHeight/clientWidth
。也就是说区别: clienHeight/clienWidth 不会将滚动条算在内, 而innerWidth/innerHeight 会将滚动条计算在内
alert( window.innerWidth ); // 整个窗口的宽度
alert( document.documentElement.clientWidth ); // 减去滚动条宽度后的窗口宽度
**DOCTYPE**
很重要请注意:当 HTML 中没有
<!DOCTYPE HTML>
时,顶层级(top-level)几何属性的工作方式可能就会有所不同。可能会出现一些稀奇古怪的情况。在现代 HTML 中,我们始终都应该写
DOCTYPE
。
文档的 width / heigth
从理论上讲,由于根文档元素是 document.documentElement
,并且它包围了所有内容,因此我们可以通过使用 documentElement.scrollWidth/scrollHeight
来测量文档的完整大小。
但是在该元素上,对于整个文档,这些属性均无法正常工作。在 Chrome/Safari/Opera 中,如果没有滚动条,documentElement.scrollHeight
甚至可能小于 documentElement.clientHeight
!很奇怪,对吧?
为了可靠地获得完整的文档高度,我们应该采用以下这些属性的最大值:
let scrollHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
alert('Full document height, with scrolled out part: ' + scrollHeight);
获得当前滚动
DOM 元素的当前滚动状态在其 scrollLeft/scrollTop
属性中。
对于文档滚动,在大多数浏览器中,我们可以使用 document.documentElement.scrollLeft/scrollTop
,但在较旧的基于 WebKit 的浏览器中则不行,例如在 Safari(bug 5991)中,我们应该使用 document.body
而不是 document.documentElement
。
幸运的是,我们根本不必记住这些特性,因为滚动在 window.pageXOffset/pageYOffset
中可用:
alert('Current scroll from the top: ' + window.pageYOffset);
alert('Current scroll from the left: ' + window.pageXOffset);
这些属性是只读的。
滚动: scrollTo , scrollBy , scrollintoView
重要:
必须在 DOM 完全构建好之后才能通过 JavaScript 滚动页面。
例如,如果我们尝试通过
<head>
中的脚本滚动页面,它将无法正常工作。
可以通过更改 scrollTop/scrollLeft
来滚动常规元素。
我们可以使用 document.documentElement.scrollTop/scrollLeft
对页面进行相同的操作(Safari 除外,而应该使用 document.body.scrollTop/Left
代替)。
或者,有一个更简单的通用解决方案:使用特殊方法 window.scrollBy(x,y) 和 window.scrollTo(pageX,pageY)。
- 方法
scrollBy(x,y)
将页面滚动至 相对于当前位置的**(x, y)**
位置。例如,scrollBy(0,10)
会将页面向下滚动10px
。 - 方法
scrollTo(pageX,pageY)
将页面滚动至 绝对坐标,使得可见部分的左上角具有相对于文档左上角的坐标(pageX, pageY)
。就像设置了scrollLeft/scrollTop
一样。
scrollintoView
为了完整起见,让我们再介绍一种方法:elem.scrollIntoView(top)。
对 elem.scrollIntoView(top)
的调用将滚动页面以使 elem
可见。它有一个参数:
- 如果
top=true
(默认值),页面滚动,使elem
出现在窗口顶部。元素的上边缘将与窗口顶部对齐。 - 如果
top=false
,页面滚动,使elem
出现在窗口底部。元素的底部边缘将与窗口底部对齐。 - 类似于翻半页
进制滚动
有时候我们需要使文档“不可滚动”。例如,当我们需要用一条需要立即引起注意的大消息来覆盖文档时,我们希望访问者与该消息而不是与文档进行交互。
要使文档不可滚动,只需要设置 document.body.style.overflow = "hidden"
。该页面将“冻结”在其当前滚动位置上。
总结
几何:
- 文档可见部分的 width/height(内容区域的 width/height):
document.documentElement.clientWidth/clientHeight
- 整个文档的 width/height,其中包括滚动出去的部分:
let scrollHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
滚动:
- 读取当前的滚动:
window.pageYOffset/pageXOffset
。 - 更改当前的滚动:
window.scrollTo(pageX,pageY)
— 绝对坐标,window.scrollBy(x,y)
— 相对当前位置进行滚动,elem.scrollIntoView(top)
— 滚动以使elem
可见(elem
与窗口的顶部/底部对齐)。
浏览器事件简介
事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。
这是最有用的 DOM 事件的列表,你可以浏览一下:
鼠标事件:
click
—— 当鼠标点击一个元素时(触摸屏设备会在点击时生成)。contextmenu
—— 当鼠标右键点击一个元素时。mouseover
/mouseout
—— 当鼠标指针移入/离开一个元素时。mousedown
/mouseup
—— 当在元素上按下/释放鼠标按钮时。mousemove
—— 当鼠标移动时。
键盘事件:
keydown
和keyup
—— 当按下和松开一个按键时。
表单(form)元素事件:
submit
—— 当访问者提交了一个<form>
时。focus
—— 当访问者聚焦于一个元素时,例如聚焦于一个<input>
。
Document 事件:
DOMContentLoaded
—— 当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
CSS 事件:
transitionend
—— 当一个 CSS 动画完成时。
还有很多其他事件。我们将在下一章中详细介绍具体事件。
HTML 特性
处理程序可以设置在 HTML 中名为 on<event>
的特性(attribute)中。
例如,要为一个 input
分配一个 click
处理程序,我们可以使用 onclick
,像这样;
<input value="Click me" onclick="alert('Click!')" type="button">
在鼠标点击时,onclick
中的代码就会运行。
请注意,在 onclick
中,我们使用单引号,因为特性本身使用的是双引号。如果我们忘记了代码是在特性中的,而使用了双引号,像这样:onclick="alert("Click!")"
,那么它就无法正确运行。
HTML 特性不是编写大量代码的好位置,因此我们最好创建一个 JavaScript 函数,然后在 HTML 特性中调用这个函数。
在这里点击会运行 countRabbits()
:
<script>
function countRabbits() {
for (let i = 1; i <= 3; i++) {
alert("Rabbits number: " + i);
}
}
</script>
<!-- 点击表单执行函数 -->
<input type="button" value="Click me" onclick="countRabbits()">
DOM属性
我们可以使用 DOM 属性(property)on<event>
来分配处理程序。
例如 elem.onclick
:
<input id="elem" type="button" value="Click me">
<script>
elem.onclick = function() {
alert('Thank you');
};
</script>
如果一个处理程序是通过 HTML 特性(attribute)分配的,那么随后浏览器读取它,并从特性的内容创建一个新函数,并将这个函数写入 DOM 属性(property)。
因此,这种方法实际上与前一种方法相同。
这两段代码工作相同:
- 只有 HTML:
<input type="button" onclick="alert('Click!')" value="Button">
- HTML + JS:
<input type="button" id="button" value="Button">
<script>
button.onclick = function() {
alert('Click!');
};
</script>
在第一个例子中,button.onclick
是通过 HTML 特性(attribute)初始化的,而在第二个例子中是通过脚本初始化的。这是它们唯一的不同之处。
因为这里只有一个 **onclick**
属性,所以我们无法分配更多事件处理程序。
在下面这个示例中,我们使用 JavaScript 添加了一个处理程序,覆盖了现有的处理程序:
<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
elem.onclick = function() { // 覆盖了现有的处理程序
alert('After'); // 只会显示此内容
};
</script>
要移除一个处理程序 —— 赋值 elem.onclick = null
。
访问元素: this
处理程序中的 this
的值是对应的元素。就是处理程序所在的那个元素。
下面这行代码中的 button
使用 this.innerHTML
来显示它的内容:
<button onclick="alert(this.innerHTML)">Click me</button>
可能出现的错误
如果你刚开始写事件 —— 请注意一些细微之处。
我们可以将一个现存的函数用作处理程序:
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks;
但要注意:函数应该是以 sayThanks
的形式进行赋值,而不是 sayThanks()
。
// 正确
button.onclick = sayThanks;
// 错误
button.onclick = sayThanks();
如果我们添加了括号,那么 sayThanks()
就变成了一个函数调用。所以,最后一行代码实际上获得的是函数执行的 结果,即 undefined
(因为这个函数没有返回值)。此代码不会工作。
……但在标记(markup)中,我们确实需要括号:
<input type="button" id="button" onclick="sayThanks()">
这个区别很容易解释。当浏览器读取 HTML 特性(attribute)时,浏览器将会使用 特性中的内容 创建一个处理程序。
addEventListener
上述分配处理程序的方式的根本问题是 —— 我们不能为一个事件分配多个处理程序。
假设,在我们点击了一个按钮时,我们代码中的一部分想要高亮显示这个按钮,另一部分则想要显示一条消息。
我们想为此事件分配两个处理程序。但是,新的 DOM 属性将覆盖现有的 DOM 属性:
input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // 替换了前一个处理程序
Web 标准的开发者很早就了解到了这一点,并提出了一种使用特殊方法 addEventListener
和 removeEventListener
来管理处理程序的替代方法。它们没有这样的问题。
添加处理程序的语法:
element.addEventListener(event, handler[, options]);
event
事件名,例如:"click"
。handler
处理程序。options
具有以下属性的附加可选对象:once
:如果为true
,那么会在被触发后自动删除监听器。capture
:事件处理的阶段,我们稍后将在 冒泡和捕获 一章中介绍。由于历史原因,options
也可以是false/true
,它与{capture: false/true}
相同。passive
:如果为true
,那么处理程序将不会调用preventDefault()
,我们稍后将在 浏览器默认行为 一章中介绍。
要移除处理程序,可以使用 removeEventListener
:
element.removeEventListener(event, handler[, options]);
移除需要相同的函数
要移除处理程序,我们需要传入与分配的函数完全相同的函数。
这不起作用:
处理程序不会被移除,因为
removeEventListener
获取了另一个函数 —— 使用相同的代码,但这并不起作用,因为它是一个不同的函数对象。下面是正确方法:
请注意 —— 如果我们不将函数存储在一个变量中,那么我们就无法移除它。由
addEventListener
分配的处理程序将无法被“读回”。
elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));
function handler() {
alert( 'Thanks!' );
}
input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);
对象处理程序 : handleEvent
我们不仅可以分配函数,还可以使用 addEventListener
将一个对象分配为事件处理程序。当事件发生时,就会调用该对象的 handleEvent
方法。
例如:
<button id="elem">Click me</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
};
elem.addEventListener('click', obj);
</script>
正如我们所看到的,当 addEventListener
接收一个对象作为处理程序时,在事件发生时,它就会调用 obj.handleEvent(event)
来处理事件。
我们也可以对此使用一个类:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
这里,同一个对象处理两个事件。请注意,我们需要使用 addEventListener
来显式设置事件,以指明要监听的事件。这里的 menu
对象只监听 mousedown
和 mouseup
,而没有任何其他类型的事件。
handleEvent
方法不必通过自身完成所有的工作。它可以调用其他特定于事件的方法,例如:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
elem.innerHTML = "Mouse button pressed";
}
onMouseup() {
elem.innerHTML += "...and released.";
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
现在事件处理程序已经明确地分离了出来,这样更容易进行代码编写和后续维护。
总结
这里有 3 种分配事件处理程序的方式:
- HTML 特性(attribute):
onclick="..."
。 - DOM 属性(property):
elem.onclick = function
。 - 方法(method):
elem.addEventListener(event, handler[, phase])
用于添加,removeEventListener
用于移除。
HTML 特性很少使用,因为 HTML 标签中的 JavaScript 看起来有些奇怪且陌生。而且也不能在里面写太多代码。
DOM 属性用起来还可以,但我们无法为特定事件分配多个处理程序。在许多场景中,这种限制并不严重。
最后一种方式是最灵活的,但也是写起来最长的。有少数事件只能使用这种方式。例如 transtionend
和 DOMContentLoaded
(上文中讲到了)。addEventListener
也支持对象作为事件处理程序。在这种情况下,如果发生事件,则会调用 handleEvent
方法。
无论你如何分类处理程序 —— 它都会将获得一个事件对象作为第一个参数。该对象包含有关所发生事件的详细信息。
冒泡和捕获
冒泡
冒泡(bubbling)原理很简单。
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。 也就是从小到大执行内容
假设我们有 3 层嵌套 FORM > DIV > P
,它们各自拥有一个处理程序:、
<!-- 当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,
然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。 -->
<form onclick="alert('form')">
form
<div onclick="alert('div')">
div
<p onclick="alert('p')">
p
</p>
</div>
</form>
点击内部的 <p>
会首先运行 onclick
:
- 在该
<p>
上的。 - 然后是外部
<div>
上的。 - 然后是外部
<form>
上的。 - 以此类推,直到最后的
document
对象。
因此,如果我们点击 <p>
,那么我们将看到 3 个 alert:p
→ div
→ form
。
这个过程被称为“冒泡(bubbling)”,因为事件从内部元素“冒泡”到所有父级,就像在水里的气泡一样。
几乎 所有事件都会冒泡
这句话中的关键词是“几乎”。
例如,
focus
事件不会冒泡。同样,我们以后还会遇到其他例子。但这仍然是例外,而不是规则,大多数事件的确都是冒泡的。
event.target
父元素上的处理程序始终可以获取事件实际发生位置的详细信息。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 **event.target**
访问。
注意与 this
(=event.currentTarget
)之间的区别:
event.target
—— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。this
—— 是“当前”元素,其中有一个当前正在运行的处理程序。
例如,如果我们有一个处理程序 form.onclick
,那么它可以“捕获”表单内的所有点击。无论点击发生在哪里,它都会冒泡到 <form>
并运行处理程序。
在 form.onclick
处理程序中:
this
(=event.currentTarget
)是<form>
元素,因为处理程序在它上面运行。event.target
是表单中实际被点击的元素。
event.target
可能会等于 this
—— 当点击事件发生在 <form>
元素上时,就会发生这种情况。
停止冒泡
冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 <html>
,然后再到 document
对象,有些事件甚至会到达 window
,它们会调用路径上所有的处理程序。
但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。
用于停止冒泡的方法是 event.stopPropagation()
。
例如,如果你点击 <button>
,这里的 body.onclick
不会工作:
<body onclick="alert(`the bubbling doesn't reach here`)">
<!--event.stopPropagation() 停止冒泡 -->
<button onclick="event.stopPropagation()">Click me</button>
</body>
event.stopImmediatePropagation()
如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。
换句话说,
event.stopPropagation()
停止向上移动,但是当前元素上的其他处理程序都会继续运行。有一个
event.stopImmediatePropagation()
方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。不要在没有需要的情况下停止冒泡!
冒泡很方便。不要在没有真实需求时阻止它:除非是显而易见的,并且在架构上经过深思熟虑的。
有时
event.stopPropagation()
会产生隐藏的陷阱,以后可能会成为问题。例如:
- 我们创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用
stopPropagation
,以便不会触发外部菜单。- 之后,我们决定捕获在整个窗口上的点击,以追踪用户的行为(用户点击的位置)。有些分析系统会这样做。通常,代码会使用
document.addEventListener('click'…)
来捕获所有的点击。- 我们的分析不适用于被
stopPropagation
所阻止点击的区域。太伤心了,我们有一个“死区”。通常,没有真正的必要去阻止冒泡。一项看似需要阻止冒泡的任务,可以通过其他方法解决。其中之一就是使用自定义事件,稍后我们会介绍它们此外,我们还可以将我们的数据写入一个处理程序中的
event
对象,并在另一个处理程序中读取该数据,这样我们就可以向父处理程序传递有关下层处理程序的信息。
捕获
事件处理的另一个阶段被称为“捕获(capturing)”。它很少被用在实际开发中,但有时是有用的。
DOM 事件标准描述了事件传播的 3 个阶段:
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。