Web浏览器中的JavaScript

客户端JavaScript

Window对象是所有客户端JavaScript特性和API的主要接入点。它表示Web浏览器的一个窗口或窗体,并且可以用标识符window来引用它。Window对象定义了一些属性,比如,指代Location对象的location属性,Location对象指定当前显示在窗口中的URL,并允许脚本往窗口里载入新的URL:

//设置location属性,从而跳转到新的Web页面
window.location="http://www.oreilly.com/";


Window对象还定义了一些方法,比如alert(),可以弹出一个对话框用来显示一些信息。还有setTimeout(),可以注册一个函数,在给定的一段时间之后触发一个回调:

//等待两秒,然后说hello
setTimeout(function(){alert("hello world");},2000);


注意上面的代码并没有显式地使用window属性。在客户端JavaScript中,Window对象也是全局对象。这意味着Window对象处于作用域链的顶部,它的属性和方法实际上是全局变量和全局函数。Window对象有一个引用自身的属性,叫做window。如果需要引用窗口对象本身,可以用这个属性,但是如果只是想要引用全局窗口对象的属性,通常并不需要用到window。

Window对象还定义了很多其他重要的属性、方法和构造函数。

Window对象中其中一个最重要的属性是document,它引用Document对象,后者表示显示在窗口中的文档。Document对象有一些重要方法,比如getElementById(),可以基于元素id属性的值返回单一的文档元素(表示HTML标签的一对开始/结束标记,以及它们之间的所有内容):

//查找id="timestamp"的元素
var timestamp=document.getElementById("timestamp");


getElementById()返回的Element对象有其他重要的属性和方法,比如允许脚本获取它的内容,设置属性值等:

//如果元素为空,往里面插入当前的日期和时间
if(timestamp.firstChild==null)
timestamp.appendChild(document.createTextNode(new Date().toString()));

 

每个Element对象都有style和className属性,允许脚本指定文档元素的CSS样式,或修改应用到元素上的CSS类名。设置这些CSS相关的属性会改变文档元素的呈现:

//显式修改目标元素的呈现
timestamp.style.backgroundColor="yellow";//或者只改变类,让样式表指定具体内容
timestamp.className="highlight";

Window、Document和Element对象上另一个重要的属性集合是事件处理程序相关的属性。可以在脚本中为之绑定一个函数,这个函数会在某个事件发生时以异步的方式调用。事件处理程序可以让JavaScript代码修改窗口、文档和组成文档的元素的行为。事件处理程序的属性名是以单词"on"开始的,用法如下:

//当用户单击timestamp元素时,更新它的内容
timestamp.οnclick=function(){this.innerHTML=new Date().toString();}


Window对象的onload处理程序是最重要的事件处理程序之一。当显示在窗口中的文档内容稳定并可以操作时会触发它。JavaScript代码通常封装在onload事件处理程序里。例13-1是onload处理程序的演示,并展示了客户端JavaScript的实例代码,包括查询文档元素、修改CSS类和定义事件处理程序。这个例子的JavaScript代码是放置在HTML的<script>标签之内的。注意代码里的一个函数是在另一个函数里定义的。因为事件处理程序的广泛使用,使得嵌套函数在客户端JavaScript中非常普遍。

例13-1:显示内容的简单客户端JavaScript

<!DOCTYPE html>
<html>
<head>
<style>/*本页的css样式表*/
.reveal*{display:none;}/*class="reveal"的元素的子元素都不显示*/
.reveal*.handle{display:block;}/*除了class="handle"的元素*/
</style>
<script>//所有的页面逻辑在onload事件之后启动
window.onload=function(){//找到所有class名为"reveal"的容器元素
    var elements=document.getElementsByClassName("reveal");
    for(var i=0;i<elements.length;i++){//对每个元素进行遍历
        var elt=elements[i];//找到容器中的"handle"元素
        var title=elt.getElementsByClassName("handle")[0];//当单击这个元素时,呈现剩下的内容
        addRevealHandler(title,elt);}
    function addRevealHandler(title,elt)
    {
        title.onclick=function(){
            if(elt.className=="reveal")
                elt.className="revealed";
            else if(elt.className=="revealed")
                elt.className="reveal";
        }
    }
};
</script>
</head>
<body>
<div class="reveal">
<h1 class="handle">Click Here to Reveal Hidden Text</h1>
<p>This paragraph is hidden.It appears when you click on the title.</p>
</div>
</body>
</html>

 

