当浏览器加载页面时,它会“读取”(或者称之为:“解析”)HTML 并从中生成 DOM 对象。对于元素节点,大多数标准的 HTML 特性(attributes)会自动变成 DOM 对象的属性(properties)。(译注:attribute 和 property 两词意思相近,为作区分,全文将 attribute 译为“特性”,property 译为“属性”,请读者注意区分。)

例如,如果标签是 <body id="page">,那么 DOM 对象就会有 body.id="page"

但特性—属性映射并不是一一对应的!在本章,我们将带领你一起分清楚这两个概念,了解如何使用它们,了解它们何时相同何时不同。

DOM 属性

我们已经见过了内建 DOM 属性。它们数量庞大。但是从技术上讲,没有人会限制我们,如果我们觉得这些 DOM 还不够,我们可以添加我们自己的。

DOM 节点是常规的 JavaScript 对象。我们可以 alert 它们。

例如,让我们在 document.body 中创建一个新的属性:

  1. document.body.myData = {
  2. name: 'Caesar',
  3. title: 'Imperator'
  4. };
  5. alert(document.body.myData.title); // Imperator

我们也可以像下面这样添加一个方法:

  1. document.body.sayTagName = function() {
  2. alert(this.tagName);
  3. };
  4. document.body.sayTagName(); // BODY(这个方法中的 "this" 的值是 document.body)

我们还可以修改内建属性的原型,例如修改 Element.prototype 为所有元素添加一个新方法:

  1. Element.prototype.sayHi = function() {
  2. alert(`Hello, I'm ${this.tagName}`);
  3. };
  4. document.documentElement.sayHi(); // Hello, I'm HTML
  5. document.body.sayHi(); // Hello, I'm BODY

所以,DOM 属性和方法的行为就像常规的 Javascript 对象一样:

  • 它们可以有很多值。
  • 它们是大小写敏感的(要写成 elem.nodeType,而不是 elem.NoDeTyPe)。

HTML 特性

在 HTML 中,标签可能拥有特性(attributes)。当浏览器解析 HTML 文本,并根据标签创建 DOM 对象时,浏览器会辨别 标准的 特性并以此创建 DOM 属性。

所以,当一个元素有 id 或其他 标准的 特性,那么就会生成对应的 DOM 属性。但是非 标准的 特性则不会。

例如:

  1. <body id="test" something="non-standard">
  2. <script>
  3. alert(document.body.id); // test
  4. // 非标准的特性没有获得对应的属性
  5. alert(document.body.something); // undefined
  6. </script>
  7. </body>

请注意,一个元素的标准的特性对于另一个元素可能是未知的。例如 "type"<input> 的一个标准的特性(HTMLInputElement),但对于 <body>HTMLBodyElement)来说则不是。规范中对相应元素类的标准的属性进行了详细的描述。

这里我们可以看到:

  1. <body id="body" type="...">
  2. <input id="input" type="text">
  3. <script>
  4. alert(input.type); // text
  5. alert(body.type); // undefined:DOM 属性没有被创建,因为它不是一个标准的特性
  6. </script>
  7. </body>

所以,如果一个特性不是标准的,那么就没有相对应的 DOM 属性。那我们有什么方法来访问这些特性吗?

当然。所有特性都可以通过使用以下方法进行访问:

  • elem.hasAttribute(name) — 检查特性是否存在。
  • elem.getAttribute(name) — 获取这个特性值。
  • elem.setAttribute(name, value) — 设置这个特性值。
  • elem.removeAttribute(name) — 移除这个特性。

这些方法操作的实际上是 HTML 中的内容。

我们也可以使用 elem.attributes 读取所有特性:属于内建 Attr 类的对象的集合,具有 namevalue 属性。

下面是一个读取非标准的特性的示例:

  1. <body something="non-standard">
  2. <script>
  3. alert(document.body.getAttribute('something')); // 非标准的
  4. </script>
  5. </body>

HTML 特性有以下几个特征:

  • 它们的名字是大小写不敏感的(idID 相同)。
  • 它们的值总是字符串类型的。

下面是一个使用特性的扩展示例:

  1. <body>
  2. <div id="elem" about="Elephant"></div>
  3. <script>
  4. alert( elem.getAttribute('About') ); // (1) 'Elephant',读取
  5. elem.setAttribute('Test', 123); // (2) 写入
  6. alert( elem.outerHTML ); // (3) 查看特性是否在 HTML 中(在)
  7. for (let attr of elem.attributes) { // (4) 列出所有
  8. alert( `${attr.name} = ${attr.value}` );
  9. }
  10. </script>
  11. </body>

