BOM 和 DOM

BOM 和 DOM

在网上经常会看到 ECMAScriptDOMBOM 这几个概念,今天我们就来缕一缕它们到底是什么东西,其实简单来说,如下

  • 核心(ECMAScript),提供核心语言功能
  • 文档对象模型(Document Object Model,简称 DOM),提供访问和操作网页内容的方法和接口
  • 浏览器对象模型(Broser Object Model,简称 BOM),提供与浏览器交互的方法和接口

简单的来说,DOMBOM 并不属于 JavaScriptECMAScript)语言的一部分,DOMBOMJavaScript 的运行平台(浏览器)提供的,比如在 Node.js 当中就没有 DOMBOMJavaScript 类型分为两大类,『原生类型』和『对象类型』,而 DOMBOM 都是『对象类型』,下面我们就一个一个来进行了解

BOM

BOMBrowser Object Model)即浏览器对象模型,主要是指一些浏览器内置对象如 windowlocationnavigatorscreenhistory 等对象,用于完成一些操作浏览器的特定 API,主要用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构

BOM 由多个对象组成,其中代表浏览器窗口的 window 对象是 BOM 的顶层对象,其他对象都是『该对象的子对象』

  • BOM 提供了独立于内容而与浏览器窗口进行交互的对象
  • 由于 BOM 主要用于管理窗口与窗口之间的通讯,因此其核心对象是 window
  • BOM 由一系列相关的对象构成,并且每个对象都提供了很多方法与属性
  • BOM 缺乏标准,JavaScript 语法的标准化组织是 ECMADOM 的标准化组织是 W3C
  • BOM 最初是 Netscape 浏览器标准的一部分

BOM 结构如下图所示

从上图可以看出 DOM 是属于 BOM 的一个属性,window 对象是 BOM 的顶层(核心)对象,所有对象都是通过它延伸出来的,所以可以称它们为 window 的子对象,由于 window 是顶层对象,因此调用它的子对象时可以不显示的指明 window 对象,比如如下两种写法均可

1
2
3
4
5
document.title = '123'

// ==> 两者是等价的

window.document.title = '123'

BOM 导图

BOM 部分主要是针对浏览器的内容,下面是一些比较常用的对象

  • window,是全局对象,很多关于浏览器的脚本设置都是通过它
  • location,则是与地址栏内容相关,比如想要跳转到某个页面,或者通过 URL 获取一定的内容
  • navigator,通常判断浏览器类型都是通过这个对象
  • screen,常常用来判断屏幕的高度宽度等
  • history,访问浏览器的历史记录,如前进、后台、跳转到指定位置

具体关系可以如下图所示

window 对象

window 对象在浏览器中具有双重角色,它既是 ECMAscript 规定的全局 global 对象,又是 JavaScript 访问浏览器窗口的一个接口,所有浏览器都支持 window 对象,它表示浏览器窗口

  • 如果文档包含框架(frameiframe 标签),浏览器会为 HTML 文档创建一个 window 对象,并为每个框架创建一个额外的 window 对象
  • 没有应用于 window 对象的公开标准,不过所有浏览器都支持该对象
  • 所有 JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员
  • 全局变量是 window 对象的属性,全局函数是 window 对象的方法

下面是一些常用的 window 方法

  • window.innerHeight,浏览器窗口的内部高度
  • window.innerWidth,浏览器窗口的内部宽度
  • window.open(),打开新窗口
  • window.close(),关闭当前窗口

window 的子对象

下面我们来看看一些 window 的子对象

浏览器对象,通过这个对象可以判定用户所使用的浏览器,包含了浏览器相关信息

1
2
3
4
5
6
7
navigator.appName                 // Web 浏览器全称
navigator.appVersion // Web 浏览器厂商和版本的详细字符串
navigator.userAgent // 客户端绝大部分信息
navigator.platform // 浏览器运行所在的操作系统
navigator.userAgent // 用户代理字符串,用于浏览器监测中
navigator.plugins // 浏览器插件数组,用于插件监测
navigator.registerContentHandler // 注册处理程序,如提供 RSS 阅读器等在线处理程序

Screen 对象

屏幕对象,一般不太常用,一些常见属性如下

  • screen.availWidth,可用的屏幕宽度
  • screen.availHeight,可用的屏幕高度