JavaScript程序的执行

客户端JavaScript程序没有严格的定义。我们可以说JavaScript程序是由Web页面中所包含的所有JavaScript代码(内联脚本、HTML事件处理程序和javascript:URL)和通过<script>标签的src属性引用的外部JavaScript代码组成。所有这些单独的代码共用同一个全局Window对象。这意味着它们都可以看到相同的Document对象,可以共享相同的全局函数和变量的集合:如果一个脚本定义了新的全局变量或函数,那么这个变量或函数会在脚本执行之后对任意JavaScript代码可见。

如果Web页面包含一个嵌入的窗体(通常使用<iframe>元素),嵌入文档中的JavaScript代码和被嵌入文档里的JavaScript代码会有不同的全局对象,它可以当做一个单独的JavaScript程序。但是,要记住,没有严格的关于JavaScript程序范围的定义。如果外面和里面的文档来自于同一个服务器,那么两个文档中的代码就可以进行交互,并且如果你愿意,就可以把它们当做是同一个程序的两个相互作用的部分。

bookmarklet里的javascript:URL存在于文档之外,可以想象成是一种用户扩展或者对于其他程序的修改。当用户执行一个bookmarklet时,书签里的JavaScript代码就可以访问全局对象和当前文档的内容,以及对它进行操作。

JavaScript程序的执行有两个阶段。在第一阶段,载入文档内容,并执行<script>元素里的代码(包括内联脚本和外部脚本)。脚本通常会按它们在文档里的出现顺序执行。所有脚本里的JavaScript代码都是从上往下,按照它在条件、循环以及其他控制语句中的出现顺序执行。

当文档载入完成,并且所有脚本执行完成后,JavaScript执行就进入它的第二阶段。这个阶段是异步的,而且由事件驱动的。在事件驱动阶段,Web浏览器调用事件处理程序函数(由第一阶段里执行的脚本指定的HTML事件处理程序,或之前调用的事件处理程序来定义),来响应异步发生的事件。调用事件处理程序通常是响应用户输入(如鼠标单击,键盘按下等)。但是,还可以由网络活动、运行时间或者JavaScript代码中的错误来触发。注意,嵌入在Web页面里的javascript:URL也可以被当做是一种事件处理程序,因为直到用户通过单击链接或提交表单来激活之后它们才会有效果。

事件驱动阶段里发生的第一个事件是load事件,指示文档已经完全载入,并可以操作。JavaScript程序经常用这个事件来触发或发送消息。我们会经常看到一些定义函数的脚本程序,除了定义一个onload事件处理程序函数外不做其他操作,这个函数会在脚本事件驱动阶段开始时被load事件触发。正是这个onload事件会对文档进行操作,并做程序想做的任何事。JavaScript程序的载入阶段是相对短暂的,通常只持续1~2秒。在文档载入完成之后,只要Web浏览器显示文档,事件驱动阶段就会一直持续下去。因为这个阶段是异步的和事件驱动的,所以可能有长时间处于不活动状态,没有JavaScript被执行,被用户或网络事件触发的活动打断。

核心JavaScript和客户端JavaScript都有一个单线程执行模型。脚本和事件处理程序(无论如何)在同一个时间只能执行一个,没有并发性。这保持了JavaScript编程的简单性。

  • 同步、异步和延迟的脚本

JavaScript第一次添加到Web浏览器时,还没有API可以用来遍历和操作文档的结构和内容。当文档还在载入时,JavaScript影响文档内容的唯一方法是快速生成内容。它使用document.write()方法完成上述任务。例13-3展示了1996年最先进的JavaScript代码的样子。

例13-3:载入时生成文档内容

<h1>Table of Factorials</h1>
<script>
function factorial(n){//用来计算阶乘的函数
    if(n<=1)return n;
else return n*factorial(n-1);
}
document.write("<table>");//开始创建HTML表
document.write("<tr><th>n</th><th>n!</th></tr>");//输出表头
for(var i=1;i<=10;i++){//输出10行
    document.write("<tr><td>"+i+"</td><td>"+factorial(i)+"</td></tr>");
}
document.write("</table>");//表格结束
document.write("Generated at"+new Date());//输出时间戳
</script>


