网页其实是一棵树
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>标题</title>
</head>
<body>
<header>
<h1>文字1</h1>
</header>
<main>
<p>文字2
<span>文字3</span>
</p>
</main>
<footer>
<div>文字4</div>
</footer>
</body>
</html>
上面的代码用树状图理解如下:
DOM(文档对象模型)
html内容由渲染引擎来渲染到页面上,而JS引擎负责的是JS内容,是操作不了文档内容的,咋办? 于是让浏览器来提供办法。 浏览器往window上加一个document对象,这时JS就可以用document来操作网页了,这就是document object model,即文档对象模型(把网页抽象成对象,然后来操作它)
获取元素
标签就是元素,元素就标签。
<body>
<div id="test" class="red">测试</div>
<script>
//1.通过id获取元素,记得带引号
document.getElementById('test')
//2.通过标签名,获取所有同名的标签,返回的是伪数组,所以要加下标
document.getElementsByTagName('div')[0]
//3.通过css类名,获取所有同css类名的标签,返回的是伪数组,也要加下标
document.getElementsByClassName('red')[0]
//4.document.querySelector('#id')
document.querySelector('#test')
//5.返回的是伪数组,要加下标
document.querySelectorAll('.red')[0]
/*
补充:
前三个getElement(s)...一般在兼容IE的时候会用,其他时候一般推荐用后两个querySelector(All)...
querySelector(All)借鉴了CSS的语法,写起来很方便一些,而且写的复杂一些也是可以的:
比如:querySelector('div>span:nth-child(2)')
*/
</script>
</body>
获取特定的元素
// 获取html元素
document.documentElement
// 获取head元素
document.head
// 获取body元素
document.body
// 获取所有元素
document.all
// 这个document.all比较奇葩,是JS中的第六个falsy值
// 比较特殊的一个:获取窗口,注意窗口不是元素
window
获取到的元素是对象
获取到的元素(标签)是对象,既然是对象,就要搞清楚它的原型链是怎样的。 通过以下代码来查看div对象的原型链:
let test = document.querySelector('#test')
console.dir(test)
查看结果可知: div的自身属性有:id、className、classList、style、onclick等,__proto__属性指向HTMLDivElement;
第一层原型:HTMLDivElement.prototype,里面包含所有div共有的属性;
第二层原型:HTMLElement.prototype,里面包含所有HTML标签共有的属性,不用细看
第三层原型:Element.prototype,里面包含XML和HTML标签的共有属性,浏览器不仅仅能展示HTML内容,也能展示XML内容;
第四层原型:Node.prototype,里面包含所有节点的共有属性,节点包括:XML文本注释、HTML标签文本注释等
第五层原型:EventTarget.prototype,里面有个比较重要的函数属性:addEventListener;
第六层原型:Object.prototype,里面包含了对象的共有属性;
下面是div的原型链图示:
节点和元素
要注意区分,节点和元素并不是完全相同的东西,节点的范围要比元素大。
浏览器控制台里使用如下代码可以获取一个东西的节点类型:
x.nodeType
// 比如:
test.nodeType
// 返回1
根据返回数字的不同,可以知道不同的节点类型:
1:元素 Element ,叫标签 Tag 也可以;
3: 文本 Text;
8:注释 Comment;
9:文档 Document;
11:文档片段 DocumentFragment
节点的增删改查
增
- 创建一个标签节点
document.createElement('div');
document.createElement('style');
document.createElement('script');
document.createElement('li');
- 创建一个文本节点
document.createTextNode('你好')
- 往标签里插入文本
let div1 = document.createElement('div');
div1.appendChild(text1);
div1.innerText = '你好'; // IE
div1.textContent = '你好'; // Chrome,Firefox等
// 易错的错误示范:
div.appendChild('你好')
创建的元素插入页面中
我们创建完标签后,标签不会直接显示在页面中,而是出于JS线程里的,必须要把它插到 head , body 或其他已经在页面里的标签里才会生效;
document.head.appendChild(test)
document.body.appendChild(test)
一个元素不能同时出现在两个地方
代码:
// html:
<body>
<div id="div1"></div>
<div id="div2"></div>
</body>
// js:
let test = document.createElement('div');
div1.appendChild(test);
div2.appendChild(test);
// 问:test最终会出现在哪里?
// 1.出现在div1里面
// 2.出现在div2里面
// 3.同时出现在div1和div2里面
// 答:div2里面
// 一个元素不能同时出现在两个地方,除非复制一份
删
方法:
let div1 = document.createElement('div');
let text = document.createTextNode('我是一个子节点')
div1.appendChild(text)
// 父节点删子节点
// parentNode.removeChild(childNode)
div1.removeChild(text);
// 子节点自删
// childNode.remove()
text.remove();
思考:一个节点被移出页面了,也就是被从 DOM 树中移除了,那么它想回到页面上,需要重新创建一个相同内容吗?
答案:不需要,因为从 DOM 树中被移除后,被移除的节点依然在JS线程中,想要再次回到页面中,直接找一个元素 appendChild( )就可以了。
改
改属性
// 改class:这样改完后会覆盖之前的类
div.className = 'red blue';
// 改class:添加一个类
div.classList.add('red');
// 改style:改变总体样式
div.style = 'width:100px;color:blue;'
// 改style:改部分样式
div.style.width = '200px';
// 改style需要注意的大小写问题:
// 比如修改背景色,在css里是:
<style>
div {
background-color:orange;
}
</stlye>
//在JS里要这样改:
div.style.backgroundColor = 'orange';
//这样改也行,但是写起来比较麻烦
div.style['backgroundColor'] = 'orange';
// 改data-自定义属性:
// 有时我们需要在文档的标签里,用 data-* 来自定义一些属性:
<body>
<div id="test" data-x="xxx"></div>
</body>
//改变自定义属性的值的方法有点奇葩,但是还是要记住:
<script>
test.dataset.x = 'hhh';
// 直接用 test.data-x是无法生效的,因为减号 - 这个符号,在JS里是不能用的,这里可以参考 css 里的 background-color
</script>
改事件处理函数
div.onclick默认值为null,也就是div被点击的时候默认不会有任何事发生,只有我们把一个函数 fn 赋值给 div.onclick 时,点击div时,浏览器才会去调用这个函数,产生相应的动作。
调用的时候,如果写成call函数形式的话是这样:fn.call(div,event),也就是说div会被当作this,浏览器会在用户点击的时候自动的调用,event则包含了点击事件的所有信息。
div.onclick = function fn(){
console.log('you clicked the div')
}
用div.onclick时,只能对应一个函数,如果再把另外一个函数赋值给div.onclick,就会覆盖掉之前的函数。那么如果想达到点击一下完成多个动作怎么办呢?这时可以使用div.onclick的升级版:div.addEventListener来改事件处理函数:
function fn1(){
console.log('hi')
}
function fn2(){
console.log('你好')
}
div.addEventListener('click',fn1);
div.addEventListener('click',fn2);
// 点击div后,会同时打印 hi 和 你好
改内容
- 改标签里的文本内容:
div.innerText = 'hi'
div.textContent = 'hi'
// 这两种方法都可以改变标签里的文本内容,两者几乎没有什么区别
// 以前 innerText 是用在IE里的,而 textContent 是用在Chrome、Firefox等其他浏览器中的
// 如今几乎所有的浏览器都同时支持这两种方法
- 改标签里的HTML内容: 如果不仅仅想在div标签里加文本内容,还要加一个span标签,这时就需要用到innerHTML了:
div.innerHTML = '<span>这里有个span标签</span>'
改父节点
假如span的父节点是div1,想把span放到另外一个div2节点下面,也就是改变它的父节点:
div2.appendChild('span')
// 直接这样写就可以了,不用div1再去removeChild了,参考之前的‘一个元素不能同时出现在两个地方,除非复制一份’
查
查、读取属性
// 不同的查询方法,读取到的属性值可能稍有不同
// html
<body>
<div id="test" class="red border"></div>
<a id="bd" href="www.baidu.com">百度</a>
</body>
// js
test.classList // 返回一个伪数组:DOMTokenList(2) ["red", "border", value: "red border"]
bd.href // 返回 http://127.0.0.1:8888/www.baidu.com 会自动帮你加上当前的ip地址
test.getAttribute('class') // 返回 red border
bd.getAttribute('href') // 返回 www.baidu.com
// 两种方法都可以,只是返回的值形式上稍微有点不同
查父节点(查爸爸)
// node.parentNode 或者 node.parentElement
// 假如div标签里有个p标签,那么想查询p标签的父节点:
p.parentNode 或 p.parentElement
查父节点的父节点(查爷爷)
node.parentElement.parentElement
查子节点
// html:
<div id="test">
<p id="p1">p标签</p>
</div>
// 两种方法:
test.children //返回一个伪数组:HTMLCollection [p#p1]
test.childNodes //返回一个伪数组:[text, p#p1, text]
使用node.childNodes查询子节点时,空格或者或者也会被认为是一个文本节点,所以两种方法的结果有所不同
查兄弟姐妹节点
//这两种方法都可以,但是要记得返回的伪数组里是包含自己的,所以要记得用remove把自己给排除
node.parentNode.childNodes
node.parentNode.children
查子节点里的老大和老小
// html:
<div id="d1">
<div id="d2"></div>
<div id="d3"></div>
<div id="d4"></div>
</div>
// 但是空格会影响返回的结果,所以需要改成这样:
<div id="d1"><div id="d2"></div><div id="d3"></div><div id="d4"></div></div>
// 查老大
d1.firstChild // d2
// 查老小
d1.lastChild // d4
查上一个节点和下一个节点
// html内容还和上一个例子一样
// 查上一个节点
d2.previousSibling // d1
// 查下一个节点
d2.nextSibling // d3
// 注意:如果没有是查第一个节点的上一个节点,或者最后一个节点的下一个节点,会返回 null
遍历div里的每一个元素
let travel = (node,fn) => {
fn(node)
if(node.children){
for(let i = 0;i<node.children.length;i++){
travel(node.children[i],fn)
}
}
}
travel(d1,(node)=>{console.log(node)})
DOM操作是跨线程的
浏览器分为渲染引擎和JS引擎,两者各司其职:
渲染引擎只负责操作页面,不能操作JS; JS引擎只负责操作JS,不能操作页面;
而我们使用代码:
document.body.appendChild(div1)
改变了页面,这是为什么?是怎么做到的?
这里用到了跨线程通信:
当浏览器发现JS在文档的body里加了一个div1对象时,就会通知渲染引擎在页面里也新加一个div元素,然后新增的div元素的所有属性都照抄div1d对象的属性;
跨线程通信图示
插入新标签的完整过程
在div1放入页面之前
放入页面前的所有操作,都属于JS线程内的操作
在div1放入页面的过程中
浏览器会发现JS的意图,然后就会通知渲染线程在页面中渲染div1对应的元素
在div1放入页面之后
放入页面后,你在JS线程中对div1的任何操作,都有可能会触发渲染引擎去重新渲染,比如:
div1.id = 'newID'
div1.title = 'newTitle'
div1.style.backgroundColor = 'red'
当然也不是一定就会重新渲染,这个要看具体情况而定。
如果连续对div1进行多次操作,浏览器可能会把多次的操作合并成一次操作,比如:
div.style.backgroundColor = 'green';
div.style.backgroundColor = 'blue';
这样这些的结果是:div不会从原来的背景色变成绿色,然后再变成蓝色,而是直接一次性变成蓝色,这就是浏览器在合并操作;
属性同步
当div1被放入页面以后,在JS线程里,对div1的标准属性进行修改,都会被浏览器同步到页面中,比如id、className、title之类的。另外修改以 “data-” 开头的自定义属性,也会被浏览器同步到页面中;
但是一些非标准属性,就是不是HTML标签的标准属性,比如在JS线程里,给div1对象添加一个x属性,对于这种属性的修改,就不会被同步到页面里了,而是只会停留在JS线程中。
示例代码:
// html:
<div id="test" data-x="hi" x="yes"></div>
// js:
let div1 = document.querySelector('#test');
div1.id = 'test2';
div1.dataset.x = 'hello';
div1.x = 'no';
console.log(div1);
// 返回结果:<div id="test2" data-x="hello" x="yes"></div>
// 只有非标准属性 x 没有同步
过程图示:
启示:如果要用到自定义属性,并且希望改完后被同步到页面里,就要使用 data- 作为前缀。
Property 和 Attribute
property
JS线程里div1对象的所有属性,就叫做div1的property;
attribute
渲染线程中div1对应的div标签里的属性,叫做attribute;
两者区别
两个单词翻译都是属性,但是实质上处于不同的线程,是不同的东西;
大部分时候,同名的 property 和 attribute 的值是相等的,参考图示:
这里的id是标准属性,如果不是标准属性的话,最开始渲染到页面上时,非标准属性的值是相等的,但是在JS线程里操作之后,页面不会同步非标准属性的值,所以后面往往就不相等了,非标准属性只有在最开始的时候相等。
另外 attribute 因为是在页面上,也就是HTML文档里,所以它的属性值只支持字符串;而 property 是在JS线程里,它的属性值可以支持字符串、布尔值等很多类型的值。