js中的DOM操作(JavaScript高级程序设计笔记)

DOM


DOM(文档对象模型)是针对HTML 和XML 文档的一个API(应用程序编程接口)。
DOM描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。


节点层次

DOM可以将任何HTML文档描绘成多层节点构成的对象模型

<html>
  <head>
    <title>Sample Page</title>
  </head>
  <body>
    <p>Hello World!</p>
  </body>
</html>

上面的html内容转换成DOM结构就是下面这张图的样子

Document节点是每个文档的根节点, 它只有一个子节点, 即<html>元素, 我们叫它文档元素。其他所有元素都包含在文档元素中。
对于每一段html标记都可以通过树中的一个节点表示: HTMl元素通过元素节点表示, 特性通过属性节点表示, 文档通过文档节点表示, 除了这些还有诸如注释节点等总共12中节点类型, 这些类型都继承于一个基类型

Node类型

DOM1级定义了一个Node类型, 所有的节点类型都继承自Node类型, 所有节点类型都共享着相同的基本属性和方法。

每个节点都有一个nodeType属性, 用于表示节点类型, 通常这个属性会返回一个常数, 每个常数对应一种类型, 任何节点都是这12种的其中一个:

  • Node.ELEMENT_NODE(1)
  • Node.ATTRIBUTE_NODE(2)
  • Node.TEXT_NODE(3)
  • Node.CDATA_SECTION_NODE(4)
  • Node.ENTITY_REFERENCE_NODE(5)
  • Node.ENTITY_NODE(6)
  • Node.PROCESSING_INSTRUCTION_NODE(7)
  • Node.COMMENT_NODE(8)
  • Node.DOCUMENT_NODE(9)
  • Node.DOCUMENT_TYPE_NODE(10)
  • Node.DOCUMENT_FRAGMENT_NODE(11)
  • Node.NOTATION_NODE(12)

通过上面的常量, 可以很容易的确定节点类型, 如:

if (someNode.nodeType == 1){ //适用于所有浏览器
  alert("Node is an element.");
}

我们可以通过节点的nodeName属性得到元素的标签名, 对于元素节点, nodeValue的值始终是null


节点关系

文档中所有元素之前都存在着父子关系, 如

  • <body>元素是<html>的子元素
  • <html>元素师<body>的父亲元素
  • <head>和<body>则是同胞元素, 因为他们都是<html>的直接子元素

每个节点都有一个childNodes属性, 其中保存着一个NodeList伪数组, NodeList中有序的保存着该节点的所有子节点, 可以通过伪数组位置来访问某个子节点, NodeList最显著地特点是他是基于DOM结构动态执行查询结果的, 会实时反映DOM结构的变化。下面例子是childNodes的典型应用:

var firstChild = someNode.childNodes[0]
var secondChild = someNode.childNodes.item(1)
var count = someNode.childNodes.length

这里需要注意的一点, 使用childNodes时, 同胞元素之间的空格和回车会被识别为文本节点

除了childNodes外, 每个节点都有一个parentNode属性, 该属性用来获取该节点的父节点, 此外还有一个previousSibling用来获取前一个同胞节点, nextSibling用来获取后一个同胞节点

这里同样需要注意, 由于元素间的空格和回车会被识别为文本节点, 所有通过previousSiblingnextSibling获取同胞节点时, 当遇到这些文本节点时, 一定要向前或向后获取两次同胞节点来跳过这些文本节点

父节点还可以通过firstChildlastChild来获得第一个子节点和最后一个子节点

下面这张图就是上面这些属性的总结


除了上面这些, 我很还可以通过hasChildNodes()方法来判断节点是否有子节点

所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。


操作节点

appendChild(newNode)
DOM提供了很多操作节点的方法, 其中最常用的方法就是appendChild(), 用于向childNodes列表末尾添加一个节点, 如下面的例子就是向body的末尾添加一个button子节点

let el = document.querySelector('body')
let btn = document.createElement('button')

el.appendChild(btn)

如果传入appendChild()中的节点已经是文档的一部分了, 那么该节点会从原来的位置转移到新位置, 原因是DOM中的任何元素不能同时出现在文档中的多个位置上, 如下面的例子

//someNode 有多个子节点
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true

insertBefore(insertNode, positionNode)
该方法是把某个放到childNodes列表的某个特定位置上, 第一个参数是要插入的节点, 第二个参数是作为参照的节点。执行后要插入的节点就会被放在参照节点的前一个位置

replaceChild(newNode, replaceNode)
该方法接受两个参数, 第一个是要插入的节点, 第二个是要被替换的节点, 返回值为被替换的节点

removeChild(Node)
该方法接受一个参数, 就是要移除的节点, 返回值是被移除的节点, removeChild和replaceChild一样, 被删除的节点并没有在文档中被删除, 只是断开了所有链接

cloneNode(boolean)
该方法接受两个参数为true时对元素进行深拷贝, 拷贝对象包括其子节点, 为false时为浅拷贝, 只拷贝元素本身