当脚本把文本传递给document.write()时,这个文本被添加到文档输入流中,HTML解析器会在当前位置创建一个文本节点,将文本插入这个文本节点后面。我们并不推荐使用document.write(),但在某些场景下它有着重要的用途。当HTML解析器遇到<script>元素时,它默认必须先执行脚本,然后再恢复文档的解析和渲染。这对于内联脚本没什么问题,但如果脚本源代码是一个由src属性指定的外部文件,这意味着脚本后面的文档部分在下载和执行脚本之前,都不会出现在浏览器中。

脚本的执行只在默认情况下是同步和阻塞的。<script>标签可以有defer和async属性,这(在支持它们的浏览器里)可以改变脚本的执行方式。这些都是布尔属性,没有值;只需要出现在<script>标签里即可。HTML5说这些属性只在和src属性联合使用时才有效,但有些浏览器还支持延迟的内联脚本:

<script defer src="deferred.js"></script>
<script async src="async.js"></script>


defer和async属性都像在告诉浏览器链接进来的脚本不会使用document.write(),也不会生成文档内容,因此浏览器可以在下载脚本时继续解析和渲染文档。defer属性使得浏览器延迟脚本的执行,直到文档的载入和解析完成,并可以操作。async属性使得浏览器可以尽快地执行脚本,而不用在下载脚本时阻塞文档解析。如果<script>标签同时有两个属性,同时支持两者的浏览器会遵从async属性并忽略defer属性。

注意,延迟的脚本会按它们在文档里的出现顺序执行。而异步脚本在它们载入后执行,这意味着它们可能会无序执行。

在撰写本书的时候,async和defer属性还没有广泛实现,它们只被一些优化建议所考虑。即便延迟和异步的脚本会同步执行,Web页面应该还可以正常工作。

甚至可以在不支持async属性的浏览器里,通过动态创建<script>元素并把它插入到文档中,来实现脚本的异步载入和执行。例13-4里的loadasync()函数完成了这个工作。第15章会介绍它使用的技术。

例13-4:异步载入并执行脚本

//异步载入并执行一个指定URL中的脚本
function loadasync(url){
var head=document.getElementsByTagName("head")[0];//找到<head>元素
var s=document.createElement("script");//创建一个<script>元素
s.src=url;//设置其src属性
head.appendChild(s);//将script元素插入head标签中
}

注意这个loadasync()函数会动态地载入脚本——脚本载入到文档中,成为正在执行的JavaScript程序的一部分,既不是通过Web页面内联包含,也不是来自Web页面的静态引用。

  • 事件驱动的JavaScript

例13-3里展示的古老的JavaScript程序是同步载入的程序:在页面载入时开始执行,生成一些输出,然后结束。这种类型的程序在今天已经不常见了。反之,我们通过注册事件处理程序函数来写程序。之后在注册的事件发生时异步调用这些函数。例如,想要为常用操作启用键盘快捷键的Web应用会为键盘事件注册事件处理程序。甚至非交互的程序也使用事件。假如想要写一个分析文档结构并自动生成文档内容的表格的程序。程序不需要用户输入事件的事件处理程序,但它还是会注册onload事件处理程序,这样就可以知道文档在什么时候载入完成并可以生成内容表格了。

事件都有名字,比如click、change、load、mouseover、keypress或readystatechange,指示发生的事件的通用类型。事件还有目标,它是一个对象,并且事件就是在它上面发生的。当我们谈论事件的时候,必须同时指定事件类型(名字)和目标:比如,一个单击事件发生在HTMLButtonElement对象上,或者一个readystatechange事件发生在XMLHttpRequest对象上。

如果想要程序响应一个事件,写一个函数,叫做“事件处理程序”、“事件监听器”或“回调”。然后注册这个函数,这样他就会在事件发生时调用它。正如前面提到的,这可以通过HTML属性来完成,但是我们不鼓励将JavaScript代码和HTML内容混淆在一起。反之,注册事件处理程序最简单的方法是把JavaScript函数赋值给目标对象的属性,类似这样的代码:

window.onload=function(){...};
document.getElementById("button1").onclick=function(){...};
function handleResponse(){...}
request.onreadystatechange=handleResponse;

