JS基础 - DOM(一)

218 阅读15分钟

JavaScript中除了提供了ECMScript之外,还提供了DOM、BOM之类的API,以便于我们对页面、浏览器进行操作

API(application programming interface)是某一个应用程序提供的可以供外部调用的方法,对象,函数等一系列内容

通过操作这些内容,我们可以对应用程序进行相应的操作,以实现对应的功能

所以为了让我们可以操作文档(整个页面的可视化区域 -- document)和浏览器,浏览器提供了两个全局对象也就是BOM和DOM

因此BOM和DOM本质就是两个存在于window上的对象

image.png

DOM:文档对象模型(Document Object Model)

  • 浏览器在渲染页面中每一个元素(包括HTML元素,也包括CSS元素)的时候

    会生成每一个元素所对应的JS对象(也被叫做文档对象模型)

  • 我们可以通过操作这些JS对象,来达到操作网页中页面元素的功能

  • 这些对象就被称之为DOM对象

  • 页面中渲染生成的节点会生成一颗HTML Tree

  • 而DOM是对每一个HTML在JS中映射形成的普通JS对象

  • 所以整个DOM也是和HTML Tree各节点 一一对应的树结构 这颗树结构被称之为DOM Tree

BOM:浏览器对象模型(Browser Object Model)

  • 由浏览器提供的用于处理文档(document)之外的所有内容的其他对象

DOM

浏览器会对我们编写的HTML、CSS进行渲染,同时它又要考虑我们可能会通过JavaScript来对其进行操作:

  • 于是浏览器将我们编写在HTML中的每一个元素(Element)都抽象成了一个个对象
  • 所有这些对象都可以通过JavaScript来对其进行访问,那么我们就可以通过JavaScript来操作页面
  • 所以,我们将这个抽象过程称之为 文档对象模型(Document Object Model)

整个文档被抽象到 document 对象中:

  • 比如document.documentElement对应的是html元素
  • 比如document.body对应的是body元素
  • 比如document.head对应的是head元素

一个页面不只是有html、head、body元素,也包括很多的子元素,

所以在html结构中,HTML元素最终会形成一个树结构,也就是HTML Tree

HTML元素在抽象成DOM对象的时候,它们也会形成一个树结构,我们称之为DOM Tree

我们可以使用JavaScript来获取对应html元素所对应的DOM,并对其进行相应的操作

从而达到通过JavaScript来操作HTML元素的目的

image.png

继承

继承的作用就是将多个类之间共有的属性和方法进行抽取,抽取到一个父类中

当子类继承自对应的父类,那么子类就可以使用父类中对应的属性和方法

相当于对应的属性和方法也在子类中进行了相应的定义

简而言之,类是对一系列对象的共同属性和方法的抽取和封装

但是类和类之间,可能也存在共同的属性和方法,那么我们可以把类和类之间共同的属性和方法再次进行抽取到另一个类中,

而抽取形成的新类就是父类

而类毕竟是对一系列类似对象的抽象,所以在实际使用的时候,并不可以通过类来操作

而是需要通过对应的类来创建对应的对象来进行使用

Snipaste_2022-05-17_14-43-16.png

继承的好处

  1. 继承可以提供代码的复用性,我们可以把公共的属性和方法抽取到父类中,在子类中实现自己特有的属性和方法
  2. 继承是多态的前提 --- 因为JavaScript是弱数据类型语言,所以多态表现并不明显

DOM继承

DOM相当于是JavaScript和HTML、CSS之间的桥梁

  • 通过浏览器提供给我们的DOM API,我们可以对元素以及其中的内容做任何事情

image.png

Document

Document节点表示的整个载入的网页,它的实例是全局的document对象

也就是说Document是对应的类,而document是对应的实例(document instanceof Document -> true)

对DOM的所有操作都是从 document 对象开始的,也就是说document对象是整个DOM的入口对象,

可以从document开始去访问任何节点元素

元素获取方式
文档声明document.doctype
htmldocument.documentElement
headdocument.head
bodydocument.body
Document和Element之间的关系

Document和Element都是直接继承自Node 类

document代表了整个文档,也就是DOM的入口对象,但其本质就是一个实例对象

而document对象上存在类似于body,head,documentElement,doctype这类的属性

