第8章 客户端JavaScript与HTML
绩效
章节 | 代码量(行) |
---|---|
8.1 | 0 |
8.2 | 164 |
8.3 | 0 |
8.4 | 144 |
8.1 客户端JavaScript的重要性
8.1.1 Web 应用程序的发展
随着互联网的发展,现在的网页已经能够支持各种复杂的功能了。这里所说的网页已经不仅仅是单纯的文档,而是变为了一种应用程序,所以也称为 Web 应用程序。
Web 应用程序的功能
Web 应用程序会在两个地方执行操作以实现其功能,即服务器端与客户端(浏览器)。对于服务器端的处理,可以使用 Java、Perl、Python、Ruby、SQL 等多种类型的语言实现。与之相对,用于描述客户端功能的语言可以说只有 JavaScript 一种。
除了 JavaScript,能够实现客户端程序功能的技术还有 Adobe Flash 和 Silverlight,不过它们只能在特定的环境中运行。鉴于这一限制,要开发、发布能够广泛运用的 Web 应用程序,最好选择 JavaScript。
现在的 Web 应用程序已经能够提供各种各样的功能。下面列举其中一些基本功能。
• 拖曳操作(Drag and drop)
• 异步读取
• 键盘快捷键(键盘访问)
• 动画效果
这些功能基本上都可以通过 JavaScript 实现。Web 应用程序所能实现的功能正在逐渐增强,今后将提供不逊于桌面应用程序的体验。
与此同时,在以 Google Chrome 的扩展程序及 Web 应用为代表的非 Web 页面环境中,JavaScript 的应用也越来越普及。因此,通过 JavaScript 来实现的功能越来越丰富,用 JavaScript 开发的情况也越来越多。
8.1.2 JavaScript 的性能提升
得益于浏览器快速的发布周期,JavaScript 处理引擎的功能得到了大幅增强,JavaScript 的性能得以提升。
8.1.3 JavaScript 的作用
JavaScript 的作用之一是提供良好的用户体验,使应用程序能够具有更加易于理解的界面外观以及更高的易用性。
应该尽可能考虑如何利用 JavaScript 实现优秀的用户界面,但是不应该认为仅仅依靠 JavaScript 就能实现所有的功能。理由有以下两点。
- 很多浏览器都禁用 JavaScript
- 有些浏览器允许用户执行自定义的 JavaScript
也就是说,在有些情况下,并不能保证 JavaScript 能够按照 Web 应用程序开发者的预期执行。所以,应该理解JavaScript 的使用范围与局限性,在服务器和客户端分别选择合适的实现方式。
8.2 HTML与JavaScript
8.2.1 网页显示过程中的处理流程
在介绍 JavaScript 之前,我们首先需要了解一下浏览器显示 Web 页面时的流程。首先来确认一下浏览器在显示以下 Web 页面时执行了哪些处理。
在下面的例子中,CSS 和 JavaScript 以单独文件的形式存在,另有一个单纯用于显示图像的 Web 页面(代码清单8.1)。
代码清单 8.1 基本的HTML
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>基本的HTML</title> <link rel="stylesheet" type="text/css" href="/css/sample.css"> <script src="/js/sample.js"></script> </head> <body> <img src="/image/sample.png"> </body> </html>
浏览器在访问该页面时执行了以下处理。
• 分析 HTML • 构造 DOM 树
• 载入外部 JavaScript 文件以及 CSS 文件 • 载入图像文件等外部资源
• JavaScript 在分析后开始执行 • 全部完成
这里的要点在于图像文件等内容是在构造完 DOM 树之后才下载的,因此如果在构造完 DOM 树后再执行
JavaScript,用户的等待时间就可以减少。
8.2.2 JavaScript 的表述方式及其执行流程
浏览器正确读取 Web 页面后就能从服务器取得 HTML 页面。之后 Web 浏览器将分析该 HTML 文件并显示画面。
为了执行 JavaScript,需要在 HTML 文件内以特定的方式书写 JavaScript 的代码。JavaScript 的书写方法有很多种,其执行的流程也各不相同。以下是记述方法一览。
<script>
标签 •- DOMContentLoaded
- 读取外部 JavaScript 文件 •
- 动态载入
- onload
接下来,我们开始逐一说明各种书写方法及其执行流程。
<script>
标签
在
<script>
标签内书写 JavaScript 是一种最为简单的方法。
这种情况下,在
<script>
标签被分析之后就会立即执行 JavaScript。需要注意的是,这样将无法操作<script>
标签之后的 DOM 元素。
由于 JavaScript 是在分析
<script>
标签之后就立即开始执行的,而这时<script>
标签之后的 DOM 元素还未构造,因此在<script>
标签内就无法取得位于其后的 DOM 元素(代码清单 8.2)。
代码清单 8.2 无法操作的元素
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>无法操作的元素</title> </head> <body> <div id="hzh1"></div> <script> var hzh1 = document.getElementById('hzh1'); console.log("测试一下能不能操作hzh1:"); console.log(hzh1 !== null); var hzh2 = document.getElementById('hzh2'); console.log("测试一下能不能操作hzh2:"); console.log(hzh2 !== null); </script> </body> </html>
html5.html:16 测试一下能不能操作hzh1: html5.html:17 true html5.html:19 测试一下能不能操作hzh2: html5.html:20 true
为了避免这个问题,最简单的方法就是在 body 的结束标签前才书写
<script>
标签(代码清单 8.3)。这样一来,在读取<script>
标签时其他所有的 DOM 元素都已分析,从而能够操作 HTML 文件中所有的 DOM元素。
即使如此,还是要避免对 body 操作。这是因为这时的 标签还没有结束,如果对其操作,后果可想而知。
如果希望对 body 操作,可以通过下面的 onload 和 DOMContentLoaded 方式来执行。它们的执行流程允许对 body 操作。
代码清单 8.3 可以对所有的元素操作的方法
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>无法操作的元素</title> </head> <body> <div id="hzh1"></div> <div id="hzh2"></div> <script> var hzh1 = document.getElementById('hzh1'); console.log("测试一下能不能操作hzh1:"); console.log(hzh1 !== null); var hzh2 = document.getElementById('hzh2'); console.log("测试一下能不能操作hzh2:"); console.log(hzh2 !== null); </script> </body> </html>
html5.html:16 测试一下能不能操作hzh1: html5.html:17 true html5.html:19 测试一下能不能操作hzh2: html5.html:20 true
读取外部 JavaScript 文件
虽然直接在
<script>
标签内书写 JavaScript 非常简单,但在实际的 Web 应用程序开发中,这种方式几乎不会被采用。大多数情况下,都会准备单独的 JavaScript文件,然后从 HTML 文件中读取这些文件。这时的书写方式如代码清单 8.4 所示。
代码清单 8.4 读取外部 JavaScript 文件
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>读取外部的JavaScript文件</title> <script src="http://example.com/js/sample.js"></script> </head> <body> </body> </html>
在这种情况下将会读取文件
http://example.com/js/sampe.js
并执行。该文件将会在<script>
标签分析之后马上读取。一旦文件读取完成,文件内的 JavaScript 就将执行。我们还可以对<script>
标签指定 defer 属性和 async属性(代码清单 8.5)。通过指定 defer 属性,可以使该<script>
标签的处理推迟至其他所有的<script>
标签之后。而如果指定了 async 属性,则会以异步方式读取外部文件,并在读取完成后依次执行。
代码清单 8.5 defer 属性与 async 属性
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>读取外部的JavaScript文件</title> <script src="http://example.com/js/sample1.js" defer></script> <script src="http://example.com/js/sample2.js" async></script> </head> <body> </body> </html>
把 JavaScript 分离至外部文件具有很多好处。
首先,浏览器就能够缓存 JavaScript 文件了。如果 JavaScript 文件的内容变化并不频繁,只要下载一次 JavaScript 文件后将其缓存,在第二次读取时就能够避免不必要的下载,直接提高运行速度。
其次,HTML 与 CSS 和 JavaScript 文件分离之后,团队分工将变得更加容易。例如,HTML 和 CSS 主要由负责界面设计的人员来书写,而 JavaScript 则由负责实现功能的人来书写。
进一步来说,一般的编辑器对于某一特定文件只能采用一种文法高亮显示模式。也就是说,如果 HTML 和CSS 与 JavaScript 都书写在同一个文件中,就可能会因为没有通用的文法高亮模式而导致无法实现文字色彩的区分,从而导致可读性的下降。
为了避免产生这种情况,最好将代码分别写在不同的文件里。
onload
如果在 onload 事件处理程序(event handler)中书写 JavaScript 代码,则能够在页面读取完成后再对其执行。因为在执行时已经完成了整个页面的读取,所以可以对所有的 DOM 元素操作。
如果要直接写在 HTML 内,可以像代码清单 8.6 这样将其写在
<body>
标签里。如果要写在外部的JavaScript 文件内,则可以像代码清单 8.7 这样书写。
代码清单 8.6 onload事件处理程序
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>onload事件处理程序</title> </head> <body onload="console.log('黄子涵')"> </body> </html>
代码清单 8.7 在外部 JavaScript 文件中使用 onload 事件处理程序
window.onload = function() { alert('hello'); };
必须注意的是,如果使用了 onload 事件处理程序,则会在读取了所有写在 HTML 文件中的图像文件之后才对其执行。因此,如果在页面内存在大型的图像文件,就可能需要花费很多不必要的时间等待图像的读取后才开始执行 JavaScript。
如果无需通过 JavaScript 对图像处理,或图像的处理内容与其体积无关,则没有必要等整个图像读取之后再开始处理。在载入图像的同时执行 JavaScript 处理的话就能够缩短用户的等待时间。
DOMContentLoaded
对于使用上述的 onload 方法,执行 JavaScript 时可能需要一定的等待时间,这可以使用DOMContentLoaded来解决。DOMContentLoaded 是在完成 HTML 解析后发生的事件。将事件侦听器设置为对该事件侦听,就能够减少执行 JavaScript 之前的不必要的等待时间(代码清单 8.8)。
代码清单 8.8 对 DOMContentLoaded 事件侦听
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>对DOMContentLoaded事件侦听</title> </head> <body> <script> document.addEventListener('DOMContentLoaded', function() { console.log('黄子涵'); }, false); </script> </body> </html>
不过 DOMContentLoaded 也存在一些问题,就是它在 Internet Explorer 8 之前的浏览器中是不受支持的。不过,也有方法能够在早期的 Internet Explorer 中实现相同的功能。
具体来说,就是等到 doScroll() 方法不再抛出异常之后才开始执行 JavaScript 部分(代码清单 8.9)。这里利用的原理是,在 DOM 树的构造过程中执行 doScroll()方法就将会引发错误。
代码清单 8.9 在 IE 中模拟 DOMContentLoaded 事件
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>在 IE 中模拟 DOMContentLoaded 事件</title> </head> <body> <script> function IEContentLoaded(callback) { (function () { try { document.documentElement.doScroll('left'); } catch(error) { setTimeout (arguments.callee, 0); return; } callback(); }()); } IEContentLoaded(function () { console.log('黄子涵'); }); </script> </body> </html>
动态载入
在 JavaScript 中,我们可以在生成 script 元素过程中动态地载入 JavaScript 文件(代码清单 8.10)。
代码清单 8.10 JavaScript的动态载入
var script = document.createElement('script'); script.src = 'other-javaScript.js'; document.getElementsByTagName('head')[0].appendChild(script);
在使用这种方法执行 JavaScript 时,JavaScript 文件在下载过程中并不会阻断其他的操作。这是一个较大的优点。如果直接在页面内书写 script 元素,则在下载该 JavaScript 文件的过程中,其他图像文件或 CSS 文件的下载将被阻断。不过,只要使用这种动态的载入方式,就能够避免下载被阻断而继续处理。
8.2.3 执行流程的小结
在考虑选用何种 JavaScript 执行流程时,DOMContentLoaded 是最为恰当的选择。尽管在 Internet Explorer 中实现同样的功能需要花费一些功夫,不过如果不考虑这个问题,可以说 DOMContentLoaded 是最佳的实现方式。
8.3 运行环境与开发环境
8.3.1 运行环境
要说起人们最熟悉的 JavaScript 运行环境,答案自然是浏览器。有以下这些著名的浏览器:Internet Explorer、Mozilla Firefox、Google Chrome、Safari 以及 Opera。
8.3.2 开发环境
编写 JavaScript 代码时,必不可少的书写工具只有以 Emacs 或 Vim 等为代表的文本编辑器。Eclipse 或 NetBeans 等多用途 IDE 也具有 JavaScript 开发所需的相关功能,使用这些进行开发也是不错的选择。此外还有 WebStorm 这样的 JavaScript+HTML 开发专用的 IDE,不过它是一款收费软件。
8.4 调试
在编写程序的过程中,调试是不可避免的一个环节。特别是最近大型 JavaScript 程序的开发越来越多,在编写程序时考虑调试难易度的问题也变得越来越重要。在此我们将说明一下 JavaScript 的调试方法。
8.4.1 alert
在 JavaScript 代码中加上 alert 语句是一种简单的调试方法。在打开浏览器时将会显示 alert 对话框。可以说,这是所谓的 printf 调试方式的 JavaScript 版本。
这种调试方法在任何浏览器中都可以实现。在显示 alert 对话框时,所有的 JavaScript 处理都会中止。因此,如果添加了大量的 alert 语句,就能够实现单步执行。不过这时需要一次次去关闭 alert 对话框,并不方便。而且,如果不小心无限循环执行了 alert 语句,就不得不强制结束浏览器的进程以关闭窗口。必须多加注意。
此外,还可以像代码清单 8.11 这样,通过覆盖对象的 toString() 方法来修改 alert 对话中显示的字符串。
代码清单 8.11 修改 alert 所显示的字符串
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>修改 alert 所显示的字符串</title> </head> <body> <script> var hzh = "黄子涵"; var Hzh = function (text) { this.text = text; }; var Hzh = new Hzh('黄子涵, alert'); alert(hzh); // => 将显示[object Object] hzh.toString = function () { return this.text; } ; alert(hzh); // => 将显示黄子涵 </script> </body> </html>
8.4.2 console
最近的一些浏览器内置了用于运行 JavaScript 的控制台功能。早先只有 Firefox 的 Firebug 插件才提供了这样的功能。由于使用 Firebug 进行开发的开发者数量众多,因此 Safari 以及 Google Chrome 等浏览器中也开始内置这一功能。
在 JavaScript 代码内部写上 console.log('foo bar') 语句之后,就能够在控制台中显示 foo bar 了。这在本质上与使用 alert 是一样的,不过因为在使用 console 时不需要一次次去关闭对话框,所以会比使用 alert 更为方便。同时,console 中也能够显示比 alert 方法更为详细的信息。
但是在 Internet Explorer 等一些没有内置 console 对象的浏览器中,这种方法自然就会引起错误。
虚拟 console 对象
按理说,在实际运行应用程序时应该删去所有 console 相关的代码。不过在开发过程中常常会需要对程序进行一些测试,如果为此每次都删去 console 部分的代码,未免有些麻烦。于是,为了在 Internet Explorer 这类不支持 console 对象的浏览器中也能够正常运行程序,有时会采用嵌入虚拟 console 对象的做法。
可以像代码清单 8.12 这样,生成一个含有 Firebug 1.7.2 中的 console 对象所支持的所有方法的虚拟 console 对象。只要在首先载入的 JavaScript 起始处添加该对象,就能够避免在调用 console 时发生错误。在实际发布程序时,这部分的代码应该和 console.log 等内容一起删去。当然,即使留在代码中也没什么。不过考虑到应该尽可能减少代码量以提高性能,还是应该将它们删去。
代码清单 8.12 虚拟 console 对象
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>虚拟 console 对象</title> </head> <body> <script> if (!window.console) { (function (win) { var names = [ 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'notifyFirebug', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'trace', 'warn']; var consoleMock = {}; for (var i = 0, len = names.length; i < len; i++) { consoleMock[names[i]] = function () {}; } win.console = consoleMock; }(window)); } </script> </body> </html>
显示消息及对象
下面的方法具有的功能基本相同,它们都会从参数中取得消息或对象,然后将取得的内容输出至控制台。这些方法可以指定多个参数,且每一个指定的参数都将输出。
• console.log() • console.warn()
• console.debug() • console.info()
• console.error()
一般来说,仅使用 log() 方法就能够满足需要。而如果使用 debug() 方法,则可以知道输出的内容是在JavaScript 文件中的哪一行。当需要使用大量 console.log() 类型方法时,选用 debug() 方法会很方便。
error()、warn()、info() 也会显示行数。它们与 debug() 的区别仅仅在于所显示的图标与文字的色彩会有所差异,因此在平时的使用中不必太在意这些。
此外,如果对第 1 个参数指定格式,则会对从第 2 个参数起的对象应用该格式(代码清单 8.13)。如果想要提高日志消息的易读性,就可以利用这一方法。
代码清单 8.13 格式
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>格式</title> </head> <body> <script> console.log('%s %d 岁.', '黄子涵今年', 28); </script> </body> </html>
分析对象并显示
console.dir() 方法可以完整输出作为参数接收到的对象,并将其一目了然地显示出来。console.dirxml() 方法可以将 DOM 元素以 HTML 的形式显示。这两种方法的输出内容都比 console.log() 等方法更为清楚,但也会让日志的篇幅变得很长,在使用时应加以注意,不要因此而忽略了其他的日志内容。
显示栈追踪
使用 console.trace() 方法就可以显示该函数的调用者,并可以据此了解具体是哪一个对象的哪一个事件触发了该函数。
在事件驱动的程序设计过程中,要清楚地知道某个函数在什么时候什么位置调用并不是一件容易的事。这时就可以使用 console.trace() 方法来理清各种调用关系,非常方便。
测量时间、次数与性能
我们可以通过 console.time() 方法与 console.timeEnd() 方法测量这两个方法之间经过的时间(代码清单8.14)。其参数分别为每次计时的名称。具有相同名称的方法将会配对,其间经过的时间则会输出。时间的显示单位为毫秒。
代码清单 8.14 console.time()
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>console.time()</title> </head> <body> <script> console.time('hzh'); alert('hzh,计时开始'); console.time('huangzihan'); alert('huangzihan,计时开始'); console.timeEnd('huangzihan'); alert('huangzihan,计时结束'); console.timeEnd('hzh'); alert('hzh,计时结束'); </script> </body> </html>
使用 console.count() 就可以知道该行具体执行了多少次(代码清单 8.15)。
代码清单 8.15 console.count()
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>console.count()</title> </head> <body> <script> for (var i = 0; i < 10; i++) { console.count('hzh'); if (i % 10 === 0) { console.count('黄子涵'); } } </script> </body> </html>
此外,如果使用 console.profile() 或 console.profileEnd(),则可以获得更为详细的测量结果。它们可以用于获取各个函数分别执行了多少次,或总的执行时间等信息。因为在性能分析的过程中,这一分析也会使用 CPU 资源,所以最终得到的结果将会比正常情况更差。不过另一方面,在性能优化的过程中,哪些函数被多次调用并花费了大量的执行时间是很重要的信息,所以这一功能仍然是不可获缺的。
使用断言
console.assert() 的功能是仅在指定条件为 false 时输出日志。例如,为了确认一个不能接收 null 的参数没有收到 null 值,就可以像下面这样使用该方法(代码清单 8.16)。
代码清单 8.16 console.assert()
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>console.assert()</title> </head> <body> <script> function hzh(notNullObj) { console.assert(notNullObj != null, 'notNullObj 是 null 或者 undefined'); // 其他代码 } console.log(hzh(1)); // => 没有显示断言。调用正确 console.log(null); // => 显示了断言。调用错误 console.log(); // => 显示了断言。调用错误 </script> </body> </html>
如果某一函数在参数为 null 时会发生错误,则可以通过断言来检查。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?