文档对象模型与计时器函数

本文介绍使用JavaScript对浏览器和页面编程的基本方法,包括BOM对象、DOM对象,以及计时器函数的应用。

BOM和DOM

通过BOM(浏览器对象模式,Browser Object Model),可以对浏览器窗口、URL地址、导航、浏览历史、显示设备、页面内容等操作。其中,window对象表示浏览器窗口,是BOM中的主对象,其子对象包括:

  • document对象,表示页面对象,是DOM的主角,也就是说,DOM是BOM的子集。
  • location对象,处理URL信息。
  • navigator对象,包含了浏览器信息。
  • history对象,控制浏览记录,如forward()方法为前进操作、back()方法为后退操作、go()方法指定前进(正数)或后退(负数)的浏览记录数量。
  • screen对象,包含了用户显示设备的信息。

实际应用中,随意修改浏览器外观和默认行为并不是好的设计方法,现代Web项目开发中几乎不再使用;本文接下来将主要讨论这些对象的常用操作。

页面节点

DOM(文档对象模型,Document Object Model)可以有效地处理HTML、XHTML等文档结构,下面先来看一个简单的HTML文件代码。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <div id="article">
        <h1 id="title1">标题一</h1>
        <p id="p1">段落一</p>
        <p id="p2">段落二</p>
    </div>
</body>
</html>

页面中的HTML元素一目了然,但现在需要了解页面中的“节点(node)”。在</html>标记后面添加如下代码。

JavaScript
<script>
    let art = document.getElementById("article");
    alert(art.childNodes.length);
</script>

即使还没有介绍代码中的doucment.getElementById()方法和art.childNodes.length属性,也应该可以大概猜出来这两行代码的功能是显示id为article的div元素的子节点数量,显示结果为7。在HTML代码中可以看到,div元素下只有一个h1元素和两个元素,那么7个节点是从哪儿来的呢?请注意,在h1和p元素的前后还有空白处,这里也是节点;这些节点不是元素,那它们是什么呢?

按下来,修改JavaScript代码如下。

JavaScript
<script>
    let art = document.getElementById("article");
    let node = art.childNodes[0];
    alert(node.nodeName);
    alert(node.nodeValue);
    alert(node.nodeType);
</script>

代码中,使用art.childNodes[0]获取了div元素下的第一个子节点,然后显示了节点的三个属性,分别是:

  • nodeType属性,显示节点类型值,1(ELEMENT_NODE)为页面元素,3(TEXT_NODE)为文本节点,本例会显示3。
  • nodeName属性,显示节点名称,本例显示为"#text"。如果是HTML元素,则会显示元素名的大写形式。
  • nodeValue属性,显示文本类节点的内容,非文本类节点返回null,本例为空白字符。DOM节点类型中,3(TEXT_NODE)、4(CDATA_NODE)、8(COMMENT_NODE)为文本类型的节点。

将art.childNodes[0]的索引值修改为1,可以看到h1元素的相关属性,会分别显示"H1"、null和1。可以看到,元素节点的nodeType返回1,nodeName属性会返回元素名的大写形式,nodeValue属性返回null值。

从测试中可以看到,代码中定义的页面,从div元素以下的节点结构如下图所示。

页面节点结构

虽然页面文档的结构是由各种节点组成,但在操作时一般会从一个确定的元素节点开始,下面就从元素节点的角度了解更多地操作。

获取和创建元素

获取元素时,可以使用document对象的以下方法和属性:

  • getElementById()方法,按元素的id属性值获取唯一的元素对象。
  • getElementsByClassName()方法,按元素的class属性值获取元素集合。
  • getElementsByName()方法,按元素的name属性值获取元素集合。
  • getElementsByTagName()方法,按元素类型名称获取元素集合,如所有P元素。
  • 元素集合属性,如images表示页面中所有img元素的集合,links表示页面中所包含href属性的a元素集合,forms表示页面中所有表单(from)元素的集合,embeds表示包含所有embed元素的集合等。