这些body,head,documentElement,doctype属性是Element的实例对象

Document类并不是继承自Element类

但是Document下的所有对象(也就是document中所有属性值为对象的属性,即document下的那些对象属性)都继承自Element类

也就是说实例对象和实例对象之间产生的联系并不一定意味着对应的类也会存在对应的联系

同样的,虽然Document和Element都直接继承自Node 类,但并不意味着Document和Element是兄弟节点

例如学生和老师都可以直接继承自Person类,但是并不意味这学生和老师这两个类一定要存在兄弟关系

导航

节点之间的导航

如果我们获取到一个节点(Node)后,可以根据这个节点去获取其他的节点,我们称之为节点之间的导航(navigator)

和类的继承不同的是,类的继承描述的是类和类之间的关系

而导航描述的是实例对象和实例对象之间的关系

image.png

tip:

在DOM中,节点是包裹元素,注释和文本在内的,所以html结构中的换行也可能会被识别为一个单独的节点(Text)

示例

HTML结构如下

<!-- node navigator -->
我是纯文本节点
<div>我是html节点</div>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

对应的节点导航

const bodyEl = document.body

// 获取所有的节点
const childNodeList = bodyEl.childNodes

// 这里的text节点 除了第二个text节点是纯文本节点 -> 我是纯文本节点外
// 其余的text节点 都是node和node之间的换行符
// 而第二个text节点中,除了包含纯文本内容外,也包含了该node和其余node之间的换行符
console.log(childNodeList) // => NodeList(8) [text, comment, text, div, text, ul, text, script]
元素之间的导航

如果我们获取到一个元素(Element)后,可以根据这个元素去获取其他的元素,我们称之为元素之间的导航

因为node中,纯文本内容,注释不属于元素,所以元素之间的导航获取到的都是HTMLElement或SVGElement类型的实例

image.png

HTML结构如下

<!-- node navigator -->
我是纯文本节点
<div>我是html节点</div>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

对应的元素导航

const bodyEl = document.body

// 获取所有的元素
const childCollection = bodyEl.children

console.log(childCollection) // => HTMLCollection(3) [div, ul, script]
表格(table)元素的导航(navigator) - 了解
<table>
  <thead>
    <tr>
      <td>id</td>
      <td>name</td>
      <td>count</td>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>001</td>
      <td>mackbook</td>
      <td>112</td>
    </tr>
    <tr>
      <td>101</td>
      <td>iphone</td>
      <td>122</td>
    </tr>
  </tbody>
</table>
// 获取table元素
const tableEl = document.body.firstElementChild

// tableEl在vscode中编写的时候,可能无法进行准确的代码提示
// 这是因为对于vscode,firstElementChild 可以推导出的类型为HTMLElement
// 所以我们可以使用ts中的类型断言,来告诉vscode某一个节点更为具体的类型
// 但类型断言是ts的语法,只有typescript支持,其余格式文件或浏览器是不原生支持的
// 所以在调试的时候,需要移除对应的类型断言,也就是说该类型断言仅仅只是方便我们进行类型提示的一种临时解决方法而已
// const tableEl = document.body.firstElementChild as HTMLTableElement

// 获取一个节点或元素,如果没有获取到的时候,返回的结果都是null
// 如果获取一系列的节点或元素的时候,如果没有获取到的时候,返回的是一个类空数组对象

// 获取caption
console.log(tableEl.caption) // => null

// 获取thead
console.log(tableEl.tHead) // => thead元素

// 获取tbody
// 一个表格中可以存在多个tbody
// 但是在绝大多数情况下,一个表格中只会存在一个tbody
console.log(tableEl.tBodies) // => HTMLCollection [tbody]

// 获取tfoot
console.log(tableEl.tFoot) // => null

// 获取所有的row
console.log(tableEl.rows) // => HTMLCollection(3) [tr, tr, tr]

const tr = tableEl.rows[2]

// 获取某一个行中的所有的td和th
console.log(tr.cells) // => HTMLCollection(3) [td, td, td]

// sectionRowIndex -- 当前tr在tbody或thead中的索引
console.log(tr.sectionRowIndex) // => 1

// rowIndex -- 当前tr在整个table中的索引
console.log(tr.rowIndex) // => 2