注意,按照约定,事件处理程序的属性的名字是以"on"开始,后面跟着事件的名字。还要注意在上面的任何代码里没有函数调用:只是把函数本身赋值给这些属性。浏览器会在事件发生时执行调用。用事件进行异步编程会经常涉及嵌套函数,也经常要在函数的函数里定义函数。

对于大部分浏览器中的大部分事件来说,会把一个对象传递给事件处理程序作为参数,那个对象的属性提供了事件的详细信息。比如,传递给单击事件的对象,会有一个属性说明鼠标的哪个按钮被单击。(在IE里,这些事件信息被存储在全局event对象里,而不是传递给处理程序函数。)事件处理程序的返回值有时用来指示函数是否充分处理了事件,以及阻止浏览器执行它默认会进行的各种操作。

有些事件的目标是文档元素,它们会经常往上传递给文档树,这个过程叫做“冒泡”。例如,如果用户在<button>元素上单击鼠标,单击事件就会在按钮上触发。如果注册在按钮上的函数没有处理(并且冒泡停止)该事件,事件会冒泡到按钮嵌套的容器元素,这样,任何注册在容器元素上的单击事件都会调用。

如果需要为一个事件注册多个事件处理程序函数,或者如果想要写一个可以安全注册事件处理程序的代码模块,就算另一个模块已经为相同的目标上的相同的事件注册了一个处理程序,也需要用到另一种事件处理程序注册技术。大部分可以成为事件目标的对象都有一个叫做addEventListaner()的方法,允许注册多个监听器:

window.onload=function(){...};
document.getElementById("button1").onclick=function(){...};
function handleResponse(){...}
request.onreadystatechange=handleResponse;

注意这个函数的第一个参数是事件的名称。虽然addEventListener()已经标准化超过了十年,而微软目前只有在IE9里实现了它。在IE8以及之前的浏览器中,必须使用一个相似的方法,叫做attachEvent():

window.attachEvent("onload",function(){...});
 

客户端JavaScript程序还使用异步通知类型,这些类型往往不是事件。如果设置Window对象的onerror属性为一个函数,会在发生JavaScript错误(或其他未捕获的异常)时调用函数。还有,setTimeout()和setInterval()函数(这些是Window对象的方法,因此是客户端JavaScript的全局函数)会在指定的一段时间之后触发指定函数的调用。传递给setTimeout()的函数和真实事件处理程序的注册不同,它们通常叫做“回调逻辑”而不是“处理程序”,但它们和事件处理程序一样,也是异步的。

例13-5演示了setTimeout()、addEventListener()和attachEvent(),定义一个onload()函数注册在文档载入完成时执行的函数。on load()是非常有用的函数,我们会在本书后面的例子中用到它。

例13-5:onLoad(),当文档载入完成时调用一个函数

//注册函数f,当文档载入完成时执行这个函数f
//如果文档已经载入完成,尽快以异步方式执行它
function onLoad(f){
if(onLoad.loaded)//如果文档已经载入完成
window.setTimeout(f,0);//将f放入异步队列,并尽快执行它
else if(window.addEventListener)//注册事件的标准方法
window.addEventListener("load",f,false);
else if(window.attachEvent)//IE8以及更早的IE版本浏览器注册事件的方法
window.attachEvent("onload",f);
}
//给onLoad设置一个标志,用来指示文档是否载入完成
onLoad.loaded=false;//注册一个函数,当文档载入完成时设置这个标志
onLoad(function(){onLoad.loaded=true;});
  • 客户端JavaScript线程模型

JavaScript语言核心并不包含任何线程机制,并且客户端JavaScript传统上也没有定义任何线程机制。HTML5定义了一种作为后台线程的"WebWorker",但是客户端JavaScript还像严格的单线程一样工作。甚至当可能并发执行的时候,客户端JavaScript也不会知晓是否真的有并行逻辑的执行。

单线程执行是为了让编程更加简单。编写代码时可以确保两个事件处理程序不会同一时刻运行,操作文档内容时也不必担心会有其他线程试图同时修改文档,并且永远不需要在写JavaScript代码的时候担心锁、死锁和竞态条件(race condition)。

