【轻聊前端】强大的DOM(上)
在系列的第一篇文里,我们说了“编程”是什么,而Web编程自然需要两个元素都在——’编程‘、’Web‘。
前面已经聊了"编程"的若干元素。
“变量”,用于存储数据; “对象”,用于封装数据; “数组”,用于存储一组(常为同类)数据; “函数”,用于封装行为。
作为程序,这些就够了,但很多人从学习JavaScript到写web页面都会有个坎——“学会了语法,写网页从何处下手?”。
要使编码能够实际作用于网页,和网页进行交互,还需要另外两样东西:DOM 和 BOM。
JavaScript = ES + DOM + BOM。
BOM——浏览器对象模型(Browser Object Model)、DOM——文档对象模型(Document Object Model)。
BOM用于承载网页,DOM用于展示网页。
本文的主角就是 DOM。
什么是 DOM
从表现看,它决定了网页中的内容;从编码看,它包含了属性和能力。
前一种很好理解,后面一种也似曾相识,这不就是对象?所以,DOM就是网页结构和编程接口的结合体。
有一点要说明的是,尽管上面提了 JavaScript 的等式,但 DOM 并不专属于 JavaScript,而是跨平台、语言无关的。之所以跟 JavaScript 产生关联,是因为 JavaScript 中提供了 DOM API。
任何HTML或XML文档都可以用DOM表示为一个由节点构成的层级结构。节点分很多类型,每种类型对应不同的信息或标记,也都有自己的特性、数据和方法,而且与其他类型有某种关系,这些关系构成了层级,让标记可以表示为一个以特定节点为根的树形结构。就是常说的DOM树了。
DOM很强大,但在很长一段时间里都令人头疼,使用繁琐、版本多、兼容难、性能损耗大...给人的印象就是,DOM是原始且不好的东西,最好敬而远之。
所以,大家谈论比较多的是怎样”减少“跟原生DOM打交道,比如几年前的Jquery/Zepto,近几年的Vue、React。前者对DOM操作进行了很好地兼容和封装,使用更方便,后者则引入”虚拟DOM“和更合理的diff算法,代码中极力避免直接操作DOM,而是操作数据,框架根据数据变化来处理从”虚拟DOM“到”真实DOM“的转换。
但是,这并不代表不需要DOM,更不是人们忽略它的理由,甚至有些场景下操作DOM是唯一途径,所以,熟练掌握DOM相关的知识依旧是现代前端开发应该做到的。
Document
在浏览器中,文档对象document表示整个HTML页面,它是window对象的属性,是一个全局对象。
文档信息
document对象包含一些页面共有信息,如:URL、domain和referrer。
URL:当前页面的完整URL domain:页面的域名 referrer:链接到当前页面的那个页面的URL。如果当前页面没有来源,则referrer属性是空字符串。
这些信息都可以在请求的HTTP头部信息中获取,只是在JavaScript中通过这几个属性暴露出来而已。
其中只有domain属性是可以设置的。但出于安全考虑,不能设置URL中不包含的值。
当页面中包含来自不同子域的或内嵌
时,设置domain是有用的。因为跨源通信存在安全隐患,所以不同子域的页面间无法通过JavaScript通信。此时,在每个页面上把domain设置为相同的值,就可以访问对方的JavaScript对象了。
文档写入
document对象有一个古老的已经不太常用的能力,即向网页写入内容。对应4个方法:write()、writeln()、open()和close()。
其中,write()和writeln()方法都接收一个字符串参数,可以将字符串写入网页中。write()简单地写入文本,而writeln()还会在字符串末尾追加一个换行符(\n)。这两个方法可以用来在页面加载期间动态添加内容。如果想用脚本最快地在浏览器输出点什么,write()是首选。
但有一点需要注意,在页面渲染期间可以通过document.write()向文档输出内容,但如果在页面加载完之后再调用document.write(),输出的内容会重写整个页面,比如onload事件的回调函数中,要格外小心。
document.write('hello world')
聊完全局,来看看具体的节点。
DOM节点
每个页面都是个元素树,由很多节点组成,所有节点类型都继承自Node类型,共享基本的属性和方法。
节点类型
每个节点都有自己的类型归属,比如:元素节点、文本节点,类型值nodeType和常用节点类型对应关系如下:
元素:1 属性:2 文本:3 注释:8
可以通过如下代码,拿到页面中一个div的节点类型:
let div = document.getElementsByTagName("div")[0];
div.nodeType //1
需要注意的是,空格和换行也会被算作节点当中,被当做文本节点处理。
除了nodeType,nodeName 与 nodeValue也可以用来对节点进行更具体的查询,只是并不总返回合理的值,最好先对节点类型做判断再进行操作。
节点关系
子节点
childNodes是元素子节点的集合,每个节点都有一个childNodes,包含一个NodeList的实例。NodeList是一个类数组对象,用于存储可以按位置存取的有序节点。
NodeList对象独特的地方在于它是一个对DOM结构的查询,因此DOM结构的变化会自动地在NodeList中反映出来。
可以使用两种方式访问nodeList中的元素——中括号、item()。
let firstChild = parent.childNodes[0]
let firstChild = parent.childNodes.item(0)
多数开发者倾向于使用中括号,更像访问数组项。
父节点
每个节点都有一个parentNode属性,指向其DOM树中的父元素。
兄弟节点
使用 previousSibling 和 nextSibling 可以在节点间导航,第一个节点的 previousSibling 属性是null,最后一个节点的 nextSibling 属性也是null。
首、尾节点
firstChild 和 lastChild ,分别指向childNodes 中的第一个和最后一个子节点。
利用这些关系指针,几乎可以访问文档树的任何节点,而这种便利性是childNodes的最大亮点。
判空
还有一个方法是hasChildNodes(),返回true说明节点有一个或多个子节点。相比查询childNodes的length属性,这个方法无疑更方便。
元素定位
不论是查询还是操作DOM,首先需要获取目标元素,所幸,它为开发者提供了多种方法。
getElementById():接收元素的ID作为参数,若页面中存在多个相同ID的元素,返回第一个。
getElementsByTagName():接收元素的标签名作为参数,返回包含零个或多个元素的NodeList。
getElementsByName():接收name属性值作为参数,返回具有给定name属性的所有元素。
除此之外,document对象上还有一些特殊的属性,代表特定的集合,它们是:
document.anchors:包含文档中所有带name属性的元素
document.forms:包含文档中所有元素
document.images:包含文档中所有