// cellIndex -- cell在当前tr中的索引值
console.log(td.cellIndex) // => 2
表单(form)元素的导航(navigator) - 了解
<form>
  <input name="username" type="text">
  <input name="password" type="password">
  <select name="fruits">
    <option value="apple">苹果</option>
    <option value="orange">橙子</option>
  </select>
</form>
// 获取HTML中所有的form元素
// 使用document.forms获取的表单具有更为具体的类型 - HTMLFormElement
const formEl = document.forms[0]

console.log(formEl) // => 表单元素

// 表单中所有的元素
console.log(formEl.elements) // => HTMLFormControlsCollection(3) [input, input, select, username: input, password: input, fruits: select]

const elems = formEl.elements

// 如果表单元素上存在对应的name,那么我们可以通过对应的name来获取到对应的元素
console.log(elems.username) // => <input name="username" type="text">

const inputEl = elems.username

console.log(inputEl.value)

获取元素的方式

当元素彼此靠近或者相邻时,DOM 导航属性(navigation property)非常有用

但是,在实际开发中,我们更多的情况下,是希望可以任意的获取到某个元素或某些元素

于是DOM为我们提供了对应的方法

image.png

  1. 可以在元素上调用 - 也就是既可以通过document来进行调用,也可以通过某一个具体的元素来进行调用
// 通过document来进行调用
const boxEl1 = document.querySelector('.box')


const container = document.querySelector('.container')
// 通过某个具体的元素来进行调用
const boxEl2 = container.querySelector('.box')
  1. 实时的 - 也就是在获取到元素后,如果后续dom发生了改变,其之前获取的变量值会不会实时自动更新
const ulEl = document.getElementById('ul')
const liEls = document.querySelectorAll('li')
const liList = document.getElementsByTagName('li')

console.log(liEls) // => NodeList(3) [li, li, li]
console.log(liList) // => HTMLCollection(3) [li, li, li]

setTimeout(() => {
  ulEl.innerHTML = `${ulEl.innerHTML} <li>4</li>`
  console.log(liEls) // => NodeList(3) [li, li, li]
  console.log(liList) // => HTMLCollection(4) [li, li, li, li]
}, 2000)
  1. querySelectorquerySelectorAll
  • querySelector获取到的是第一个符合条件的元素
  • querySelectorAll获取到的是所有符合条件的元素
  • querySelectorquerySelectorAllgetElementsByTagName是唯一几个支持通配符选择器的方法
  1. querySelectorquerySelectorAll的返回值结果的类型是NodeList,不是HTMLCollection

    NodeListHTMLCollection是两个伪数组对象, 且都是可迭代对象,所以可以使用for-of循环

    NodeList上单独实现了forEach方法,而HTMLCollection上没有单独实现forEach方法

节点属性

nodeType

不同的节点类型有可能有不同的属性, nodeType 属性提供了一种获取节点类型的方法,它是一个数值型值(numeric value)

image.png

nodeName/tagName

属性说明
nodeName获取node节点的名字
1. 注释节点 - #comment
2. 文本节点 - #text
3. 元素节点 - 全部大写形式表示的元素名 例如DIV
tagName获取元素的标签名称
只有元素才会有标签名
如果获取的是非元素节点,属性值为undefined
如果获取的是原生节点,属性值为全大写形式表示的元素名

node/nodeValue

nodeValue/data用于获取非元素节点的文本内容

nodeValue/data两个属性的功能是完全一致,除名字外,没有别的区别

<!-- comment  -->
lorem
<div class="box">
  <h2>hello world</h2>
</div>
const childNodes = document.body.childNodes

const commentNode = childNodes[1]
const textNode = childNodes[2]

console.log(commentNode.data) // => comment
console.log(textNode.data)
/*
  =>

  lorem

*/

innerHTML/textContent/outerHTML

innerHTML/textContent/outerHTML用于获取元素中的内容

innerHTMLouterHTML在解析的时候,需要先将字符串解析为对应的dom,在进行插入

textContent并不会进行相应的转换,而是直接将整个整体作为字符串进行插入

所以就解析效率而言,textContent更快,因此如果插入的内容仅仅是普通文本的时候,推荐使用textContent

