基本概念
DOM、DOM节点、DOM树
DOM(Document Object Model)是文档内容(HTML或XML)在编程语言上的抽象模型,它建模了文档的内容和结构,并提供给编程语言一套完整的操纵文档的API
DOM 节点是 DOM 模型的组成单元。HTML 的基本单元是标签,节点常常与标签对应,但连续的文本内容也是一个文本标签
DOM 树是 DOM 结构的表示形式,DOM 把文档的每个节点根据父子关系连接,形成 DOM 树
为什么需要DOM
在 JS 还未诞生时,不需要对文档内容进行修改,浏览器直接对文档内容单次解析并呈现,就不再关心文档了,那个时候没有 DOM
JS 出现后,JS 可能会修改文档,而且浏览器需要实时反映这种修改。如果没有 DOM,JS 直接修改文档的字符串内容,这将会变得很灾难:
1)在 JS 中处理庞大的 HTML 文本,必然繁琐且错漏百出
2)即使 JS 可以胜任这个工作,但当 JS 修改页面后,浏览器无法知道修改了哪里,只能重新解析整个页面,对性能有极大的消耗
3)即使前面都安好,但在用户看来,每次修改文档浏览器重新解析,页面必然会出现一段时间白屏,解析时间快还可能闪屏,体验极差
因此,需要一种便捷、高效、可跟踪的文档修改方式,这就是 DOM 做的事
浏览器首次解析文档时,把文档中的标签(文本等)解析成一个个节点,父元素与子元素用线连接,整个文档最终形成一个称为 DOM 树的树形结构。浏览器再根据 DOM 树去呈现页面,它会跟踪 DOM 树的变化,一旦 DOM 树变化,页面也会做出相应变化DOM 树在 JS 环境中是存在的(JS 执行引擎是浏览器的一部分),所以在 JS 中,原本对文档的修改现在只需要修改 DOM 树和其中的节点。DOM 节点对象就是普通的 JS 对象,有易于操作的方法和属性,这样 JS 操纵文档就像操作对象一样方便快捷,而且浏览器会知道修改了哪个节点,只需要重新渲染被修改影响的局部页面(即发生回流与重绘)
后来,DOM 组件发展成为一套不依托于浏览器和 JS 的独立规范,提供了一套完整的访问和修改文档的接口,DOM 标准由 W3C 维护
所以 DOM 这个名词应该有两层含义:
1)从浏览器来看:根据文档建模出来的一个树形模型,即 DOM 树
2)从编程语言来看:它提供了一套操纵文档的 API
节点、节点类型和节点类
节点:是 DOM 树的组成单元,在 JS 看来,一个节点就是 JS 对象,下面用 node 表示任意的节点
节点类型:有 12 种节点类型,分别用常量 1~12 表示,可以通过 node.nodeType 属性获取节点的类型常量。现在有些类型的节点已经弃用了,常见的有以下几种类型的节点:
元素节点:类型常量为 Node.ELEMENT_NODE 或 1,对应文档中的元素,大部分 DOM 操作都是在元素节点层次的文本节点:类型常量为 Node.TEXT_NODE 或 2,对应文档中的文本,任何文档内容都有对应的文本节点,即使空格和换行符(空格和换行符不会对页面内容产生影响,但确实以文本节点的形式存在于 DOM 树中)Document节点:类型常量为 Node.DOCUMENT_NODE 或 9,它不对应文档的内容,而是作为文档的入口节点,每个文档都有且仅有一个入口注释节点:类型常量为 Node.COMMENT_NODE 或 8,对应文档中的注释标签,文档的注释内容也是可读取和修改的
节点类:DOM内置许多节点类,类之间存在继承关系,形成一套节点类框架。每个节点对象都属于节点类,拥有该类和其父类的方法与属性,这使得操作节点十分简单,节点类框架的一部分大概如下图,这些节点有丰富的属性和方法,是继承的结果,可以看到一个 HTML 标签元素至少有四层的继承关系
以 标签为例,它属于 HTMLAnchorElement 类,获得了 a.target、a.download 等属性,接着继承了 HTMLElement 类上的 title、hidden 等属性和 click() 等方法,又从 Element 类继承了 tagName、className 等属性和 getAttribute()、setAttrribute() 等方法,又从 Node 类继承了 nodeType、appenChild()、removeChild() 等方法,最后从 EventTarget 类中继承了事件相关的属性和方法
PS:不要混淆节点类型和节点类两个概念,前者是一个生活中的类别,后者是编程意义上的类。 节点对象的 nodeType 属性表示了它的类型,而节点类是该节点的从属的类。因为 Node 是一个抽象类,所以如果知道了某个节点从属的类,就知道了它的节点类型
探索DOM结构
Live DOM Viewer 是一个可以根据 HTML 文档实时查看 DOM 树的网站,通过以下的例子来看看文档、DOM树与节点的关系
A simple text.
- czpcalm
它对应的 DOM 图(颜色区分了节点类型):
在这张图中,总共有 4 种类型的节点,分别是标签节点(红色)、文本节点(灰色)、注释节点(黄色)和 DOCTYPE节点(紫色)
文档没有 标签却出现了 HEAD 节点,这是因为 HTML 必然存在 、、 标签,不存在时会自动补上。当出现
标签时,也一定会有 标签
文档中的文本都会形成文本节点的内容,包括空格␣和换行↵
单独的空格和换行都会形成对应的文本节点有内容的文本节点的值包含前导和后继的空白 理解 DOM 树中的父子关系对应文档中的包含(嵌套)关系,一个很好的类比是文件树,把元素看作文件夹,文本看作文件,文件夹中可以存放文件和新的文件夹,然后一层层深入下去,DOM 树也是如此
以下几个原则能帮助快速理解这个 DOM 树的构建:
自动补全:上面提到的自动添加必要元素,也会自动补齐缺少的关闭标签文档有的 DOM 树都有:这个原则要求空白也会有,不过作为补充的, 前面的空白会被忽略(历史原因) 后如果有内容,会被移到 里面
导航与搜索
操作节点前,需要先找到节点。导航是从一个节点到另一个节点,搜索是从一个范围中选出满足条件的节点
节点导航
Node 类规定了节点具有的许多属性,方便我们从某个节点中找到与它相关的另外节点
顶级节点一般直接获取:
document:入口节点document.documentElement:HTML节点document.head:head节点document.body:body节点
对 node 节点,有以下属性:
node.parentNode:获取节点的父节点node.previousSibling:获取节点的上一个兄弟节点node.nextSibling:获取节点的下一个兄弟节点node.childNodes:获取节点的子节点列表,没有子节点返回空列表node.firstChild 和 node.lastChild:获取第一个和最后一个子节点
这些是属性,不是方法,不要错误使用 node.childNodes() 之类的
以上导航是基于节点的,包括元素节点、文本节点、注释等。因为我们经常只关心元素节点,DOM 也提供了一组纯元素的导航属性,对元素 elem 或者节点 node,有:
node.parentElement:父元素节点,该属性来自 Node 类elem.previousElementSibling:上一个兄弟元素elem.nextElementSibling:下一个兄弟元素elem.children:子元素列表elem.firstElementChild 和 elem.lastElementChild:第一个和最后一个子元素节点
搜索节点
document/element.getElementBy*()系列
document/element.getElementBy*() 表示这是两个不同的类上的方法
两个来自 Document 类的方法
document.getElementById(id):根据 id 获取文档中的元素
document.getElementsByName(name):根据 name 获取文档中的元素,很少使用
Document 类和 Element 类都具有的方法
document/element.getElementsByTagName(tagName):根据标签名获取文档或某个元素内的元素
document/element.getElementByClassName(className):根据类名获取文档或某个元素内的元素
PS:不要忘记或多加了s,除了document.getElementById(),其他方法的返回结果都是一个集合,没有满足条件的元素则是空集合
document/element.querySelectorAll/querySelector()
这组方法支持 css 选择器:
document/element.querySelectorAll(CSSSelector):返回满足选择器的一组节点列表document/element.querySelector(CSSSelector):返回第一个满足选择器的元素
elem.matches(selector)可以检查某个元素是否与选择器匹配
节点操作
通用节点操作
这些操作基于 Node 接口,对所有节点都是通用的
1)判断节点类型
node.nodeType 或 node instanceof :两者都可以用于判断节点类型,当需要明确的节点类的时候,只能通过后者
node.nodeType === Node.ELEMENT_NODE // 或node.nodeType === 1
node instanceof Element // 与上面等效
node instanceof HTMLInputElement // 判断是否是输入元素
2)获取节点名称
node.nodeName:对于元素节点,返回对应的标签名称,如audio。对其他类型节点,返回 # 与节点类型字符串,如#text,#comment,#document,也能通过节点名称判断节点类型,但基本不用
3)获取或设置节点值
node.nodeValue:文本节点或注释节点返回文本内容,元素节点与 document 节点返回 null;读写属性,空白文本也被包含在内容里
PS:文本节点和注释节点有一个 data 属性,使用与 nodeValue 相同,但它不是在 Node 接口上的
4)判断节点是否拥有子节点
node.hasChildNodes() :当节点有子节点时返回true
5)判断节点是否拥有特定子节点
node.contains(childNode) :当 childNode 是 node 的子节点时返回true
元素节点操作
大部分情况下我们都是在元素节点上操作它的文本子节点,所以元素是我们最关心的节点,Element 接口提供了很多的属性和方法,这里只考虑 HTML 元素
1)判断元素类型
elem.tagName 或 elem.nodeName:返回标签的字符串;另外,使用 instanceof 可以实现不同级别的类型判断
2)元素内容
elem.innerHTML:获取或设置元素内的 HTML 片段,设置的内容会被当成 HTML 片段解析,可能会引起文档结构的变化
PS:HTML片段内的脚本不会执行
elem.textContent:获取或设置元素的文本内容(标签被忽略),设置的文本以安全模式(不会被解析)写入
innerHTML、textContent、innerText的使用场景:
innerHTML:获取元素内的 html 内容或设置其内部HTMLtextContent:获取元素的文本内容或设置其文本内容innerText(很少使用):获取在页面呈现的文本内容
很多时候,尤其是元素内部只是普通文本的时候,三者的区别不会造成什么问题
不过,在涉及 HTML 内容时,必须使用 innerHTML,否则使用 textContent,保证安全;使用 innerText 往往是为了兼容 IE6-7
元素的特性和属性
特性是指 html 中写在标签内的特性,而属性是元素节点作为编程对象具有的属性
特征 – 属性同步机制:对标准规定的特性,元素对象具有响应的读写属性,如a.href。对不同 HTML 元素,规定的特性不同,属性也就不同
通用的特性操作接口:
elem.hasAttribute(name):检查是否存在某个特性elem.getAttribute(name):获取某一特性的值elem.setAttribute(name, value):设置某一特性elem.removeAttribute(name):删除某一特性elem.attributes():获取所有的特性对,每个特性对具有 name, value 属性
特殊的data-*:data-*特性是一种合法且安全的传递自定义数据的方式,可通过 elem.dataset.name 读取或修改 data 特性的值。属性名称采用驼峰写法,如 elem 上的 data-apple-price 对应 elem.dataset.applePrice
元素的类和样式
修改样式有两种方式:
1)把样式写到某个类里,在代码中修改元素的类;适用于随状态改变样式的情况,用得较多
2)直接修改 elem.style.*,适用于频繁计算或切换的样式
elem.classList:一个包含 elem 所有类的可迭代的类数组对象,这个对象有几个方法,方便改变元素的类:
elem.classList.contains(class):检查是否有某个类elem.classList.add(class):添加某个类elem.classList.remove(class):移除某个类elem.classList.toggle(class):切换某个类,如果有就删除,没有就添加
elem.className:一个读写属性,把元素的 class 特性当成一个整体看待
区分:通过 elem.classList 或 elem.className 都可以对元素的类进行改动,前者更灵活,且具有相应的方法,后者是一个整体的字符串属性,适合删除所有的类重新设置
如果需要直接设定元素的样式,可以设置 elem.style.*
元素的位置和尺寸
当设计元素的大小变化或位置移动时,我们需要获取元素的位置或尺寸,设置则用 CSS 方式设置
位置是相对于参照物的,一个元素有相对于定位父元素、相对于视口、相对于文档三种关系位置
相对于定位父元素:
定位父元素是指 CSS 定位元素(position为relative、absolute、fixed)或 td、th、table、body 元素elem.offsetParent:最接近的 CSS 定位的祖先elem.offsetLeft/offsetTop:相对于 offsetParent 的左上角边缘的坐标 相对于视口:
elem.getBoundingClientRect():获取元素的定位矩阵 elemRectelem.getBoundingClientRect().left/top/right/bottom:表示元素盒子(含边框)四角到视口左或上边的距离elem.getBoundingClientRect().width/height与elem.offsetWidth/offsetHeight等效,盒子的宽高 相对于文档:没有直接获取的方式,但可以通过相对于视口+滚动距离简单计算
盒子上方相对于到文档的距离:elem.scrollTop + elem.getBoundingClientRect().top盒子左边到文档的距离:elem.scrollLeft + elem.getBoundingClientRect().left
元素盒子的尺寸也有多种情况,需要考虑边框、内边距、是否为标准盒子模型、甚至是否有滚动条
elem.offsetWidth/offsetHeight或者elem.getBoundingClientRect().width/height:都可以获取含边框的宽高elem.clientLeft/clientTop:从元素左上角外角到左上角内角的距离,如果存在滚动条,也包含滚动条的宽度(补充:一般来说,上边框和左边框是常用的,如果四条边框宽度不一,可以通过getComputedStyle(elem).borderRight获取,这是含单位的字符串)elem.clientWidth/clientHeight:内容的宽高,包含padding,但不包含滚动条内边距问题与盒子类型:在涉及内边距的时候,需要 getComputedStyle(elem) 方法获取,并且需要考虑是否为标准盒子
区分:在没有内容溢出发生滚动时,clientWidth/Height 与 scrollWidth/Height 等效;存在滚动时,前者是盒子的可视内容大小,后者是内容的大小,包括需要滚动查看的部分
现代JavaScript教程:元素大小与滚动
现代JavaScript教程:坐标
修改文档
DOM操作中,经常需要修改文档结果或内容,这类操作涉及节点的插入、移除、替换等
插入节点
插入节点可以分为以下三步:
1)创建一个节点
创建一个元素节点:let elem = document.createElement(tagName)创建一个文本节点:let text = document.createTextNode(data)从已有节点克隆:let dupNode = node.cloneNode(deep),deep 为 true表示深拷贝,默认为false
2)编辑节点的属性和内容
3)把节点插入文档树中
传统方式:在父节点上执行对节点的插入
parentNode.appendChild(node):node 作为最后一个子节点插入parentNode.insertBefore(node, nextSibling):在 nextSibling 之前插入node 现代方式:实现多位置插入,可以在父节点或兄弟节点上执行插入。参数的形式说明它们支持一次插入多个,并且字符串会作为文本节点插入
parentNode.prepend(...nodes or strings):在第一个子节点之前插入parentNode.append(...nodes or strings):在最后一个子节点之后插入nextSibling.before(...nodes or strings):在本节点之前同级插入previousSibling.after(...nodes or strings):在本节点之后同级插入
如果我们希望直接描述节点的插入 HTML 代码段,可以使用elem.innerHTML属性,或者使用elem.insertAdjacentHTML(position, html)进行插入,其中,position 的可选值有:
区分:节点对象存在 不同于 节点在文档树中
1)创建节点对象只是在内存中创建了一个对象实例,不会对文档树的结构产生任何影响。对该节点进行的操作,都只是对该对象自身属性和方法的修改,不会在页面上产生任何可见的效果,因为浏览器只渲染文档树中的节点
2)只有当节点被插入到文档树后,它才会成为文档结构的一部分,浏览器会根据其在文档树中的位置和相关属性来渲染该节点,使其在页面上显示出来,并且可以响应用户的交互操作等
移除节点
从文档树中移除更加简单:
1)找到需要移除的节点
2)node.remove():移除节点,但 IE 不兼容,需要使用传统方式,获取其父节点,在父节点上移除子节点:node.parentNode.removeChild(node)
替换节点
与移除类似,使用node.replaceWith(... Nodes or strings),同样 IE 使用parentNode.replaceChild(newNode, node)