对于一个已获取的节点对象,常用的属性和方法包括:

  • parentNode属性,获取节点的上级节点。
  • hasChildNodes()方法,判断节点中是否有子节点,返回true或false。
  • firstChild属性,获取节点中的第一个子节点,没有子节点时返回undefined。
  • lastChild属性,获取节点中的最后一个子节点,没有子节点时返回undefined。
  • childNodes属性,返回节点中所有子节点的集合。
  • appendChild(node)方法,在节点中添加子节点node。
  • insertBefore(x,y)方法,在节点的子节点y前插入子节点x。

创建节点时可以使用document对象的如下方法:

  • createElement(elementName),根据元素名称创建元素节点。
  • createTextNode(text)方法,根据文本内容创建文本节点。

下面的代码会在“段落二”前添加“段落三”元素节点。

JavaScript
<script>
    let p3 = document.createElement("p");
    let p3Text = document.createTextNode("段落三");
    p3.appendChild(p3Text);
    let p2 = document.getElementById("p2");
    let art = document.getElementById("article");
    art.insertBefore(p3, p2);
</script>

运行代码,页面显示效果如下图所示。

动态创建节点

利用p2元素的父节点也可以完成插入操作,如下面的代码。

JavaScript
<script>
    let p3 = document.createElement("p");
    let p3Text = document.createTextNode("段落三");
    p3.appendChild(p3Text);
    let p2 = document.getElementById("p2");
    p2.parentNode.insertBefore(p3, p2);
</script>

执行结果与前例相同。

元素的innerHTML和innerText属性

实际应用中,还可以使用innerHTML和innerText属性修改元素中的HTML代码或文本内容。下面的代码演示了这两个属性的应用和区别。

JavaScript
<script>
    let s = "X<sup>2</sup>";
    let p1 = document.getElementById("p1");
    let p2 = document.getElementById("p2");
    p1.innerHTML = s;
    p2.innerText = s;
</script>

代码中,变量s包含了一些HTML代码,内容为大写字母X和上标格式的数字2。接下来,通过innerHTML属性将s的内容设置到p1元素,通过innerText属性将s的内容设置到p2元素,页面显示效果如下图所示。

元素的innerHTML和innerText属性

可以看到,innerHTML属性内容会解析为HTML代码,而innerText属性内容则解析为文本内容。此外,还可以通过这两个属性读取元素中的HTML代码和文本内容。

读取和设置元素属性

读取元素的属性值时,可以使用元素对象的getAttribute()方法,其参数为属性名,如下面的代码会显示页面中第一个h1元素的id属性名。

JavaScript
<script>
    let arr = document.getElementsByTagName("h1");
    let e = arr[0];
    alert(e.getAttribute("id"));
</script>

执行代码会显示"title1"。

设置元素对象的属性值时可以使用元素对象的setAttribute()方法,参数分别是属性名和属性值,如下面的代码会设置第一个h1元素的style属性以改变其显示的样式。

JavaScript
<script>
    let arr = document.getElementsByTagName("h1");
    let e = arr[0];
    e.setAttribute("style","font-style:italic;color:red;");
</script>

代码中设置第一个h1元素文本显示为斜体和红色,效果如下图所示。

设置元素的style属性

可以通过元素对象的style属性中的样式属性设置元素样式,如下面的代码。

JavaScript
<script>
    let arr = document.getElementsByTagName("h1");
    let e = arr[0];
    e.style.fontStyle = "italic";
    e.style.color = "red";
</script>

代码执行效果与前例相同。

window对象

首先了解window对象的open()方法与close()方法,其中,open()方法用于打开新的浏览器窗口,其参数包括:

  • url,设置打开的资源路径。
  • target,设置资源的打开方式,默认为"_blank",指定在新的窗口或标签中打开;设置为"_self"时指定在当前窗口或标签打开。
  • features,可以通过一系列参数设置新窗口的外观,一般不需要使用。
  • replace,是否替换浏览器历史记录,默认为false。