属性说明
innerHTML1. 获取内容 --- 带HTML标签的内容字符串
2. 设置内容 --- 会解析内容字符串中的HTML标签
textContent1. 获取内容 --- 剔除HTML标签的内容字符串
2. 设置内容 --- 不会解析内容字符串总的HTML标签
3. 如果设置的内容不是字符串,会进行类型转换 --- 网页的本质就是一段特殊的字符串
outerHTML作用类似于innerHTML,区别是在获取的时候,输出的内容为innerHTML 加上元素本身
<!-- comment  -->
lorem
<div class="box">
  <h2>hello world</h2>
</div>
const childNodes = document.body.childNodes

const elNode = childNodes[3]

console.log(elNode.innerHTML)
/*
  =>

    <h2>hello world</h2>

*/

console.log(elNode.textContent)
/*
  =>

    hello world

*/

console.log(elNode.outerHTML)
/*
  =>
  <div class="box">
    <h2>hello world</h2>
  </div>
*/

// img会被作为html元素进行解析,所以界面中会出现对应的图片
elNode.innerHTML = '<img src="./images/favicon.png" />'

// img会被作为纯文本进行解析,所以界面中输出的内容为
// <img src="./images/favicon.png" />
elNode.textContent = '<img src="./images/favicon.png" />'

元素属性

我们知道,一个元素除了有开始标签、结束标签、内容之外,还有很多的属性(attribute)

浏览器在解析HTML元素时,会将对应的attribute也创建出来放到对应的元素对象上

  • 比如id、class就是全局的attribute,会有对应的id、class属性
  • 比如href属性是针对a元素的,type、value属性是针对input元素的

元素属性分类

类别说明
标准的attribute某些attribute属性是标准的,比如id、class、href、type、value等
tips:在这些标准属性中,有一部分是针对某一类特定的元素的
例如: type,value,name属性是针对于表单元素的,href是针对于a元素的
而其它的一些属性,如title,id,class,style是针对于所有元素的
非标准的attribute (自定义attribute)某些attribute属性是自定义的,比如abc、age、height等

hidden

hidden属性也是一个全局属性,可以用于设置元素隐藏,其本质是通过display: none;来实现的

const btnEl = document.getElementById('btn')

btn.onclick = () =>
  (document.getElementById('box').hidden =
    !document.getElementById('box').hidden)

属性的特点

  • 它们的名字是大小写不敏感的(id 与 ID 相同)
console.log(boxEl.hasAttribute('name'))
// 等价于
console.log(boxEl.hasAttribute('NAME'))
  • 它们的值总是字符串类型的
<input type="checkbox" checked> checkbox
const inputEl = document.querySelector('input')

// checked是boolean类型属性,而getAttribute方法的返回值一定是字符串
// 所以对于boolean类型属性,其返回值都是空字符串
console.log(inputEl.getAttribute('checked')) // => ''(空字符串)

元素属性操作

所有属性

所有属性(无论是标准的属性还是自定义的属性)都存在如下的属性和方法

属性或方法功能说明
elem.hasAttribute(name)检查属性是否存在返回布尔值
elem.getAttribute(name)获取属性值1. 有值返值,无值返null
2. 返回值的类型都是string
elem.setAttribute(name, value)设置属性值1. 有则修改,无则添加
2. 设置的属性值会自动转换为字符串类型 ( 如果不是字符串类型的话 )
elem.removeAttribute(name)移除属性1. 没有返回值
2. 移除失败,静默失效
attributes获取元素所有的属性集合1. 结果是一个可迭代的伪数组对象
2. 伪数组中的每一项都是一个由name属性和value属性组成的对象
+ name 表示 属性名
+ value 表示 熟悉值
<div class="box" id="dv" name="Klaus" age="23"></div>
// hasAttribute
const boxEl = document.getElementById('dv')

// 判断一个属性是否存在
console.log(boxEl.hasAttribute('name')) // => true
console.log(boxEl.hasAttribute('class')) // => true
console.log(boxEl.hasAttribute('height')) // => false
// getAttribute
const boxEl = document.getElementById('dv')

// 获取属性
console.log(boxEl.getAttribute('name')) // => Klaus
console.log(boxEl.getAttribute('class')) // => box