History 对象

浏览历史对象,包含了用户对当前页面的浏览历史,但我们无法查看具体的地址,只能简单的用来前进或后退一个页面,也可以使用 go() 实现在用户的浏览记录中跳转

1
2
3
4
history.go(-1)    // 等价于 history.back()
history.go(1) // 等价于 history.forward()
history.back() // 后退一页
history.forward() // 前进一页

Location 对象

location 对象提供了当前窗口加载的文档的相关信息,还提供了一些导航功能,事实上这是一个很特殊的对象,location 既是 window 对象的属性,又是 document 对象的属性,window.location 对象用于获得当前页面的地址(URL),并把浏览器重定向到新的页面,常用属性和方法有

1
2
3
location.href          // 获取 URL
location.href = 'xxx' // 跳转到指定页面
location.reload() // 重新加载页面

弹出框

可以在 JavaScript 中创建三种消息框,警告框、确认框、提示框

1
2
3
4
5
6
7
8
// 警告框
alert(123)

// 确认框
confirm('是否提交?')

// 提示框
prompt('请在下方输入收货地址', '收货地址')

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 代表元素,普通元素节点,比如 divp
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
2
3
4
5
6
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>

div 中没有文本节点的时候,此时应该为 4 个节点,但是 IE9ChromeFireFox 会认为存在 9 个节点,而 IE8 则认为只有 4 个节点,这是因为高级浏览器会把空文本节点也当作为一个节点,标签前后的空文本也会被算作一个节点,而且对于注释的前后算不算空文本节点,每个浏览器的解释也有不相同,所以我们在使用节点的时候,一定要过滤,比如判断节点的 nodeType 是不是 1(普通元素节点)

1
2
3
4
5
6
7
8
9
10
// 得到真正的标签子节点
function getRealChild(elem) {
var result = []
for (var i = 0; i < elem.childNodes.length; i++) {
if (elem.childNodes[i].nodeType == 1) {
result.push(elem.childNodes[i])
}
}
return result
}

另外,如果要改变文本节点的内容(nodeType3),需要改变其 nodeValue 属性

1
oDiv.childNodes[0].nodeValue = '张三'

parentNode

parentNode 属性表示父节点,任何节点的 parentNodenodeType 一定为 1,也就是说父节点一定是标签节点

previousSibling 和 nextSibling

表示一个兄弟节点,需要注意的是,其可能是文本或者注释节点,而原生 JavaScript 当中并没有提供 prevAll()nextAll()siblings() 等方法,如果不存在兄弟节点,则会返回 null,所以可以利用这个特性来写一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// prev
function getRealPrev(elem) {
// 原理就是遍历 elem 节点的前面,直到返回第一个 nodeType 为 1 的节点
var o = elem

// 循环遍历,将循环的结果再次赋予 o,依次向上查询
while (o = o.previousSibling) {
if (o.nodeType == 1) {
return o
}
return null
}
}

// next,同 prev 类似,不过这次换成了后面
function getRealNext(elem) {
var o = elem
while (o = o.nextSibling) {
if (o.nodeType == 1) {
return o
}
return null
}
}

// prevAll,同 prev 类似,不过将遍历到的元素放到了一个结果数组当中
function getRealprevAll(elem) {
var o = elem, result = []
while (o = o.previousSibling) {
if (o.nodeType == 1) {
result.unshift(o)
}
return result
}
}

// nextAll
function getRealnextAll(elem) {
var o = elem, result = []
while (o = o.nextSibling) {
if (o.nodeType == 1) {
result.push(o)
}
return result
}
}

siblings() 方法则可以使用双重循环来实现,比如下面这个方法

1
2
3
4
5
6
7
8
9
10
11
12
function toggleActive() {
var span = document.querySelectorAll('span')
for (var i = 0; i < span.length; i++) {
span[i].addEventListener('click', function () {
// 点击的时候清空所有,然后为当前选中的添加焦点
for (var j = 0; j < span.length; j++) {
span[j].classList.remove('active')
}
this.classList.add('active')
})
}
}

创建节点

