《JavaScript 闯关记》之 DOM(上)

DOM(文档对象模型)是针对 HTML 和 XML 文档的一个 API。DOM 描绘了一个层次化的节点树,容许开发人员添加、移除和修改页面的某一部分。javascript

节点层次

DOM 能够将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。节点分为几种不一样的类型,每种类型分别表示文档中不一样的信息及(或)标记。每一个节点都拥有各自的特色、数据和方法,另外也与其余节点存在某种关系。节点之间的关系构成了层次,而全部页面标记则表现为一个以特定节点为根节点的树形结构。如下面的 HTML 为例:html

Sample Page
    
    
        

Hello World!

复制代码

能够将这个简单的 HTML 文档表示为一个层次结构,如图下图所示。java

在这个例子中,文档元素是文档的最外层元素,文档中的其余全部元素都包含在文档元素中。每一个文档只能有一个文档元素。node

每一段标记均可以经过树中的一个节点来表示:HTML 元素经过元素节点表示,特性(attribute)经过特性节点表示,文档类型经过文档类型节点表示,而注释则经过注释节点表示。总共有12种节点类型,这些类型都继承自一个基类型。ios

Node 类型

DOM1 级定义了一个 Node 接口,该接口将由 DOM 中的全部节点类型实现。这个 Node 接口在 JavaScript 中是做为 Node 类型实现的;除了 IE 以外,在其余全部浏览器中均可以访问到这个类型。JavaScript 中的全部节点类型都继承自 Node 类型,所以全部节点类型都共享着相同的基本属性和方法。git

每一个节点都有一个 nodeType 属性,用于代表节点的类型。节点类型由在 Node 类型中定义的下列12个数值常量来表示,任何节点类型必居其一:github

  • Node.ELEMENT_NODE(1);
  • Node.ATTRIBUTE_NODE(2);
  • Node.TEXT_NODE(3);
  • Node.CDATA_SECTION_NODE(4);
  • Node.ENTITY_REFERENCE_NODE(5);
  • Node.ENTITY_NODE(6);
  • Node.PROCESSING_INSTRUCTION_NODE(7);
  • Node.COMMENT_NODE(8);
  • Node.DOCUMENT_NODE(9);
  • Node.DOCUMENT_TYPE_NODE(10);
  • Node.DOCUMENT_FRAGMENT_NODE(11);
  • Node.NOTATION_NODE(12)。

经过比较上面这些常量,能够很容易地肯定节点的类型,例如:api

if (someNode.nodeType == Node.ELEMENT_NODE){   // 在IE中无效
    console.log("Node is an element.");
}复制代码

这个例子比较了 someNode.nodeTypeNode.ELEMENT_NODE 常量。若是两者相等,则意味着 someNode 确实是一个元素。然而,因为 IE 没有公开 Node 类型的构造函数,所以上面的代码在 IE 中会致使错误。为了确保跨浏览器兼容,最好仍是将 nodeType 属性与数字值进行比较,以下所示:数组

if (someNode.nodeType == 1){    // 适用于全部浏览器
    console.log("Node is an element.");
}复制代码

并非全部节点类型都受到 Web 浏览器的支持。开发人员最经常使用的就是元素和文本节点。浏览器

Node 属性概述

Node 经常使用属性主要有如下10个,接下来咱们会着重讲解部分属性。

  • nodeType:显示节点的类型
  • nodeName:显示节点的名称
  • nodeValue:显示节点的值
  • attributes:获取一个属性节点
  • firstChild:表示某一节点的第一个节点
  • lastChild:表示某一节点的最后一个子节点
  • childNodes:表示所在节点的全部子节点
  • parentNode:表示所在节点的父节点
  • nextSibling:紧挨着当前节点的下一个节点
  • previousSibling:紧挨着当前节点的上一个节点

nodeNamenodeValue 属性

要了解节点的具体信息,可使用 nodeNamenodeValue 这两个属性。这两个属性的值彻底取决于节点的类型。在使用这两个值之前,最好是像下面这样先检测一下节点的类型。

if (someNode.nodeType == 1){
    value = someNode.nodeName;    // nodeName的值是元素的标签名
}复制代码

在这个例子中,首先检查节点类型,看它是否是一个元素。若是是,则取得并保存 nodeName 的值。对于元素节点,nodeName 中保存的始终都是元素的标签名,而 nodeValue 的值则始终为 null

节点关系

文档中全部的节点之间都存在这样或那样的关系。节点间的各类关系能够用传统的家族关系来描述,至关于把文档树比喻成家谱。

