前端 - JS DOM
初识DOM
DOM
JS通过文档对象模型 (Document Object Model, DOM) 对HTML文档进行操作:
- 文档:整个HTML页面的文档
- 对象:将网页中的每一个部分都转换为了对象
- 模型:使用模型来表示对象之间的关系
节点
节点(Node)是构成页面最基本的组成部分,HTML标签、属性、文本、注释都可以称为节点,不同类型的节点,拥有不同的属性和方法。
- 文档节点:整个HTML文档
- 元素节点:文档中的HTML标签
- 属性节点:元素的属性
- 文本节点:标签中的文本
节点的属性如下:
节点 | nodeName |
nodeType |
nodeValue |
---|---|---|---|
文档节点 | #document |
9 | null |
元素节点 | 标签名 | 1 | null |
属性节点 | 属性名 | 2 | 属性值 |
文本节点 | #text |
3 | 文本内容 |
浏览器已经为我们提供了文档节点对象document
,它是window
对象的一个属性,即一个全局变量,所以我们可以直接使用。
console.log(document.nodeName); // #document
console.log(document.nodeType); // 9
console.log(document.nodeValue); // null
事件
事件是文档或浏览器窗口发生的一些特定的交互瞬间,JS和HTML之间的交互是通过事件实现的。
var btn = document.getElementById('test-btn');
console.log(btn); // <button id="test-btn">Test</button>
/* 给onclick事件绑定处理函数 */
btn.onclick = function() {
console.log('test button onclick');
};
浏览器在加载页面时,按照自上而下的顺序,读一行运行一行,所以我们需要将<script>
标签写在最后。如果要将<script>
标签写在<head>
标签,可以进行下面的操作:
window.onload = function() {
/* js脚本内容 */
};
onload
事件在加载完触发,window
对象的onload
事件就在页面加载完成之后执行。
DOM查询
获取元素节点
- 通过
document
对象调用
方法 | 描述 |
---|---|
getElementById() |
通过id属性获取一个元素节点对象 |
getElementsByTagName() |
通过标签名获取一组元素节点对象 |
getElementsByName() |
通过name属性获取一组元素节点对象 |
getElementsByClassName() |
通过class属性获取一组元素节点对象 |
getElementsByTagName()
等方法,返回一个类数组对象,可以用索引操作,并用length
获取查询到的个数。即使结果小于等于一个,也会被封装在数组中返回。
- 对于一般的属性,可以直接获取,但是
class
需要替换成className
<input class="input-test" type="radio" name="gender" value="male">
var input = document.getElementsByName('gender')[0];
console.log(input.name); // gender
console.log(input.tagName); // INPUT
console.log(input.value); // male
console.log(input.innerHTML); // 对于自结束标签,innerHTML返回空
/* 不能直接使用class */
console.log(input.class); // undefined
console.log(input.className); // input-test
获取元素节点的子节点
- 通过具体的元素节点调用:
方法 | 类型 | 描述 |
---|---|---|
getElementsByTagName() |
方法 | 返回当前节点的指定标签的后代节点 |
childNodes |
属性 | 返回当前节点的所有字节点 |
firstChild |
属性 | 返回当前节点的第一个字节点 |
lastChild |
属性 | 返回当前节点的最后一个字节点 |
childNodes
也会获取文本节点,包括标签之间的空格
<ul class="box">
<li>abc1</li>
<li>abc2</li>
<li>abc3</li>
</ul>
var box = document.getElementsByClassName('box')[0];
children = box.childNodes;
console.log(children[2].nodeValue); // 7
/*
NodeList [#text "
", <li>, #text "
", <li>, #text "
", <li>, #text "
"]
*/
console.log(children);
/* firstChild和lastChild同样也会获取到文本节点 */
console.log(children[0]); // #text " "
console.log(box.firstChild); // #text " "
console.log(children[children.length - 1]); // #text " "
console.log(box.lastChild); // #text " "
获取元素节点的父节点与兄弟节点
- 通过具体的元素节点调用:
方法 | 类型 | 描述 |
---|---|---|
parentNode |
属性 | 返回当前节点的父节点 |
previousSibling |
属性 | 返回当前节点的前一个兄弟节点 |
nextSibling |
属性 | 返回当前节点的后一个兄弟节点 |
- 注意
innerHTML
和innerText
的区别
<body>
<ul class="cities">
<li>Beijing</li>
<li>Shanghai</li>
<li id="hangzhou">Hangzhou</li>
</ul>
</body>
var hz = document.getElementById('hangzhou');
var cities = hz.parentNode;
/*
<li>Beijing</li>
<li>Shanghai</li>
<li id="hangzhou">Hangzhou</li>
*/
console.log(cities.innerHTML);
/*
Beijing
Shanghai
Hangzhou
*/
console.log(cities.innerText);
previousSibling
和nextSibling
也可能获取到空白的文本节点- 如果需要确保获取到的是元素节点,可以使用
previousElementSibling
和nextElementSibling
var hz = document.getElementById('hangzhou');
var space = hz.previousSibling;
var sh = hz.previousElementSibling;
/* #text "
" */
console.log(space);
/* <li>Shanghai</li> */
console.log(sh);
其他常用查询
查询方式 | 描述 |
---|---|
document.body |
获取<body> 标签 |
document.html |
获取<html> 标签 |
- 获取
body
标签与获取html
标签
/* 二者等价 */
var body = document.body;
var body1 = document.getElementsByTagName('body')[0];
console.log(body == body1); // true
/* 二者等价 */
var html = document.documentElement;
var html1 = document.getElementsByTagName('html')[0];
console.log(html == html1); // true
查询方式 | 描述 |
---|---|
querySelector() |
使用CSS选择器查询,只会返回第一个符合条件的元素节点 |
querySelectorAll() |
使用CSS选择器查询,会以NodeList 形式返回所有符合条件的元素节点 |
- 使用CSS选择器查找
var sh = document.querySelector('ul li:nth-child(2)');
console.log(sh); // <li>Shanghai</li>
/* NodeList (3)
<li>Beijing</li>
<li>Shanghai</li>
<li id="hangzhou">Hangzhou</li>
*/
var lis = document.querySelectorAll('ul li');
console.log(lis);
DOM增删改
方法 | 描述 |
---|---|
createElement() |
创建一个元素节点 |
createTextNode() |
创建一个文本节点 |
appendChild() |
给当前节点添加一个子节点 |
- 创建一个广州节点,并添加到列表中:
var gz = document.createElement('li'); // 创建元素节点
var gzText = document.createTextNode('Guangzhou'); // 创建文本节点
gz.appendChild(gzText); // 将文本节点设置为元素节点的子节点
var cities = document.getElementsByClassName('cities')[0]; // 获取ul节点
cities.appendChild(gz); // 将gz节点设置为ul节点的子节点
- 通过
innerHTML
,将广州节点追加到列表尾部:
cities.innerHTML += '<li>Guangzhou</li>';
方法 | 描述 |
---|---|
insertBefore() |
在指定的子节点前插入新的子节点 |
replaceChild() |
使用新节点替换指定的子节点 |
removeChild() |
删除指定的子节点 |
- 创建广州节点后,插入到杭州节点之前:
var cities = document.getElementsByClassName('cities')[0]; // 获取ul节点
var hz = document.getElementById('hangzhou');
cities.insertBefore(gz, hz); // 在杭州之前插入广州节点
- 创建广州节点之后,替换杭州节点:
cities.replaceChild(gz, hz); // 使用广州节点替换杭州节点
- 删除杭州节点:
hz.parentNode.removeChild(hz); // 删除杭州节点
阻止标签的默认行为
通过返回false
阻止<a>
标签默认的跳转行为:
<a id="baidu" href="https://www.baidu.com">百度</a>
var baidu = document.getElementById('baidu');
baidu.onclick = function() {
return false;
};
响应函数
创建3个<a>
标签,每个标签记录循环中i
的值
for (var i = 0; i < 3; i++) {
// 创建a标签
var a = document.createElement('a');
a.innerHTML = 'i = ' + i;
a.href = 'javascript:;';
// 为a标签绑定响应函数, 记录i的值
a.onclick = function() {
console.log('i = ' + i);
}
// 将a标签追加到body末尾
document.body.appendChild(a);
// 追加br标签, 用于换行
var br = document.createElement('br')
document.body.appendChild(br);
}
然而,尽管创建的HTML标签如下:
<body>
<a href="javascript:;">i = 0</a> <br>
<a href="javascript:;">i = 1</a> <br>
<a href="javascript:;">i = 2</a> <br>
</body>
但是点击每个<a>
标签,都只会显示i = 3
,这是因为:
for
循环会在页面加载完成后就执行完毕- 响应函数会在被点击时才执行
样式
内联样式
通过style
修改相应的样式,例如box.style.width = '300px';
:
- 对于
background-color
这种样式,由于在JS中不合法,需要修改为驼峰命名法
<div id="box"></div>
<button id="btn">Change</button>
#box {
width: 150px;
height: 100px;
background-color: orangered;
}
var btn = document.getElementById('btn');
var box = document.getElementById('box');
btn.onclick = function() {
box.style.width = '300px';
box.style.height = '200px';
box.style.backgroundColor = 'skyblue';
};
我们通过上面的修改style
方式设置的样式都是内联样式,观察HTML文件可以发现,点按按钮之后,标签变成了:
<div id="box" style="width: 300px; height: 200px; background-color: skyblue;"></div>
所以需要注意:
- 如果没有写内联样式,直接使用
box.style.width
读到的是一个string
空串 - 此时,是由于内联样式的高优先级,覆盖了CSS文件中的样式,才显示出的新样式
- 如果CSS文件中出现了
!important
,则内联样式的优先级不足,修改无效:
#box {
width: 150px;
height: 100px;
background-color: orangered !important;
}
读取元素样式
获取元素当前显示的样式:
- 在IE8中使用
currentStyle
获取,例如box.currentStyle.width
- 在其他浏览器使用
window
对象的getComputedStyle()
方法获取,返回一个CSSStyleDeclaration
对象,需要两个参数:- 第一个:需要获取样式的元素
- 第二个:一个伪元素,一般为
null
var box = document.getElementById('box');
// 获取所有样式
var boxStyle = getComputedStyle(box, null);
console.log(boxStyle.height); // 100px
console.log(boxStyle['width']); // 150px
其他常用属性与方法
宽高
获取元素高度和宽度的属性如下表,需要注意:
- 不带
px
,直接返回数值 client
包括内容区和内边距offset
包括内容区、内边距和边框- 都是只读的
属性 | 描述 |
---|---|
clientHeight |
元素高度 (内容区和内边距) |
clientWidth |
元素宽度 (内容区和内边距) |
offsetHeight |
元素高度 (内容区、内边距和边框) |
offsetWidth |
元素宽度 (内容区、内边距和边框) |
<div id="box"></div>
#box {
width: 150px;
height: 100px;
margin: 30px;
padding: 5px;
border: 20px solid orange;
background-color: orangered;
}
var box = document.getElementById('box');
var boxStyle = getComputedStyle(box, null);
console.log(boxStyle.height); // 100px
/* 包括内容区和内边距 */
console.log(box.clientHeight); // 110
console.log(box.clientWidth); // 160
/* 包括内容区、内边距和边框 */
console.log(box.offsetHeight); // 150
console.log(box.offsetWidth); // 200
定位相关
属性 | 描述 |
---|---|
offsetParent |
距离当前元素最近的开启定位的祖先元素,如果都没有定位,则为body |
offsetLeft |
当前元素相对于其定位元素的水平偏移量 |
offsetTop |
当前元素相对于其定位元素的垂直偏移量 |
<div id="box1">
<div>
<div id="box2"></div>
</div>
</div>
#box1 {
position: relative;
height: 400px;
background-color: skyblue;
}
#box2 {
position: absolute;
left: 15px;
top: 20px;
width: 150px;
height: 100px;
margin: 30px;
padding: 5px;
border: 20px solid orange;
background-color: orangered;
}
var box2 = document.getElementById('box2');
var box1 = box2.offsetParent; // 定位的祖先元素为box1
console.log(box1.offsetParent); // <body>…</body>
console.log(box2.offsetTop); // 45
console.log(box2.offsetLeft); // 50
滚动相关
<div id="box">
<p>123123123123123123123123123123123123123123123123
123123123123123123123123123123123123123123123123
123123123123123123123123123123123123123123123123
123123123123123123123123123123123123123123123123
</p>
</div>
#box {
height: 80px;
width: 100px;
background-color: skyblue;
overflow: scroll;
}
效果如下:
123123123123123123123123123123123123123123123123 123123123123123123123123123123123123123123123123 123123123123123123123123123123123123123123123123 123123123123123123123123123123123123123123123123
属性 | 描述 |
---|---|
clientHeight |
元素高度 (内容区和内边距) |
clientWidth |
元素宽度 (内容区和内边距) |
offsetHeight |
元素高度 (内容区、内边距和边框) |
offsetWidth |
元素宽度 (内容区、内边距和边框) |
var box = document.getElementById('box');
var p = document.getElementsByTagName('p')[0];
console.log(box.clientWidth); // 100
console.log(box.offsetWidth); // 100
console.log(box.scrollWidth); // 360
console.log(box.clientHeight); // 80
console.log(box.offsetHeight); // 80
console.log(box.scrollHeight); // 88
console.log(p.scrollWidth); // 360
console.log(p.scrollHeight); // 88
适当滑动box
中的文字,得到:
console.log(box.scrollLeft); // 168
console.log(box.scrollTop); // 5
console.log(p.scrollLeft); // 0
console.log(p.scrollTop); // 0
- 当
box.scrollWidth - box.scrollLeft == box.clientWidth
返回true
时,说明水平滚动条到底了 - 当
box.scrollHeight - box.scrollTop == box.clientHeight
返回true
时,说明垂直滚动条到底了
设计一个用户读完声明才能注册的功能:
var box = document.getElementById('box'); // 声明所在的标签
var btn = document.getElementById('btn'); // 注册按钮
btn.disabled = true; // 设置注册按钮不可用
box.onscroll = function() {
if (!box.disabled && box.scrollHeight - box.scrollTop == box.clientHeight
&& box.scrollWidth - box.scrollLeft == box.clientWidth) {
btn.disabled = false; // 设置注册按钮可用
}
};
var box = document.getElementById('box'); // 声明所在的标签
var btn = document.getElementById('btn'); // 注册按钮
btn.disabled = true; // 设置注册按钮不可用
box.onscroll = function () {
if (btn.disabled
&& box.scrollHeight - box.scrollTop == box.clientHeight
&& box.scrollWidth - box.scrollLeft == box.clientWidth) {
btn.disabled = false; // 设置注册按钮可用
}
};
事件对象
当事件的响应函数被触发时,浏览器每次都会将一个事件对象作为实参传递进响应函数,对象中封装了 事件相关的信息。例如,在鼠标经过box
标签时,实时显示当前鼠标的坐标:
box.onmousemove = function(event) {
console.log('(' + event.clientX + ', ' + event.clientY + ')');
};
实现<div>
盒子跟随鼠标移动的功能:
/*
* clientX和clientY是鼠标相对于可见窗口的坐标(不受滚动条影响)
* pageX和pageY可以获取鼠标相对于页面的坐标
*/
document.onmousemove = function(event) {
box.style.left = event.pageX + 'px';
box.style.top = event.pageY + 'px';
};
事件的冒泡
冒泡指的是事件的向上传导,当后代元素的事件触发时,其祖先元素的相同事件也会触发。
<body>
<div id="box">
<span id="inside">test</span>
</div>
</body>
var body = document.body;
var box = document.getElementById('box');
var inside = document.getElementById('inside');
/* 绑定响应函数 */
body.onclick = function() {
console.log('Click body!');
};
box.onclick = function() {
console.log('Click box!');
};
inside.onclick = function() {
console.log('Click inside!');
};
点击test
文本之后,输出如下:
Click inside!
Click box!
Click body!
- 如果想阻止事件冒泡,可以通过事件对象取消
inside.onclick = function(event) {
console.log('Click inside!');
event.stopPropagation(); // 取消事件冒泡
// event.cancelBubble = true;
};
点击test
文本之后,输出如下:
Click inside!
cancelBubble
标记为Deprecated,实测可用,MDN文档描述如下:
Event.cancelBubble
属性是Event.stopPropagation()
的一个曾用名。在从事件处理程序返回之前将其值设置为true可阻止事件的传播。
事件的委派
有时我们希望只进行一次响应函数绑定,即可应用到多个元素上,即使是后添加的元素。可以尝试将其绑定给元素的共同祖先。事件的委派是指:
- 将事件统一绑定给元素的共同祖先元素,这样后台元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理事件
<ul id="father">
<li><a href="#">Click</a></li>
<li><a href="#">Click</a></li>
<li><a href="#">Click</a></li>
</ul>
var ul = document.getElementById('father');
ul.onclick = function(event) {
// 如果点击的标签是a标签, 输出
if(event.target.tagName == 'A') {
console.log('Click a!');
}
};
事件的传播
W3C将事件传播分成了3个阶段:
- 捕获阶段:从最外层的祖先元素,向目标元素进行事件的捕获,但是默认此时不触发事件
- 目标阶段:事件捕获到目标元素,捕获结束开始在目标元素上触发事件
- 冒泡阶段:事件从目标元素向它的祖先传递,依次触发祖先元素上的事件
事件的绑定
像上面的直接使用属性绑定,响应函数只能有一个,如果绑定了多个,最后一个响应函数会覆盖之前的。如果想为元素绑定多个响应函数,可以使用addEventListener()
方法,其参数有:
- 事件的字符串,没有
on
- 回调函数
- 是否在捕获阶段触发事件,默认为
false
ul.addEventListener('click', function() {
console.log('1');
});
ul.addEventListener('click', function() {
console.log('2');
});
ul.addEventListener('click', function() {
console.log('3');
}, false);
点击<ul>
标签后,输出:
1
2
3
事件的常见应用
拖拽
拖拽的流程:
- 当鼠标在目标元素按下时,开始拖拽,对应事件
onmousedown
- 当鼠标移动时,目标元素紧跟鼠标移动,对应事件
onmousemove
- 当鼠标松开时,目标元素固定在当前位置,对应事件
onmouseup
var box = document.getElementById('box');
box.onmousedown = function (event) {
// 计算点击位置和box的偏移
var ol = event.pageX - box.offsetLeft;
var ot = event.pageY - box.offsetTop;
/*
* 需要在document的事件绑定
* 下面两个事件都是一次性的, 也就是说只有在box被点击后, 才有存在的意义
*/
document.onmousemove = function (event) {
// 根据鼠标位置和鼠标相对box的偏移量来设置box的位置
box.style.left = event.pageX - ol + 'px';
box.style.top = event.pageY - ot + 'px';
}
document.onmouseup = function() {
// 取消两个事件的响应函数(两个响应函数都是一次性的)
document.onmousemove = null;
document.onmouseup = null;
};
};
滚轮
使用鼠标滚轮控制标签的高度:滚轮向下滚动,高度变大;滚轮向上滚动,高度变小。
box.onwheel = function(event) {
console.log(box.clientHeight);
var currHeight = box.clientHeight;
var delta = event.wheelDelta;
// 向上滚动
if (delta > 0 && currHeight > 30) {
currHeight = Math.max(30, currHeight - delta / 10);
}
// 向下滚动
else {
currHeight = Math.min(2000, currHeight - delta / 10);
}
box.style.height = currHeight + 'px';
/*
* 鼠标滚轮滚动, 浏览器随之滚动是默认行为, 返回false取消默认行为
* 当使用addEventListener()绑定响应函数时, 无法通过返回false取消,
* 需要使用事件对象的方法取消默认行为: event.preventDefault()
*/
return false;
}
键盘
键盘按键被按下和松开触发的事件为:onkeydown
与onkeyup
,一般绑定给:
- 可以获取到焦点的对象,例如
<input type="text">
document
使用时需要注意:
- 按住一个键时会一直触发
onkeydown
事件,并且第一次触发之后,间隔一小段时间,才会连续、快速触发onkeydown
事件 - 通过
event
的keyCode
属性获取按键的编码 - 通过
event
的altKey
,ctrlKey
,shiftKey
属性判断alt, ctrl, shift是否被按下 event
对象的部分内容:
KeyboardEvent {
altKey: false,
code: "KeyR",
ctrlKey: true,
key: "r",
keyCode: 82,
keyIdentifier: "U+0052",
shiftKey: false,
srcElement: <input id="input-text">,
target: <input id="input-text">,
timeStamp: 9682,
type: "keydown"
}
使标签内无法输入数字:
inputText.onkeydown = function(event) {
if (event.keyCode < 48 || event.keyCode > 57) {
/* 按下按键, input标签内输入对应字符是默认行为, 当输入数字时, 取消默认行为 */
return false;
}
}
方向键控制标签移动:
var box = document.getElementById('box');
document.onkeydown = function(event) {
var key = event.key;
var left = box.offsetLeft;
var top = box.offsetTop;
console.log(left, top);
switch (key) {
case "ArrowLeft":
box.style.left = Math.max(left - 10, 0) + 'px';
break;
case "ArrowRight":
box.style.left = Math.min(left + 10, 300) + 'px';
break;
case "ArrowUp":
box.style.top = Math.max(top - 10, 0) + 'px';
break;
case "ArrowDown":
box.style.top = Math.min(top + 10, 300) + 'px';
break;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构