// 如果一个属性不存在,那么获取的值是null
console.log(boxEl.getAttribute('height')) // => null
// setAttribute
const boxEl = document.getElementById('dv')

// 设置属性,有则修改,无则添加
boxEl.setAttribute('name', 'Alex')
boxEl.setAttribute('class', 'foo')

// 所有的属性值,在被设置后,其都会被转换为字符串类型
// 也就是这里的 1.88 -> '1.88'
boxEl.setAttribute('height', 1.88)
// removeAttribute
const boxEl = document.getElementById('dv')

// 移除属性,没有返回值
boxEl.removeAttribute('class')
boxEl.removeAttribute('age')

// 移除不存在的时候,会静默失效
boxEl.removeAttribute('height')
const boxEl = document.getElementById('dv')

// boxEl.attributes 属性可以获取由当前元素中所有属性组成的可迭代对象
const attributes = boxEl.attributes

console.log(attributes.id) // -> 一个有name和age属性的对象
console.log(attributes.name) // -> 一个有name和age属性的对象

console.log(attributes.id.name) // => id - 属性key
console.log(attributes.id.value) // => dv - 属性值

// attributes是一个可迭代对象
for (const attr of attributes) {
  console.log(attr.name, attr.value)
  /*
    =>
    class box
    id dv
    name Klaus
    age 23
  */
}

标准属性

对于标准的attribute,会在DOM对象上创建与其对应的property属性

<!-- HTML元素标签上的属性 被称之为attribute -->
<div class="box" id="dv" name="Klaus" age="23"></div>
const boxEl = document.getElementById('dv')

// 对于标准的属性,会在对应dom对象中生成同名属性
// 这类属性被称之为property
console.log(boxEl.className) // => box

// 对于标准属性,如果某一个属性没有显示设置对应的值
// 那么在dom对象中,会存在对应属性的默认值,且该默认值为空字符串
console.log(boxEl.title) // => ''(空字符串)

// 非标准属性不会有property存在于对应的dom对象中
// 所以获取对应属性值的时候,得到的结果是undefined
console.log(boxEl.name) // => undefined
const boxEl = document.getElementById('dv')

console.log(boxEl.className) // => box

// attribute 和 property 是互通的
// 修改其中任何一个,另一个也会相应发生改变
boxEl.className = 'active'

console.log(boxEl.getAttribute('class')) // => active
const checkboxEl = document.getElementById('check')

// 通过property获取的属性值是有类型的
console.log(checkboxEl.checked, typeof checkboxEl.checked) // => true Boolean

// 通过attribute获取的属性值永远为字符串类型
console.log(
  checkboxEl.getAttribute('checked'),
  typeof checkboxEl.getAttribute('checked')
) // => ''(空字符串) String

// 所以除非特别情况,大多数情况下,设置、获取attribute,推荐使用property的方式
// 因为这种方式获取的值是有具体的类型的
const inputEl = document.getElementById('foo')

// 如果同时通过property属性和attribute属性去设置同一个属性的属性值的时候
// property和attribute之间没有优先级,他们的优先级取决于设置的先后顺序
inputEl.className = 'property'
inputEl.setAttribute('class', 'attribute')
// => inputEl.className -> 'attribute'

// 但是这些中有一个特例,那就是value属性
// 对于value属性,浏览器内部在设置对应的属性值的时候
// property方式设置的属性值,一定是在attribute方式设置的属性值后边设置的
// 所以下边示例代码中
// inputEl.value -> 'property方式设置的值'
inputEl.value = 'property方式设置的值'
inputEl.setAttribute('value', 'attribute方式设置的值')

非标准属性

如果我们需要获取元素中的非标准属性,除了可以通过操作attribute来进行进行操作

我们也可以使用data-*属性来进行操作

<!--
  data-*属性需要以data-开头
  对应的属性会被加载到dom对象的dateset property属性中
  并且对应的属性在被加入到dataset中的时候,会自动移除data-前缀
-->
<div class="box" data-name="Klaus" data-age="23">lorem</div>
const boxEl = document.querySelector('.box')

console.log(boxEl.dataset)
/*
  =>
  {
    age: "23",
    name: "Klaus"
  }
*/

console.log(boxEl.dataset.name) // => Klaus
console.log(boxEl.dataset.age) // => 23