// 假设我们有一个<ul>元素, 里面包含三个<li>, 我们用myList保存获取的<ul>
var deepList = myList.cloneNode(true);
alert(deepList.childNodes.length); //3(IE < 9)或7(其他浏览器)
var shallowList = myList.cloneNode(false);
alert(shallowList.childNodes.length);

cloneNode()方法不会复制添加到DOM 节点中的JavaScript 属性,例如事件处理程序等。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切都不会复制。IE 在此存在一个bug,即它会复制事件处理程序,所以建议在复制之前最好先移除事件处理序。

最后一个方法是normalize(),这个方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点


Document类型节点相关操作

在浏览器中, document对象是HTMLDocument(继承自Document类型)的一个实例, 表示整个HTML页面, document对象是window对象的一个属性, 可以通过全局访问

我们可以通过其documentElement属性或者childNodes来访问<html>元素

document对象还有一个body属性指向<body>元素。document.body在js代码中出现的频率非常高, 请大家熟练掌握

document我们用的最多的是查找元素相关操作
我们可以通过document.getElementById()document.getElementsByTagName()这两个方法获取相关节点

现在更多的用document.querySelector()document.querySelectorAll()这两个方法来获取元素


Element类型

除了document类型外, Element类型就要算是web编程中最常用的类型了。

nodeNametagName
这两个属性都返回标签名, 返回的是大写, 例如:

<div id="myDiv"></div>
var div = document.getElementById("myDiv");
alert(div.tagName); //"DIV"
alert(div.tagName == div.nodeName); //true

所以判断时不要忘记用toLowerCase()转换成小写, 不然很容易出错

if (element.tagName == "div"){ //不能这样比较,很容易出错!
//在此执行某些操作
}
if (element.tagName.toLowerCase() == "div"){ //这样最好(适用于任何文档)
//在此执行某些操作
}

HTML元素
html一般有以下表示特性的属性, 我们用例子来表示:

<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
var div = document.getElementById("myDiv");
alert(div.id); //"myDiv"
alert(div.className); //"bd"
alert(div.title); //"Body text"
alert(div.lang); //"en" 元素内的语言代码, 很少使用
alert(div.dir); //"ltr" 语言方向, ,值为"ltr"(left-to-right,从左至右)或"rtl"(right-to-left,从右至左, 很少使用。

当然,像下面这样通过为每个属性赋予新的值,也可以修改对应的每个特性:

div.id = "someOtherId";
div.className = "ft";
div.title = "Some other text";
div.lang = "fr";
div.dir ="rtl";

取得特性
元素有很多的特性, 我们主要通过getAttribute(), setAttribute()removeAttribute()来操作特性

var div = document.getElementById("myDiv");
alert(div.getAttribute("id")); //"myDiv"
alert(div.getAttribute("class")); //"bd"
alert(div.getAttribute("title")); //"Body text"
alert(div.getAttribute("lang")); //"en"
alert(div.getAttribute("dir")); //"ltr"

getAttribute也可以获取自定义属性

<div id="myDiv" my_special_attribute="hello!"></div>
var value = div.getAttribute("my_special_attribute"); //hello!

特性的名称是不区分大小写的,即"ID"和"id"代表的都是同一个特性。另外也要注意,根据HTML5 规范,自定义特性应该加上data-前缀以便验证。

自定义的属性只能通过getAttribute获取, 不能直接用属性名获取, 下面的代码就获取自定义属性得到的是undefined

alert(div.id); //"myDiv"
alert(div.my_special_attribute); //undefined(IE 除外)
alert(div.align); //"left"

style和事件绑定getAttribute和直接调用属性名返回的内容是不同的

  • 在通过getAttribute()访问时,返回的style 特性值中包含的是CSS 文本,而通过属性来访问它则会返回一个对象。
  • 通过getAttribute()访问事件时,会返回相应代码的字符串。而在访问s事件属性(如onclick)时,则会返回一个JavaScript 函数(如果未在元素中指定相应特性,则返回null)

设置特性
getAttribute()对应的方法是setAttribute()这个方法接受两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,setAttribute()则创建该属性并设置相应的值。来看下面的例子:

div.setAttribute("id", "someOtherId");
div.setAttribute("class", "ft");
div.setAttribute("title", "Some other text");
div.setAttribute("lang","fr");
div.setAttribute("dir", "rtl");

也可以通过直接给属性赋值设置元素的特性

div.id = "someOtherId";
div.align = "left";

直接给属性赋值对于自定义属性依然不适用

removeAttribute(),这个方法用于彻底删除元素的特性。

Element类型的attributes属性

  • getNamedItem(name):返回nodeName 属性等于name 的节点;
  • removeNamedItem(name):从列表中移除nodeName 属性等于name 的节点;
  • setNamedItem(node):向列表中添加节点,以节点的nodeName 属性为索引;
  • item(pos):返回位于数字pos 位置处的节点。

创建元素
使用document.createElement()方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名