window.open()方法会返回一个窗口对象,而窗体对象的close()方法可以关闭窗口。下面的代码演示了相关应用。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <div id="article">
        <button type="button" onclick="winOpen();">打开</button>
        <button type="button" onclick="winClose();">关闭</button>
    </div>
</body>
</html>
<script>
    let win = null;
    //
    function winOpen() {
        win = window.open("http://caohuayu.com");
    }
    //
    function winClose() {
        if (win === null) alert("没有可关闭的窗口");
        else win.close();
    }
</script>

页面中定义了两个按钮,其中,“打开”按钮会在新窗口或标签中打开作者的个人网站,回到初始页面,点击“关闭”按钮可以关闭打开的新网站窗口或标签。

此外,调用window对象的close()方法可以关闭当前窗口标签。

window对象的onload事件会在页面元素完全加载后触发,所以,这里是进行页面初始化工作的好地方,下面的代码演示了相关应用。

JavaScript
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
<h1 id="title1"></h1>
</body>
</html>
<script>
    window.onload = function () {
        document.getElementById("title1").innerText = "页面已加载";
    };
</script>

本例,页面加载后会在h1元素中显示“页面已加载”。

有些时间,特别是在大型Web项目中,一个页面的初始化工作可能不止一次,此时就需要一个通用的机制能够正确地添加多个初始化函数,如下面的代码。

JavaScript
<script>
    function addWinLoadFunc(fn) {
        if (typeof fn !== "function") return;
        let oldFn = window.onload;
        window.onload = function () {
            if (typeof oldFn === "function") oldFn();
            fn();
        };
    }
    //
    function pageInit1() {
        alert("初始化1");
    }
    //
    addWinLoadFunc(pageInit1);
    addWinLoadFunc(function () { alert("初始化2"); });
    addWinLoadFunc(() => alert("初始化3"));
</script>

代码中首先定义了addWinLoadFunc()函数,参数fn指定为函数类型。addWinLoadFunc()函数中,当参数fn不是函数类型时会退出函数;然后,通过oldFn变量备份window.onload事件的原始代码;接下来定义window.onload事件执行的新函数,其中,当onload原始代码为函数时先调用它,最后调用fn指定的函数,这样就保证了初始化代码的执行顺序。

pageInit1()函数为第一个初始化函数,第一次调用addWinLoadFunc()函数时直接使用函数名作为参数;第二次调用addWinLoadFunc()函数时使用了匿名函数;第三次调用addWinLoadFunc()函数时使用了=>运算符创建的简化函数。打开页面,会依次显示"初始化1"、"初始化2"、"初始化3"。

URL与文本编码

location对象可以处理地址栏显示的URL地址信息,常用的属性包括:

  • href属性,获取地址栏显示的完整的URL地址。
  • host和hostname,获取URL中的服务器主机名称。
  • pathname属性,获取服务器中的资源路径。
  • port属性,获取URL中的服务器端口。
  • protocol属性,获取URL中使用的协议,如http:、https:。
  • search属性,获取问号(?)及以后的查询参数。
  • hash属性,获取#符号及以后的内容。

下面的代码会显示相关的属性值,请注意,打开测试页面后可以在网址的最后手动添加"?name=Tom&age=25"。

JavaScript
<script>
    alert(location.href); // http://localhost:58476/demo/test.html?name=Tom&age=25
    alert(location.host); // localhost:58476
    alert(location.pathname);  // /demo/test.html
    alert(location.port);  // 58476
    alert(location.protocol);  // http:
    alert(location.search);  // ?name=Tom&age=25
</script>

