当前位置: 首页 > news >正文

米 建设网站广州h5设计网站公司

米 建设网站,广州h5设计网站公司,怎么做能上谷歌网站,北京建设网站的公司兴田德润优惠十四、DOM 文档对象模型#xff08;DOM#xff0c;Document Object Model#xff09;是 HTML 和 XML 文档的编程接口。DOM 表示由多层节点构成的文档#xff0c;通过它开发者可以添加、删除和修改页面的各个部分。脱胎于网景和微软早期的动态 HTML#xff08;DHTML#x…十四、DOM 文档对象模型DOMDocument Object Model是 HTML 和 XML 文档的编程接口。DOM 表示由多层节点构成的文档通过它开发者可以添加、删除和修改页面的各个部分。脱胎于网景和微软早期的动态 HTMLDHTMLDynamic HTMLDOM 现在是真正跨平台、语言无关的表示和操作网页的方式。 DOM Level 1 在 1998 年成为 W3C 推荐标准提供了基本文档结构和查询的接口。本章之所以介绍DOM主要因为它与浏览器中的 HTML 网页相关并且在 JavaScript 中提供了 DOM API。注意 IE8 及更低版本中的 DOM 是通过 COM 对象实现的。这意味着这些版本的 IE 中DOM 对象跟原生 JavaScript 对象具有不同的行为和功能。 节点层级 任何 HTML 或 XML 文档都可以用 DOM 表示为一个由节点构成的层级结构。节点分很多类型每种类型对应着文档中不同的信息和或标记也都有自己不同的特性、数据和方法而且与其他类型有某种关系。这些关系构成了层级让标记可以表示为一个以特定节点为根的树形结构。以下面的 HTML为例html head titleSample Page/title /head body pHello World!/p /body /htmldocument 节点表示每个文档的根节点。在这里根节点的唯一子节点是html元素我们称之为文档元素documentElement。文档元素是文档最外层的元素所有其他元素都存在于这个元素之内。每个文档只能有一个文档元素。在 HTML 页面中文档元素始终是html元素。在 XML 文档中则没有这样预定义的元素任何元素都可能成为文档元素。 HTML 中的每段标记都可以表示为这个树形结构中的一个节点。元素节点表示 HTML 元素属性节点表示属性文档类型节点表示文档类型注释节点表示注释。DOM 中总共有 12 种节点类型这些类型都继承一种基本类型。 Node 类型 DOM Level 1 描述了名为 Node 的接口这个接口是所有 DOM 节点类型都必须实现的。Node 接口在 JavaScript中被实现为 Node 类型在除 IE之外的所有浏览器中都可以直接访问这个类型。在 JavaScript中所有节点类型都继承 Node 类型因此所有类型都共享相同的基本属性和方法。每个节点都有 nodeType 属性表示该节点的类型。节点类型由定义在 Node 类型上的 12 个数值常量表示 Node.ELEMENT_NODE1Node.ATTRIBUTE_NODE2Node.TEXT_NODE3Node.CDATA_SECTION_NODE4Node.ENTITY_REFERENCE_NODE5Node.ENTITY_NODE6Node.PROCESSING_INSTRUCTION_NODE7Node.COMMENT_NODE8Node.DOCUMENT_NODE9Node.DOCUMENT_TYPE_NODE10Node.DOCUMENT_FRAGMENT_NODE11Node.NOTATION_NODE12 节点类型可通过与这些常量比较来确定比如if (someNode.nodeType Node.ELEMENT_NODE){ alert(Node is an element.); }这个例子比较了 someNode.nodeType 与 Node.ELEMENT_NODE 常量。如果两者相等则意味着someNode 是一个元素节点。 浏览器并不支持所有节点类型。开发者最常用到的是元素节点和文本节点。本章后面会讨论每种节点受支持的程度及其用法。 nodeName 与 nodeValue。nodeName 与 nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。在使用这两个属性前最好先检测节点类型如下所示 if (someNode.nodeType 1){ value someNode.nodeName; // 会显示元素的标签名 }在这个例子中先检查了节点是不是元素。如果是则将其 nodeName 的值赋给一个变量。对元素而言nodeName 始终等于元素的标签名而 nodeValue 则始终为 null。 节点关系 文档中的所有节点都与其他节点有关系。这些关系可以形容为家族关系相当于把文档树比作家谱。在 HTML 中body元素是html元素的子元素而html元素则是body元素的父元素。head元素是body元素的同胞元素因为它们有共同的父元素html。每个节点都有一个 childNodes 属性其中包含一个 NodeList 的实例。NodeList 是一个类数组对象用于存储可以按位置存取的有序节点。注意NodeList 并不是 Array 的实例但可以使用中括号访问它的值而且它也有 length 属性。NodeList 对象独特的地方在于它其实是一个对 DOM 结构的查询因此 DOM 结构的变化会自动地在 NodeList 中反映出来。我们通常说 NodeList 是实时的活动对象而不是第一次访问时所获得内容的快照。下面的例子展示了如何使用中括号或使用 item()方法访问 NodeList 中的元素let firstChild someNode.childNodes[0]; let secondChild someNode.childNodes.item(1); let count someNode.childNodes.length;无论是使用中括号还是 item()方法都是可以的但多数开发者倾向于使用中括号因为它是一个类数组对象。注意length 属性表示那一时刻 NodeList 中节点的数量。 使用 Array.prototype.slice()可以像前面介绍 arguments 时一样把 NodeList 对象转换为数组。比如let arrayOfNodes Array.prototype.slice.call(someNode.childNodes,0);当然使用 ES6 的 Array.from()静态方法可以替换这种笨拙的方式let arrayOfNodes Array.from(someNode.childNodes);每个节点都有一个 parentNode 属性指向其 DOM 树中的父元素。childNodes 中的所有节点都有同一个父元素因此它们的 parentNode 属性都指向同一个节点。此外childNodes 列表中的每个节点都是同一列表中其他节点的同胞节点。而使用 previousSibling 和 nextSibling 可以在这个列表的节点间导航。这个列表中第一个节点的 previousSibling 属性是 null最后一个节点的nextSibling 属性也是 null。注意如果 childNodes 中只有一个节点则它的 previousSibling 和 nextSibling 属性都是null。父节点和它的第一个及最后一个子节点也有专门属性firstChild 和 lastChild 分别指向childNodes 中的第一个和最后一个子节点。someNode.firstChild 的值始终等于 someNode. childNodes[0]而 someNode.lastChild 的值始终等于 someNode.childNodes[someNode。childNodes.length-1]。如果只有一个子节点则 firstChild 和 lastChild 指向同一个节点。如果没有子节点则 firstChild 和 lastChild 都是 null。上述这些节点之间的关系为在文档树的节点之间导航提供了方便。还有一个便利的方法是 hasChildNodes()这个方法如果返回 true 则说明节点有一个或多个子节点。相比查询childNodes 的 length 属性这个方法无疑更方便。最后还有一个所有节点都共享的关系。ownerDocument 属性是一个指向代表整个文档的文档节点的指针。所有节点都被创建它们或自己所在的文档所拥有因为一个节点不可能同时存在于两个或者多个文档中。这个属性为迅速访问文档节点提供了便利因为无需在文档结构中逐层上溯了。 操纵节点 因为所有关系指针都是只读的所以 DOM 又提供了一些操纵节点的方法。最常用的方法是appendChild()用于在 childNodes 列表末尾添加节点。添加新节点会更新相关的关系指针包括父节点和之前的最后一个子节点。appendChild()方法返回新添加的节点。如果把文档中已经存在的节点传给 appendChild()则这个节点会从之前的位置被转移到新位置。即使 DOM 树通过各种关系指针维系一个节点也不会在文档中同时出现在两个或更多个地方。因此如果调用 appendChild()传入父元素的第一个子节点则这个节点会成为父元素的最后一个子节点。// 假设 someNode 有多个子节点 let returnedNode someNode.appendChild(someNode.firstChild); alert(returnedNode someNode.firstChild); // false alert(returnedNode someNode.lastChild); // true如果想把节点放到 childNodes 中的特定位置而不是末尾则可以使用 insertBefore()方法。这个方法接收两个参数要插入的节点和参照节点。调用这个方法后要插入的节点会变成参照节点的前一个同胞节点并被返回。如果参照节点是 null则 insertBefore()与 appendChild()效果相同。appendChild() 和 insertBefore() 在插入节点时不会删除任何已有节点。相对地replaceChild()方法接收两个参数要插入的节点和要替换的节点。要替换的节点会被返回并从文档树中完全移除要插入的节点会取而代之。使用 replaceChild()插入一个节点后所有关系指针都会从被替换的节点复制过来。虽然被替换的节点从技术上说仍然被同一个文档所拥有但文档中已经没有它的位置。要移除节点而不是替换节点可以使用 removeChild()方法。这个方法接收一个参数即要移除的节点。被移除的节点会被返回。与 replaceChild()方法一样通过 removeChild()被移除的节点从技术上说仍然被同一个文档所拥有但文档中已经没有它的位置。上面介绍的 4 个方法都用于操纵某个节点的子元素也就是说使用它们之前必须先取得父节点使用前面介绍的 parentNode 属性。并非所有节点类型都有子节点如果在不支持子节点的节点上调用这些方法则会导致抛出错误。 其他方法 所有节点类型还共享了两个方法。第一个是 cloneNode()会返回与调用它的节点一模一样的节点。cloneNode()方法接收一个布尔值参数表示是否深复制。在传入 true 参数时会进行深复制即复制节点及其整个子 DOM 树。如果传入 false则只会复制调用该方法的节点。复制返回的节点属于文档所有但尚未指定父节点所以可称为孤儿节点orphan。可以通过 appendChild()、insertBefore()或 replaceChild()方法把孤儿节点添加到文档中。注意cloneNode()方法不会复制添加到 DOM 节点的 JavaScript 属性比如事件处理程序。这个方法只复制 HTML 属性以及可选地复制子节点。除此之外则一概不会复制。IE 在很长时间内会复制事件处理程序这是一个 bug所以推荐在复制前先删除事件处理程序。本节要介绍的最后一个方法是 normalize()。这个方法唯一的任务就是处理文档子树中的文本节点。由于解析器实现的差异或 DOM 操作等原因可能会出现并不包含文本的文本节点或者文本节点之间互为同胞关系。在节点上调用 normalize()方法会检测这个节点的所有后代从中搜索上述两种情形。如果发现空文本节点则将其删除如果两个同胞节点是相邻的则将其合并为一个文本节点。 Document 类型 Document 类型是 JavaScript 中表示文档节点的类型。在浏览器中文档对象 document 是HTMLDocument 的实例HTMLDocument 继承 Document表示整个 HTML 页面。document 是 window对象的属性因此是一个全局对象。Document 类型的节点有以下特征 nodeType 等于 9nodeName 值为#documentnodeValue 值为 nullparentNode 值为 nullownerDocument 值为 null子节点可以是 DocumentType最多一个、Element最多一个、ProcessingInstruction 或 Comment 类型。 Document 类型可以表示 HTML 页面或其他 XML 文档但最常用的还是通过 HTMLDocument 的实例取得 document 对象。document 对象可用于获取关于页面的信息以及操纵其外观和底层结构。 文档子节点 虽然 DOM 规范规定 Document 节点的子节点可以是 DocumentType、Element、ProcessingInstruction 或 Comment但也提供了两个访问子节点的快捷方式。第一个是 documentElement 属性始终指向 HTML 页面中的html元素。虽然 document.childNodes 中始终有html元素但使用 documentElement 属性可以更快更直接地访问该元素。假如有以下简单的页面html body /body /html浏览器解析完这个页面之后文档只有一个子节点即html元素。这个元素既可以通过documentElement 属性获取也可以通过 childNodes 列表访问如下所示let html document.documentElement; // 取得对html的引用 alert(html document.childNodes[0]); // true alert(html document.firstChild); // true作为 HTMLDocument 的实例document 对象还有一个 body 属性直接指向元素。因为这个元素是开发者使用最多的元素所以 JavaScript 代码中经常可以看到 document.body。所有主流浏览器都支持 document.documentElement 和 document.body。Document 类型另一种可能的子节点是 DocumentType。!doctype标签是文档中独立的部分其信息可以通过 doctype 属性在浏览器中是 document.doctype来访问。另外严格来讲出现在html元素外面的注释也是文档的子节点它们的类型是 Comment。不过由于浏览器实现不同这些注释不一定能被识别或者表现可能不一致。比如以下 HTML 页面!-- 第一条注释 -- html body /body /html !-- 第二条注释 --这个页面看起来有 3 个子节点注释、html元素、注释。逻辑上讲document.childNodes应该包含 3 项对应代码中的每个节点。但实际上浏览器有可能以不同方式对待html元素外部的注释比如忽略一个或两个注释。 一般来说appendChild()、removeChild()和 replaceChild()方法不会用在 document 对象上。这是因为文档类型如果存在是只读的而且只能有一个 Element 类型的子节点即html已经存在了。 元素是 HTMLHtmlElement 的实例HTMLHtmlElement 继承 HTMLElementHTMLElement 继承 Element因此 HTML文档可以包含子节点但不能多于一个。 文档信息 document 作为 HTMLDocument 的实例还有一些标准 Document 对象上所没有的属性。这些属性提供浏览器所加载网页的信息。其中第一个属性是 title包含title元素中的文本通常显示在浏览器窗口或标签页的标题栏。通过这个属性可以读写页面的标题修改后的标题也会反映在浏览器标题栏上。不过修改 title 属性并不会改变title元素。下面是一个例子// 读取文档标题 let originalTitle document.title; // 修改文档标题 document.title New page title;接下来要介绍的 3 个属性是 URL、domain 和 referrer。其中URL 包含当前页面的完整 URL地址栏中的 URLdomain 包含页面的域名而 referrer 包含链接到当前页面的那个页面的 URL。如果当前页面没有来源则 referrer 属性包含空字符串。所有这些信息都可以在请求的 HTTP 头部信息中获取只是在 JavaScript 中通过这几个属性暴露出来而已。URL 跟域名是相关的。比如如果 document.URL 是 http://www.wrox.com/WileyCDA/则document.domain 就是 www.wrox.com。在这些属性中只有 domain 属性是可以设置的。出于安全考虑给 domain 属性设置的值是有限制的。如果 URL包含子域名如 p2p.wrox.com则可以将 domain 设置为wrox.comURL包含“www”时也一样比如 www.wrox.com。不能给这个属性设置 URL 中不包含的值比如// 页面来自 p2p.wrox.com document.domain wrox.com; // 成功 document.domain nczonline.net; // 出错当页面中包含来自某个不同子域的窗格frame或内嵌窗格iframe时设置document.domain 是有用的。因为跨源通信存在安全隐患所以不同子域的页面间无法通过 JavaScript通信。此时在每个页面上把 document.domain 设置为相同的值这些页面就可以访问对方的 JavaScript对象了。比如一个加载自 www.wrox.com 的页面中包含一个内嵌窗格其中的页面加载自p2p.wrox.com。这两个页面的 document.domain 包含不同的字符串内部和外部页面相互之间不能访问对方的 JavaScript 对象。如果每个页面都把 document.domain 设置为 wrox.com那这两个页面之间就可以通信了。浏览器对 domain 属性还有一个限制即这个属性一旦放松就不能再收紧。比如把document.domain 设置为wrox.com之后就不能再将其设置回p2p.wrox.com后者会导致错误比如// 页面来自 p2p.wrox.com document.domain wrox.com; // 放松成功 document.domain p2p.wrox.com; // 收紧错误定位元素 使用 DOM 最常见的情形可能就是获取某个或某组元素的引用然后对它们执行某些操作。document 对象上暴露了一些方法可以实现这些操作。getElementById()和 getElementsByTagName()就是 Document 类型提供的两个方法。getElementById()方法接收一个参数即要获取元素的 ID如果找到了则返回这个元素如果没找到则返回 null。参数 ID 必须跟元素在页面中的 id 属性值完全匹配包括大小写。比如页面中有以下元素div idmyDivSome text/div可以使用如下代码取得这个元素let div document.getElementById(myDiv); // 取得对这个div元素的引用但参数大小写不匹配会返回 nulllet div document.getElementById(mydiv); // null如果页面中存在多个具有相同 ID 的元素则 getElementById()返回在文档中出现的第一个元素。getElementsByTagName()是另一个常用来获取元素引用的方法。这个方法接收一个参数即要获取元素的标签名返回包含零个或多个元素的 NodeList。在 HTML 文档中这个方法返回一个HTMLCollection 对象。考虑到二者都是“实时”列表HTMLCollection 与 NodeList 是很相似的。例如下面的代码会取得页面中所有的img元素并返回包含它们的 HTMLCollectionlet images document.getElementsByTagName(img);这里把返回的 HTMLCollection 对象保存在了变量 images 中。与 NodeList 对象一样也可以使用中括号或 item()方法从 HTMLCollection 取得特定的元素。而取得元素的数量同样可以通过length 属性得知如下所示alert(images.length); // 图片数量 alert(images[0].src); // 第一张图片的 src 属性 alert(images.item(0).src); // 同上HTMLCollection 对象还有一个额外的方法 namedItem()可通过标签的 name 属性取得某一项的引用。例如假设页面中包含如下的img元素img srcmyimage.gif namemyImage那么也可以像这样从 images 中取得对这个img元素的引用let myImage images.namedItem(myImage);这样HTMLCollection 就提供了除索引之外的另一种获取列表项的方式从而为取得元素提供了便利。 对于 name 属性的元素还可以直接使用中括号来获取如下面的例子所示let myImage images[myImage];对 HTMLCollection 对象而言中括号既可以接收数值索引也可以接收字符串索引。而在后台数值索引会调用 item()字符串索引会调用 namedItem()。要取得文档中的所有元素可以给 getElementsByTagName()传入*。在 JavaScript 和 CSS 中*一般被认为是匹配一切的字符。来看下面的例子let allElements document.getElementsByTagName(*);这行代码可以返回包含页面中所有元素的 HTMLCollection 对象顺序就是它们在页面中出现的顺序。因此第一项是html元素第二项是head元素以此类推。 注意对于 document.getElementsByTagName()方法虽然规范要求区分标签的大小写但为了最大限度兼容原有 HTML 页面实际上是不区分大小写的。如果是在 XML 页面如 XHTML中使用那么 document.getElementsByTagName()就是区分大小写的。HTMLDocument 类型上定义的获取元素的第三个方法是 getElementsByName()。顾名思义这个方法会返回具有给定 name 属性的所有元素。getElementsByName()方法最常用于单选按钮因为同一字段的单选按钮必须具有相同的 name 属性才能确保把正确的值发送给服务器比如下面的例子fieldset legendWhich color do you prefer?/legend ul li input typeradio valuered namecolor idcolorRed label forcolorRedRed/label /li li input typeradio valuegreen namecolor idcolorGreen label forcolorGreenGreen/label /li li input typeradio valueblue namecolor idcolorBluelabel forcolorBlueBlue/label /li /ul /fieldset这里所有的单选按钮都有名为color的 name 属性但它们的 ID 都不一样。这是因为 ID 是为了匹配对应的label元素而 name 相同是为了保证只将三个中的一个值发送给服务器。然后就可以像下面这样取得所有单选按钮let radios document.getElementsByName(color);与 getElementsByTagName()一样getElementsByName()方法也返回 HTMLCollection。不过在这种情况下namedItem()方法只会取得第一项因为所有项的 name 属性都一样。 特殊集合 document 对象上还暴露了几个特殊集合这些集合也都是 HTMLCollection 的实例。这些集合是访问文档中公共部分的快捷方式列举如下。 document.anchors 包含文档中所有带 name 属性的a元素。document.applets 包含文档中所有applet元素因为applet元素已经不建议使用所以这个集合已经废弃。document.forms 包含文档中所有form元素与 document.getElementsByTagName (“form”)返回的结果相同。document.images 包含文档中所有img元素与 document.getElementsByTagName (“img”)返回的结果相同。document.links 包含文档中所有带 href 属性的a元素。 这些特殊集合始终存在于 HTMLDocument 对象上而且与所有 HTMLCollection 对象一样其内容也会实时更新以符合当前文档的内容。 DOM 兼容性检测 由于 DOM 有多个 Level 和多个部分因此确定浏览器实现了 DOM 的哪些部分是很必要的。document.implementation 属性是一个对象其中提供了与浏览器 DOM 实现相关的信息和能力。DOM Level 1 在 document.implementation 上只定义了一个方法即 hasFeature()。这个方法接收两个参数特性名称和 DOM 版本。如果浏览器支持指定的特性和版本则 hasFeature()方法返回true如下面的例子所示 let hasXmlDom document.implementation.hasFeature(XML, 1.0);可以使用 hasFeature()方法测试的特性及版本如下表所列。 特性支持的版本说明Core1.0、2.0、3.0定义树形文档结构的基本 DOMXML1.0、2.0、3.0Core 的 XML 扩展增加了对 CDATA 区块、处理指令和实体的支持HTML1.0、2.0XML 的 HTML 扩展增加了 HTML 特定的元素和实体Views2.0文档基于某些样式的实现格式StyleSheets2.0文档的相关样式表CSS2.0Cascading Style Sheets Level 1CSS22.0Cascading Style Sheets Level 2Events2.0、3.0通用 DOM 事件UIEvents2.0、3.0用户界面事件TextEvents3.0文本输入设备触发的事件MouseEvents2.0、3.0鼠标导致的事件单击、悬停等MutationEvents2.0、3.0DOM 树变化时触发的事件MutationNameEvents3.0DOM 元素或元素属性被重命名时触发的事件HTMLEvents2.0HTML 4.01 事件Range2.0在 DOM 树中操作一定范围的对象和方法Traversal2.0遍历 DOM 树的方法LS3.0文件与 DOM 树之间的同步加载与保存LS-Async3.0文件与 DOM 树之间的异步加载与保存Validation3.0修改 DOM 树并保证其继续有效的方法XPath3.0访问 XML 文档不同部分的语言 由于实现不一致因此 hasFeature()的返回值并不可靠。目前这个方法已经被废弃不再建议使用。为了向后兼容目前主流浏览器仍然支持这个方法但无论检测什么都一律返回 true。 文档写入 document 对象有一个古老的能力即向网页输出流中写入内容。这个能力对应 4 个方法write()、writeln()、open()和 close()。其中write()和 writeln()方法都接收一个字符串参数可以将这个字符串写入网页中。write()简单地写入文本而 writeln()还会在字符串末尾追加一个换行符\n。这两个方法可以用来在页面加载期间向页面中动态添加内容如下所示html head titledocument.write() Example/title /head body pThe current date and time is: script typetext/javascript document.write(strong (new Date()).toString() /strong); /script /p /body /html这个例子会在页面加载过程中输出当前日期和时间。日期放在了strong元素中如同它们之前就包含在 HTML 页面中一样。这意味着会创建一个 DOM 元素以后也可以访问。通过 write()和writeln()输出的任何 HTML 都会以这种方式来处理。 write()和 writeln()方法经常用于动态包含外部资源如 JavaScript 文件。在包含 JavaScript 文件时记住不能像下面的例子中这样直接包含字符串/script因为这个字符串会被解释为脚本块的结尾导致后面的代码不能执行html headtitledocument.write() Example/title /head body script typetext/javascript document.write(script type\text/javascript\ src\file.js\ /script); /script /body /html虽然这样写看起来没错但输出之后的/script“会匹配最外层的script标签导致页面中显示出”);。为避免出现这个问题需要对前面的例子稍加修改html headtitledocument.write() Example/title /head body script typetext/javascript document.write(script type\text/javascript\ src\file.js\ \/script);/script /body /html这里的字符串/script不会再匹配最外层的script标签因此不会在页面中输出额外内容。 前面的例子展示了在页面渲染期间通过 document.write()向文档中输出内容。如果是在页面加载完之后再调用 document.write()则输出的内容会重写整个页面如下面的例子所示html head titledocument.write() Example/title /head body pThis is some content that you wont get to see because it will be overwritten./p script typetext/javascript window.onload function(){ document.write(Hello world!); };/script /body /html这个例子使用了 window.onload 事件处理程序将调用 document.write()的函数推迟到页面加载完毕后执行。执行之后字符串Hello world!会重写整个页面内容。 open()和 close()方法分别用于打开和关闭网页输出流。在调用 write()和 writeln()时这两个方法都不是必需的。注意严格的 XHTML 文档不支持文档写入。对于内容类型为 application/xmlxhtml 的页面这些方法不起作用。 Element 类型 除了Document 类型Element 类型就是Web开发中最常用的类型了。Element 表示XML或HTML元素对外暴露出访问元素标签名、子节点和属性的能力。Element 类型的节点具有以下特征 nodeType 等于 1nodeName 值为元素的标签名nodeValue 值为 nullparentNode 值为 Document 或 Element 对象子节点可以是 Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference 类型。 可以通过 nodeName 或 tagName 属性来获取元素的标签名。这两个属性返回同样的值添加后一个属性明显是为了不让人误会。比如有下面的元素div idmyDiv/div可以像这样取得这个元素的标签名let div document.getElementById(myDiv); alert(div.tagName); // DIV alert(div.tagName div.nodeName); // true例子中的元素标签名为 divID 为myDiv。注意div.tagName 实际上返回的是DIV而不是div。在 HTML 中元素标签名始终以全大写表示在 XML包括 XHTML中标签名始终与源代码中的大小写一致。如果不确定脚本是在 HTML 文档还是 XML 文档中运行最好将标签名转换为小写形式以便于比较if (element.tagName div){ // 不要这样做可能出错// do something here } if (element.tagName.toLowerCase() div){ // 推荐适用于所有文档// 做点什么 }这个例子演示了比较 tagName 属性的情形。第一个是容易出错的写法因为 HTML 文档中 tagName返回大写形式的标签名。第二个先把标签名转换为全部小写后再比较这是推荐的做法因为这对 HTML和 XML 都适用。 HTML 元素 所有 HTML 元素都通过 HTMLElement 类型表示包括其直接实例和间接实例。另外HTMLElement直接继承 Element 并增加了一些属性。每个属性都对应下列属性之一它们是所有 HTML 元素上都有的标准属性 id元素在文档中的唯一标识符title包含元素的额外信息通常以提示条形式展示lang元素内容的语言代码很少用dir语言的书写方向ltr表示从左到右rtl表示从右到左同样很少用className相当于 class 属性用于指定元素的 CSS 类因为 class 是 ECMAScript 关键字所以不能直接用这个名字。 所有这些都可以用来获取对应的属性值也可以用来修改相应的值。比如有下面的 HTML 元素 div idmyDiv classbd titleBody text langen dirltr/div这个元素中的所有属性都可以使用下列 JavaScript 代码读取let div document.getElementById(myDiv); alert(div.id); // myDiv alert(div.className); // bd alert(div.title); // Body text alert(div.lang); // en alert(div.dir); // ltr而且可以使用下列代码修改元素的属性div.id someOtherId; div.className ft; div.title Some other text; div.lang fr; div.dir rtl;并非所有这些属性的修改都会对页面产生影响。比如把 id 或 lang 改成其他值对用户是不可见的假设没有基于这两个属性应用 CSS 样式而修改 title 属性则只会在鼠标移到这个元素上时才会反映出来。修改 dir 会导致页面文本立即向左或向右对齐。修改 className 会立即反映应用到新类名的 CSS 样式如果定义了不同的样式 如前所述所有 HTML 元素都是 HTMLElement 或其子类型的实例。下表列出了所有 HTML 元素及其对应的类型斜体表示已经废弃的元素 元素类型AHTMLAnchorElementABBRHTMLElementACRONYMHTMLElementADDRESSHTMLElementAPPLETHTMLAppletElementAREAHTMLAreaElementBHTMLElementBASEHTMLBaseElementBASEFONTHTMLBaseFontElementBDOHTMLElementBIGHTMLElementBLOCKQUOTEHTMLQuoteElementBODYHTMLBodyElementBRHTMLBRElementBUTTONHTMLButtonElementCAPTIONHTMLTableCaptionElementCENTERHTMLElementCITEHTMLElementCODEHTMLElementCOLHTMLTableColElementCOLGROUPHTMLTableColElementDDHTMLElementDELHTMLModElementDFNHTMLElementDIRHTMLDirectoryElementDIVHTMLDivElementDLHTMLDListElementDTHTMLElementEMHTMLElementFIELDSETHTMLFieldSetElementFONTHTMLFontElementFORMHTMLFormElementFRAMEHTMLFrameElementFRAMESETHTMLFrameSetElementH1HTMLHeadingElementH2HTMLHeadingElementH3HTMLHeadingElementH4HTMLHeadingElementH5HTMLHeadingElementH6HTMLHeadingElementHEADHTMLHeadElementHRHTMLHRElementHTMLHTMLHtmlElementIHTMLElementIFRAMEHTMLIFrameElementIMGHTMLImageElementINPUTHTMLInputElementINSHTMLModElementISINDEXHTMLIsIndexElementKBDHTMLElementLABELHTMLLabelElementLEGENDHTMLLegendElementLIHTMLLIElementLINKHTMLLinkElementMAPHTMLMapElementMENUHTMLMenuElementMETAHTMLMetaElementNOFRAMESHTMLElementNOSCRIPTHTMLElementOBJECTHTMLObjectElementOLHTMLOListElementOPTGROUPHTMLOptGroupElementOPTIONHTMLOptionElementPHTMLParagraphElementPARAMHTMLParamElementPREHTMLPreElementQHTMLQuoteElementSHTMLElementSAMPHTMLElementSCRIPTHTMLScriptElementSELECTHTMLSelectElementSMALLHTMLElementSPANHTMLElementSTRIKEHTMLElementSTRONGHTMLElementSTYLEHTMLStyleElementSUBHTMLElementSUPHTMLElementTABLEHTMLTableElementTBODYHTMLTableSectionElementTDHTMLTableCellElementTEXTAREAHTMLTextAreaElementTFOOTHTMLTableSectionElementTHHTMLTableCellElementTHEADHTMLTableSectionElementTITLEHTMLTitleElementTRHTMLTableRowElementTTHTMLElementUHTMLElementULHTMLUListElementVARHTMLElement 这里列出的每种类型都有关联的属性和方法。本书会涉及其中的很多类型。 取得属性 每个元素都有零个或多个属性通常用于为元素或其内容附加更多信息。与属性相关的 DOM 方法主要有 3 个getAttribute()、setAttribute()和 removeAttribute()。这些方法主要用于操纵属性包括在 HTMLElement 类型上定义的属性。下面看一个例子let div document.getElementById(myDiv); alert(div.getAttribute(id)); // myDiv alert(div.getAttribute(class)); // bd alert(div.getAttribute(title)); // Body text alert(div.getAttribute(lang)); // en alert(div.getAttribute(dir)); // ltr注意传给 getAttribute()的属性名与它们实际的属性名是一样的因此这里要传class而非classNameclassName 是作为对象属性时才那么拼写的。如果给定的属性不存在则 getAttribute()返回 null。 getAttribute()方法也能取得不是 HTML 语言正式属性的自定义属性的值。比如下面的元素div idmyDiv my_special_attributehello!/div这个元素有一个自定义属性 my_special_attribute值为hello!。可以像其他属性一样使用getAttribute()取得这个属性的值let value div.getAttribute(my_special_attribute);注意属性名不区分大小写因此ID和id被认为是同一个属性。另外根据 HTML5 规范的要求自定义属性名应该前缀 data-以方便验证。元素的所有属性也可以通过相应 DOM 元素对象的属性来取得。当然这包括 HTMLElement 上定义的直接映射对应属性的 5 个属性还有所有公认非自定义的属性也会被添加为 DOM 对象的属性。比如下面的例子div idmyDiv alignleft my_special_attributehello/div因为 id 和 align 在 HTML 中是div元素公认的属性所以 DOM 对象上也会有这两个属性。但my_special_attribute 是自定义属性因此不会成为 DOM 对象的属性。 通过 DOM 对象访问的属性中有两个返回的值跟使用 getAttribute()取得的值不一样。首先是style 属性这个属性用于为元素设定 CSS 样式。在使用 getAttribute()访问 style 属性时返回的是 CSS 字符串。而在通过 DOM 对象的属性访问时style 属性返回的是一个CSSStyleDeclaration对象。DOM 对象的 style 属性用于以编程方式读写元素样式因此不会直接映射为元素中 style 属性的字符串值。第二个属性其实是一类即事件处理程序或者事件属性比如 onclick。在元素上使用事件属性时比如 onclick属性的值是一段 JavaScript 代码。如果使用 getAttribute()访问事件属性则返回的是字符串形式的源代码。而通过 DOM 对象的属性访问事件属性时返回的则是一个 JavaScript函数未指定该属性则返回 null。这是因为 onclick 及其他事件属性是可以接受函数作为值的。考虑到以上差异开发者在进行DOM编程时通常会放弃使用getAttribute()而只使用对象属性。getAttribute()主要用于取得自定义属性的值。 设置属性 与 getAttribute()配套的方法是 setAttribute()这个方法接收两个参数要设置的属性名和属性的值。如果属性已经存在则 setAttribute()会以指定的值替换原来的值如果属性不存在则 setAttribute()会以指定的值创建该属性。下面看一个例子div.setAttribute(id, someOtherId); div.setAttribute(class, ft); div.setAttribute(title, Some other text); div.setAttribute(lang,fr); div.setAttribute(dir, rtl);setAttribute()适用于 HTML 属性也适用于自定义属性。另外使用 setAttribute()方法设置的属性名会规范为小写形式因此ID会变成id。因为元素属性也是 DOM 对象属性所以直接给 DOM 对象的属性赋值也可以设置元素属性的值如下所示div.id someOtherId; div.align left;注意在 DOM 对象上添加自定义属性如下面的例子所示不会自动让它变成元素的属性div.mycolor red; alert(div.getAttribute(mycolor)); // nullIE 除外这个例子添加了一个自定义属性 mycolor 并将其值设置为red。在多数浏览器中这个属性不会自动变成元素属性。因此调用 getAttribute()取得 mycolor 的值会返回 null。 最后一个方法 removeAttribute()用于从元素中删除属性。这样不单单是清除属性的值而是会把整个属性完全从元素中去掉如下所示div.removeAttribute(class);这个方法用得并不多但在序列化 DOM 元素时可以通过它控制要包含的属性。 attributes 属性 Element 类型是唯一使用 attributes 属性的 DOM 节点类型。attributes 属性包含一个NamedNodeMap 实例是一个类似 NodeList 的“实时”集合。元素的每个属性都表示为一个 Attr 节点并保存在这个 NamedNodeMap 对象中。NamedNodeMap 对象包含下列方法 getNamedItem(name)返回 nodeName 属性等于 name 的节点removeNamedItem(name)删除 nodeName 属性等于 name 的节点setNamedItem(node)向列表中添加 node 节点以其 nodeName 为索引item(pos)返回索引位置 pos 处的节点。 attributes 属性中的每个节点的 nodeName 是对应属性的名字nodeValue 是属性的值。比如要取得元素 id 属性的值可以使用以下代码let id element.attributes.getNamedItem(id).nodeValue;下面是使用中括号访问属性的简写形式let id element.attributes[id].nodeValue;同样也可以用这种语法设置属性的值即先取得属性节点再将其 nodeValue 设置为新值如下所示element.attributes[id].nodeValue someOtherId;removeNamedItem()方法与元素上的 removeAttribute()方法类似也是删除指定名字的属性。下面的例子展示了这两个方法唯一的不同之处就是removeNamedItem()返回表示被删除属性的Attr节点let oldAttr element.attributes.removeNamedItem(id);setNamedItem()方法很少使用它接收一个属性节点然后给元素添加一个新属性如下所示element.attributes.setNamedItem(newAttr);一般来说因为使用起来更简便通常开发者更喜欢使用 getAttribute()、removeAttribute()和 setAttribute()方法而不是刚刚介绍的 NamedNodeMap 对象的方法。attributes 属性最有用的场景是需要迭代元素上所有属性的时候。这时候往往是要把 DOM 结构序列化为 XML 或 HTML 字符串。比如以下代码能够迭代一个元素上的所有属性并以 attribute1“value1” attribute2value2的形式生成格式化字符串function outputAttributes(element) { let pairs []; for (let i 0, len element.attributes.length; i len; i) { const attribute element.attributes[i]; pairs.push(${attribute.nodeName}${attribute.nodeValue}); } return pairs.join( ); }这个函数使用数组存储每个名/值对迭代完所有属性后再将这些名/值对用空格拼接在一起。这个技术常用于序列化为长字符串。这个函数中的 for 循环使用 attributes.length 属性迭代每个属性将每个属性的名字和值输出为字符串。不同浏览器返回的 attributes 中的属性顺序也可能不一样。HTML 或 XML 代码中属性出现的顺序不一定与 attributes 中的顺序一致。 创建元素 可以使用 document.createElement()方法创建新元素。这个方法接收一个参数即要创建元素的标签名。在 HTML 文档中标签名是不区分大小写的而 XML 文档包括 XHTML是区分大小写的。要创建div元素可以使用下面的代码let div document.createElement(div);使用 createElement()方法创建新元素的同时也会将其 ownerDocument 属性设置为 document。此时可以再为其添加属性、添加更多子元素。比如div.id myNewDiv; div.className box;在新元素上设置这些属性只会附加信息。因为这个元素还没有添加到文档树所以不会影响浏览器显示。要把元素添加到文档树可以使用 appendChild()、insertBefore()或 replaceChild()。比如以下代码会把刚才创建的元素添加到文档的body元素中document.body.appendChild(div);元素被添加到文档树之后浏览器会立即将其渲染出来。之后再对这个元素所做的任何修改都会立即在浏览器中反映出来。 元素后代 元素可以拥有任意多个子元素和后代元素因为元素本身也可以是其他元素的子元素。childNodes属性包含元素所有的子节点这些子节点可能是其他元素、文本节点、注释或处理指令。不同浏览器在识别这些节点时的表现有明显不同。比如下面的代码ul idmyList liItem 1/li liItem 2/li liItem 3/li /ul在解析以上代码时ul元素会包含 7 个子元素其中 3 个是li元素还有 4 个 Text 节点表示li元素周围的空格。如果把元素之间的空格删掉变成下面这样则所有浏览器都会返回同样数量的子节点ul idmyListliItem 1/liliItem 2/liliItem 3/li/ul所有浏览器解析上面的代码后ul元素都会包含 3 个子节点。 考虑到这种情况通常在执行某个操作之后需要先检测一下节点的 nodeType如下所示for (let i 0, len element.childNodes.length; i len; i) { if (element.childNodes[i].nodeType 1) { // 执行某个操作} }以上代码会遍历某个元素的子节点并且只在 nodeType 等于 1即 Element 节点时执行某个操作。 要取得某个元素的子节点和其他后代节点可以使用元素的 getElementsByTagName()方法。在元素上调用这个方法与在文档上调用是一样的只不过搜索范围限制在当前元素之内即只会返回当前元素的后代。对于本节前面ul的例子可以像下面这样取得其所有的li元素let ul document.getElementById(myList); let items ul.getElementsByTagName(li);这里例子中的ul元素只有一级子节点如果它包含更多层级则所有层级中的li元素都会返回。 Text 类型 Text 节点由 Text 类型表示包含按字面解释的纯文本也可能包含转义后的 HTML 字符但不含 HTML 代码。Text 类型的节点具有以下特征 nodeType 等于 3nodeName 值为#textnodeValue 值为节点中包含的文本parentNode 值为 Element 对象不支持子节点。 Text 节点中包含的文本可以通过 nodeValue 属性访问也可以通过 data 属性访问这两个属性包含相同的值。修改 nodeValue 或 data 的值也会在另一个属性反映出来。文本节点暴露了以下操作文本的方法 appendData(text)向节点末尾添加文本 textdeleteData(offset, count)从位置 offset 开始删除 count 个字符insertData(offset, text)在位置 offset 插入 textreplaceData(offset, count, text)用 text 替换从位置 offset 到 offset count 的文本splitText(offset)在位置 offset 将当前文本节点拆分为两个文本节点substringData(offset, count)提取从位置 offset 到 offset count 的文本。 除了这些方法还可以通过 length 属性获取文本节点中包含的字符数量。这个值等于 nodeValue.length 和 data.length。默认情况下包含文本内容的每个元素最多只能有一个文本节点。例如!-- 没有内容因此没有文本节点 -- div/div!-- 有空格因此有一个文本节点 -- div /div !-- 有内容因此有一个文本节点 -- divHello World!/div示例中的第一个div元素中不包含内容因此不会产生文本节点。只要开始标签和结束标签之间有内容就会创建一个文本节点因此第二个div元素会有一个文本节点的子节点虽然它只包含空格。这个文本节点的 nodeValue 就是一个空格。第三个div元素也有一个文本节点的子节点其nodeValue 的值为Hello World!。 下列代码可以用来访问这个文本节点let textNode div.firstChild; // 或 div.childNodes[0]取得文本节点的引用后可以像这样来修改它div.firstChild.nodeValue Some other message;只要节点在当前的文档树中这样的修改就会马上反映出来。 修改文本节点还有一点要注意就是HTML 或 XML 代码取决于文档类型会被转换成实体编码即小于号、大于号或引号会被转义如下所示// 输出为Some lt;stronggt;otherlt;/stronggt; message div.firstChild.nodeValue Some strongother/strong message;这实际上是在将 HTML 字符串插入 DOM 文档前进行编码的有效方式。 创建文本节点 document.createTextNode()可以用来创建新文本节点它接收一个参数即要插入节点的文本。跟设置已有文本节点的值一样这些要插入的文本也会应用 HTML 或 XML 编码如下面的例子所示let textNode document.createTextNode(strongHello/strong world!);创建新文本节点后其 ownerDocument 属性会被设置为 document。但在把这个节点添加到文档树之前我们不会在浏览器中看到它。以下代码创建了一个div元素并给它添加了一段文本消息let element document.createElement(div); element.className message; let textNode document.createTextNode(Hello world!); element.appendChild(textNode); document.body.appendChild(element);这个例子首先创建了一个div元素并给它添加了值为message的 class 属性然后又创建了一个文本节点并添加到该元素。最后一步是把这个元素添加到文档的主体上这样元素及其包含的文本会出现在浏览器中。 一般来说一个元素只包含一个文本子节点。不过也可以让元素包含多个文本子节点如下面的例子所示let element document.createElement(div); element.className message; let textNode document.createTextNode(Hello world!); element.appendChild(textNode); let anotherTextNode document.createTextNode(Yippee!); element.appendChild(anotherTextNode); document.body.appendChild(element);在将一个文本节点作为另一个文本节点的同胞插入后两个文本节点的文本之间不会包含空格。 规范化文本节点 DOM 文档中的同胞文本节点可能导致困惑因为一个文本节点足以表示一个文本字符串。同样DOM 文档中也经常会出现两个相邻文本节点。为此有一个方法可以合并相邻的文本节点。这个方法叫 normalize()是在 Node 类型中定义的因此所有类型的节点上都有这个方法。在包含两个或多个相邻文本节点的父节点上调用 normalize()时所有同胞文本节点会被合并为一个文本节点这个文本节点的 nodeValue 就等于之前所有同胞节点 nodeValue 拼接在一起得到的字符串。来看下面的例子let element document.createElement(div); element.className message; let textNode document.createTextNode(Hello world!); element.appendChild(textNode); let anotherTextNode document.createTextNode(Yippee!); element.appendChild(anotherTextNode); document.body.appendChild(element); alert(element.childNodes.length); // 2 element.normalize(); alert(element.childNodes.length); // 1 alert(element.firstChild.nodeValue); // Hello world!Yippee! 浏览器在解析文档时永远不会创建同胞文本节点。同胞文本节点只会出现在 DOM 脚本生成的文档树中。 拆分文本节点 Text 类型定义了一个与 normalize()相反的方法——splitText()。这个方法可以在指定的偏移位置拆分 nodeValue将一个文本节点拆分成两个文本节点。拆分之后原来的文本节点包含开头到偏移位置前的文本新文本节点包含剩下的文本。这个方法返回新的文本节点具有与原来的文本节点相同的 parentNode。来看下面的例子let element document.createElement(div); element.className message; let textNode document.createTextNode(Hello world!); element.appendChild(textNode); document.body.appendChild(element); let newNode element.firstChild.splitText(5); alert(element.firstChild.nodeValue); // Hello alert(newNode.nodeValue); // world! alert(element.childNodes.length); // 2在这个例子中包含Hello world!“的文本节点被从位置 5 拆分成两个文本节点。位置 5 对应Hello和world!“之间的空格因此原始文本节点包含字符串Hello”而新文本节点包含文本” world!包含空格。 拆分文本节点最常用于从文本节点中提取数据的 DOM 解析技术。 Comment 类型 DOM 中的注释通过 Comment 类型表示。Comment 类型的节点具有以下特征 nodeType 等于 8nodeName 值为#commentnodeValue 值为注释的内容parentNode 值为 Document 或 Element 对象不支持子节点。 Comment 类型与 Text 类型继承同一个基类CharacterData因此拥有除 splitText()之外Text 节点所有的字符串操作方法。与 Text 类型相似注释的实际内容可以通过 nodeValue 或 data属性获得。注释节点可以作为父节点的子节点来访问。比如下面的 HTML 代码div idmyDiv!-- A comment --/div这里的注释是div元素的子节点这意味着可以像下面这样访问它let div document.getElementById(myDiv); let comment div.firstChild; alert(comment.data); // A comment可以使用 document.createComment()方法创建注释节点参数为注释文本如下所示let comment document.createComment(A comment);显然注释节点很少通过 JavaScrpit 创建和访问因为注释几乎不涉及算法逻辑。此外浏览器不承认结束的/html标签之后的注释。如果要访问注释节点则必须确定它们是html元素的后代。 CDATASection 类型 CDATASection 类型表示 XML 中特有的 CDATA 区块。CDATASection 类型继承 Text 类型因此拥有包括 splitText()在内的所有字符串操作方法。CDATASection 类型的节点具有以下特征 nodeType 等于 4nodeName 值为#cdata-sectionnodeValue 值为 CDATA 区块的内容parentNode 值为 Document 或 Element 对象不支持子节点。 CDATA 区块只在 XML 文档中有效因此某些浏览器比较陈旧的版本会错误地将 CDATA 区块解析为 Comment 或 Element。比如下面这行代码div idmyDiv![CDATA[This is some content.]]/div这里div的第一个子节点应该是 CDATASection 节点。但主流的四大浏览器没有一个将其识别为CDATASection。即使在有效的 XHTML 文档中这些浏览器也不能恰当地支持嵌入的 CDATA 区块。 在真正的 XML 文档中可以使用 document.createCDataSection()并传入节点内容来创建CDATA 区块。 DocumentType 类型 DocumentType 类型的节点包含文档的文档类型doctype信息具有以下特征 nodeType 等于 10nodeName 值为文档类型的名称nodeValue 值为 nullparentNode 值为 Document 对象不支持子节点。 DocumentType 对象在 DOM Level 1 中不支持动态创建只能在解析文档代码时创建。对于支持这个类型的浏览器DocumentType 对象保存在 document.doctype 属性中。DOM Level 1 规定了DocumentType 对象的 3 个属性name、entities 和 notations。其中name 是文档类型的名称entities 是这个文档类型描述的实体的 NamedNodeMap而 notations 是这个文档类型描述的表示法的 NamedNodeMap。因为浏览器中的文档通常是 HTML 或 XHTML 文档类型所以 entities 和notations 列表为空。这个对象只包含行内声明的文档类型。无论如何只有 name 属性是有用的。这个属性包含文档类型的名称即紧跟在!DOCTYPE 后面的那串文本。比如下面的 HTML 4.01 严格文档类型!DOCTYPE HTML PUBLIC -// W3C// DTD HTML 4.01// EN http:// www.w3.org/TR/html4/strict.dtd对于这个文档类型name 属性的值是htmlalert(document.doctype.name); // htmlDocumentFragment 类型 在所有节点类型中DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM 将文档片段定义为“轻量级”文档能够包含和操作节点却没有完整文档那样额外的消耗。DocumentFragment 节点具有以下特征 nodeType 等于 11nodeName 值为#document-fragmentnodeValue 值为 nullparentNode 值为 null子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或EntityReference。 不能直接把文档片段添加到文档。相反文档片段的作用是充当其他要被添加到文档的节点的仓库。可以使用 document.createDocumentFragment()方法像下面这样创建文档片段let fragment document.createDocumentFragment();文档片段从 Node 类型继承了所有文档类型具备的可以执行 DOM 操作的方法。如果文档中的一个节点被添加到一个文档片段则该节点会从文档树中移除不会再被浏览器渲染。添加到文档片段的新节点同样不属于文档树不会被浏览器渲染。可以通过 appendChild()或 insertBefore()方法将文档片段的内容添加到文档。在把文档片段作为参数传给这些方法时这个文档片段的所有子节点会被添加到文档中相应的位置。文档片段本身永远不会被添加到文档树。以下面的 HTML 为例ul idmyList/ul假设想给这个ul元素添加 3 个列表项。如果分 3 次给这个元素添加列表项浏览器就要重新渲染 3 次页面以反映新添加的内容。为避免多次渲染下面的代码示例使用文档片段创建了所有列表项然后一次性将它们添加到了ul元素let fragment document.createDocumentFragment(); let ul document.getElementById(myList); for (let i 0; i 3; i) { let li document.createElement(li); li.appendChild(document.createTextNode(Item ${i 1})); fragment.appendChild(li); } ul.appendChild(fragment);这个例子先创建了一个文档片段然后取得了ul元素的引用。接着通过 for 循环创建了 3 个列表项每一项都包含表明自己身份的文本。为此先创建li元素再创建文本节点并添加到该元素。然后通过 appendChild()把li元素添加到文档片段。循环结束后通过把文档片段传给 appendChild()将所有列表项添加到了ul元素。此时文档片段的子节点全部被转移到了ul元素。 Attr 类型 元素数据在 DOM 中通过 Attr 类型表示。Attr 类型构造函数和原型在所有浏览器中都可以直接访问。技术上讲属性是存在于元素 attributes 属性中的节点。Attr 节点具有以下特征 nodeType 等于 2nodeName 值为属性名nodeValue 值为属性值parentNode 值为 null在 HTML 中不支持子节点在 XML 中子节点可以是 Text 或 EntityReference。 属性节点尽管是节点却不被认为是 DOM 文档树的一部分。Attr 节点很少直接被引用通常开发者更喜欢使用 getAttribute()、removeAttribute()和 setAttribute()方法操作属性。Attr 对象上有 3 个属性name、value 和 specified。其中name 包含属性名与 nodeName一样value 包含属性值与 nodeValue 一样而 specified 是一个布尔值表示属性使用的是默认值还是被指定的值。可以使用 document.createAttribute()方法创建新的 Attr 节点参数为属性名。比如要给元素添加 align 属性可以使用下列代码let attr document.createAttribute(align); attr.value left; element.setAttributeNode(attr); alert(element.attributes[align].value); // left alert(element.getAttributeNode(align).value); // left alert(element.getAttribute(align)); // left在这个例子中首先创建了一个新属性。调用 createAttribute()并传入align为新属性设置了 name 属性因此就不用再设置了。随后value 属性被赋值为left。为把这个新属性添加到元素上可以使用元素的 setAttributeNode()方法。添加这个属性后可以通过不同方式访问它包括 attributes 属性、getAttributeNode()和 getAttribute()方法。其中attributes 属性和getAttributeNode()方法都返回属性对应的 Attr 节点而 getAttribute()方法只返回属性的值。 注意将属性作为节点来访问多数情况下并无必要。推荐使用 getAttribute()、removeAttribute()和 setAttribute()方法操作属性而不是直接操作属性节点。 DOM 编程。很多时候操作 DOM 是很直观的。通过 HTML 代码能实现的也一样能通过 JavaScript 实现。但有时候DOM 也没有看起来那么简单。浏览器能力的参差不齐和各种问题也会导致 DOM 的某些方面会复杂一些。 动态脚本 script元素用于向网页中插入 JavaScript 代码可以是 src 属性包含的外部文件也可以是作为该元素内容的源代码。动态脚本就是在页面初始加载时不存在之后又通过 DOM 包含的脚本。与对应的HTML 元素一样有两种方式通过script动态为网页添加脚本引入外部文件和直接插入源代码。动态加载外部文件很容易实现比如下面的script元素script srcfoo.js/script可以像这样通过 DOM 编程创建这个节点let script document.createElement(script); script.src foo.js; document.body.appendChild(script);这里的 DOM 代码实际上完全照搬了它要表示的 HTML 代码。注意在上面最后一行把script元素添加到页面之前是不会开始下载外部文件的。当然也可以把它添加到head元素同样可以实现动态脚本加载。这个过程可以抽象为一个函数比如function loadScript(url) { let script document.createElement(script); script.src url; document.body.appendChild(script); }然后就可以像下面这样加载外部 JavaScript 文件了loadScript(client.js);加载之后这个脚本就可以对页面执行操作了。这里有个问题怎么能知道脚本什么时候加载完这个问题并没有标准答案。第 17 章会讨论一些与加载相关的事件具体情况取决于使用的浏览器。 另一个动态插入 JavaScript 的方式是嵌入源代码如下面的例子所示script function sayHi() { alert(hi); } /script使用 DOM可以实现以下逻辑let script document.createElement(script); script.appendChild(document.createTextNode(function sayHi(){alert(hi);})); document.body.appendChild(script);以上代码可以在 Firefox、Safari、Chrome 和 Opera 中运行。不过在旧版本的 IE 中可能会导致问题。这是因为 IE 对script元素做了特殊处理不允许常规 DOM 访问其子节点。但script元素上有一个 text 属性可以用来添加 JavaScript 代码如下所示var script document.createElement(script); script.text function sayHi(){alert(hi);}; document.body.appendChild(script);这样修改后上面的代码可以在 IE、Firefox、Opera 和 Safari 3 及更高版本中运行。Safari 3 之前的版本不能正确支持这个 text 属性但这些版本却支持文本节点赋值。对于早期的 Safari 版本需要使用以下代码var script document.createElement(script); var code function sayHi(){alert(hi);}; try { script.appendChild(document.createTextNode(code)); } catch (ex){ script.text code; } document.body.appendChild(script);这里先尝试使用标准的 DOM 文本节点插入方式因为除 IE 之外的浏览器都支持这种方式。IE 此时会抛出错误那么可以在捕获错误之后再使用 text 属性来插入 JavaScript 代码。于是我们就可以抽象出一个跨浏览器的函数function loadScriptString(code){ var script document.createElement(script); script.type text/javascript; try { script.appendChild(document.createTextNode(code)); } catch (ex){ script.text code; } document.body.appendChild(script); }这个函数可以这样调用loadScriptString(function sayHi(){alert(hi);});以这种方式加载的代码会在全局作用域中执行并在调用返回后立即生效。基本上这就相当于在全局作用域中把源代码传给 eval()方法。 注意通过 innerHTML 属性创建的script元素永远不会执行。浏览器会尽责地创建script元素以及其中的脚本文本但解析器会给这个script元素打上永不执行的标签。只要是使用innerHTML 创建的script元素以后也没有办法强制其执行。 动态样式 CSS 样式在 HTML 页面中可以通过两个元素加载。link元素用于包含 CSS 外部文件而style元素用于添加嵌入样式。与动态脚本类似动态样式也是页面初始加载时并不存在而是在之后才添加到页面中的。来看下面这个典型的link元素link relstylesheet typetext/css hrefstyles.css这个元素很容易使用 DOM 编程创建出来let link document.createElement(link); link.rel stylesheet; link.type text/css; link.href styles.css; let head document.getElementsByTagName(head)[0]; head.appendChild(link);以上代码在所有主流浏览器中都能正常运行。注意应该把link元素添加到head元素而不是body元素这样才能保证所有浏览器都能正常运行。这个过程可以抽象为以下通用函数function loadStyles(url){ let link document.createElement(link); link.rel stylesheet; link.type text/css; link.href url; let head document.getElementsByTagName(head)[0]; head.appendChild(link); }然后就可以这样调用这个 loadStyles()函数了loadStyles(styles.css);通过外部文件加载样式是一个异步过程。因此样式的加载和正执行的 JavaScript 代码并没有先后顺序。一般来说也没有必要知道样式什么时候加载完成。另一种定义样式的方式是使用script元素包含嵌入的 CSS 规则例如style typetext/css body { background-color: red; } /style逻辑上下列 DOM 代码会有同样的效果let style document.createElement(style); style.type text/css; style.appendChild(document.createTextNode(body{background-color:red})); let head document.getElementsByTagName(head)[0]; head.appendChild(style);以上代码在 Firefox、Safari、Chrome 和 Opera 中都可以运行但 IE 除外。IE 对style节点会施加限制不允许访问其子节点这一点与它对script元素施加的限制一样。事实上IE 在执行到给style添加子节点的代码时会抛出与给script添加子节点时同样的错误。对于 IE解决方案是访问元素的 styleSheet 属性这个属性又有一个 cssText 属性然后给这个属性添加 CSS 代码let style document.createElement(style); style.type text/css; try{ style.appendChild(document.createTextNode(body{background-color:red})); } catch (ex){ style.styleSheet.cssText body{background-color:red}; } let head document.getElementsByTagName(head)[0]; head.appendChild(style);与动态添加脚本源代码类似这里也使用了 try…catch 语句捕获 IE 抛出的错误然后再以 IE 特有的方式来设置样式。这是最终的通用函数function loadStyleString(css){ let style document.createElement(style); style.type text/css; try{ style.appendChild(document.createTextNode(css)); } catch (ex){ style.styleSheet.cssText css; } let head document.getElementsByTagName(head)[0]; head.appendChild(style); }可以这样调用这个函数loadStyleString(body{background-color:red});这样添加的样式会立即生效因此所有变化会立即反映出来。 注意对于 IE要小心使用 styleSheet.cssText。如果重用同一个style元素并设置该属性超过一次则可能导致浏览器崩溃。同样将 cssText 设置为空字符串也可能导致浏览器崩溃。 操作表格 表格是 HTML 中最复杂的结构之一。通过 DOM 编程创建table元素通常要涉及大量标签包括表行、表元、表题等等。因此通过 DOM 编程创建和修改表格时可能要写很多代码。假设要通过DOM 来创建以下 HTML 表格table border1 width100% tbody tr tdCell 1,1/td tdCell 2,1/td /tr tr tdCell 1,2/td tdCell 2,2/td /tr /tbody /table下面就是以 DOM 编程方式重建这个表格的代码// 创建表格 let table document.createElement(table); table.border 1; table.width 100%; // 创建表体 let tbody document.createElement(tbody); table.appendChild(tbody); // 创建第一行 let row1 document.createElement(tr); tbody.appendChild(row1); let cell1_1 document.createElement(td); cell1_1.appendChild(document.createTextNode(Cell 1,1)); row1.appendChild(cell1_1); let cell2_1 document.createElement(td); cell2_1.appendChild(document.createTextNode(Cell 2,1)); row1.appendChild(cell2_1); // 创建第二行 let row2 document.createElement(tr); tbody.appendChild(row2); let cell1_2 document.createElement(td); cell1_2.appendChild(document.createTextNode(Cell 1,2)); row2.appendChild(cell1_2); let cell2_2 document.createElement(td); cell2_2.appendChild(document.createTextNode(Cell 2,2)); row2.appendChild(cell2_2); // 把表格添加到文档主体 document.body.appendChild(table);以上代码相当烦琐也不好理解。为了方便创建表格HTML DOM 给table、tbody和tr元素添加了一些属性和方法。 table元素添加了以下属性和方法 caption指向caption元素的指针如果存在tBodies包含tbody元素的 HTMLCollectiontFoot指向tfoot元素如果存在tHead指向thead元素如果存在rows包含表示所有行的 HTMLCollectioncreateTHead()创建thead元素放到表格中返回引用createTFoot()创建tfoot元素放到表格中返回引用createCaption()创建caption元素放到表格中返回引用deleteTHead()删除thead元素deleteTFoot()删除tfoot元素deleteCaption()删除caption元素deleteRow(pos)删除给定位置的行insertRow(pos)在行集合中给定位置插入一行。 tbody元素添加了以下属性和方法 rows包含tbody元素中所有行的 HTMLCollectiondeleteRow(pos)删除给定位置的行insertRow(pos)在行集合中给定位置插入一行返回该行的引用。 tr元素添加了以下属性和方法 cells包含tr元素所有表元的 HTMLCollectiondeleteCell(pos)删除给定位置的表元insertCell(pos)在表元集合给定位置插入一个表元返回该表元的引用。 这些属性和方法极大地减少了创建表格所需的代码量。例如使用这些方法重写前面的代码之后是这样的// 创建表格 let table document.createElement(table); table.border 1; table.width 100%; // 创建表体 let tbody document.createElement(tbody); table.appendChild(tbody); // 创建第一行 tbody.insertRow(0); tbody.rows[0].insertCell(0); tbody.rows[0].cells[0].appendChild(document.createTextNode(Cell 1,1)); tbody.rows[0].insertCell(1); tbody.rows[0].cells[1].appendChild(document.createTextNode(Cell 2,1)); // 创建第二行 tbody.insertRow(1); tbody.rows[1].insertCell(0); tbody.rows[1].cells[0].appendChild(document.createTextNode(Cell 1,2)); tbody.rows[1].insertCell(1); tbody.rows[1].cells[1].appendChild(document.createTextNode(Cell 2,2)); // 把表格添加到文档主体 document.body.appendChild(table);这里创建table和tbody元素的代码没有变。变化的是创建两行的部分这次使用了 HTML DOM 表格的属性和方法。创建第一行时在tbody元素上调用了 insertRow()方法。传入参数 0表示把这一行放在什么位置。然后使用 tbody.rows[0]来引用这一行因为这一行刚刚创建并被添加到了tbody的位置 0。 创建表元的方式也与之类似。在tr元素上调用 insertCell()方法传入参数 0表示把这个表元放在什么位置上。然后使用 tbody.rows[0].cells[0]来引用这个表元因为这个表元刚刚创建并被添加到了tr的位置 0。虽然以上两种代码在技术上都是正确的但使用这些属性和方法创建表格让代码变得更有逻辑性也更容易理解。 使用 NodeList 理解 NodeList 对象和相关的 NamedNodeMap、HTMLCollection是理解 DOM 编程的关键。这3 个集合类型都是“实时的”意味着文档结构的变化会实时地在它们身上反映出来因此它们的值始终代表最新的状态。实际上NodeList 就是基于 DOM 文档的实时查询。例如下面的代码会导致无穷循环let divs document.getElementsByTagName(div); for (let i 0; i divs.length; i){ let div document.createElement(div); document.body.appendChild(div); }第一行取得了包含文档中所有div元素的 HTMLCollection。因为这个集合是“实时的”所以任何时候只要向页面中添加一个新div元素再查询这个集合就会多一项。因为浏览器不希望保存每次创建的集合所以就会在每次访问时更新集合。这样就会出现前面使用循环的例子中所演示的问题。每次循环开始都会求值 i divs.length。这意味着要执行获取所有div元素的查询。因为循环体中会创建并向文档添加一个新div元素所以每次循环 divs.length 的值也会递增。因为两个值都会递增所以 i 将永远不会等于 divs.length。 使用 ES6 迭代器并不会解决这个问题因为迭代的是一个永远增长的实时集合。以下代码仍然会导致无穷循环for (let div of document.getElementsByTagName(div)){ let newDiv document.createElement(div); document.body.appendChild(newDiv); }任何时候要迭代 NodeList最好再初始化一个变量保存当时查询时的长度然后用循环变量与这个变量进行比较如下所示let divs document.getElementsByTagName(div); for (let i 0, len divs.length; i len; i) { let div document.createElement(div); document.body.appendChild(div); }在这个例子中又初始化了一个保存集合长度的变量 len。因为 len 保存着循环开始时集合的长度而这个值不会随集合增大动态增长所以就可以避免前面例子中出现的无穷循环。本章还会使用这种技术来演示迭代 NodeList 对象的首选方式。 另外如果不想再初始化一个变量也可以像下面这样反向迭代集合let divs document.getElementsByTagName(div); for (let i divs.length - 1; i 0; --i) { let div document.createElement(div); document.body.appendChild(div); }一般来说最好限制操作 NodeList 的次数。因为每次查询都会搜索整个文档所以最好把查询到的 NodeList 缓存起来。 MutationObserver 接口 不久前添加到 DOM 规范中的 MutationObserver 接口可以在 DOM 被修改时异步执行回调。使用 MutationObserver 可以观察整个文档、DOM 树的一部分或某个元素。此外还可以观察元素属性、子节点、文本或者前三者任意组合的变化。注意新引进 MutationObserver 接口是为了取代废弃的 MutationEvent。 基本用法。MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建 let observer new MutationObserver(() console.log(DOM was mutated!));observe()方法 新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observer 与 DOM 关联起来需要使用 observe()方法。这个方法接收两个必需的参数要观察其变化的 DOM 节点以及一个 MutationObserverInit 对象。MutationObserverInit 对象用于控制观察哪些方面的变化是一个键/值对形式配置选项的字典。例如下面的代码会创建一个观察者observer并配置它观察body元素上的属性变化let observer new MutationObserver(() console.log(body attributes changed)); observer.observe(document.body, { attributes: true });执行以上代码后body元素上任何属性发生变化都会被这个 MutationObserver 实例发现然后就会异步执行注册的回调函数。body元素后代的修改或其他非属性修改都不会触发回调进入任务队列。可以通过以下代码来验证let observer new MutationObserver(() console.log(body attributes changed)); observer.observe(document.body, { attributes: true }); document.body.className foo; console.log(Changed body class); // Changed body class // body attributes changed注意回调中的 console.log()是后执行的。这表明回调并非与实际的 DOM 变化同步执行。 回调与 MutationRecord 每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息包括发生了什么变化以及 DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足观察条件的事件所以每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组。 下面展示了反映一个属性变化的 MutationRecord 实例的数组 let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); document.body.setAttribute(foo, bar); // [ // { // addedNodes: NodeList [], // attributeName: foo, // attributeNamespace: null, // nextSibling: null, // oldValue: null, // previousSibling: null // removedNodes: NodeList [], // target: body // type: attributes // } // ]下面是一次涉及命名空间的类似变化 let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); document.body.setAttributeNS(baz, foo, bar); // [ // { // addedNodes: NodeList [], // attributeName: foo, // attributeNamespace: baz, // nextSibling: null, // oldValue: null, // previousSibling: null // removedNodes: NodeList [], // target: body // type: attributes // } // ] 连续修改会生成多个 MutationRecord 实例下次回调执行时就会收到包含所有这些实例的数组顺序为变化事件发生的顺序 let observer new MutationObserver((mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); document.body.className foo; document.body.className bar; document.body.className baz; // [MutationRecord, MutationRecord, MutationRecord]下表列出了 MutationRecord 实例的属性。 属性说明target被修改影响的目标节点type字符串表示变化的类型“attributes”、“characterData或childList”oldValue如果在 MutationObserverInit 对象中启用attributeOldValue 或 characterData OldValue为 trueattributes或characterData的变化事件会设置这个属性为被替代的值childList类型的变化始终将这个属性设置为 nullattributeName对于attributes类型的变化这里保存被修改属性的名字。其他变化事件会将这个属性设置为 nullattributeNamespace对于使用了命名空间的attributes类型的变化这里保存被修改属性的名字其他变化事件会将这个属性设置为 nulladdedNodes对于childList类型的变化返回包含变化中添加节点的 NodeList默认为空 NodeListremovedNodes对于childList类型的变化返回包含变化中删除节点的 NodeList默认为空 NodeListpreviousSibling对于childList类型的变化返回变化节点的前一个同胞 Node默认为 nullnextSibling对于childList类型的变化返回变化节点的后一个同胞 Node默认为 null 传给回调函数的第二个参数是观察变化的 MutationObserver 的实例演示如下 let observer new MutationObserver( (mutationRecords, mutationObserver) console.log(mutationRecords, mutationObserver)); observer.observe(document.body, { attributes: true }); document.body.className foo; // [MutationRecord], MutationObserverdisconnect()方法 默认情况下只要被观察的元素不被垃圾回收MutationObserver 的回调就会响应 DOM 变化事件从而被执行。要提前终止执行回调可以调用 disconnect()方法。下面的例子演示了同步调用disconnect()之后不仅会停止此后变化事件的回调也会抛弃已经加入任务队列要异步执行的回调let observer new MutationObserver(() console.log(body attributes changed)); observer.observe(document.body, { attributes: true }); document.body.className foo; observer.disconnect(); document.body.className bar; //没有日志输出要想让已经加入任务队列的回调执行可以使用 setTimeout()让已经入列的回调执行完毕再调用disconnect()let observer new MutationObserver(() console.log(body attributes changed)); observer.observe(document.body, { attributes: true });document.body.className foo; setTimeout(() { observer.disconnect(); document.body.className bar; }, 0); // body attributes changed复用 MutationObserver 多次调用 observe()方法可以复用一个 MutationObserver 对象观察多个不同的目标节点。此时MutationRecord 的 target 属性可以标识发生变化事件的目标节点。下面的示例演示了这个过程let observer new MutationObserver( (mutationRecords) console.log(mutationRecords.map((x) x.target))); // 向页面主体添加两个子节点 let childA document.createElement(div), childB document.createElement(span); document.body.appendChild(childA); document.body.appendChild(childB); // 观察两个子节点 observer.observe(childA, { attributes: true }); observer.observe(childB, { attributes: true }); // 修改两个子节点的属性 childA.setAttribute(foo, bar); childB.setAttribute(foo, bar); // [div, span] disconnect()方法是一个“一刀切”的方案调用它会停止观察所有目标let observer new MutationObserver( (mutationRecords) console.log(mutationRecords.map((x) x.target))); // 向页面主体添加两个子节点 let childA document.createElement(div), childB document.createElement(span); document.body.appendChild(childA); document.body.appendChild(childB); // 观察两个子节点 observer.observe(childA, { attributes: true }); observer.observe(childB, { attributes: true }); observer.disconnect(); // 修改两个子节点的属性 childA.setAttribute(foo, bar); childB.setAttribute(foo, bar); // 没有日志输出重用 MutationObserver。调用 disconnect()并不会结束 MutationObserver 的生命。还可以重新使用这个观察者再将它关联到新的目标节点。下面的示例在两个连续的异步块中先断开然后又恢复了观察者与body元素的关联 let observer new MutationObserver(() console.log(body attributes changed)); observer.observe(document.body, { attributes: true }); // 这行代码会触发变化事件 document.body.setAttribute(foo, bar); setTimeout(() { observer.disconnect(); // 这行代码不会触发变化事件document.body.setAttribute(bar, baz); }, 0); setTimeout(() { // Reattach observer.observe(document.body, { attributes: true }); // 这行代码会触发变化事件document.body.setAttribute(baz, qux); }, 0); // body attributes changed // body attributes changedMutationObserverInit 与观察范围 MutationObserverInit 对象用于控制对目标节点的观察范围。粗略地讲观察者可以观察的事件包括属性变化、文本变化和子节点变化。 下表列出了 MutationObserverInit 对象的属性。 属性说明subtree布尔值表示除了目标节点是否观察目标节点的子树后代。如果是 false则只观察目标节点的变化如果是 true则观察目标节点及其整个子树。默认为 falseattributes布尔值表示是否观察目标节点的属性变化。默认为 falseattributeFilter字符串数组表示要观察哪些属性的变化。把这个值设置为 true 也会将 attributes 的值转换为 true 。默认为观察所有属性attributeOldValue布尔值表示 MutationRecord 是否记录变化之前的属性值。把这个值设置为 true 也会将 attributes 的值转换为 true。默认为 falsecharacterData布尔值表示修改字符数据是否触发变化事件。默认为 falsecharacterDataOldValue布尔值表示 MutationRecord 是否记录变化之前的字符数据。把这个值设置为 true 也会将 characterData 的值转换为 true。默认为 falsechildList布尔值表示修改目标节点的子节点是否触发变化事件。默认为 false 注意在调用 observe()时MutationObserverInit 对象中的 attribute、characterData和 childList 属性必须至少有一项为 true无论是直接设置这几个属性还是通过设置attributeOldValue 等属性间接导致它们的值转换为 true。否则会抛出错误因为没有任何变化事件可能触发回调。 观察属性 MutationObserver 可以观察节点属性的添加、移除和修改。要为属性变化注册回调需要在MutationObserverInit 对象中将 attributes 属性设置为 true如下所示let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); // 添加属性 document.body.setAttribute(foo, bar); // 修改属性 document.body.setAttribute(foo, baz); // 移除属性 document.body.removeAttribute(foo); // 以上变化都被记录下来了 // [MutationRecord, MutationRecord, MutationRecord]把 attributes 设置为 true 的默认行为是观察所有属性但不会在 MutationRecord 对象中记录原来的属性值。如果想观察某个或某几个属性可以使用 attributeFilter 属性来设置白名单即一个属性名字符串数组let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributeFilter: [foo] }); // 添加白名单属性 document.body.setAttribute(foo, bar); // 添加被排除的属性 document.body.setAttribute(baz, qux);// 只有 foo 属性的变化被记录了 // [MutationRecord] 如果想在变化记录中保存属性原来的值可以将 attributeOldValue 属性设置为 truelet observer new MutationObserver( (mutationRecords) console.log(mutationRecords.map((x) x.oldValue))); observer.observe(document.body, { attributeOldValue: true }); document.body.setAttribute(foo, bar); document.body.setAttribute(foo, baz); document.body.setAttribute(foo, qux); // 每次变化都保留了上一次的值 // [null, bar, baz]观察字符数据 MutationObserver 可以观察文本节点如 Text、Comment 或 ProcessingInstruction 节点中字符的添加、删除和修改。要为字符数据注册回调需要在 MutationObserverInit 对象中将characterData 属性设置为 true如下所示let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); // 创建要观察的文本节点 document.body.firstChild.textContent foo; observer.observe(document.body.firstChild, { characterData: true }); // 赋值为相同的字符串 document.body.firstChild.textContent foo; // 赋值为新字符串 document.body.firstChild.textContent bar; // 通过节点设置函数赋值 document.body.firstChild.textContent baz; // 以上变化都被记录下来了 // [MutationRecord, MutationRecord, MutationRecord]设置元素文本内容的标准方式是 textContent 属性。Element 类也定义了 innerText 属性与 textContent 类似。但 innerText 的定义不严谨浏览器间的实现也存在兼容性问题因此不建议再使用了。将 characterData 属性设置为 true 的默认行为不会在 MutationRecord 对象中记录原来的字符数据。如果想在变化记录中保存原来的字符数据可以将 characterDataOldValue 属性设置为 truelet observer new MutationObserver( (mutationRecords) console.log(mutationRecords.map((x) x.oldValue))); document.body.innerText foo; observer.observe(document.body.firstChild, { characterDataOldValue: true }); document.body.innerText foo; document.body.innerText bar; document.body.firstChild.textContent baz; // 每次变化都保留了上一次的值 // [foo, foo, bar]观察子节点 MutationObserver 可以观察目标节点子节点的添加和移除。要观察子节点需要在 MutationObserverInit 对象中将 childList 属性设置为 true。下面的例子演示了添加子节点// 清空主体 document.body.innerHTML ; let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { childList: true }); document.body.appendChild(document.createElement(div)); // [ // { // addedNodes: NodeList[div], // attributeName: null, // attributeNamespace: null, // oldValue: null, // nextSibling: null, // previousSibling: null, // removedNodes: NodeList[], // target: body, // type: childList, // } // ]对子节点重新排序尽管调用一个方法即可实现会报告两次变化事件因为从技术上会涉及先移除和再添加// 清空主体 document.body.innerHTML ; let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); // 创建两个初始子节点 document.body.appendChild(document.createElement(div)); document.body.appendChild(document.createElement(span)); observer.observe(document.body, { childList: true }); // 交换子节点顺序 document.body.insertBefore(document.body.lastChild, document.body.firstChild); // 发生了两次变化第一次是节点被移除第二次是节点被添加 // [ // { // addedNodes: NodeList[], // attributeName: null, // attributeNamespace: null, // oldValue: null, // nextSibling: null, // previousSibling: div, // removedNodes: NodeList[span], // target: body, // type: childList, // }, // { // addedNodes: NodeList[span], // attributeName: null, // attributeNamespace: null, // oldValue: null, // nextSibling: div, // previousSibling: null, // removedNodes: NodeList[], // target: body, // type: childList, // } // ]观察子树 默认情况下MutationObserver 将观察的范围限定为一个元素及其子节点的变化。可以把观察的范围扩展到这个元素的子树所有后代节点这需要在 MutationObserverInit 对象中将 subtree属性设置为 true。下面的代码展示了观察元素及其后代节点属性的变化// 清空主体 document.body.innerHTML ; let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); // 创建一个后代 document.body.appendChild(document.createElement(div));// 观察body元素及其子树 observer.observe(document.body, { attributes: true, subtree: true }); // 修改body元素的子树 document.body.firstChild.setAttribute(foo, bar); // 记录了子树变化的事件 // [ // { // addedNodes: NodeList[], // attributeName: foo, // attributeNamespace: null, // oldValue: null, // nextSibling: null, // previousSibling: null, // removedNodes: NodeList[], // target: div, // type: attributes, // } // ]有意思的是被观察子树中的节点被移出子树之后仍然能够触发变化事件。这意味着在子树中的节点离开该子树后即使严格来讲该节点已经脱离了原来的子树但它仍然会触发变化事件。下面的代码演示了这种情况// 清空主体 document.body.innerHTML ; let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); let subtreeRoot document.createElement(div), subtreeLeaf document.createElement(span); // 创建包含两层的子树 document.body.appendChild(subtreeRoot); subtreeRoot.appendChild(subtreeLeaf); // 观察子树 observer.observe(subtreeRoot, { attributes: true, subtree: true }); // 把节点转移到其他子树 document.body.insertBefore(subtreeLeaf, subtreeRoot); subtreeLeaf.setAttribute(foo, bar); // 移出的节点仍然触发变化事件 // [MutationRecord]异步回调与记录队列。MutationObserver 接口是出于性能考虑而设计的其核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能每次变化的信息由观察者实例决定会保存在 MutationRecord实例中然后添加到记录队列。这个队列对每个 MutationObserver 实例都是唯一的是所有 DOM变化事件的有序列表。 记录队列 每次 MutationRecord 被添加到 MutationObserver 的记录队列时仅当之前没有已排期的微任务回调时队列中微任务长度为 0才会将观察者注册的回调在初始化 MutationObserver 时传入作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。不过在回调的微任务异步执行期间有可能又会发生更多变化事件。因此被调用的回调会接收到一个 MutationRecord 实例的数组顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例因为函数退出之后这些实现就不存在了。回调执行后这些 MutationRecord 就用不着了因此记录队列会被清空其内容会被丢弃。 takeRecords()方法 调用 MutationObserver 实例的 takeRecords()方法可以清空记录队列取出并返回其中的所有 MutationRecord 实例。看这个例子let observer new MutationObserver( (mutationRecords) console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); document.body.className foo; document.body.className bar; document.body.className baz; console.log(observer.takeRecords()); console.log(observer.takeRecords()); // [MutationRecord, MutationRecord, MutationRecord] // []这在希望断开与观察目标的联系但又希望处理由于调用 disconnect()而被抛弃的记录队列中的MutationRecord 实例时比较有用。 性能、内存与垃圾回收 DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于浏览器事件的实现机制这个接口出现了严重的性能问题。因此DOM Level 3 规定废弃了这些事件。MutationObserver 接口就是为替代这些事件而设计的更实用、性能更好的方案。将变化回调委托给微任务来执行可以保证事件同步触发同时避免随之而来的混乱。为 MutationObserver 而实现的记录队列可以保证即使变化事件被爆发式地触发也不会显著地拖慢浏览器。无论如何使用 MutationObserver 仍然不是没有代价的。因此理解什么时候避免出现这种情况就很重要了。 MutationObserver 的引用 MutationObserver 实例与目标节点之间的引用关系是非对称的。MutationObserver 拥有对要观察的目标节点的弱引用。因为是弱引用所以不会妨碍垃圾回收程序回收目标节点。然而目标节点却拥有对 MutationObserver 的强引用。如果目标节点从 DOM 中被移除随后被垃圾回收则关联的 MutationObserver 也会被垃圾回收。 MutationRecord 的引用 记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用。如果变化是childList 类型则会包含多个节点的引用。记录队列和回调处理的默认行为是耗尽这个队列处理每个 MutationRecord然后让它们超出作用域并被垃圾回收。有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord 实例也就会保存它们引用的节点因而会妨碍这些节点被回收。如果需要尽快地释放内存建议从每个 MutationRecord中抽取出最有用的信息然后保存到一个新对象中最后抛弃 MutationRecord。 小结 文档对象模型DOMDocument Object Model是语言中立的 HTML 和 XML 文档的 API。DOM Level 1 将 HTML 和 XML 文档定义为一个节点的多层级结构并暴露出 JavaScript 接口以操作文档的底层结构和外观。DOM 由一系列节点类型构成主要包括以下几种。 Node 是基准节点类型是文档一个部分的抽象表示所有其他类型都继承 Node。Document 类型表示整个文档对应树形结构的根节点。在 JavaScript 中document 对象是 Document 的实例拥有查询和获取节点的很多方法。Element 节点表示文档中所有 HTML 或 XML 元素可以用来操作它们的内容和属性。其他节点类型分别表示文本内容、注释、文档类型、CDATA 区块和文档片段。 DOM 编程在多数情况下没什么问题在涉及script和style元素时会有一点兼容性问题。因为这些元素分别包含脚本和样式信息所以浏览器会将它们与其他元素区别对待。要理解 DOM最关键的一点是知道影响其性能的问题所在。DOM 操作在 JavaScript 代码中是代价比较高的NodeList 对象尤其需要注意。NodeList 对象是“实时更新”的这意味着每次访问它都会执行一次新的查询。考虑到这些问题实践中要尽量减少 DOM 操作的数量。MutationObserver 是为代替性能不好的 MutationEvent 而问世的。使用它可以有效精准地监控DOM 变化而且 API 也相对简单。
http://www.hkea.cn/news/14479957/