请注意:

  1. getAttribute('About') — 这里的第一个字母是大写的,但是在 HTML 中,它们都是小写的。但这没有影响:特性的名称是大小写不敏感的。
  2. 我们可以将任何东西赋值给特性,但是这些东西会变成字符串类型的。所以这里我们的值为 "123"
  3. 所有特性,包括我们设置的那个特性,在 outerHTML 中都是可见的。
  4. attributes 集合是可迭代对象,该对象将所有元素的特性(标准和非标准的)作为 namevalue 属性存储在对象中。

属性—特性同步

当一个标准的特性被改变,对应的属性也会自动更新,(除了几个特例)反之亦然。

在下面这个示例中,id 被修改为特性,我们可以看到对应的属性也发生了变化。然后反过来也是同样的效果:

  1. <input>
  2. <script>
  3. let input = document.querySelector('input');
  4. // 特性 => 属性
  5. input.setAttribute('id', 'id');
  6. alert(input.id); // id(被更新了)
  7. // 属性 => 特性
  8. input.id = 'newId';
  9. alert(input.getAttribute('id')); // newId(被更新了)
  10. </script>

但这里也有些例外,例如 input.value 只能从特性同步到属性,反过来则不行:

  1. <input>
  2. <script>
  3. let input = document.querySelector('input');
  4. // 特性 => 属性
  5. input.setAttribute('value', 'text');
  6. alert(input.value); // text
  7. // 这个操作无效,属性 => 特性
  8. input.value = 'newValue';
  9. alert(input.getAttribute('value')); // text(没有被更新!)
  10. </script>

在上面这个例子中:

  • 改变特性值 value 会更新属性。
  • 但是属性的更改不会影响特性。

这个“功能”在实际中会派上用场,因为用户行为可能会导致 value 的更改,然后在这些操作之后,如果我们想从 HTML 中恢复“原始”值,那么该值就在特性中。

DOM 属性是多类型的

DOM 属性不总是字符串类型的。例如,input.checked 属性(对于 checkbox 的)是布尔型的。

  1. <input id="input" type="checkbox" checked> checkbox
  2. <script>
  3. alert(input.getAttribute('checked')); // 特性值是:空字符串
  4. alert(input.checked); // 属性值是:true
  5. </script>

还有其他的例子。style 特性是字符串类型的,但 style 属性是一个对象:

  1. <div id="div" style="color:red;font-size:120%">Hello</div>
  2. <script>
  3. // 字符串
  4. alert(div.getAttribute('style')); // color:red;font-size:120%
  5. // 对象
  6. alert(div.style); // [object CSSStyleDeclaration]
  7. alert(div.style.color); // red
  8. </script>

尽管大多数 DOM 属性都是字符串类型的。

有一种非常少见的情况,即使一个 DOM 属性是字符串类型的,但它可能和 HTML 特性也是不同的。例如,href DOM 属性一直是一个 完整的 URL,即使该特性包含一个相对路径或者包含一个 #hash

这里有一个例子:

  1. <a id="a" href="#hello">link</a>
  2. <script>
  3. // 特性
  4. alert(a.getAttribute('href')); // #hello
  5. // 属性
  6. alert(a.href ); // http://site.com/page#hello 形式的完整 URL
  7. </script>

如果我们需要 href 特性的值,或者其他与 HTML 中所写的完全相同的特性,则可以使用 getAttribute

非标准的特性,dataset

当编写 HTML 时,我们会用到很多标准的特性。但是非标准的,自定义的呢?首先,让我们看看它们是否有用?用来做什么?

有时,非标准的特性常常用于将自定义的数据从 HTML 传递到 JavaScript,或者用于为 JavaScript “标记” HTML 元素。

像这样:

  1. <!-- 标记这个 div 以在这显示 "name" -->
  2. <div show-info="name"></div>
  3. <!-- 标记这个 div 以在这显示 "age" -->
  4. <div show-info="age"></div>
  5. <script>
  6. // 这段代码找到带有标记的元素,并显示需要的内容
  7. let user = {
  8. name: "Pete",
  9. age: 25
  10. };
  11. for(let div of document.querySelectorAll('[show-info]')) {
  12. // 在字段中插入相应的信息
  13. let field = div.getAttribute('show-info');
  14. div.innerHTML = user[field]; // 首先 "name" 变为 Pete,然后 "age" 变为 25
  15. }
  16. </script>

它们还可以用来设置元素的样式。

例如,这里使用 order-state 特性来设置订单状态:

  1. <style>
  2. /* 样式依赖于自定义特性 "order-state" */
  3. .order[order-state="new"] {
  4. color: green;
  5. }
  6. .order[order-state="pending"] {
  7. color: blue;
  8. }
  9. .order[order-state="canceled"] {
  10. color: red;
  11. }
  12. </style>
  13. <div class="order" order-state="new">
  14. A new order.
  15. </div>
  16. <div class="order" order-state="pending">
  17. A pending order.
  18. </div>
  19. <div class="order" order-state="canceled">
  20. A canceled order.
  21. </div>

