前端 - JS DOM

JS文档

初识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 属性 返回当前节点的后一个兄弟节点
  • 注意innerHTMLinnerText的区别
<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);
  • previousSiblingnextSibling也可能获取到空白的文本节点
  • 如果需要确保获取到的是元素节点,可以使用previousElementSiblingnextElementSibling
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个阶段:

  1. 捕获阶段:从最外层的祖先元素,向目标元素进行事件的捕获,但是默认此时不触发事件
  2. 目标阶段:事件捕获到目标元素,捕获结束开始在目标元素上触发事件
  3. 冒泡阶段:事件从目标元素向它的祖先传递,依次触发祖先元素上的事件

事件的绑定

像上面的直接使用属性绑定,响应函数只能有一个,如果绑定了多个,最后一个响应函数会覆盖之前的。如果想为元素绑定多个响应函数,可以使用addEventListener()方法,其参数有:

  1. 事件的字符串,没有on
  2. 回调函数
  3. 是否在捕获阶段触发事件,默认为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

事件的常见应用

拖拽

拖拽的流程:

  1. 当鼠标在目标元素按下时,开始拖拽,对应事件onmousedown
  2. 当鼠标移动时,目标元素紧跟鼠标移动,对应事件onmousemove
  3. 当鼠标松开时,目标元素固定在当前位置,对应事件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;
}

键盘

键盘按键被按下和松开触发的事件为:onkeydownonkeyup,一般绑定给:

  • 可以获取到焦点的对象,例如<input type="text">
  • document

使用时需要注意:

  • 按住一个键时会一直触发onkeydown事件,并且第一次触发之后,间隔一小段时间,才会连续、快速触发onkeydown事件
  • 通过eventkeyCode属性获取按键的编码
  • 通过eventaltKey, 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;
    }
}

posted @   lv6laserlotus  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示