var div = document.createElement("div");

元素的子节点
正如我们之前说的, 元素的childNodes 属性中包含了它的所有子节点,这些子节点有可能是元素、文本节点、注释或处理指令。不同浏览器在看待这些节点方面存在显著的不同,以下面的代码为例。

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul >

在浏览器中,<ul>元素都会有7 个元素,包括3 个<li>元素和4 个文本节点(表示<li>元素之间的空
白符)。如果像下面这样将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点。

<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>

如果需要通过childNodes 属性遍历子节点,那么一定不要忘记有些浏览器会识别标签间的空格。这意味着在执行某项操作以前,通常都要先检查一下nodeTpye 属性,如下面的例子所示。

for (var i=0, len=element.childNodes.length; i < len; i++){
  if (element.childNodes[i].nodeType == 1){
    //执行某些操作
  }
}


DOM操作技术

很多时候,DOM 操作都比较简明,因此用JavaScript 生成那些通常原本是用HTML代码生成的内容并不麻烦。不过,也有一些时候,操作DOM 并不像表面上看起来那么简单。由于浏览器中充斥着隐藏的陷阱和不兼容问题,用JavaScript 代码处理DOM 的某些部分要比处理其他部分更复杂一些。

动态脚本
我们都知道使用<script>标签可以向页面中插入JavaScript代码, 一种方式是通过src特性包含外部文件, 另一种方式就是用这个元素本身来包含代码。

  • 我们可以通过以下方式引入外部JavaScript文件
<script type="text/javascript" src="client.js"></script>

通过js代码创建这个节点的代码如下:

var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js"; 
document.body.appendChild(script);

我们可以用一个函数讲代码封装

function loadScript(url) {
  let script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = url
  document.body.appendChild(script)
}

然后调用loadScript('client.js')就能加载外部js文件了

  • 我们还可以直接在<script>标签中指定js代码来运行js
<script type="text/javascript">
  function sayHi(){
    alert("hi");
  }
</script>

同样的我们也可以通过js代码创建这个标签

let script = document.createElement("script")
script.type = "text/javascript"
script.text = "(function() {alert('hi')})()"
document.body.appendChild(script);

同样我们可以封装

function loadScriptString(code){
  let script = document.createElement("script")
  script.type = "text/javascript"
  script.text = code
  document.body.appendChild(script)
}

之后调用就行了loadScriptString('(function() {console.log("a")})()')

以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行 代码与在全局作用域中把相同的字符串传递给 eval()是一样的。


动态样式

动态样式的套路跟上面以上, 也是用js代码动态插入标签, 我们知道引给页面添加css样式有两种方式, 一种<link>引入外部css文件, 另一种<style>直接嵌入样式

如果我们想动态引入外部css文件, 我们可以通过一个函数封装来实现

function loadStyles(url){
  let link = document.createElement("link")
  link.rel = "stylesheet"
  link.type = "text/css"
  link.href = url
  let head = document.getElementsByTagName("head")[0]
  head.appendChild(link)
}

如果想直接嵌入css则使用下面的封装来实现

function loadStyleString(css){
  let style = document.createElement("style")
  style.type = "text/css"
  style.styleSheet.cssText = css
  let head = document.getElementsByTagName("head")[0]
  head.appendChild(style)
}

// 想下面这样调用函数即可
loadStyleString("body{background-color:red}")


NodeList

NodeList是"动态的", 每当文档结构发生变化时, 它都会更新。本质上来说, NodeList对象是在访问DOM文档时实时运行的查询, 例如, 下面的代码就会无限循环:

let divs = document.getElementsByTagName("div"), i, div;
for(i=0; i < divs.length; i++) {
  div = document.createElement("div")
  document.body.appendChild(div)
}

第一行代码会取得文档中所有<div>元素的 HTMLCollection。由于这个集合是“动态的”,因此, 只要有新<div>元素被添加到页面中,这个元素也会被添加到该集合中。浏览器不会将创建的所有集合都保存在一个列表中,而是在下一次访问集合时再更新集合。结果,在遇到上例中所示的循环代码时,就会导致一个有趣的问题。每次循环都要对条件 i < divs.length 求值,意味着会运行取得所有 <div> 元素的询。 考虑到循环体每次都会创建一个新 <div> 元素并将其添加到文档中, 因此 divs.length 的值在每次循环后都会递增。既然 i 和 divs.length 每次都会同时递增,结果它们的 值永远也不会相等。

var divs = document.getElementsByTagName("div"), i, len, div;

for(i=0, len=divs.length; i < len; i++) {
  div = document.createElement("div");
  document.body.appendChild(div);
}

这个例子中初始化了第二个变量 len。由于 len 中保存着对 divs.length 最初的值,因此就会避免上一个例子中出现的无限循环问题。

一般来说,应该尽量减少访问 NodeList 的次数。因为每次访问 NodeList,都会运行一次基于文档的查询。所以,可以考虑将从 NodeList 中取得的值缓存起来。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容