■ 前置知识
1.理解简单的JS语法,如变量(var、let、const ),上述三种变量的区别和用法,可以看我之前的文章,if else语句,循环(while、for、forEach)待定
2.会背JS的七种数据类型
四基两空一对象
字符串(String)
布尔(bool)
符号(symbol)
数字(number)
null(空)
undefined(未定义)
对象(Object)数组/函数以及日期都属于对象,但不是数据类型
3.会背JS的五个false值
NaN、undefined、0、""、null
4.知道函数是对象,数组也是对象
5.会用div和span标签
6.会用简单的CSS布局
■ 程序员的宿命
1.获(获取)、2.增(增加)、3.删(删除)、4.改(修改)、5.查(查询)
■ 什么是 DOM?
外行看来前端工程师的工作就是改页面(HTML、CSS),写脚本(JavaScript)。当你意识到你不是在改HTML而是在操作DOM时,你就升级了! 那么什么是DOM?
MDN:文档对象模型 (DOM)是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。
简言之,它会把 web页面和 脚本或程序语言连接起来。
DOM :浏览器为 JavaScript 提供的一系列接口(通过window.documnet提供的),通过这些接口我们可以操作web页面。
但DOM并不是编程语言,它是文档对象的模型,该模型是独立于编程语言的。
Web前端常讲的DOM API (web 或 XML 页面) = DOM + JS (脚本语言)
■ JavaScript中原生DOM操纵API(原生JS 操作 DOM)
前端开发始终绕不过的就是操作DOM,在以前,我们使用Jquery来操作DOM,之后在vue,React等框架出现后,我们通过操作数据来控制DOM,越来越少的去直接操作DOM,更不用说用原生的JS来操作DOM了。
这里就算是回顾一下JS那些熟悉却也陌生的DOM操作函数。
JavaScript的DOM操作也是面试中的常见问题,尤其是当你需要回答jQuery的性能问题时,便需要再次回到JavaScript DOM API。 本文便总结一下常见的 JavaScript DOM 操作方法,
■ 网页其实就是一棵树
问题:JS如何操作这棵树(JS不能直接操作网页)?
浏览器往window上加一个document即可
window.document 获取网页内所有的元素
■ JS用document来操作网页
这就是Document Object Model文档对象模型(DOM)
■ 获取元素的API
获取元素(获取标签),有很多API
- 按照ID获取
id()
制作demo时可以用
window.idxxx或者直接idxxx
document.getElementById(id)
可以访问documnent中的某一特定元素,顾名思义,就是通过ID来取得元素,所以只能访问设置了ID的元素,兼容性是最好的
// HTMl
<body>
<div id="main">
<div id="content"></div>
</div>
</body>
// Script
const mainDom = document.getElementById('main');
const contentDom = document.getElementById('content');
推荐:querySelector(query:查询)
推荐:document.querySelector('#idxxx') //按照ID查询
定义:
仅仅返回匹配指定选择器的第一个元素
用法:
- 获取第一个p标签:querySelector(“p”)
- 获取第一个为demo的类名:querySelector(".demo")
- 获取第一个属性为target的a标签: querySelector(“a[target]”)
- 获取多个选择器:querySelector(“h2,h3”)
//获取文档中 class="example" 的第一个元素:
document.querySelector(".example");
//获取文档中 class="example" 的第一个 <p> 元素:
document.querySelector("p.example");
//获取文档中有 "target" 属性的第一个 <a> 元素:
document.querySelector("a[target]");
//假定你选择了两个选择器: <h2> 和 <h3> 元素。
//以下代码将为文档的第一个 <h2> 元素添加背景颜色:(仅仅返回匹配指定选择器的第一个元素)
<h2>A h2 element</h2>
<h3>A h3 element</h3>
document.querySelector("h2, h3").style.backgroundColor = "red";
- 按照class获取
getElementsByClassName()
getElementsByClassName
淘汰:getElementById()
淘汰:getElementsByClassName()
不推荐:document.getElementsByTagName('idxxx')[0]//可返回有顺序的、带有指定标签名的对象的集合(HTML集合),注意是标签名,不是元素!!
不推荐:document.getElementsByClassName('idxxx')[0]//返回文档中所有指定类名的元素集合(HTML集合)!
不推荐:document.getElementsByClassName('red')[0]//获取不同类名的数组(不同类名要空格隔开),注意类名前不需要点!
■ querySelectorAll()
推荐:document.querySelectorAll('red')[0] //按照class查询
定义:
返回文档中匹配指定 CSS 选择器的所有元素,返回 NodeList 对象。
用法:
- 获取同一类名/标签名/id/属性值…:querySelectorAll(".类名/标签名…")
- 获取多个(用逗号隔开): querySelectorAll("#id,.class,标签名…")
// 获取文档中所有的 <p> 标签
var x = document.querySelectorAll("p");
//获取文档中所有 class="example" 的 <p> 元素
var x = document.querySelectorAll("p.example");
//获取文档中所有 class="example" 元素
var x = document.querySelectorAll(".example");
//查找文档中共包含 "target" 属性的 <a> 标签
var x = document.querySelectorAll("a[target]");
//查找文档中所有的 <h2>, <div> 和 <span> 元素
var x = document.querySelectorAll("h2, div, span");
代码测试:
// demo
// HTMl
<body>
<div id="main">
<div id="content">
<p class="info test">info1</p>
<p class="info">info2</p>
<p class="info">info3</p>
</div>
</div>
</body>
// Script
const infoDom = document.querySelectorAll('.info');
- 按照Tag查询(标签)
element.getElementsByTagName //
// demo
// HTMl
<body>
<div id="main">
<div id="content">
<p class="info test">info1</p>
<p class="info">info2</p>
<p class="info">info3</p>
</div>
</div>
</body>
// Script
const divDom = document.getElementsByTagName('div');
const pDom = divDom[0].getElementsByTagName('p');
■ 那到底要用哪个API来获取元素的?
工作中用querySelector和querySelectAll,而且querySelector()内可以使用CSS语法结构
document.querySelector('div>span:nth-child(2)')
document.querySelectorAll('div>span:nth-child(2)')[1]//获取第一个div元素中span元素中第二个span元素
做demo直接用idxxx,千万别人发现哦
不推荐:要兼容IE的可怜虫才用getElement(s)Byxxx
不过有时候还是有用的:假设 id被篡改或是跟 全局属性冲突时,例如parent
全局属性冲突
■ querySelectorAll与querySelector 的区别
✔ querySelector 仅返回符合 Selector 条件的第一个节点内容,是个 Node。
//HTML
<div id="left">
left
</div>
<div id="right">
right
</div>
//JS
var div = document.querySelector("div");
console.log(div);
显示如下:
如果获取不存在的节点呢:
var div = document.querySelector("p");
console.log(div);
显示如下:
✔ querySelectorAll 返回符合 Selector 条件的所有节点内容,是个 NodeList;
返回指定元素节点的子树中匹配selector的节点集合,采用的是深度优先预查找;如果没有匹配的,这个方法返回空集合
//HTML
<div id="left">
left
</div>
<div id="right">
right
</div>
//JS
var div = document.querySelectorAll("div");
console.log(div);
显示如下:
如果查找不存在的节点:
var div = document.querySelectorAll("p");
console.log(div);
querySelectorAll可以选择多个节点,以","分隔开,返回的是个数组
✔ 注意:获取元素后要加上下标[] 因为获取的元素时(伪)数组,可能有多个,所以必须用下标来指定某一元素
例:document.getElementsByTagName('div')[1].style.border='1px solid red'
■ 获取特定元素
获取html元素
document.documentElement
获取head元素
document.head
获取body元素
document.body
获取窗口(注意窗口不是元素)
window
window.onclick=()=>{
console.log('hi')
}
获取所有元素
document.all
注意:这个document.all是个奇葩,是第六个false值,常常用来区别是否伪IE浏览器
区别是否额为IE浏览器
if(document.all){
console.log('ie浏览器');
}else{
console.log('其他浏览器');
}
Chrome浏览器下测试
■ 元素的6层原型链
抓一只div来看看!
let div=document.getElementByTagName('div')[2]
console.dir(div)
元素的6层原型链
记住:每个层构造函数都会往div(元素)身上加东西
■ 节点?元素?傻傻分不清楚
元素是节点的其中一种
- 节点Node包括以下几种
MDN有完整描述,X.nodeType得到一个数字
1.表示元素Element也叫做标签Tag
3.表示文本Text
8.表示注释Comment
9.表示文档Document
11.表示文档片段DocumentFragment
记住1和3即可
1.表示元素Element也叫做标签Tag
3.表示文本Text
节点的增删改查
程序员的宿命就是 增删改查
■ 增
- 创建标签节点
语法: document.createElement()
let div1=document.createElement('div')
document.createElement('style')
document.createElement('script')
document.createElement('li')
- 创建文本节点
语法: document.createTextNode()
text1=document.createTextNode('你好')
- 标签里面插入文本
三种方法( appendChild、innerText、textContent )
div1.appendChild(text1)
div1.innerText='你好'
div1.textContent='你好'
注意:appendChild不能直接写入文本,只能写入文本节点 错误:div1.appendChild('你好')
- appendChild
注意:你创建的标签默认处于JS线程中,你必须把它插入到head或者body里面,它才会生效
✔ 插入页面中
appendChild
document.body.appendChild(div)或者已在页面中元素.appendChild(div)
✔ 用于新增儿子(标签里面插入标签)
appendChild 问题:页面中有div#test1和div#test2
let div=document.createElement('div')
test1.appendChild(div)
test2.appendChild(div)
请问最终div出现在哪里?
答案:test2里面。
一个元素不能出现两个地方,除非复制一份。
✔ 克隆节点(实现上述复制)
cloneNode(deep) deep有两个参数:true/false
true: 深度克隆,后代节点也克隆
false:浅克隆
div1.style.backgroundColor='white'
div1.style.fontSize='100px'
let div2=div1.cloneNode(true)
head下的dvi1
body下的dvi2
- 用于新增弟弟/妹妹 Node原生内置中没有insertAfter()。不过,可以使用insertBefore和Node.nextSibling来模拟它
parentNode.insertBefore(node2, node.nextSibling);
//node2:插入节点
//node.nextSibling:当前节点的下一节点
如果node没有下一个节点,则它肯定是最后一个节点,则node.nextSibling返回
null,且node2被插入到子节点列表的最后面(即node后面)
返回值:函数返回被插入过的子节点
- 用于新增哥哥/姐姐
Node.insertBefore()
在参考节点之前插入一个拥有指定父节点的子节点。
parentNode.insertBefore(node2, node);
//node2:插入节点
//node:当前节点
通过insertBefore方法可以将node2插入到node前面,如果node是
null则将node2插入到Node的尾部。
如果node2是一个已经存在在文档中的DOM,insertBefore则会表现为移动该DOM(将会保留所有的事件)。
返回值:函数返回被插入过的子节点 ✔ 示例:
<div id="parentElement">
<span id="childElement">foo bar</span>
</div>
<script>
//创建一个新的、普通的<span>元素
var sp1 = document.createElement("span");
//插入节点之前,要获得节点的引用
var sp2 = document.getElementById("childElement");
//获得父节点的引用
var parentDiv = sp2.parentNode;
//在DOM中在sp2之前插入一个新元素
parentDiv.insertBefore(sp1, sp2);
</script>
- 用于新增爸爸
Node原生内置中没有wrap()新增爸爸。不过,可以使用Node.insertBefore()和appendChild()来模拟它
- 用于添加class
node.classList.add(className)
- 用于删除class
node.classList.remove(className)
- 用于添加事件监听
node.addEventListener(eventName, fn)
//HTML
<button id="btn">点这里!</button>
//JavaScript
const buttonElement = document.getElementById('btn');
buttonElement.addEventListener('click', function (event) {
alert('Element clicked through function!');
});
// 由于兼容性原因,一个带有 handleEvent 函数属性的对象也可以达到相同的效果。
buttonElement.addEventListener('click', {
handleEvent: function (event) {
alert('Element clicked through handleEvent property!');
}
});
- 用于删除事件监听
node.removeEventListener(eventName, fn)
删除使用removeEventListener()方法添加的事件。
element.addEventListener("mousedown", handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, true); // 成功
■ 删
- 两种方法旧方法
✔ 旧方法(淘汰):parentNode.removeChild(childNode)
原理:找到你爸爸删除你儿子,智障!(不用)
let div1=document.createElement("div")
let text1=document.createTextNode("你好")
div1.appendChild(text1)
document.body.appendChild(div1)
div1.parentNode.removeChild(div1)
旧方法删除
✔ 新方法:childNode.remove()
IE不支持,是后发明的
let div1=document.createElement("div")
let text1=document.createTextNode("你好")
div1.appendChild(text1)
document.body.appendChild(div1)
div1.remove()
div1.remove()新方法删除
- 问题:如果一个node被移除页面(DOM树),还能恢复吗?
答案:可以的,两种方法删除后,还可以进行恢复!重新添加即可(移除只是被放回到内存中)
恢复node
- 问题:如果想要一个node完全被抹掉呢?
使node的值等于为null
div1.remove()
div1=null//与内存断开联系,被垃圾回收掉
完全抹掉
- 清空子节点
没有专门的函数,可以遍历removeChild来实现
var element = document.getElementById("top");
while (element.firstChild) {
element.removeChild(element.firstChild);
}
■ 改
- 写标准属性
✔ 改class:
div.className='red blue'
再加:div.className +='green'
class是保留字,所有只能用className代替
className ✔ 改class:
查询元素的class:div.classList
div.classList.add('green')
✔ 改Id:
div.id='xxx'
✔ 改style:(淘汰,会覆盖其他属性)
div.style='width:100px;color:blue;'
✔ 针对style某部分进行修改:
div.style.width='200px'
✔ 大小写:(淘汰)
div.style['background-color']='black'
✔ 大小写:
div.style.backgroundcolor='white'
- 在div上添加属性(data-*)--自定义属性
语法:div.setAttrbute();现不常用,库开发者会用到
div.setAttrbute('data-x','text')
key:data-x
value:text
*✔ 获取data-值
如果属性是以"data-"开头,那么可通过dataset. 进行查找
方法一:div.getAttribute('data-x')
方法二:div.dataset.x
*✔ 改data-值
div.dataset.x='frank'
- 设置指定元素上属性值
Element.setAttribute() 设置指定元素上的某个属性值。如果属性已经存在,则更新该值;否则,使用指定的名称和值添加一个新的属性。
如果指定的属性已经存在,则其值变为传递的值。如果不存在,则创建指定的属性。
//HTML
<button>Hello World</button>
//JS
var b = document.querySelector("button");
b.setAttribute("name", "helloButton");
b.setAttribute("disabled", "");
- 读标准属性
方法一:div.classList/a.href
方法二:div.getAttribute('class')/a.getAttribute('href')
两种方法都可以,但值可能会稍微有些不同 ✔ 方法一:
方法一:div.classList
方法一:a.href
✔ 方法二:
方法一:div.getAttribute(&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#39;class&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#39;)
方法二:a.getAttribute(&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#39;href&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#39;)
✔ getAttribute
getAttribute()返回元素上一个指定的属性值。如果指定的属性不存在,则返回null或""
getAttribute()
■ 改事件处理函数(on开头属性)
- div.onclick默认为null
默认点击div不会有任何事情发生
但是如果你把div.onclick改为一个函数fn,那么点击div的时候,浏览器就会调用这个函数
点击test调用函数
- 并且是这样调用的fn.call(div,event)
div会被当做this,event则包含了点击事件的所有信息,如坐标
利用fn.call(div,event)调用函数
- div.addEventListener
是div.onclick的升级版,之后会单独讲
- 改文本内容:
innerText与textContent,两者几乎没有区别
✔ div.innerText='xxx'
✔ div.textContent='xxx'
- 改HTML内容:
引号
div.innerHTML='<strong>重要内容</strong>'
间隔号
div.innerHTML=`
<p>
<strong>重要内容</strong>
</p>
`
- 改标签(改儿子):
div.innerHTML='' //先清空
div.appendChild(div2)//再添加内容
打印出div2
div1清空后存放div2
- 改爸爸:
想要一个新爸爸?
语法:appendChild()//直接这样就可以,直接从原来的地方消失
newParent.appendChild(div)
打印出div2
把div1的爸爸改为div2
■ 查
- 查爸爸
元素是节点的一部分
方法一:node.parentNode
方法二:node.parentElement
- 查爷爷
node.parentNode.parentNode
- 查子代
由Node提供:node.childNodes
由Element提供:node.children
✔ 测试一:node.childNodes
特点:可能会获取到你不需要的,HTML空格也会被当作一个Node节点
element.childNodes不止会获取到DOM,也会获取到文字等,只有当nodeType === 1时才表示DOM
node.childNodes
✔ 测试二:node.children
特点:不会获取空格,只会获取标签
node.children
思考:当子代变化时,获取的也会实时变化吗?
答案:当子代变化时, 两者也会实时变化,但有一个例外querySelectorAll获取所有querySelectAll不会实时根据页面长度而改变自己的长度,获取一次后length就不会改变
querySelector实时变化
querySelectorAll无变化
- 查兄弟姐妹
注意:一定要排除自己!
第一步:
由Node提供:node.parentNode.childNodes
由Element提供:node.parentNode.children
parentNode.childNodes与parentNode.children
第二步:
最重要步骤:排除自己
let siblings=[] //Sibling兄弟姐妹
let a=lg.parentElement.children
for(let i=0;i<a.length;i++){
if(a[i]!==lg){
siblings.push(a[i])
}
}
查兄弟姐妹(排除自己)
- 查老大
node.firstChild
document.body.children[0]
node.firstChild
document.body.children[0]
- 查老幺
node.lastChild
node.lastChild
- node提供:查看上一个哥哥/姐姐
node.previousSibling
缺点:Gecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如 Node.firstChild 和 Node.previousSibling 之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点.
- node提供:查看下一个弟弟/妹妹
node.nextSibling
node.nextSibling查看弟弟
缺点:Gecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如 Node.firstChild 和 Node.previousSibling 之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点.
- Element提供:查看上一个元素哥哥/姐姐(排除文本节点)
node.previousElementSibling
node.previousElementSibling查看哥哥
- Element提供:查看下一个元素弟弟/妹妹(排除文本节点)
node.nextElementSibling
node.nextElementSibling查看弟弟
查
原理:DOM本身就是树数据结构
查看(遍历)一个div里面的所有元素
travel=(node,fn)=>{
fn(node)
if(node.children){
for(let i=0;i<node,children.length;i++){
travel(node.children[i],fn)
}
}
}
travel(div1,(node)=>{console.log(node)})
■ 查
■ DOM操作时跨线程的
为什么DOM操作会比较慢?思考
■ 《JS世界》里讲浏览器功能划分:
■ 浏览器分为渲染引擎和JS引擎
- 跨线程操作
各线程各司其职
✔ JS引擎不操作页面,只能操作JS
✔ 渲染引擎不能操作JS,只能操作页面
✔ document.body.appendChild(div1)
✔ 这句话JS是如何改变页面的?
- 跨线程通信
✔ 当浏览器发现JS在body里面加了个div1对象
✔ 浏览器就会通知渲染引擎在页面里也新增一个div元素
✔ 新增的div元素所有属性都照抄div1对象
■ 插入新标签的完整过程
- 在div1放入页面之前
✔ 你对div1所有的操作都属于JS线程内的操作
- 把div1放入页面之时
✔ 浏览器会发现JS的意图
✔ 就会通知渲染线程在页面中渲染div1对应的元素
- 把div1放入页面之后 ✔ 你对div1的操作都有可能会触发重新渲染
✔ div1.id='newid'有可能会重新渲染,也有可能不会(如果包含CSS样式,则会触发)
✔ div1.title='new'可能会重新渲染,也可能不会
✔ 如果你连续对div1多次操作,浏览器可能会合并一次操作,也可能不会
■ 属性同步
- 标准属性
✔ 对div1的标准属性的修改,会被浏览器同步到页面中
✔ 比如id、className、title等
- *data-属性
✔ 同上
- 非标准属性(不会同步!)
✔ 对非标准属性的修改,则只会停留在JS线程中
✔ 不会同步到页面里
比如x属性,代码如下:
JS Binjs.jirengu.com
- 启示 ✔ 如果你有自定义属性,又想被同步到页面中,请使用data-作为前缀
//HTML
<body>
<div id='test' x='test' data-x='test'>
</div>
</body>
//JS
let div1=document.querySelector('#test')
div1.id='frank' //成功 同步过去
div1.x='frank' //失败 没有同步
div1.dataset.x='frank' //成功 同步过去
■ Property v.s. Attribute
Property 属性(JS线程)
JS线程中div1的所有属性,叫做div1的property
百度翻译fanyi.baidu.com Attribute属性(渲染线程)
渲染引擎中div1对应标签的属性,叫做attribute
百度翻译fanyi.baidu.com
- 区别
✔ 大部分时候,同名的proprety和attribute值相等
✔ 但如果不是标准属性,那么它俩只会在一开始时相等,之后修改时不会同步的!
✔ 但注意attribute只支持字符串
✔ 而proprety支持字符串,布尔等类型