只要是学前端的同学,肯定都听说过 DOM
什么是 DOM
在 JS 中,常常使用 document.xxx 来对页面进行操作。那实际操作的是什么呢?
在开发者工具中运行
1 | let div=document.querySelector('#container') |
就可以看到打印出了一个 id 为 container 的元素
这个元素就是 DOM 元素
然后去 Elements 中查看,可以发现这些元素呈现层次关系,这个关系正是树形结构
所以文档结构就是 DOM 树,全称 文档对象模型树(Document Object Model)
通过打印原型链,可以发现以下原型关系
Object -> EventTarget -> Node -> Element -> HTMLElement -> HTMLDivElement
显然,每个 DOM 元素都有自己的属性,但属性原则上属于 DOM 元素的一部分,对属性的操作等效于对元素的操作,故而在 Update 中统一描述
Retrieve
查自己
首先要获取,才能做各种操作
对于普通的 DOM 元素,一般有如下 API
- document.getElementById(‘xxx’),获取 id 为 xxx 的元素,包括 IE,全都支持
- window.xxx(或直接使用 xxx),获取 id 为 xxx 的元素,除了 IE,全都支持
- document.getElementsByTagName(‘div’),获取所有标签名为 div 的元素,返回伪数组
- document.getElementsByClassName(‘yyy’),获取所有 CSS 类名为 yyy 的元素,返回伪数组
- document.querySelector(selector),获取第一个满足传入的 CSS 选择器的元素
- document.querySelectorAll(selector),获取所有满足传入的 CSS 选择器的元素,返回伪数组
对于特殊的元素,一般有以下 API
- document.documentElement,获取 html 根元素
- document.head,获取 head 元素
- document.body,获取 body 元素
- window,获取窗口(注意窗口不是元素
- document.all,获取所有元素
在使用 document.all 的时候需要注意,虽然现在所有浏览器都支持这个查询了,但因为历史遗留原因,虽然能获取到值,但转化为布尔值的时候,在非 IE 的浏览器上都返回 false,只有在 IE 上才返回 true
查祖先
显然除了根结点,每个结点有且仅有一个父结点
那么可以通过 div.parentNode
不断向上查找
查子代
可以通过 div.childNodes
或 div.children
来查找
需要注意的是,div.childNodes
返回的是 NodeList,单位类型是 Node,是伪数组,直接 concat 空数组的话不能正常展开,需要用空数组 concat 一个 Array,from 才行
div.children
返回的是 ElementCollection,单位类型均为 Element 的派生类型,与数组操作时注意点同上
并且,div.childNode
返回值包括文本结点等不直接显示的结点,而 div.children
没有这个问题
子代中对特定元素也有专有的方法
- 查看第一个子结点,div.firstChild
- 查看最后一个子结点,div.lastChild
查兄弟
可以通过先查父结点,再查父结点的子结点来做到,但要排除自己才是兄弟结点
同样,兄弟也有专有方法
- 查看相邻的上一个兄弟,div.previousSibling
- 查看相邻的下一个兄弟,div.nextSibling
Create
通常使用
1 | document.createElement([tagName]) |
来创建一个指定标签元素
或
1 | document.createTextNode([string]) |
来创建一个包含指定文本的文本结点
但创建之后的元素或节点,均还在 JS 线程中,必须通过 API 添加到页面上,由渲染线程处理后,才能在页面上显示
通常通过
1 | [parentNode].appendChild([childNode]) |
来将指定的结点添加到某个结点中
关于添加结点,有以下两种特殊情况
同一结点被多次添加到不同结点的子结点列表中
此时该子结点会出现在最后一次被添加到的位置
意图向某结点添加文本
此时不可以直接 appendChild(string),必须先将 string 转换成文本结点,或用 innerText、textContent 属性来添加文本
innerText 是 IE 产物,textContent 是其它浏览器产物,但现在所有浏览器都同时支持两个
Delete
一般有两种方法
[parentNode].removeChild([childNode])
[childNode].remove()
对于被从 DOM 树中移除的结点,只要还没丢失对它的引用,就还可以通过 appendChild 再次回到页面上
Update
如同前述,对元素的修改包含对属性的修改
一般分为四种属性
- HTML 标签属性
- CSS 属性
- data 属性
- 自定义属性
不论是何种属性,都可以通过点符号进行 get/set
但是 JS 中不能使用连接符来访问,所以在使用连接符的地方,要改用驼峰命名法
HTML 标签属性
直接使用赋值语句改值即可,注意值必须是字符串
如 img.width = '200px'
但是有的属性不太一样,比如 a 标签的 href 属性
如果通过 a.href
来获取,浏览器会自动补全 href 的根路径,最终获取到的字符串是 http/https 开头的
此时应该通过 a.getAttribute('href')
来获取,能保证获取到的字符串不会被浏览器动手脚
CSS 属性
有 class 和 style 两种
对于 class,使用 [node].className='newClass'
来修改
或使用 [node].classList.add('newClass')
来添加适配的 class
对于 style,可以直接赋值 style 字符串
如 div.style='width:100px; height: 200px'
也可以针对性赋值,如 div.style.width='100px'
data 属性
常常可以见到形如 data-*
的属性
这些属性类似于 style,可以通过统一入口 dataset 来访问
如属性 data-x-err
,可以通过 div.dataset.xErr
来访问
自定义属性
修改方法与 HTML 属性相同,直接通过点符号访问即可
但一般不建议采用不是 data 的自定义属性
因为当目标结点现在在页面中时,对以上三种属性的修改都会直接同步到页面上,但对自定义属性的修改不能同步到页面上
事件
每个元素都会有各种事件,此处以 click 事件为例
要为 click 事件绑定处理函数,可以用如下两种方法
div.onclick=()=>{}
,此时 div 只能有一个方法,新方法会覆盖旧方法,在冒泡阶段执行div.addEventListener('click',()=>{},false)
,此时 div 有若干个方法,新方法与旧方法都会在 click 事件被触发此处的第三个参数默认值 false,表示事件在冒泡阶段触发。如果改为 true 就是在捕获阶段触发
需要注意的是,IE 只有冒泡阶段,并且事件列表执行顺序是 FIFO,其它非 IE 浏览器则有捕获和冒泡,且执行顺序是 LIFO
不论是如何添加事件,在事件被触发时,都有两个默认参数
一个是 this,指向事件触发所在的元素
一个是 event,包含该事件触发时的所有相关信息
文本内容
通过 innerText、textContent 属性来添加文本
innerText 是 IE 产物,textContent 是其它浏览器产物,但现在所有浏览器都同时支持两个
父子结点关系
可以通过结点操作或 innerHTML 注入来实现
例如 div.innerHTML='<span>233</span>'
就是向 div 中插入了一个内容为 233 的 span 标签
注意,此时会覆盖 div 中的所有内容(包括子元素)
如果想添加新儿子,可以使用 appendChild 方法,不再赘述
如果想换个新父结点,则只需要利用 appendChild 会出现在最新位置的特点,直接向新父结点插入当前结点即可
封装
拓展
既然 DOM API 这么复杂,有没有更方便的方法呢?
答案是有,典型的例子就是 JQuery 库