- 浏览器环境
- 文档对象模型(DOM)
- 浏览器对象模型(BOM)
- 在最顶层:documentElement 和 body
- 子节点:childNodes,firstChild,lastChild
- document.getElementById 或者只使用 id
- querySelectorAll
- querySelector
- matches
- closest
- getElementsBy*
- 实时的集合
- 总结
- 任务
- textContent:纯文本
- “hidden” 属性
- 更多属性
- 总结
- 导航:表单和元素
- DOM 属性
- HTML 特性
- 属性—特性同步
- DOM 属性是多类型的
- 非标准的特性,dataset
- 总结
- 例子:展示一条消息
- 创建一个元素
- 插入方法
- insertAdjacentHTML/Text/Element
- 节点移除
- 克隆节点:cloneNode
- DocumentFragment
- 老式的 insert/remove 方法
- 聊一聊 “document.write”
- 总结
浏览器环境
JavaScript 语言最初是为 Web 浏览器创建的。
下面是 JavaScript 在浏览器中运行时的鸟瞰示意图:
有一个叫做 window
的“根”对象。它有两个角色:
- 首先,它是 JavaScript 代码的全局对象,如 全局对象 一章所述。
- 其次,它代表“浏览器窗口”,并提供了控制它的方法。
例如,在这里我们将它用作全局对象:
function sayHi() { alert("Hello");}// 全局函数是全局对象的方法:window.sayHi();
在这里,我们将它用作浏览器窗口,以查看窗口高度:
alert(window.innerHeight); // 内部窗口高度
还有更多窗口特定的方法和属性,我们稍后会介绍它们。
文档对象模型(DOM)
文档对象模型(Document Object Model),简称 DOM,将所有页面内容表示为可以修改的对象。
document
对象是页面的主要“入口点”。我们可以使用它来更改或创建页面上的任何内容。
例如:
// 将背景颜色修改为红色
document.body.style.background = "red";
// 在 1 秒后将其修改回来
setTimeout(() => document.body.style.background = "", 1000);
浏览器对象模型(BOM)
浏览器对象模型(Browser Object Model),简称 BOM,表示由浏览器(主机环境)提供的用于处理文档(document)之外的所有内容的其他对象。
例如:
alert(location.href);
// 显示当前 URL
if (confirm("Go to Wikipedia?")) {
location.href = "https://wikipedia.org"; // 将浏览器重定向到另一个 URL
}
函数 alert/confirm/prompt
也是 BOM 的一部分:它们与文档(document)没有直接关系,但它代表了与用户通信的纯浏览器方法。
在最顶层:documentElement 和 body
最顶层的树节点可以直接作为 document
的属性来使用:
<html> = document.documentElement
最顶层的 document 节点是 document.documentElement
。这是对应 <html>
标签的 DOM 节点。
<body>= document.body
另一个被广泛使用的 DOM 节点是 <body>
元素 — document.body
。
<head> = document.head
<head>
标签可以通过 document.head
访问。
这里有个问题:document.body
的值可能是 null
脚本无法访问在运行时不存在的元素。
尤其是,如果一个脚本是在 <head>
中,那么脚本是访问不到 document.body
元素的,因为浏览器还没有读到它。
所以,下面例子中的第一个 alert
显示 null
:
<html>
<head>
<script>
alert( "From HEAD: " + document.body ); // null,这里目前还没有 <body>
</script>
</head>
<body>
<script>
alert( "From BODY: " + document.body ); // HTMLBodyElement,现在存在了
</script>
</body>
</html>
在 DOM 的世界中,null
就意味着“不存在”
在 DOM 中,null
值就意味着“不存在”或者“没有这个节点”。
子节点:childNodes,firstChild,lastChild
从现在开始,我们将使用下面这两个术语:
- 子节点(或者叫作子) — 对应的是直系的子元素。换句话说,它们被完全嵌套在给定的元素中。例如,
<head>
和<body>
就是<html>
元素的子元素。 - 子孙元素 — 嵌套在给定元素中的所有元素,包括子元素,以及子元素的子元素等。
例如,这里 <body>
有子元素 <div>
和 <ul>
(以及一些空白的文本节点):
<html>
<body>
<div>Begin</div>
<ul>
<li>
<b>Information</b>
</li>
</ul>
</body>
</html>
……<body>
元素的子孙元素不仅包含直接的子元素 <div>
和 <ul>
,还包含像 <li>
(<ul>
的子元素)和 <b>
(<li>
的子元素)这样的元素 — 整个子树。
childNodes
集合列出了所有子节点,包括文本节点。
下面这个例子显示了 document.body
的子元素:
<html>
<body>
<div>Begin</div>
<ul>
<li>Information</li>
</ul>
<div>End</div>
<script>
for (let i = 0; i < document.body.childNodes.length; i++) {
alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
}
</script>
...more stuff...
</body>
</html>
document.getElementById 或者只使用 id
如果一个元素有 id
特性(attribute),那我们就可以使用 document.getElementById(id)
方法获取该元素,无论它在哪里。
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
// 获取该元素
let elem = document.getElementById('elem');
// 将该元素背景改为红色
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';
// id="elem-content" 内有连字符,所以它不能成为一个变量
// ...但是我们可以通过使用方括号 window['elem-content'] 来访问它
</script>
……除非我们声明一个具有相同名称的 JavaScript 变量,否则它具有优先权:
<div id="elem"></div>
<script>
let elem = 5; // 现在 elem 是 5,而不是对 <div id="elem"> 的引用
alert(elem); // 5
</script>
在实际开发中,document.getElementById
是首选方法。
id
必须是唯一的
id
必须是唯一的。在文档中,只能有一个元素带有给定的 id
。
如果有多个元素都带有同一个 id
,那么使用它的方法的行为是不可预测的,例如 document.getElementById
可能会随机返回其中一个元素。因此,请遵守规则,保持 id
的唯一性。
只有 document.getElementById
,没有 anyElem.getElementById
getElementById
方法只能被在 document
对象上调用。它会在整个文档中查找给定的 id
。
querySelectorAll
到目前为止,最通用的方法是 elem.querySelectorAll(css)
,它返回 elem
中与给定 CSS 选择器匹配的所有元素。
在这里,我们查找所有为最后一个子元素的 <li>
元素:
<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
<script>
let elements = document.querySelectorAll('ul > li:last-child');
for (let elem of elements) {
alert(elem.innerHTML); // "test", "passed"
}
</script>
这个方法确实功能强大,因为可以使用任何 CSS 选择器。
也可以使用伪类
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
。
当我们遍历元素(例如数组或其他内容)并试图过滤那些我们感兴趣的元素时,这个方法会很有用。
例如:
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
// 不一定是 document.body.children,还可以是任何集合
for (let elem of document.body.children) {
if (elem.matches('a[href$="zip"]')) {
alert("The archive reference: " + elem.href );
}
}
</script>
closest
元素的祖先(ancestor)是:父级,父级的父级,它的父级等。祖先们一起组成了从元素到顶端的父级链。
elem.closest(css)
方法会查找与 CSS 选择器匹配的最近的祖先。elem
自己也会被搜索。
换句话说,方法 closest
在元素中得到了提升,并检查每个父级。如果它与选择器匹配,则停止搜索并返回该祖先。
例如:
<h1>Contents</h1>
<div class="contents">
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 1</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
alert(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
标签:
<table id="table">
<tr>
<td>Your age:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked> less than 18
</label>
<label>
<input type="radio" name="age" value="mature"> from 18 to 50
</label>
<label>
<input type="radio" name="age" value="senior"> more than 60
</label>
</td>
</tr>
</table>
<script>
let inputs = table.getElementsByTagName('input');
for (let input of inputs) {
alert( input.value + ': ' + input.checked );
}
</script>
不要忘记字母 "s"
!
新手开发者有时会忘记字符 "s"
。也就是说,他们会调用 getElementByTagName
而不是 getElementsByTagName
。
getElementById
中没有字母 "s"
,是因为它只返回单个元素。但是 getElementsByTagName
返回的是元素的集合,所以里面有 "s"
。
它返回的是一个集合,不是一个元素!
新手的另一个普遍的错误是写:
// 行不通
document.getElementsByTagName('input').value = 5;
这是行不通的,因为它需要的是一个 input 的 集合,并将值赋(assign)给它,而不是赋值给其中的一个元素。
我们应该遍历集合或通过对应的索引来获取元素,然后赋值,如下所示:
// 应该可以运行(如果有 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 中搜素节点:
Method | Searches by… | Can call on an element? | Live? |
---|---|---|---|
querySelector |
CSS-selector | ✔ | - |
querySelectorAll |
CSS-selector | ✔ | - |
getElementById |
id |
- | - |
getElementsByName |
name |
- | ✔ |
getElementsByTagName |
tag or ‘*’ |
✔ | ✔ |
getElementsByClassName |
class | ✔ | ✔ |
目前为止,最常用的是 querySelector
和 querySelectorAll
,但是 getElementBy*
可能会偶尔有用,或者可以在就脚本中找到。
此外:
elem.matches(css)
用于检查elem
与给定的 CSS 选择器是否匹配。elem.closest(css)
用于查找与给定 CSS 选择器相匹配的最近的祖先。elem
本身也会被检查。
让我们在这里提一下另一种用来检查子级与父级之间关系的方法,因为它有时很有用:
- 如果
elemB
在elemA
内(elemA
的后代)或者elemA==elemB
,elemA.contains(elemB)
将返回 true。
任务
搜索元素
重要程度: 4
这是带有表格(table)和表单(form)的文档。
如何查找?……
- 带有
id="age-table"
的表格。 - 表格内的所有
label
元素(应该有三个)。 - 表格中的第一个
td
(带有 “Age” 字段)。 - 带有
name="search"
的form
。 - 表单中的第一个
input
。 - 表单中的最后一个
input
。
在一个单独的窗口中打开 table.html 页面,并对此页面使用浏览器开发者工具。
解决方案
实现的方式有很多种。
以下列举的是其中一些方法:
// 1. 带有 id="age-table" 的表格。
let table = document.getElementById('age-table')
// 2. 表格内的所有 label 元素
table.getElementsByTagName('label')
// 或
document.querySelectorAll('#age-table label')
// 3. 表格中的第一个 td(带有 "Age" 字段)
table.rows[0].cells[0]
// 或
table.getElementsByTagName('td')[0]
// 或
table.querySelector('td')
// 4. 带有 name="search" 的 form。
// 假设文档中只有一个 name="search" 的元素
let form = document.getElementsByName('search')[0]
// 或者,专门对于 form
document.querySelector('form[name="search"]')
// 5. 表单中的第一个 input
form.getElementsByTagName('input')[0]
// 或
form.querySelector('input')
// 6. 表单中的最后一个 input
let inputs = form.querySelectorAll('input') // 查找所有 input
inputs[inputs.length-1] // 取出最后一个
textContent:纯文本
textContent
提供了对元素内的 文本 的访问权限:仅文本,去掉所有 <tags>
。
例如:
<div id="news">
<h1>Headline!</h1>
<p>Martians attack people!</p>
</div>
<script>
// Headline! Martians attack people!
alert(news.textContent);
</script>
正如我们所看到,只返回文本,就像所有 <tags>
都被剪掉了一样,但实际上其中的文本仍然存在。
在实际开发中,用到这样的文本读取的场景非常少。
写入 textContent
要有用得多,因为它允许以“安全方式”写入文本。
假设我们有一个用户输入的任意字符串,我们希望将其显示出来。
- 使用
innerHTML
,我们将其“作为 HTML”插入,带有所有 HTML 标签。 - 使用
textContent
,我们将其“作为文本”插入,所有符号(symbol)均按字面意义处理。
比较两者:
<div id="elem1"></div>
<div id="elem2"></div>
<script>
let name = prompt("What's your name?", "<b>Winnie-the-pooh!</b>");
elem1.innerHTML = name;
elem2.textContent = name;
</script>
- 第一个
<div>
获取 name “作为 HTML”:所有标签都变成标签,所以我们可以看到粗体的 name。 - 第二个
<div>
获取 name “作为文本”,因此我们可以从字面上看到<b>Winnie-the-pooh!</b>
。
在大多数情况下,我们期望来自用户的文本,并希望将其视为文本对待。我们不希望在我们的网站中出现意料不到的 HTML。对 textContent
的赋值正好可以做到这一点。
“hidden” 属性
“hidden” 特性(attribute)和 DOM 属性(property)指定元素是否可见。
我们可以在 HTML 中使用它,或者使用 JavaScript 进行赋值,如下所示:
<div>Both divs below are hidden</div>
<div hidden>With the attribute "hidden"</div>
<div id="elem">JavaScript assigned the property "hidden"</div>
<script>
elem.hidden = true;
</script>
从技术上来说,hidden
与 style="display:none"
做的是相同的事。但 hidden
写法更简洁。
这里有一个 blinking 元素:
<div id="elem">A blinking element</div>
<script>
setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>
更多属性
DOM 元素还有其他属性,特别是那些依赖于 class 的属性:
value
—<input>
,<select>
和<textarea>
(HTMLInputElement
,HTMLSelectElement
……)的 value。href
—<a href="...">
(HTMLAnchorElement
)的 href。id
— 所有元素(HTMLElement
)的 “id” 特性(attribute)的值。- ……以及更多其他内容……
例如:
<input type="text" id="elem" value="value">
<script>
alert(elem.type); // "text"
alert(elem.id); // "elem"
alert(elem.value); // value
</script>
总结
每个 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
时,执行与 CSS display:none
相同的事。
DOM 节点还具有其他属性,具体有哪些属性则取决于它们的类。例如,<input>
元素(HTMLInputElement
)支持 value
,type
,而 <a>
元素(HTMLAnchorElement
)则支持 href
等。大多数标准 HTML 特性(attribute)都具有相应的 DOM 属性。
导航:表单和元素
文档中的表单是特殊集合 document.forms
的成员。
这就是所谓的“命名的集合”:既是被命名了的,也是有序的。我们既可以使用名字,也可以使用在文档中的编号来获取表单。
document.forms.my // name="my" 的表单
document.forms[0] // 文档中的第一个表单
<form name="my">
<input name="one" value="1">
<input name="two" value="2">
</form>
<script>
// 获取表单
let form = document.forms.my; // <form name="my"> 元素
// 获取表单中的元素
let elem = form.elements.one; // <input name="one"> 元素
alert(elem.value); // 1
</script>
<form>
<input type="radio" name="age" value="10">
<input type="radio" name="age" value="20">
</form>
<script>
let form = document.forms[0];
let ageElems = form.elements.age;
alert(ageElems[0]); // [object HTMLInputElement]
</script>
DOM 属性
我们已经见过了内建 DOM 属性。它们数量庞大。但是从技术上讲,没有人会限制我们,如果我们觉得这些 DOM 还不够,我们可以添加我们自己的。
DOM 节点是常规的 JavaScript 对象。我们可以 alert 它们。
例如,让我们在 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>
请注意,一个元素的标准的特性对于另一个元素可能是未知的。例如 "type"
是 <input>
的一个标准的特性(HTMLInputElement),但对于 <body>
(HTMLBodyElement)来说则不是。规范中对相应元素类的标准的属性进行了详细的描述。
这里我们可以看到:
<body id="body" type="...">
<input id="input" type="text">
<script>
alert(input.type); // text
alert(body.type); // undefined:DOM 属性没有被创建,因为它不是一个标准的特性
</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');
alert(input.id); // id(被更新了)
// 属性 => 特性
input.id = 'newId';
alert(input.getAttribute('id')); // newId(被更新了)
</script>
但这里也有些例外,例如 input.value
只能从特性同步到属性,反过来则不行:
<input>
<script>
let input = document.querySelector('input'); // 特性 => 属性
input.setAttribute('value', 'text');
alert(input.value); // text
// 这个操作无效,属性 => 特性
input.value = 'newValue';
alert(input.getAttribute('value')); // text(没有被更新!)
</script>
在上面这个例子中:
- 改变特性值
value
会更新属性。 - 但是属性的更改不会影响特性。
这个“功能”在实际中会派上用场,因为用户行为可能会导致 value
的更改,然后在这些操作之后,如果我们想从 HTML 中恢复“原始”值,那么该值就在特性中。
DOM 属性是多类型的
DOM 属性不总是字符串类型的。例如,input.checked
属性(对于 checkbox 的)是布尔型的。
<input id="input" type="checkbox" checked>
checkbox
<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 属性都是字符串类型的。
有一种非常少见的情况,即使一个 DOM 属性是字符串类型的,但它可能和 HTML 特性也是不同的。例如,href
DOM 属性一直是一个 完整的 URL,即使该特性包含一个相对路径或者包含一个 #hash
。
这里有一个例子:
<a id="a" href="#hello">link</a>
<script>
// 特性
alert(a.getAttribute('href')); // #hello
// 属性
alert(a.href ); // http://site.com/page#hello 形式的完整 URL
</script>
如果我们需要 href
特性的值,或者其他与 HTML 中所写的完全相同的特性,则可以使用 getAttribute
。
非标准的特性,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>
它们还可以用来设置元素的样式。
例如,这里使用 order-state
特性来设置订单状态:
<style>
/* 样式依赖于自定义特性 "order-state" */
.order[order-state="new"] {
color: green;
}
.order[order-state="pending"] {
color: blue;
}
.order[order-state="canceled"] {
color: red;
}
</style>
<div class="order" order-state="new">
A new order.
</div>
<div class="order" order-state="pending">
A pending order.
</div>
<div class="order" order-state="canceled">
A canceled order.
</div>
为什么使用特性比使用 .order-state-new
,.order-state-pending
,order-state-canceled
这些样式类要好?
因为特性值更容易管理。我们可以轻松地更改状态:
// 比删除旧的或者添加一个新的类要简单一些
div.setAttribute('order-state', 'canceled');
但是自定义的特性也存在问题。如果我们出于我们的目的使用了非标准的特性,之后它被引入到了标准中并有了其自己的用途,该怎么办?HTML 语言是在不断发展的,并且更多的特性出现在了标准中,以满足开发者的需求。在这种情况下,自定义的属性可能会产生意料不到的影响。
为了避免冲突,存在 data-* 特性。
所有以 “data-” 开头的特性均被保留供程序员使用。它们可在 dataset
属性中使用。
例如,如果一个 elem
有一个名为 "data-about"
的特性,那么可以通过 elem.dataset.about
取到它。
像这样:
<body data-about="Elephants">
<script>
alert(document.body.dataset.about); // Elephants
</script>
像 data-order-state
这样的多词特性可以以驼峰式进行调用:dataset.orderState
。
这里是 “order state” 那个示例的重构版:
<style>
.order[data-order-state="new"] {
color: green;
}
.order[data-order-state="pending"] {
color: blue;
}
.order[data-order-state="canceled"] {
color: red;
}
</style>
<div id="order" class="order" data-order-state="new">
A new order.
</div>
<script>
// 读取
alert(order.dataset.orderState); // new
// 修改
order.dataset.orderState = "pending"; // (*)
</script>
使用 data-*
特性是一种合法且安全的传递自定义数据的方式。
请注意,我们不仅可以读取数据,还可以修改数据属性(data-attributes)。然后 CSS 会更新相应的视图:在上面这个例子中的最后一行 (*)
将颜色更改为了蓝色。
总结
- 特性(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,但是我们想要的是“原始的”值。
修改文档(document)
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');
创建一条消息
在我们的例子中,消息是一个带有 alert
类和 HTML 的 div
:
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
我们创建了元素,但到目前为止,它还只是在变量中。我们无法在页面上看到该元素,因为它还不是文档的一部分。
插入方法
为了让 div
显示出来,我们需要将其插入到 document
中的某处。例如,在 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>
下面这些方法提供了更多的插入方式:
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
替换为给定的节点或字符串。
下面是使用这些方法将列表项添加到列表中,以及将文本添加到列表前面和后面的示例:
<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>
这张图片直观地显示了这些方法所做的工作:
因此,最终列表将为:
before
<ol id="ol">
<li>prepend</li>
<li>0</li>
<li>1</li>
<li>2</li>
<li>append</li>
</ol>
after
这些方法可以在单个调用中插入多个节点列表和文本片段。
例如,在这里插入了一个字符串和一个元素:
<div id="div"></div>
<script>
div.before('<p>Hello</p>', document.createElement('hr'));
</script>
所有内容都被“作为文本”插入。
所以,最终的 HTML 为:
<p>Hello</p>
<hr>
<div id="div"></div>
换句话说,字符串被以一种安全的方式插入到页面中,就像 elem.textContent
所做的一样。
所以,这些方法只能用来插入 DOM 节点或文本片段。
但是,如果我们想在所有标签和内容正常工作的情况下,将这些内容“作为 HTML” 插入到 HTML 中,就像 elem.innerHTML
方法一样,那有什么方法可以实现吗?
insertAdjacentHTML/Text/Element
为此,我们可以使用另一个非常通用的方法:elem.insertAdjacentHTML(where, html)
。
该方法的第一个参数是代码字(code word),指定相对于 elem
的插入位置。必须为以下之一:
"beforebegin"
— 将html
插入到elem
前插入,"afterbegin"
— 将html
插入到elem
开头,"beforeend"
— 将html
插入到elem
末尾,"afterend"
— 将html
插入到elem
后。
第二个参数是 HTML 字符串,该字符串会被“作为 HTML” 插入。
例如:
<div id="div"></div>
<script>
div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>
……将导致:
<p>Hello</p><div id="div"></div><p>Bye</p>
这就是我们可以在页面上附加任意 HTML 的方式。
这是插入变体的示意图:
我们很容易就会注意到这张图片和上一张图片的相似之处。插入点实际上是相同的,但此方法插入的是 HTML。
这个方法有两个兄弟:
elem.insertAdjacentText(where, text)
— 语法一样,但是将text
字符串“作为文本”插入而不是作为 HTML,elem.insertAdjacentElement(where, elem)
— 语法一样,但是插入的是一个元素。
它们的存在主要是为了使语法“统一”。实际上,大多数时候只使用 insertAdjacentHTML
。因为对于元素和文本,我们有 append/prepend/before/after
方法 — 它们也可以用于插入节点/文本片段,但写起来更短。
所以,下面是显示一条消息的另一种变体:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>`);
</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>
克隆节点:cloneNode
如何再插入一条类似的消息?
我们可以创建一个函数,并将代码放在其中。但是另一种方法是 克隆 现有的 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();
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 元素,我们将在以后讨论。
老式的 insert/remove 方法
Old school
This information helps to understand old scripts, but not needed for new development.
由于历史原因,还存在“老式”的 DOM 操作方法。
这些方法来自真正的远古时代。如今,没有理由再使用它们了,因为诸如 append
,prepend
,before
,after
,remove
,replaceWith
这些现代方法更加灵活。
我们在这儿列出这些方法的唯一原因是,你可能会在许多就脚本中遇到它们。
parentElem.appendChild(node)
将 node
附加为 parentElem
的最后一个子元素。
下面这个示例在 <ol>
的末尾添加了一个新的 <li>
:
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Hello, world!';
list.appendChild(newLi);
</script>
在 parentElem
的 nextSibling
前插入 node
。
下面这段代码在第二个 <li>
前插入了一个新的列表项:
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Hello, world!';
list.insertBefore(newLi, list.children[1]);
</script>
如果要将 newLi
插入为第一个元素,我们可以这样做:
list.insertBefore(newLi, list.firstChild);
parentElem.replaceChild(node, oldChild)
将 parentElem
的后代中的 oldChild
替换为 node
。
parentElem.removeChild(node)
从 parentElem
中删除 node
(假设 node
为 parentElem
的后代)。
下面这个示例从 <ol>
中删除了 <li>
:
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let li = list.firstElementChild;
list.removeChild(li);
</script>
所有这些方法都会返回插入/删除的节点。换句话说,parentElem.appendChild(node)
返回 node
。但是通常我们不会使用返沪值,我们只是使用对应的方法。
聊一聊 “document.write”
还有一个非常古老的向网页添加内容的方法:document.write
。
语法如下:
<p>Somewhere in the page...</p>
<script>
document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>
调用 document.write(html)
意味着将 html
“就地马上”写入页面。html
字符串可以是动态生成的,所以它很灵活。我们可以使用 JavaScript 创建一个完整的页面并对其进行写入。
这个方法来自于没有 DOM,没有标准的上古时期……。但这个方法依被保留了下来,因为还有脚本在使用它。
由于以下重要的限制,在现代脚本中我们很少看到它:
document.write
调用只在页面加载时工作。
如果我们稍后调用它,则现有文档内容将被擦除。
例如:
<p>After one second the contents of this page will be replaced...</p>
<script>
// 1 秒后调用 document.write
// 这时页面已经加载完成,所以它会擦除现有内容
setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>
因此,在某种程度上讲,它在“加载完成”阶段是不可用的,这与我们上面介绍的其他 DOM 方法不同。
这是它的缺陷。
还有一个好处。从技术上讲,当在浏览器正在读取(“解析”)传入的 HTML 时调用 document.write
方法来写入一些东西,浏览器会像它本来就在 HTML 文本中那样使用它。
所以它运行起来出奇的快,因为它 不涉及 DOM 修改。它直接写入到页面文本中,而此时 DOM 尚未构建。
因此,如果我们需要向 HTML 动态地添加大量文本,并且我们正处于页面加载阶段,并且速度很重要,那么它可能会有帮助。但实际上,这些要求很少同时出现。我们可以在脚本中看到此方法,通常是因为这些脚本很旧。
总结
-
创建新节点的方法:
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)
页面加载完成后,这样的调用将会擦除文档。多见于旧脚本。