相关文章:

  • 房产中介网站开发模板经典页游排行榜前十名
  • django 做网站的代码重庆御临建筑公司官网
  • 一个空间做2个网站吗seo软件哪个好
  • 网站开发软件手机版关于seo的行业岗位有哪些
  • 怎样做网站的反链创建网站为啥要钱
  • 玛丁图商城网站开发用php做购物网站视频
  • 有做网站设计吗百度网站推广价格
  • 济南建站公司注意事项网站建站网站域名申请
  • 怎么做网站差不多站长申请网站需要什么资料
  • 旅游网站设计总结关键词搜索网站
  • dw网站开发与设计实训总结平面logo设计公司
  • 做网站用什么软件定制合同模板
  • 企业网站页脚信息wordpress集成后台无法登录
  • 个人博客网站设计模板北京定制网站
  • 响应式网站设计案例软件设计工具有哪些
  • 千博企业网站管理系统2013安卓小程序开发入门
  • 重庆seo整站优化设置简洁ppt模板下载免费
  • 崂山区城市规划建设局网站wordpress作者信息
  • 海外站推广宝应百度贴吧
  • 花生壳网站无法登陆建行网站
  • 网站开发 适应 手机 电脑上传文章网站
  • 微信清粉网站开发中国最大跨境电商平台
  • 网站数据库怎么建立有没有给人做简历的网站
  • 会员网站建设网站推广策划思路与执行
  • 做体育最好的网站广告设计与制作工资
  • 网站那个做的比较好网站建设优化是干嘛
  • 建站论坛网络营销公司排名
  • 网站建设员岗位职责做游戏模型挣钱的网站
  • 网站建设 知识库有做义工的相亲网站吗
  • 栾川网站建设手机优化游戏性能的软件