瀏覽器的DOM和BOM
共 14815字,需瀏覽 30分鐘
·
2024-05-29 08:31
JavaScript有一個(gè)非常重要的運(yùn)行環(huán)境就是瀏覽器,而且瀏覽器本身又作為一個(gè)應(yīng)用程序需要對(duì)其本身進(jìn)行操作,所以通常瀏覽器會(huì)有對(duì)應(yīng)的對(duì)象模型(BOM,Browser Object Model)。
我們可以將BOM看成是連接JavaScript腳本與瀏覽器窗口的橋梁。
BOM主要包括一下的對(duì)象模型:
window:包括全局屬性、方法,控制瀏覽器窗口相關(guān)的屬性、方法; location:瀏覽器連接到的對(duì)象的位置(URL); history:操作瀏覽器的歷史; document:當(dāng)前窗口操作文檔的對(duì)象;
一. window對(duì)象
window對(duì)象在瀏覽器中有兩個(gè)身份:
-
身份一:全局對(duì)象。 -
我們知道ECMAScript其實(shí)是有一個(gè)全局對(duì)象的,這個(gè)全局對(duì)象在Node中是global; -
在瀏覽器中就是window對(duì)象; -
身份二:瀏覽器窗口對(duì)象。 -
作為瀏覽器窗口時(shí),提供了對(duì)瀏覽器操作的相關(guān)的API;
1.1. global全局對(duì)象
在瀏覽器中,window對(duì)象就是之前經(jīng)常提到的全局對(duì)象,也就是我們之前提到過GO對(duì)象:
-
比如在全局通過var聲明的變量,會(huì)被添加到GO中,也就是會(huì)被添加到window上; -
比如window默認(rèn)給我們提供了全局的函數(shù)和類:setTimeout、Math、Date、Object等;
通過var聲明的變量:
var message = "Hello World"
function foo() {
console.log("foo function")
}
console.log(window.message)
window.foo()
全局提供的類和方法:
window.setTimeout(() => {
console.log("setTimeout")
}, 1000)
const obj = new window.Object()
console.log(obj)
const date = new window.Date()
console.log(date)
這些用法是我們之前講過的,并且也是作為JavaScript語言本身所擁有的一些特性。
那么接下來我們來看一下作為窗口對(duì)象,它擁有哪些特性。
1.2. window窗口對(duì)象
事實(shí)上window對(duì)象上肩負(fù)的重?fù)?dān)是非常大的:
-
第一:包含大量的屬性,localStorage、console、location、history、screenX、scrollX等等(大概60+個(gè)屬性); -
第二:包含大量的方法,alert、close、scrollTo、open等等(大概40+個(gè)方法); -
第三:包含大量的事件,focus、blur、load、hashchange等等(大概30+個(gè)事件); -
第四:包含從EventTarget繼承過來的方法,addEventListener、removeEventListener、dispatchEventListener方法;
那么這些大量的屬性、方法、事件在哪里查看呢?
-
MDN文檔:https://developer.mozilla.org/zh-CN/docs/Web/API/Window
查看MDN文檔時(shí),我們會(huì)發(fā)現(xiàn)有很多不同的符號(hào),這里我解釋一下是什么意思:
-
刪除符號(hào):表示這個(gè)API已經(jīng)廢棄,不推薦繼續(xù)使用了。
-
點(diǎn)踩符號(hào):表示這個(gè)API不屬于W3C規(guī)范,某些瀏覽器有實(shí)現(xiàn)(所以兼容性的問題)
-
實(shí)驗(yàn)符號(hào):該API是實(shí)驗(yàn)性特性,以后可能會(huì)修改,并且存在兼容性問題。
實(shí)驗(yàn)符號(hào)
1.2.1. 常見的屬性
// 瀏覽器高度
console.log(window.outerHeight)
console.log(window.innerHeight)
console.log("screenX:", window.screenX)
console.log("screenY:", window.screenY)
// 監(jiān)聽
window.addEventListener("scroll", (event) => {
console.log(window.scrollY)
console.log(window.scrollX)
})
1.2.2. 常見的方法
// alert("Hello World")
// close方法
const closeBtn = document.querySelector("#close")
closeBtn.onclick = function() {
close()
}
// moveTo
const scrollBtn = document.querySelector("#scroll")
scrollBtn.onclick = function() {
scrollTo({ top: 1000 })
}
// 打開新創(chuàng)建
const openBtn = document.querySelector("#open")
openBtn.onclick = function() {
open("./about.html", "_self")
}
1.2.3. 常見的事件
window.onfocus = function() {
console.log("窗口獲取到焦點(diǎn)")
}
window.onblur = function() {
console.log("窗口失去了焦點(diǎn)")
}
// 整個(gè)頁面以及所有的資源都加載完成
window.onload = function() {
console.log("頁面加載完成")
}
// hash改變
const hashBtn = document.querySelector("#hash")
hashBtn.onclick = function() {
location.hash = "aaa"
}
window.onhashchange = function() {
console.log("hash被修改了")
}
1.2.4. EventTarget
Window繼承自EventTarget,所以會(huì)繼承其中的屬性和方法:
-
addEventListener:注冊(cè)某個(gè)事件類型以及事件處理函數(shù); -
removeEventListener:移除某個(gè)事件類型以及事件處理函數(shù); -
dispatchEvent:派發(fā)某個(gè)事件類型到EventTarget上;
const scrollHandler = () => {
console.log("window發(fā)生了滾動(dòng)~")
}
const clickHandler = () => {
console.log("window發(fā)生了點(diǎn)擊~")
}
window.addEventListener("scroll", scrollHandler)
window.addEventListener("click", clickHandler)
const removeBtn = document.querySelector("#removeEvent")
removeBtn.onclick = function() {
console.log("-----")
window.removeEventListener("click", clickHandler)
window.removeEventListener("scroll", scrollHandler)
}
自己來派發(fā)事件:
const dispatchBtn = document.querySelector("#dispatch")
dispatchBtn.onclick = function() {
window.dispatchEvent(new Event("coderwhy"))
}
window.addEventListener("coderwhy", () => {
console.log("監(jiān)聽到了coderwhy事件")
})
默認(rèn)事件監(jiān)聽:
-
https://developer.mozilla.org/zh-CN/docs/Web/Events
1.3. location位置
1.3.1. 常見的屬性
比如我們有一個(gè)地址:
-
http://coderwhy:[email protected]:5500/27_%E6%B5%8F%E8%A7%88%E5%99%A8%E5%AF%B9%E8%B1%A1/index.html?name=why&age=18#abc
// Location類型的對(duì)象
console.log(window.location)
// href: 當(dāng)前window對(duì)應(yīng)的超鏈接URL, 整個(gè)URL
console.log(location.href)
// protocol: 當(dāng)前的協(xié)議
console.log(location.protocol)
// host: 主機(jī)地址
console.log(location.host)
// hostname: 主機(jī)地址(不帶端口)
console.log(location.hostname)
// port: 端口
console.log(location.port)
// pathname: 路徑
console.log(location.pathname)
// search: 查詢字符串
console.log(location.search)
// hash:
console.log(location.hash)
// username:
console.log(location.username)
// password
console.log(location.password)
我們會(huì)發(fā)現(xiàn)location其實(shí)是URL的一個(gè)抽象實(shí)現(xiàn):
1.3.2. 常見的操作
location有如下常用的方法:
-
assign:賦值一個(gè)新的URL,并且跳轉(zhuǎn)到該URL中; -
replace:打開一個(gè)新的URL,并且跳轉(zhuǎn)到該URL中(不同的是不會(huì)在瀏覽記錄中留下之前的記錄); -
reload:重新加載頁面,可以傳入一個(gè)Boolean類型;
const locationBtn = document.querySelector("#location")
locationBtn.onclick = function() {
// location.assign("http://www.baidu.com")
// location.replace("http://www.baidu.com")
location.reload()
}
另外我們修改location的很多屬性,也會(huì)造成瀏覽器重新加載地址:
// location.href = "http://www.baidu.com"
// location.host = "www.baidu.com:80"
1.4. history屬性
history對(duì)象允許我們?cè)L問瀏覽器曾經(jīng)的會(huì)話歷史記錄。
有兩個(gè)屬性:
-
length:會(huì)話中的記錄條數(shù); -
state:當(dāng)前保留的狀態(tài)值;
有五個(gè)方法:
-
back():返回上一頁,等價(jià)于history.go(-1); -
forward():前進(jìn)下一頁,等價(jià)于history.go(1); -
go():加載歷史中的某一頁; -
pushState():打開一個(gè)指定的地址; -
replaceState():打開一個(gè)新的地址,并且使用replace;
console.log(history.length)
console.log(history.state)
const jumpBtn = document.querySelector("#jump")
const backBtn = document.querySelector("#back")
jumpBtn.onclick = function() {
history.pushState({name: "why"}, "11", "aaa")
console.log(history.length, history.state)
}
backBtn.onclick = function() {
history.back()
console.log(history.length, history.state)
}
二. document對(duì)象
2.1. 整體架構(gòu)
瀏覽器是用來展示網(wǎng)頁的,而網(wǎng)頁中最重要的就是里面各種的標(biāo)簽元素,JavaScript很多時(shí)候是需要操作這些元素的。
-
JavaScript如何操作元素呢?通過Document Object Model(DOM,文檔對(duì)象模型)。 -
DOM給我們提供了一系列的模型和對(duì)象,讓我們可以方便的來操作Web頁面。
當(dāng)我們有一個(gè)頁面時(shí),這個(gè)頁面就可以用上面的模式來表示出來:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div>
<!-- 我是注釋 -->
<h2>哈哈哈</h2>
<strong>呵呵呵</strong>
</div>
</body>
</html>
這個(gè)網(wǎng)頁會(huì)形式一個(gè)對(duì)象樹,這些對(duì)象都是我們上面的模型所創(chuàng)建出來的:
-
比如整個(gè)的頁面是HTMLDocument對(duì)象; -
比如body、div、h2等都是HTMLElement對(duì)象; -
比如哈哈哈、呵呵呵文本都是Text對(duì)象; -
比如我是注釋都是Comment對(duì)象; -
比如其中的屬性是Attr對(duì)象;
2.2. EventTarget
document.addEventListener("click", () => {
console.log("ducument被點(diǎn)擊")
})
const boxDiv = document.querySelector("#box")
boxDiv.addEventListener("click", () => {
console.log("box被點(diǎn)擊")
})
2.3. Node節(jié)點(diǎn)
所有的DOM節(jié)點(diǎn)類型都繼承自Node接口。
-
https://developer.mozilla.org/zh-CN/docs/Web/API/Node
Node有幾個(gè)非常重要的屬性:
nodeName:node節(jié)點(diǎn)的名稱。
nodeType:可以區(qū)分節(jié)點(diǎn)的類型。
childNodes
firstChild
2.4. document
// title
document.title = "Coderwhy"
// body/head
console.log(document.body)
console.log(document.head)
// children
console.log(document.children)
// location
console.log(document.location)
console.log(window.location === document.location)
// 方法
// 1.創(chuàng)建和添加createElement
const h2El = document.createElement("h2")
h2El.textContent = "Hello World"
document.body.appendChild(h2El)
// 2.刪除元素
setTimeout(() => {
document.body.removeChild(h2El)
}, 2000);
// 3.獲取元素
const el1 = document.getElementsByName("abc")
const el2 = document.getElementsByTagName("div")
console.log(el1, el2)
const el3 = document.querySelector("div")
const el4 = document.querySelectorAll("div")
console.log(el3, el4)
2.5. element
const boxDiv = document.querySelector("#box")
// 1.獲取子元素
console.log(boxDiv.children)
console.log(boxDiv.childNodes)
// 2.tagName
console.log(boxDiv.tagName)
// 3.id/class
console.log(boxDiv.id)
console.log(boxDiv.className)
console.log(boxDiv.classList)
// 4.clientWidth/clientHeight/clientLeft/clientTop
console.log(boxDiv.clientWidth, boxDiv.clientHeight)
// 邊框?qū)挾群透叨?/span>
console.log(boxDiv.clientLeft, boxDiv.clientTop)
// offsetWidth/offsetHeight
console.log(boxDiv.offsetLeft, boxDiv.offsetTop)
// 方法(操作屬性)
const attr1 = boxDiv.getAttribute("name")
console.log(attr1)
boxDiv.setAttribute("height", "1.88")
三. 事件處理
3.1. 事件監(jiān)聽
前面我們講到了JavaScript腳本和瀏覽器之間交互時(shí),瀏覽器給我們提供的BOM、DOM等一些對(duì)象模型。
事實(shí)上還有一種需要和瀏覽器經(jīng)常交互的事情就是事件監(jiān)聽:
-
瀏覽器在某個(gè)時(shí)刻可能會(huì)發(fā)生一些事件,比如鼠標(biāo)點(diǎn)擊、移動(dòng)、滾動(dòng)、獲取、失去焦點(diǎn)、輸入內(nèi)容等等一系列的事件; -
我們需要以某種方式(代碼)來對(duì)其進(jìn)行響應(yīng),進(jìn)行一些事件的處理;
在Web當(dāng)中,事件在瀏覽器窗口中被觸發(fā),并且通過綁定到某些元素上或者瀏覽器窗口本身,那么我們就可以給這些元素或者window窗口來綁定事件的處理程序,來對(duì)事件進(jìn)行監(jiān)聽。
事件監(jiān)聽方式一:
<button onclick="console.log('按鈕1被點(diǎn)擊了')">按鈕1</button>
<button onclick="btnClick()">按鈕2</button>
<script>
function btnClick() {
console.log("按鈕2被點(diǎn)擊了")
}
</script>
事件監(jiān)聽方式二:
const btn3 = document.querySelector("#btn3")
btn3.onclick = function() {
console.log("按鈕3被點(diǎn)擊了")
}
事件監(jiān)聽方式三:
btn3.addEventListener("click", () => {
console.log("按鈕3被點(diǎn)擊了")
})
3.2. 事件流
事實(shí)上對(duì)于事件有一個(gè)概念叫做事件流,為什么會(huì)產(chǎn)生事件流呢?
-
我們可以想到一個(gè)問題:當(dāng)我們?cè)跒g覽器上對(duì)著一個(gè)元素點(diǎn)擊時(shí),你點(diǎn)擊的不僅僅是這個(gè)元素本身; -
這是因?yàn)槲覀兊腍TML元素是存在父子元素疊加層級(jí)的; -
比如一個(gè)span元素是放在div元素上的,div元素是放在body元素上的,body元素是放在html元素上的;
<body>
<div class="box">
<span class="span">我是span元素</span>
</div>
</body>
我們可以監(jiān)聽這些元素的點(diǎn)擊:
document.body.addEventListener("click", () => {
console.log("body被點(diǎn)擊")
})
const divEl = document.querySelector(".box")
const spanEl = document.querySelector(".span")
divEl.addEventListener("click", () => {
console.log("div被點(diǎn)擊")
})
spanEl.addEventListener("click", () => {
console.log("span被點(diǎn)擊")
})
我們會(huì)發(fā)現(xiàn)默認(rèn)情況下事件是從最內(nèi)層的span向外依次傳遞的順序,這個(gè)順序我們稱之為事件冒泡(Event Bubble)。
-
事實(shí)上,還有另外一種監(jiān)聽事件流的方式就是從外層到內(nèi)層(body -> span),這種稱之為事件捕獲(Event Capture); -
為什么會(huì)產(chǎn)生兩種不同的處理流呢? -
這是因?yàn)樵缙跒g覽器開發(fā)時(shí),不管是IE還是Netscape公司都發(fā)現(xiàn)了這個(gè)問題,但是他們采用了完全相反的事件流來對(duì)事件進(jìn)行了傳遞; -
IE采用了事件冒泡的方式,Netscape采用了事件捕獲的方式;
那么我們?nèi)绾稳ケO(jiān)聽事件捕獲的過程呢?
// 事件捕獲的監(jiān)聽
document.body.addEventListener("click", () => {
console.log("body被點(diǎn)擊")
}, true)
divEl.addEventListener("click", () => {
console.log("div被點(diǎn)擊")
}, true)
spanEl.addEventListener("click", () => {
console.log("span被點(diǎn)擊")
}, true)
并且會(huì)發(fā)現(xiàn),如果我們同時(shí)有事件冒泡和時(shí)間捕獲的監(jiān)聽,那么會(huì)優(yōu)先監(jiān)聽到事件捕獲的:
事件捕獲階段: body被點(diǎn)擊
事件捕獲階段: div被點(diǎn)擊
事件捕獲階段: span被點(diǎn)擊
事件冒泡階段: span被點(diǎn)擊
事件冒泡階段: div被點(diǎn)擊
事件冒泡階段: body被點(diǎn)擊
3.3. 事件對(duì)象
當(dāng)一個(gè)事件發(fā)生時(shí),就會(huì)有和這個(gè)事件相關(guān)的很多信息:
-
比如事件的類型是什么,你點(diǎn)擊的是哪一個(gè)元素,點(diǎn)擊的位置是哪里等等相關(guān)的信息; -
那么這些信息會(huì)被封裝到一個(gè)Event對(duì)象中; -
該對(duì)象給我們提供了想要的一些屬性,以及可以通過該對(duì)象進(jìn)行某些操作;
常見的屬性:
-
type:事件的類型; -
target:當(dāng)前事件發(fā)生的元素; -
currentTarget:當(dāng)前處理事件的元素; -
offsetX、offsetY:點(diǎn)擊元素的位置;
spanEl.addEventListener("click", (event) => {
console.log("事件冒泡階段: span被點(diǎn)擊:", event)
console.log("事件的類型:", event.type)
console.log("點(diǎn)擊的元素:", event.target, event.currentTarget)
console.log("點(diǎn)擊的位置:", event.offsetX, event.offsetY)
console.log("點(diǎn)擊的數(shù)據(jù):", event.target.dataset)
})
常見的方法:
-
preventDefault:取消事件的默認(rèn)行為; -
stopPropagation:阻止事件的進(jìn)一步傳遞;
// 阻止a元素的默認(rèn)行為
const aEl = document.querySelector("a")
aEl.addEventListener("click", (event) => {
event.preventDefault()
window.open(aEl.href)
})
// 事件捕獲的監(jiān)聽
document.body.addEventListener("click", (event) => {
console.log("事件捕獲階段: body被點(diǎn)擊")
}, true)
divEl.addEventListener("click", (event) => {
console.log("事件捕獲階段: div被點(diǎn)擊")
event.stopPropagation()
}, true)
spanEl.addEventListener("click", () => {
console.log("事件捕獲階段: span被點(diǎn)擊")
}, true)
事件類型:https://developer.mozilla.org/zh-CN/docs/Web/Events