每一个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。NodeList 是一种类数组对象,用于保存一组有序的节点,能够经过位置来访问这些节点。请注意,虽然能够经过方括号语法来访问 NodeList 的值,并且这个对象也有 length 属性,但它并非 Array 的实例。NodeList 对象的独特之处在于,它其实是基于 DOM 结构动态执行查询的结果,所以 DOM 结构的变化可以自动反映在 NodeList 对象中。

下面的例子展现了如何访问保存在 NodeList 中的节点——能够经过方括号,也可使用 item() 方法。

var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;复制代码

不管使用方括号仍是使用 item() 方法都没有问题,但使用方括号语法看起来与访问数组类似,所以颇受一些开发人员的青睐。另外,要注意 length 属性表示的是访问 NodeList 的那一刻,其中包含的节点数量。

每一个节点都有一个 parentNode 属性,该属性指向文档树中的父节点。包含在 childNodes 列表中的全部节点都具备相同的父节点,所以它们的 parentNode 属性都指向同一个节点。此外,包含在 childNodes 列表中的每一个节点相互之间都是同胞节点。经过使用列表中每一个节点的 previousSiblingnextSibling 属性,能够访问同一列表中的其余节点。列表中第一个节点的 previousSibling 属性值为 null,而列表中最后一个节点的 nextSibling 属性的值一样也为 null,以下面的例子所示:

if (someNode.nextSibling === null){
    console.log("Last node in the parent’s childNodes list.");
} else if (someNode.previousSibling === null){
    console.log("First node in the parent’s childNodes list.");
}复制代码

固然,若是列表中只有一个节点,那么该节点的 nextSiblingpreviousSibling 都为 null

父节点与其第一个和最后一个子节点之间也存在特殊关系。父节点的 firstChildlastChild 属性分别指向其 childNodes 列表中的第一个和最后一个节点。其中,someNode.firstChild 的值始终等于 someNode.childNodes[0],而 someNode.lastChild 的值始终等于 someNode.childNodes [someNode.childNodes.length-1]。在只有一个子节点的状况下, firstChildlastChild 指向同一个节点。若是没有子节点,那么 firstChildlastChild 的值均为 null。明确这些关系可以对咱们查找和访问文档结构中的节点提供极大的便利。下图形象地展现了上述关系。

在反映这些关系的全部属性当中,childNodes 属性与其余属性相比更方便一些,由于只须使用简单的关系指针,就能够经过它访问文档树中的任何节点。另外,hasChildNodes() 也是一个很是有用的方法,这个方法在节点包含一或多个子节点的状况下返回 true;应该说,这是比查询 childNodes 列表的 length 属性更简单的方法。

全部节点都有的最后一个属性是 ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。经过这个属性,咱们能够没必要在节点层次中经过层层回溯到达顶端,而是能够直接访问文档节点。

操做节点

由于关系指针都是只读的,因此 DOM 提供了一些操做节点的方法。其中,最经常使用的方法是 appendChild(),用于向 childNodes 列表的末尾添加一个节点。添加节点后,childNodes 的新增节点、父节点及之前的最后一个子节点的关系指针都会相应地获得更新。更新完成后,appendChild() 返回新增的节点。来看下面的例子:

var returnedNode = someNode.appendChild(newNode);
console.log(returnedNode == newNode);         // true
console.log(someNode.lastChild == newNode);   // true复制代码

若是传入到 appendChild() 中的节点已是文档的一部分了,那结果就是将该节点从原来的位置转移到新位置。即便能够将 DOM 树当作是由一系列指针链接起来的,但任何 DOM 节点也不能同时出如今文档中的多个位置上。所以,若是在调用 appendChild() 时传入了父节点的第一个子节点,那么该节点就会成为父节点的最后一个子节点,以下面的例子所示。

// someNode 有多个子节点
var returnedNode = someNode.appendChild(someNode.firstChild);
console.log(returnedNode == someNode.firstChild);   // false
console.log(returnedNode == someNode.lastChild);    // true复制代码

若是须要把节点放在 childNodes 列表中某个特定的位置上,而不是放在末尾,那么可使用 insertBefore() 方法。这个方法接受两个参数:要插入的节点和做为参照的节点。插入节点后,被插入的节点会变成参照节点的前一个同胞节点 previousSibling,同时被方法返回。若是参照节点是 null,则 insertBefore()appendChild() 执行相同的操做,以下面的例子所示。

// 插入后成为最后一个子节点
returnedNode = someNode.insertBefore(newNode, null);
console.log(newNode == someNode.lastChild);   // true