使用 document.createElement('标签名') 来创建一个节点,需要注意的是,创建出来的节点是不存在与 DOM 树上的,即孤儿节点,需要手动添加至 DOM 树中

1
2
3
4
var oBox = document.getElementById('div')
var oDiv = document.createElement('div')

oBox.appendChild(oDiv)

一个需要注意的地方,JavaScript 中存储 DOM 节点的变量是动态的,比如如下例子

1
2
3
4
5
6
7
8
9
var oBox = document.getElementById('box')
var oDiv = oBox.getElementsByTagName('div')

// 会造成死循环,因为 oDiv.length 会动态增加
for (var i = 0; i < oDiv.length; i++) {
var oP = document.createElement('p')
oP.innerHTML = '123'
oBox.appendChild(oP)
}

解决方法很简单,用一个变量将 length 存储起来即可

1
2
3
for (var i = 0; l = oDiv.length, i < l; i++) {
// ...
}

插入节点

appendChild()

常用的方法是使用 appendChild() 来追加至元素的末尾,需要注意的地方就是,如果节点已经存在(比如 DOM 树中已经存在),而不是新创建的,这个时候则会移动该节点(不会克隆)

insetBefore()

接收两个参数,一个是新创建的元素,另一个为参照点

1
oBox.insetBefore('新创建的元素', '参照元素')

这样插入的元素会以参照的元素依次往上添加(即添加的为 321参照),如果想让顺序变为正序,使用 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 类型分为原生类型和对象类型,而 DOMBOM 都是对象类型,下面我们就来深入的了解一下

比如 HTML 中的段落 p 映射为 JavaScript 对象是 HTMLParagraphElement,顾名思义 Paragraph 就是英语段落的意思,我们看看 HTMLParagraphElement 的继承关系

1
2
3
4
5
6
HTMLParagraphElement
- HTMLElement
- Element
- Node
- EventTarget
- Object

所有的 DOMBOM 没有任何特殊之处,都是一个 Object 的子类

1
2
3
4
5
6
7
8
9
// 创建一个 div dom
var div = document.createElement('div')

// 给 div 添加属性
div.foo = 1234

div.sayHello = function (str) {
console.log('hello ' + str)
}

当我们创建了一个 DOM Object 后,我们就可以把这个 DOM Object 当作一个普通的 JavaScript 对象来使用,说他特殊,大概是因为 DOM 的属性和方法会被引擎映射到 HTML 标签上,有些会,有些不会,有些只能从 HTMLDOM 映射,有些只能从 DOMHTML 映射,如果但从对象的角度讲,特殊的不是 DOMBOM,而是另一个值

1
Object.create(null)

所有的 JavaScript 对象都是继承自 Object,即使我们经常创建的空对象也是 Object 的子类,如下

1
let obj = { }

如下图所示

但是 Object.create(null) 却是实实在在的空对象

在引擎(V8)内部,DOM 对象映射为 C++ Object

而每个 HTML 标签都对应一个 DOM Object,我们看如下代码:

1
2
3
4
5
6
var div = document.createElement('div')   // 创建一个 div dom
div.foo = 1234 // 给 div 添加属性
var p = document.createElement('p') // 创建一个 p dom
p.appendChild(div) // 把 div 添加为 p 的子节点
div = null // div 设置为空
console.log(p.firstChild.foo)

可能我们会觉得最终输出的是 null 或者 undefined 亦或是抛出异常,但是这行代码会输出 1234DOM 如何与 JavaScript Object 关联在一起的规范定义在 WebIDL Level 1 当中,WebIDL 就是 Web Interface Definition Language 的缩写,DOM Object 在引擎内部是一个 C++ Object,当 JavaScript 操作这个 DOM 时,引擎使用一个 wrapper object,也就是 JavaScript Object

Wrapper ObjectDOM Object 的关系是 n : 1n >= 0

其中(n >= 0

  • n == 0 时,此 DOM Object 不能通过 JavaScript 访问
  • n == 1 时,此 DOM Object 只有一个 JavaScript Object 可以访问
  • n > 1 时,此 DOM Object 可以通过多个 JavaScript 访问

举个例子

1
2
3
div = document.createElement('div')
div.innerHTML = '<p><span>foo</span><br></p>'
div.firstChild

上面代码的对应关系图

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×