网页其实是一棵树,JS 如何操作这棵树?
- 浏览器往 window 上加一个 document 即可
- JS 用 document 操作网页 ,就是 Document Object Model 文档对象模型
DOM 操作是跨线程的
- 浏览器分为渲染引擎和 JS 引擎
跨线程操作
各线程各司其职
- JS 引擎不能操作页面,只能操作 JS
- 渲染引擎不能操作 JS ,只能操作页面
【思考】**document.body.appendChild(div1)**
这句话是如何改变页面的?
跨线程通信
- 当浏览器发现 JS 在 body 里面加了个 div1 对象
- 浏览器会通知渲染引擎在页面里也新增一个 div 元素
- 新增的 div 元素所有属性都照抄 div1 对象
插入新标签得到完整过程
- 在 div1 放入页面之前
- 对 div1 所有的操作都属于 JS 线程内的操作
- 把 div1 放入页面之时
- 浏览器会发现 JS 的意图,就会通知渲染线程在页面中渲染 div1 对应的元素
- 把 div1 放入页面之后
获取元素(也叫标签)
/* 工作中常用 */
document.querySelector('#idxxx')
document.querySelectorAll('.red')[0] // 获取任意元素的方法
/* 做 demo 时可直接用idxxx */
window.idxxx 或直接 idxxx
/* 需要兼容 ie 的才用 getElement(s)ByXXX */
document.getElementById('idxxx')
document.getElementsByTagName('div')[0]:获取 div 元素
document.getElementsByClassName('red')[0]:根据类名获取元素
// 有 s 的需要加下标才能获取
获取特定元素
获取 HTML 元素
-
获取 head 元素
document.head
-
获取 body 元素
-
获取窗口(窗口不是元素)
-
获取所有元素
document.all
这算第 6 个 falsy 值,因为它过不了 if显然是对象,我们需要搞清楚它的原型
console.dir(div1) 看原型链
- 自身属性和共有属性
- 自身属性:
className
、id
、style
等等 - 共有属性:
- 第一层原型:
HTMLDivElement.prototype
——所有 div 共有的属性 - 第二层原型:
HTMLElement.prototype
——所有 HTML 标签共有的属性 - 第三层原型:
Element.prototype
——所有 XML、HTML 标签的共有属性 - 第四层原型:
node.prototype
——所有节点的共有属性,节点包括 XML、HTML 标签文本注释 - 第五层原型:
EventTarget.prototype
—— 里面最重要的函数属性是addEventListener
- 第六层原型:
Obiect.prototype
- 第一层原型:
节点 Node 包括以下几种:
- MDN 有 完整描述,
x.nodeType
得到一个数字 - 1 :表示元素 Element,也叫标签 Tag
- 3 :表示文本 Text
- 8 :表示注释 Comment
- 9 :表示文档 Docment
- 11 :表示文档片段 DocumentFragment
- 记住 1 和 3 即可
节点的增删改查
增
/* 创建一个标签节点 */
let div1 = document.createElement('div')
document.createElement('style')
document.createElement('script')
document.createElement('li')
/* 创建一个文本节点 */
text1 = document.createTextNode('你好')
/* 标签里插入文本 */
div1.appendChild(text1)
div1.innerText = '你好'或者 div.textContent = '你好'
// 但是不能用 div1.appendChild('你好')
/* 插入页面中 */
○ 建的 标签默认处于 JS 线程中
○ 必须把它插到 head 或者 body 里面,它才会生效
document.body.appendChild(div) 或者已在页面中的元素 .appendChild(div)
【思考】页面中有 div#test1 和 div#test2,请问最终 div 出现在哪里?
// 代码如下
let div = document.createElement('div')
test1.appendChild(div)
test2.appendChild(div)
- test1 里面
- test2 里面:因为一个元素不能出现在两个地方,除非复制一份
- test1 里面和 test2 里面
删
两种方法:
/* 旧方法 */
parentNode.childChild(childNode) // node 提供的
// 找到父节点删除子节点
/* 新方法 */
childNode.remove() // Element 提供的,不能兼容 ie 浏览器
【思考】如果一个 node 被移除页面(DOM 树),那么它还可以再次回到页面中吗?
- 可以,因为这个删除只是把它从树立删到内存里面,并没有彻底删除
- 如何彻底删除?
// 代码示例
div1.remove() --> // undefined
div1 = null
// 先把 div1 移除,再让 div1 等于空值,就使得 div1 和内存断开联系了,然后这个内存就会被垃圾回收掉了。
改
改属性
早期的 JS 有个 bug :JS 的对象是不能拥有一个保留字作为它的 key 的
- 比如
**if**
是 JS 保留字,就不能写~~div.if~~
- 因为关键字的优先级最高, JS 会以为是 if 语句就会报错
- 比如
**class**
也是 JS 保留字,不能写~~div.class~~
- 要写成
**div.className**
**className**
有一个 bug:每次改**className**
的时候会把之前的覆盖了
- 如果不想删之前的,只加个红色怎么操作?
**div2.className += ' red'**
(red 前面要留个空格,因为要保留之前的,然后再加 red)- 这样太麻烦了,就有了新的 API
div.classList
打印出目前的值,要加的话就div.classList.add('green')
图示:
- 新的好用,旧的难用
写标准属性
- (旧)改 class:
div.**className** = 'red blue'
(会导致全覆盖) - (新)改 class 的部分:
div.**classList.add**('red')
- (旧)改 style:
div.**style** = 'width: 100px;color:blue;'
(会导致全覆盖) - (新)改 style 的部分:
div.**style.width** = '200px'
- 注意大小写:所有用中划线隔开的都用大写代替:
**background-Color**
可写成div.style.**backgroundColor** = 'white'
- 注意大小写:所有用中划线隔开的都用大写代替:
- 改 data- 属性:`div.*dataset.x = ‘tk’` 在每个元素上加特定的属性(现在没怎么用)
读标准属性
div.classList / a.href // 直接用 JS 的属性读的话,有可能浏览器会加工补全域名
div.getAttribute('class') / a.getAttribute('href') // 直接获取原本的 href(相对更保险一点)
// 两种方法都可以,但值可能稍有不同
例子:
<a id = test href = '/xxx'> /xxx </a>
console.log(test.href) --> // "http://js.jirengu.com/xxx"
console.log(test.getAttribute('href')) --> // "/xxx"
改事件处理函数
**div.onclik**
默认为 null- 默认点击 div 不会有任何事情发生
- 但是如果把
div.onclik
改为一个函数fn
,那么点击 div 的时候,浏览器就会调用这个函数,并且是这样调用的fn.call(div,event)
div
会被当作this
- (如果需要用 this ,就不能用箭头函数,因为箭头函数不支持 this,应改用 function())
event
则包含了点击事件的所有信息,如坐标event
这个参数是浏览器在用户点击的时候用call
传进来的
**div.addEventListener**
- 是
div.onclik
的升级版
- 是
改内容
改文本内容
div.innerText = 'xxx'
div.textContent = 'xxx' // 二者几乎没有差别
改 HTML 内容
div.innerHTML = '<strong>重要内容</strong>' // innerHTML 虽然好用,但如果内容过长会卡
// innerHTML 中的 HTML 必须大写
改标签
div.innerHTML = '' // 先清空
div.appendChild(div1) // 再加内容
改父节点
newParent.appendChild(div)
// 直接找到新父节点的引用,然后调用 appendChild 把自己放进去,就直接从原来的父节点消失去了新父节点
查
/* 查爸爸 */
node.parentNode 或 node.parentElement
/* 查爷爷 */
node.parentNode.parentNode
/* 查兄弟姐妹 */
node.parentNode.childNodes // 还要排除自己
node.parentNode.children // 还要排除自己
/* 查子代 */
node.childNodes // 可能会获取到不想要的元素
node.children // 一般用这个
【思考】当子代变化时,两者也会实时变化吗?会自动变化
续
/* 查看老大 */
node.firstChild
/* 查看老幺 */
node.lastChild
/* 查看上一个哥哥/姐姐 */
node.previouSibling
/* 查看下一个弟弟/妹妹 */
node.nextSibling
遍历一个 div 里面的所有元素
trave1 = (node, fn) => {
fn(node)
if(node.children) {
for(let i = 0; i < node.children.length; i++) {
trave1(node.children[i], fn)
}
}
}
trave1(div1, (node) => console.log(node))
属性同步
- 标准属性
- 对 div1 的标准属性的修改,会被浏览器同步到页面中
- 比如
id
、className
、title
等
- data-* 属性
- 同上
- 非标准属性
- 对非标准属性的修改,则只会停留在 JS 线程中,不会同步到页面里
- 比如 x 属性,示例代码
- 启示
- 如果有自定义属性,又不想被同步到页面中,就使用
data-
作为前缀
- 如果有自定义属性,又不想被同步到页面中,就使用
Propert V.S. Attribute
- property 属性
- JS 线程中 div1 的所有属性,叫做 div1 的
**property**
- JS 线程中 div1 的所有属性,叫做 div1 的
- attribute 也是属性
- 渲染引擎中 div1 对应标签的属性(而不是 div1 对象的属性),叫做
**attribute**
- 比如 div1 中的 id = 字符串,那么 id = 字符串就是它的属性
- 渲染引擎中 div1 对应标签的属性(而不是 div1 对象的属性),叫做
- 区别:
- 大部分时候,同名的 property 和 attribute 值相等
- 但如果不是标准属性,那么它俩只会在一开始时相等
**attribute**
只支持字符串- 比如
id = 1
,也只是省略了""
的字符串而已。
- 比如
**property**
支持字符串、布尔等类型