// 插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
console.log(returnedNode == newNode);         // true
console.log(newNode == someNode.firstChild);  // true

// 插入到最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
console.log(newNode == someNode.childNodes[someNode.childNodes.length-2]); // true复制代码

前面介绍的 appendChild()insertBefore() 方法都只插入节点,不会移除节点。而下面要介绍的 replaceChild() 方法接受的两个参数是:要插入的节点和要替换的节点。要替换的节点将由这个方法返回并从文档树中被移除,同时由要插入的节点占据其位置。来看下面的例子。

// 替换第一个子节点
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);

// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);复制代码

在使用 replaceChild() 插入一个节点时,该节点的全部关系指针都会从被它替换的节点复制过来。尽管从技术上讲,被替换的节点仍然还在文档中,但它在文档中已经没有了本身的位置。

若是只想移除而非替换节点,可使用 removeChild() 方法。这个方法接受一个参数,即要移除的节点。被移除的节点将成为方法的返回值,以下面的例子所示。

// 移除第一个子节点
var formerFirstChild = someNode.removeChild(someNode.firstChild);

// 移除最后一个子节点
var formerLastChild = someNode.removeChild(someNode.lastChild);复制代码

与使用 replaceChild() 方法同样,经过 removeChild() 移除的节点仍然为文档全部,只不过在文档中已经没有了本身的位置。

前面介绍的四个方法操做的都是某个节点的子节点,也就是说,要使用这几个方法必须先取得父节点(使用 parentNode 属性)。另外,并非全部类型的节点都有子节点,若是在不支持子节点的节点上调用了这些方法,将会致使错误发生。

Document 类型

JavaScript 经过 Document 类型表示文档。在浏览器中,document 对象是 HTMLDocument(继承自 Document 类型)的一个实例,表示整个 HTML 页面。并且,document 对象是 window 对象的一个属性,所以能够将其做为全局对象来访问。Document 节点具备下列特征:

  • nodeType 的值为9;
  • nodeName 的值为 "#document"
  • nodeValue 的值为 null
  • parentNode 的值为 null
  • ownerDocument 的值为 null
  • 其子节点多是一个 DocumentType(最多一个)、Element(最多一个)、ProcessingInstructionComment

Document 类型能够表示 HTML 页面或者其余基于 XML 的文档。不过,最多见的应用仍是做为 HTMLDocument 实例的 document 对象。经过这个文档对象,不只能够取得与页面有关的信息,并且还能操做页面的外观及其底层结构。

文档的子节点

虽然 DOM 标准规定 Document 节点的子节点能够是DocumentTypeElementProcessingInstructionComment,但还有两个内置的访问其子节点的快捷方式。第一个就是documentElement 属性,该属性始终指向 HTML 页面中的 html 元素。另外一个就是经过 childNodes 列表访问文档元素,但经过 documentElement 属性则能更快捷、更直接地访问该元素。如下面这个简单的页面为例。

复制代码

这个页面在通过浏览器解析后,其文档中只包含一个子节点,即 html 元素。能够经过 documentElementchildNodes 列表来访问这个元素,以下所示。

var html = document.documentElement;      // 取得对的引用
console.log(html === document.childNodes[0]);   // true
console.log(html === document.firstChild);      // true复制代码

这个例子说明,documentElementfirstChildchildNodes[0] 的值相同,都指向 元素。

做为 HTMLDocument 的实例,document 对象还有一个 body 属性,直接指向 元素。由于开发人员常常要使用这个元素,因此 document.body 在 JavaScript 代码中出现的频率很是高,其用法以下。

var body = document.body;    // 取得对的引用复制代码

全部浏览器都支持 document.documentElementdocument.body 属性。

Document 另外一个可能的子节点是 DocumentType。一般将 标签当作一个与文档其余部分不一样的实体,能够经过 doctype 属性(在浏览器中是 document.doctype )来访问它的信息。

var doctype = document.doctype;     // 取得对的引用复制代码

浏览器对 document.doctype 的支持差异很大,能够给出以下总结。

  • IE8 及以前版本:若是存在文档类型声明,会将其错误地解释为一个注释并把它看成 Comment 节点;而 document.doctype 的值始终为 null
  • IE9+ 及 Firefox:若是存在文档类型声明,则将其做为文档的第一个子节点;document.doctype 是一个 DocumentType 节点,也能够经过 document.firstChilddocument.childNodes[0] 访问同一个节点。
  • Safari、Chrome 和 Opera:若是存在文档类型声明,则将其解析,但不做为文档的子节点。document.doctype 是一个 DocumentType 节点,但该节点不会出如今 document.childNodes 中。