为什么使用特性比使用 .order-state-new.order-state-pendingorder-state-canceled 这些样式类要好?

因为特性值更容易管理。我们可以轻松地更改状态:

  1. // 比删除旧的或者添加一个新的类要简单一些
  2. div.setAttribute('order-state', 'canceled');

但是自定义的特性也存在问题。如果我们出于我们的目的使用了非标准的特性,之后它被引入到了标准中并有了其自己的用途,该怎么办?HTML 语言是在不断发展的,并且更多的特性出现在了标准中,以满足开发者的需求。在这种情况下,自定义的属性可能会产生意料不到的影响。

为了避免冲突,存在 data-* 特性。

所有以 “data-” 开头的特性均被保留供程序员使用。它们可在 dataset 属性中使用。

例如,如果一个 elem 有一个名为 "data-about" 的特性,那么可以通过 elem.dataset.about 取到它。

像这样:

  1. <body data-about="Elephants">
  2. <script>
  3. alert(document.body.dataset.about); // Elephants
  4. </script>

data-order-state 这样的多词特性可以以驼峰式进行调用:dataset.orderState

这里是 “order state” 那个示例的重构版:

  1. <style>
  2. .order[data-order-state="new"] {
  3. color: green;
  4. }
  5. .order[data-order-state="pending"] {
  6. color: blue;
  7. }
  8. .order[data-order-state="canceled"] {
  9. color: red;
  10. }
  11. </style>
  12. <div id="order" class="order" data-order-state="new">
  13. A new order.
  14. </div>
  15. <script>
  16. // 读取
  17. alert(order.dataset.orderState); // new
  18. // 修改
  19. order.dataset.orderState = "pending"; // (*)
  20. </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,但是我们想要的是“原始的”值。

任务

获取特性

重要程度: 5

编写代码,从文档(document)中获取带有 data-widget-name 特性(attribute)的元素,并读取它的值。

  1. <!DOCTYPE html>
  2. <html>
  3. <body>
  4. <div data-widget-name="menu">Choose the genre</div>
  5. <script>
  6. /* your code */
  7. </script>
  8. </body>
  9. </html>

解决方案

  1. <!DOCTYPE html>
  2. <html>
  3. <body>
  4. <div data-widget-name="menu">Choose the genre</div>
  5. <script>
  6. // 获取它
  7. let elem = document.querySelector('[data-widget-name]');
  8. // 读取值
  9. alert(elem.dataset.widgetName);
  10. // 或
  11. alert(elem.getAttribute('data-widget-name'));
  12. </script>
  13. </body>
  14. </html>

将外部链接设为橙色

重要程度: 3

通过修改 style 属性,将所有外部链接变为橙色。

如果一个链接是外部的:

  • href 中包含 ://
  • 但不是以 http://internal.com 开头。

例如:

  1. <a name="list">the list</a>
  2. <ul>
  3. <li><a href="http://google.com">http://google.com</a></li>
  4. <li><a href="/tutorial">/tutorial.html</a></li>
  5. <li><a href="local/path">local/path</a></li>
  6. <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li>
  7. <li><a href="http://nodejs.org">http://nodejs.org</a></li>
  8. <li><a href="http://internal.com/test">http://internal.com/test</a></li>
  9. </ul>
  10. <script>
  11. // 为单个链接设置样式
  12. let link = document.querySelector('a');
  13. link.style.color = 'orange';
  14. </script>

结果应该是:

打开一个任务沙箱。

解决方案

首先,我们需要找到所有外部链接。

这里有两种方式。

第一种是使用 document.querySelectorAll('a') 找到所有链接,然后过滤出我们需要的部分:

  1. let links = document.querySelectorAll('a');
  2. for (let link of links) {
  3. let href = link.getAttribute('href');
  4. if (!href) continue; // 没有特性
  5. if (!href.includes('://')) continue; // 没有协议
  6. if (href.startsWith('http://internal.com')) continue; // 内部的
  7. link.style.color = 'orange';
  8. }

请注意:我们用的是 link.getAttribute('href')。而不是 link.href,因为我们需要的是来自 HTML 的值。

……另一种更简单的方法,是使用 CSS 选择器进行检查:

  1. // 查找所有 href 中包含 :// 的链接
  2. // 但 href 不是以 http://internal.com 开头
  3. let selector = 'a[href*="://"]:not([href^="http://internal.com"])';
  4. let links = document.querySelectorAll(selector);
  5. links.forEach(link => link.style.color = 'orange');

使用沙箱打开解决方案。