单线程执行意味着浏览器必须在脚本和事件句处理程序执行的时候停止响应用户输入。这为JavaScript程序员带来了负担,它意味着JavaScript脚本和事件处理程序不能运行太长时间。如果一个脚本执行计算密集的任务,它将会给文档载入带来延迟,而用户无法在脚本完成前看到文档内容。如果事件处理程序执行计算密集的任务,浏览器可能变得无法响应,可能会导致用户认为浏览器崩溃了[7]。

如果应用程序不得不执行太多的计算而导致明显的延迟,应该允许文档在执行这个计算之前完全载入,并确保能够告知用户计算正在进行并且浏览器没有挂起。如果可能将计算分解为离散的子任务,可以使用setTimeout()和setInterval()方法在后台运行子任务,同时更新一个进度指示器向用户显示反馈。

HTML5定义了一种并发的控制方式,叫做"Web worker"。Web worker是一个用来执行计算密集任务而不冻结用户界面的后台线程。运行在Web worker线程里的代码不能访问文档内容,不能和主线程或其他worker共享状态,只可以和主线程和其他worker通过异步事件进行通信,所以主线程不能检测并发性,并且Web worker不能修改JavaScript程序的基础单线程执行模型。

  • 客户端JavaScript时间线

我们已经看到了JavaScript程序从脚本执行阶段开始,然后切换到事件处理阶段。下面会更详细地解释了JavaScript程序执行的时间线。

1.Web浏览器创建Document对象,并且开始解析Web页面,解析HTML元素和它们的文本内容后添加Element对象和Text节点到文档中。在这个阶段document.readystate属性的值是"loading"。

2.当HTML解析器遇到没有async和defer属性的<script>元素时,它把这些元素添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载(如果需要)和执行时解析器会暂停。这样脚本就可以用document.write()来把文本插入到输入流中。解析器恢复时这些文本会成为文档的一部分。同步脚本经常简单定义函数和注册后面使用的注册事件处理程序,但它们可以遍历和操作文档树,因为在它们执行时已经存在了。这样,同步脚本可以看到它自己的<script>元素和它们之前的文档内容。

3.当解析器遇到设置了async属性的<script>元素时,它开始下载脚本文本,并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器没有停下来等它下载。异步脚本禁止使用document.write()方法。它们可以看到自己的<script>元素和它之前的所有文档元素,并且可能或干脆不可能访问其他的文档内容。

4.当文档完成解析,document.readyState属性变成"interactive"。

5.所有有defer属性的脚本,会按它们在文档的里的出现顺序执行。异步脚本可能也会在这个时间执行。延迟脚本能访问完整的文档树,禁止使用document.write()方法。

6.浏览器在Document对象上触发DOMContentLoaded事件。这标志着程序执行从同步脚本执行阶段转换到了异步事件驱动阶段。但要注意,这时可能还有异步脚本没有执行完成。

7.这时,文档已经完全解析完成,但是浏览器可能还在等待其他内容载入,如图片。当所有这些内容完成载入时,并且所有异步脚本完成载入和执行,document.readyState属性改变为"complete",Web浏览器触发Window对象上的load事件。

8.从此刻起,会调用异步事件,以异步响应用户输入事件、网络事件、计时器过期等。

这是一条理想的时间线,但是所有浏览器都没有支持它的全部细节。所有浏览器普遍都支持load事件,都会触发它,它是决定文档完全载入并可以操作最通用的技术。DOMContentLoaded事件在load事件之前触发,当前所有浏览器都支持这个事件,除了IE之外,document.readyState属性在写本书时已被大部分浏览器实现,但是属性的值在浏览器之间有细微的差别。defer属性被所有当前版本的IE支持,但是现在还未被其他浏览器实现。async属性的支持在写本书时还不通用,但是例13-4里展示的异步脚本执行技术被当前所有当前浏览器支持。(但是,要注意用类似loadasync()函数动态载入脚本的能力让程序执行的脚本载入阶段和事件驱动阶段之间的界限更加模糊。)

这条时间线没有指定什么时候文档开始对用户可见或什么时候Web浏览器必须开始响应用户输入事件。这些是实现细节。对于很长的文档或非常慢的网络链接,Web浏览器理论上会渲染一部分文档,并且在所有脚本执行之前,就能允许用户开始和页面产生一些交互。这种情况下,用户输入事件可能在程序执行的事件驱动阶段开始之前触发n

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值