因为浏览器对 document.doctype 的支持不一致,所以这个属性的用处颇有限。

文档信息

做为 HTMLDocument 的一个实例,document 对象还有一些标准的 Document 对象所没有的属性。这些属性提供了 document 对象所表现的网页的一些信息。其中第一个属性就是 title,包含着 元素中的文本——显示在浏览器窗口的标题栏或标签页上。经过这个属性能够取得当前页面的标题,也能够修改当前页面的标题并反映在浏览器的标题栏中。

// 取得文档标题
var originalTitle = document.title;

// 设置文档标题
document.title = "New page title";复制代码

接下来要介绍的3个属性都与对网页的请求有关,它们是 URLdomainreferrerURL 属性中包含页面完整的 URL(即地址栏中显示的URL),domain 属性中只包含页面的域名,而 referrer 属性中则保存着连接到当前页面的那个页面的 URL。在没有来源页面的状况下,referrer 属性中可能会包含空字符串。全部这些信息都存在于请求的 HTTP 头部,只不过是经过这些属性让咱们可以在 JavaScrip 中访问它们而已,以下面的例子所示。

// 取得完整的URL
var url = document.URL;

// 取得域名
var domain = document.domain;

// 取得来源页面的URL
var referrer = document.referrer;复制代码

查找元素

说到最多见的 DOM 应用,恐怕就要数取得特定的某个或某组元素的引用,而后再执行一些操做了。取得元素的操做可使用 document 对象的几个方法来完成。其中,Document 类型为此提供了两个方法:getElementById()getElementsByTagName()

第一个方法,getElementById(),接收一个参数:要取得的元素的 ID。若是找到相应的元素则返回该元素,若是不存在带有相应 ID 的元素,则返回 null。注意,这里的 ID 必须与页面中元素的 id 特性(attribute)严格匹配,包括大小写。如下面的元素为例。

 
Some text复制代码

可使用下面的代码取得这个元素:

var div = document.getElementById("myDiv");   // 取得
  
  
  

 
元素的引用
复制代码

可是,下面的代码在除 IE7 及更早版本以外的全部浏览器中都将返回 null

var div = document.getElementById("mydiv");   // 无效的ID(在IE7及更早版本中能够)复制代码

IE8 及较低版本不区分 ID 的大小写,所以 "myDiv""mydiv" 会被看成相同的元素 ID。若是页面中多个元素的ID值相同,getElementById() 只返回文档中第一次出现的元素。

另外一个经常使用于取得元素引用的方法是 getElementsByTagName()。这个方法接受一个参数,即要取得元素的标签名,而返回的是包含零或多个元素的 NodeList。在HTML文档中,这个方法会返回一个HTMLCollection 对象,做为一个“动态”集合,该对象与 NodeList很是相似。例如,下列代码会取得页面中全部的 元素,并返回一个 HTMLCollection

var images = document.getElementsByTagName("img");复制代码

这行代码会将一个 HTMLCollection 对象保存在 images 变量中。与 NodeList 对象相似,可使用方括号语法或 item() 方法来访问 HTMLCollection 对象中的项。而这个对象中元素的数量则能够经过其 length 属性取得,以下面的例子所示。

console.log(images.length);        // 输出图像的数量
console.log(images[0].src);        // 输出第一个图像元素的src特性
console.log(images.item(0).src);   // 输出第一个图像元素的src特性复制代码

HTMLCollection 对象还有一个方法,叫作 namedItem(),使用这个方法能够经过元素的 name 特性取得集合中的项。例如,假设上面提到的页面中包含以下 元素:

复制代码

那么就能够经过以下方式从 images 变量中取得这个 元素:

var myImage = images.namedItem("myImage");复制代码

在提供按索引访问项的基础上,HTMLCollection 还支持按名称访问项,这就为咱们取得实际想要的元素提供了便利。并且,对命名的项也可使用方括号语法来访问,以下所示:

var myImage = images["myImage"];复制代码

HTMLCollection 而言,咱们能够向方括号中传入数值或字符串形式的索引值。在后台,对数值索引就会调用 item(),而对字符串索引就会调用 namedItem()

要想取得文档中的全部元素,能够向 getElementsByTagName() 中传入 "*"。在 JavaScript 及 CSS 中,星号(*)一般表示“所有”。下面看一个例子。

var allElements = document.getElementsByTagName("*");复制代码

仅此一行代码返回的 HTMLCollection 中,就包含了整个页面中的全部元素——按照它们出现的前后顺序。换句话说,第一项是 元素,第二项是 元素,以此类推。因为 IE 将注释(Comment)实现为元素(Element),所以在IE中调用 getElementsByTagName("*") 将会返回全部注释节点。

