在网上经常会看到 ECMAScript
,DOM
,BOM
这几个概念,今天我们就来缕一缕它们到底是什么东西,其实简单来说,如下
- 核心(
ECMAScript
),提供核心语言功能 - 文档对象模型(
Document Object Model
,简称DOM
),提供访问和操作网页内容的方法和接口 - 浏览器对象模型(
Broser Object Model
,简称BOM
),提供与浏览器交互的方法和接口
简单的来说,DOM
和 BOM
并不属于 JavaScript
(ECMAScript
)语言的一部分,DOM
和 BOM
是 JavaScript
的运行平台(浏览器)提供的,比如在 Node.js
当中就没有 DOM
和 BOM
,JavaScript
类型分为两大类,『原生类型』和『对象类型』,而 DOM
和 BOM
都是『对象类型』,下面我们就一个一个来进行了解
BOM
BOM
(Browser Object Model
)即浏览器对象模型,主要是指一些浏览器内置对象如 window
,location
,navigator
,screen
,history
等对象,用于完成一些操作浏览器的特定 API
,主要用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构
BOM
由多个对象组成,其中代表浏览器窗口的 window
对象是 BOM
的顶层对象,其他对象都是『该对象的子对象』
BOM
提供了独立于内容而与浏览器窗口进行交互的对象- 由于
BOM
主要用于管理窗口与窗口之间的通讯,因此其核心对象是window
BOM
由一系列相关的对象构成,并且每个对象都提供了很多方法与属性BOM
缺乏标准,JavaScript
语法的标准化组织是ECMA
,DOM
的标准化组织是W3C
BOM
最初是Netscape
浏览器标准的一部分
BOM
结构如下图所示
从上图可以看出 DOM
是属于 BOM
的一个属性,window
对象是 BOM
的顶层(核心)对象,所有对象都是通过它延伸出来的,所以可以称它们为 window
的子对象,由于 window
是顶层对象,因此调用它的子对象时可以不显示的指明 window
对象,比如如下两种写法均可
1 | document.title = '123' |
BOM 导图
BOM
部分主要是针对浏览器的内容,下面是一些比较常用的对象
window
,是全局对象,很多关于浏览器的脚本设置都是通过它location
,则是与地址栏内容相关,比如想要跳转到某个页面,或者通过URL
获取一定的内容navigator
,通常判断浏览器类型都是通过这个对象screen
,常常用来判断屏幕的高度宽度等history
,访问浏览器的历史记录,如前进、后台、跳转到指定位置
具体关系可以如下图所示
window 对象
window
对象在浏览器中具有双重角色,它既是 ECMAscript
规定的全局 global
对象,又是 JavaScript
访问浏览器窗口的一个接口,所有浏览器都支持 window
对象,它表示浏览器窗口
- 如果文档包含框架(
frame
或iframe
标签),浏览器会为HTML
文档创建一个window
对象,并为每个框架创建一个额外的window
对象 - 没有应用于
window
对象的公开标准,不过所有浏览器都支持该对象 - 所有
JavaScript
全局对象、函数以及变量均自动成为window
对象的成员 - 全局变量是
window
对象的属性,全局函数是window
对象的方法
下面是一些常用的 window
方法
window.innerHeight
,浏览器窗口的内部高度window.innerWidth
,浏览器窗口的内部宽度window.open()
,打开新窗口window.close()
,关闭当前窗口
window 的子对象
下面我们来看看一些 window
的子对象
Navigator 对象
浏览器对象,通过这个对象可以判定用户所使用的浏览器,包含了浏览器相关信息
1 | navigator.appName // Web 浏览器全称 |
Screen 对象
屏幕对象,一般不太常用,一些常见属性如下
screen.availWidth
,可用的屏幕宽度screen.availHeight
,可用的屏幕高度
History 对象
浏览历史对象,包含了用户对当前页面的浏览历史,但我们无法查看具体的地址,只能简单的用来前进或后退一个页面,也可以使用 go()
实现在用户的浏览记录中跳转
1 | history.go(-1) // 等价于 history.back() |
Location 对象
location
对象提供了当前窗口加载的文档的相关信息,还提供了一些导航功能,事实上这是一个很特殊的对象,location
既是 window
对象的属性,又是 document
对象的属性,window.location
对象用于获得当前页面的地址(URL
),并把浏览器重定向到新的页面,常用属性和方法有
1 | location.href // 获取 URL |
弹出框
可以在 JavaScript
中创建三种消息框,警告框、确认框、提示框
1 | // 警告框 |
DOM
DOM
是一套对文档的内容进行抽象和概念化的方法,当网页被加载时,浏览器会创建页面的文档对象模型(Document Object Model
),而 HTML DOM
模型会被构造为对象的树,如下图所示
DOM
模型将整个文档(XML
文档和 HTML
文档)看成一个树形结构,并用 document
对象表示该文档,DOM
规定 HTML
文档中的每个成分都是一个节点(node
)
- 文档节点(
Document
),代表整个文档 - 元素节点(
Element
),文档中的一个标记 - 文本节点(
Text
),标记中的文本 - 属性节点(
Attr
),代表一个属性,元素才有属性 - 注释节点(
Comment
),表示注释
JavaScript
可以通过 DOM
创建动态的 HTML
- 可以改变页面中的所有
HTML
元素及其属性 - 可以改变页面中的所有
CSS
样式 - 可以对页面中的所有事件做出反应
节点类型
使用 NodeType
属性来表明节点类型
节点类型 | 描述 | |
---|---|---|
1 | Element |
代表元素,普通元素节点,比如 div ,p 等 |
2 | Attr |
代表属性 |
3 | Text |
代表元素或属性中的文本内容,文本节点 |
4 | CDATASection |
代表文档中的 CDATA 部分(不会由解析器解析的文本) |
5 | EntityReference |
代表实体引用 |
6 | Entity |
代表实体 |
7 | ProcessingInstruction |
代表处理指令 |
8 | Comment |
代表注释,注释节点 |
9 | Document |
代表整个文档(DOM 树的根节点) |
10 | DocumentType |
向为文档定义的实体提供接口 |
11 | DocumentFragment |
代表轻量级的 Document 对象,能够容纳文档的某个部分 |
12 | Notation |
代表 DTD 中声明的符号 |
节点关系
属性 | 描述 |
---|---|
nodeType |
返回节点类型的数字值(1 ~ 12 ) |
nodeName |
节点名称 |
nodeValue |
节点值 |
parentNode |
父节点 |
parentElement |
父节点标签元素 |
childNodes |
所有子节点 |
children |
第一层子节点 |
firstChild |
第一个子节点,Node 对象形式 |
firstElementChild |
第一个子标签元素 |
lastChild |
最后一个子节点 |
lastElementChild |
最后一个子标签元素 |
previousSibling |
上一个兄弟节点 |
previousElementSibling |
上一个兄弟标签元素 |
nextSibling |
下一个兄弟节点 |
nextElementSibling |
下一个兄弟标签元素 |
childElementCount |
第一层子元素的个数(不包括文本节点和注释) |
ownerDocument |
指向整个文档的文档节点 |
一些判断节点之间关系的方法
hasChildNodes()
,包含一个或多个节点时返回true
contains()
,如果是后代节点返回true
isSameNode()
和isEqualNode()
,传入节点与引用节点的引用为同一个对象返回true
compareDocumentPostion()
,确定节点之间的各种关系
节点操作
这一部分主要涉及节点相关的一些操作,也是在平常开发当中经常会遇到的地方,所以我们会介绍的稍微详细一些
操作 | 描述 |
---|---|
nodeName |
访问元素的标签名 |
tagName |
访问元素的标签名 |
createElement() |
创建节点 |
appendChild() |
末尾添加节点,并返回新增节点 |
insertBefore() |
参照节点之前插入节点,参数有两个,要插入的节点和参照节点 |
insertAfter() |
参照节点之后插入节点,参数有两个,要插入的节点和参照节点 |
replaceChild() |
替换节点,参数有两个,要插入的节点和要替换的节点(被移除) |
removeChild() |
移除节点 |
cloneNode() |
克隆,一个布尔值参数,true 为深拷贝,false 为浅拷贝 |
importNode() |
从文档中复制一个节点,参数有两个,要复制的节点和布尔值(是否复制子节点) |
有一个比较特殊的 insertAdjacentHTML()
,作用是插入文本,参数有两个,插入的位置和要插入文本
beforebegin
,在该元素前插入afterbegin
,在该元素第一个子元素前插入beforeend
,在该元素最后一个子元素后面插入afterend
,在该元素后插入
childNodes
用来获取子节点,注意,返回的是一个『类数组对象』
1 | var childs = oDiv.childNodes |
这里存在一个坑,比如如下
1 | <div> |
当 div
中没有文本节点的时候,此时应该为 4
个节点,但是 IE9
,Chrome
,FireFox
会认为存在 9
个节点,而 IE8
则认为只有 4
个节点,这是因为高级浏览器会把空文本节点也当作为一个节点,标签前后的空文本也会被算作一个节点,而且对于注释的前后算不算空文本节点,每个浏览器的解释也有不相同,所以我们在使用节点的时候,一定要过滤,比如判断节点的 nodeType
是不是 1
(普通元素节点)
1 | // 得到真正的标签子节点 |
另外,如果要改变文本节点的内容(nodeType
为 3
),需要改变其 nodeValue
属性
1 | oDiv.childNodes[0].nodeValue = '张三' |
parentNode
parentNode
属性表示父节点,任何节点的 parentNode
的 nodeType
一定为 1
,也就是说父节点一定是标签节点
previousSibling 和 nextSibling
表示一个兄弟节点,需要注意的是,其可能是文本或者注释节点,而原生 JavaScript
当中并没有提供 prevAll()
,nextAll()
,siblings()
等方法,如果不存在兄弟节点,则会返回 null
,所以可以利用这个特性来写一个方法
1 | // prev |
而 siblings()
方法则可以使用双重循环来实现,比如下面这个方法
1 | function toggleActive() { |
创建节点
使用 document.createElement('标签名')
来创建一个节点,需要注意的是,创建出来的节点是不存在与 DOM
树上的,即孤儿节点,需要手动添加至 DOM
树中
1 | var oBox = document.getElementById('div') |
一个需要注意的地方,JavaScript
中存储 DOM
节点的变量是动态的,比如如下例子
1 | var oBox = document.getElementById('box') |
解决方法很简单,用一个变量将 length
存储起来即可
1 | for (var i = 0; l = oDiv.length, i < l; i++) { |
插入节点
appendChild()
常用的方法是使用 appendChild()
来追加至元素的末尾,需要注意的地方就是,如果节点已经存在(比如 DOM
树中已经存在),而不是新创建的,这个时候则会移动该节点(不会克隆)
insetBefore()
接收两个参数,一个是新创建的元素,另一个为参照点
1 | oBox.insetBefore('新创建的元素', '参照元素') |
这样插入的元素会以参照的元素依次往上添加(即添加的为 3
,2
,1
参照),如果想让顺序变为正序,使用 oBox.childNodes[0]
为参照点即可
需要注意,如果使用
childNodes[0]
来做参照删除元素的话,会存在空白节点
删除节点
节点不能自己删除,如果想要删除节点,必须使用父元素参照
1 | '父元素'.removeChild('删除的元素') |
如果不知道父元素是谁,则可以使用
1 | '需要删除的元素'.parentNode.removeChild('需要删除的元素') |
替换节点
使用 replaceChild()
方法,一般使用的不是很多
1 | '父元素'.replaceChild('新节点', '旧节点') |
比如 oBox.replaceChild(div1, div2)
结果是将 div1
节点处的内容替换至 div2
处(div1
处的节点内容就不存在了)
克隆节点
比较常用的方式是使用 innerHTML
的方式来进行克隆(亦或是修改),但是执行效率没有 DOM
原生方法速度快,原生的方法是 cloneNode([true])
,可以追加一个布尔值参数 true
,表示深度克隆,克隆其所有的子节点
对象类型
部分内容截取自 知乎 - justjavac 的回答,这里主要涉及到 ECMAScript
中的对象和 DOM/BOM
对象,我们在文章的开头部分提到过,JavaScript
类型分为原生类型和对象类型,而 DOM
和 BOM
都是对象类型,下面我们就来深入的了解一下
比如 HTML
中的段落 p
映射为 JavaScript
对象是 HTMLParagraphElement
,顾名思义 Paragraph
就是英语段落的意思,我们看看 HTMLParagraphElement
的继承关系
1 | HTMLParagraphElement |
所有的 DOM
和 BOM
没有任何特殊之处,都是一个 Object
的子类
1 | // 创建一个 div dom |
当我们创建了一个 DOM Object
后,我们就可以把这个 DOM Object
当作一个普通的 JavaScript
对象来使用,说他特殊,大概是因为 DOM
的属性和方法会被引擎映射到 HTML
标签上,有些会,有些不会,有些只能从 HTML
到 DOM
映射,有些只能从 DOM
到 HTML
映射,如果但从对象的角度讲,特殊的不是 DOM
和 BOM
,而是另一个值
1 | Object.create(null) |
所有的 JavaScript
对象都是继承自 Object
,即使我们经常创建的空对象也是 Object
的子类,如下
1 | let obj = { } |
如下图所示
但是 Object.create(null)
却是实实在在的空对象
在引擎(V8
)内部,DOM
对象映射为 C++ Object
而每个 HTML
标签都对应一个 DOM Object
,我们看如下代码:
1 | var div = document.createElement('div') // 创建一个 div dom |
可能我们会觉得最终输出的是 null
或者 undefined
亦或是抛出异常,但是这行代码会输出 1234
,DOM
如何与 JavaScript Object
关联在一起的规范定义在 WebIDL Level 1 当中,WebIDL
就是 Web Interface Definition Language
的缩写,DOM Object
在引擎内部是一个 C++ Object
,当 JavaScript
操作这个 DOM
时,引擎使用一个 wrapper object
,也就是 JavaScript Object
Wrapper Object
和 DOM Object
的关系是 n : 1
(n >= 0
)
其中(n >= 0
)
- 当
n == 0
时,此DOM Object
不能通过JavaScript
访问 - 当
n == 1
时,此DOM Object
只有一个JavaScript Object
可以访问 - 当
n > 1
时,此DOM Object
可以通过多个JavaScript
访问
举个例子
1 | div = document.createElement('div') |
上面代码的对应关系图