处理问号(?)后面的参数时,可删除问号,然后使用&符号分割成数组,数组中的每一个元素就是“参数名=值”的格式,需要再次使用等号(=)分割为参数名和值。下面的代码可以将URL中的查询参数转换为Map对象。

JavaScript
<script>
    function getQueryParam() {
        let result = new Map();
        let s = location.search;
        if (s.length === 0) return result;
        let arr = s.substring(1).split(/&/g);
        let kvArr;
        for (let kv of arr) {
            kvArr = kv.split("=");
            result.set(kvArr[0], kvArr[1]);
        }
        return result;
    }
    //
    let qryParam = getQueryParam();
    qryParam.forEach((v, k) => alert(k + " : " + v));
</script>

本例,如果访问网址为“http://localhost:58476/demo/test.html?name=Tom&age=25”,则会依次显示"name : Tom"、"age : 25"。代码中,getQueryParam()函数会返回一个Map对象,如果URL中没有查询参数,则返回一个没有元素的空集合。

需要注意的是,URL地址在传递过程中可能会进行编码处理,此时,可以使用以下函数进行处理:

  • encodeURI()和decodeURI()函数,对URL进行编码和解码操作,不处理的字符包括字母、数字,以及字符# - _ . ! ~ * ' ( ) ; , / ? : @ & = + $。
  • encodeURIComponent()和decodeURIComponent()函数,对URL进行编码和解码操作,不处理的字符包括字母、数字,以及字符( ) . ! ~ * ' - _。

计时器函数

setTimeount()函数需要指定两个参数,其中,参数一指定执行的函数,参数二指定等待多少毫秒以后开始执行;函数会返回一个计时器标识,可以使用clearTimeout()函数终止指定标识的计时器代码。需要注意的是,setTimeout()指定的函数只会执行一次,如果需要再次执行,需要重复调用setTimeout()函数,下面的代码演示了相关应用。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1 id="title1"></h1>
    <button type="button" onclick="clearTimeout(timerFlag);">停止</button>
</body>
</html>
<script>
    let timerFlag = null;
    function showTime() {
        let h1 = document.getElementById("title1");
        h1.innerText = new Date().toLocaleTimeString();
        timerFlag = setTimeout(showTime, 1000);
    }
    //
    showTime();
</script>

JavaScript代码中,timerFlag变量会保存最后一次调用setTimeout()函数的计时器标识,showTime()函数中,会将系统当前时间显示到h1元素中,每隔1秒更新一次。点击页面中的“停止”按钮后,会调用clearTimeout()函数停止时间的更新。

setInterval()函数的第一个参数同样指定执行函数,第二个参数指定每次执行的间隔毫秒数,与setTimeout()函数不同的是,setInterval()函数会严格地按照指定的时间间隔重复执行代码;此外,setInterval()函数同样会返回一个计时器标识,可以使用clearInterval()函数终止指定标识的计时器代码。下面的代码演示了setInterval()和clearInterval()函数的应用。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1 id="title1"></h1>
    <button type="button" onclick="clearTimeout(timerFlag);">停止</button>
</body>
</html>
<script>
    function showTime() {
        let h1 = document.getElementById("title1");
        h1.innerText = new Date().toLocaleTimeString();
    }
    //
    showTime();
    let timerFlag = setInterval(showTime, 1000);
</script>

代码功能与前例相同,打开页面后会在h1元素中显示系统当前时间,点击“停止”按钮后会停止时间更新。

setTimeout()函数调用的代码会在每次执行完成后再按指定的间隔时间执行,可以保证每次代码执行的完整性,适用于每次执行代码都需要完整性和正确性的场景。而setInterval()则会保证在指定间隔时间时准时开始执行新的代码,即使上一次代码还没有执行完成也是这样,典型的情况可以参考游戏循环,以执行的时效性为主;但是,如果一个循环没有完成就开始下一循环,就可能出现“跳帧”的情况。实际应用中,可以根据执行代码的性质和功能需要合理选择合适的计时器函数。