第三个方法,也是只有 HTMLDocument 类型才有的方法,是 getElementsByName()。顾名思义,这个方法会返回带有给定 name 特性的全部元素。最常使用 getElementsByName() 方法的状况是取得单选按钮;为了确保发送给浏览器的值正确无误,全部单选按钮必须具备相同的 name 特性,以下面的例子所示。

 
Which color do you prefer?
  • Red
  • Green
  • Blue 复制代码

    如这个例子所示,其中全部单选按钮的 name 特性值都是 "color",但它们的 ID 能够不一样。ID 的做用在于将 元素应用到每一个单选按钮,而 name 特性则用以确保三个值中只有一个被发送给浏览器。这样,咱们就可使用以下代码取得全部单选按钮:

    var radios = document.getElementsByName("color");复制代码

    getElementsByTagName() 相似,getElementsByName() 方法也会返回一个 HTMLCollectioin。可是,对于这里的单选按钮来讲,namedItem() 方法则只会取得第一项(由于每一项的 name 特性都相同)。

    特殊集合

    除了属性和方法,document 对象还有一些特殊的集合。这些集合都是 HTMLCollection 对象,为访问文档经常使用的部分提供了快捷方式,包括:

    • document.anchors,包含文档中全部带 name 特性的 元素;
    • document.applets,包含文档中全部的 元素,由于再也不推荐使用 元素,因此这个集合已经不建议使用了;
    • document.forms,包含文档中全部的
      元素,与document.getElementsByTagName("form")获得的结果相同;
    • document.images,包含文档中全部的 元素,与document.getElementsByTagName("img")获得的结果相同;
    • document.links,包含文档中全部带href特性的 元素。

    这个特殊集合始终均可以经过 HTMLDocument 对象访问到,并且,与 HTMLCollection 对象相似,集合中的项也会随着当前文档内容的更新而更新。

    文档写入

    有一个 document 对象的功能已经存在不少年了,那就是将输出流写入到网页中的能力。这个能力体如今下列4个方法中:write()writeln()open()close()。其中,write()writeln() 方法都接受一个字符串参数,即要写入到输出流中的文本。write() 会原样写入,而 writeln() 则会在字符串的末尾添加一个换行符 \n。在页面被加载的过程当中,可使用这两个方法向页面中动态地加入内容,以下面的例子所示。

    document.write() Example
    
    
        

    The current date and time is: document.write("" + (new Date()).toString() + "");

    复制代码

    这个例子展现了在页面加载过程当中输出当前日期和时间的代码。其中,日期被包含在一个 元素中,就像在 HTML 页面中包含普通的文本同样。这样作会建立一个 DOM 元素,并且能够在未来访问该元素。经过 write()writeln() 输出的任何 HTML 代码都将如此处理。

    此外,还可使用 write()writeln() 方法动态地包含外部资源,例如 JavaScript 文件等。在包含 JavaScript 文件时,必须注意不能像下面的例子那样直接包含字符串 "",由于这会致使该字符串被解释为脚本块的结束,它后面的代码将没法执行。

    document.write() Example 2
    
    
        
            document.write("");
        
    
    复制代码

    即便这个文件看起来没错,但字符串 "" 将被解释为与外部的 document.write("

    字符串 "<\ script="">" 不会被看成外部 window.onload = function(){ document.write("Hello world!"); };

    在这个例子中,咱们使用了 window.onload 事件处理程序,等到页面彻底加载以后延迟执行函数。函数执行以后,字符串 "Hello world!" 会重写整个页面内容。

    方法 open()close() 分别用于打开和关闭网页的输出流。若是是在页面加载期间使用 write()writeln() 方法,则不须要用到这两个方法。

    关卡

    仔细想一想,下面代码块会输出什么结果呢?

     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t"); document.writeln(d.firstChild.innerHTML); // ??? document.writeln(d.lastChild.innerHTML); // ??? 复制代码
     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t"); document.writeln(d.childNodes[1].innerHTML); // ??? document.writeln(d.parentNode.getAttribute("name")); // ??? 复制代码
     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t").childNodes[1]; document.writeln(d.nextSibling.innerHTML); // ??? document.writeln(d.previousSibling.innerHTML); // ??? 复制代码

    更多

    关注微信公众号「劼哥舍」回复「答案」,获取关卡详解。
    关注 github.com/stone0090/j…,获取最新动态。

    相关文章
    相关标签/搜索