JavaScript DOM编程艺术

前言

归根结底,代码都是思想和概念的体现。没人能把一种程序设计语言的所有语法和关键字都记住,可以查阅参考书来解决。平稳退化、渐进增强、以用户为中心的设计对任何前端Web开发工作都非常重要。

第1章 JavaScript简史

1 JavaScript的起源

Netscape公司和Sun公司合作开发。第一个版本出现在1995年推出的Netscape Navigator2浏览器中。IE也以JScript为名发布了一个版本,面对IE的竞争,Netscape和Sun联合ECMA对JavaScript进行了标准化——ECMAScript语言,就是现在谈论的JavaScript(以下均简称JS)。与Sun公司开发的Java程序语言没有任何关系。

2 DOM

DOM是一套对文档内容进行抽象和概念化的方法。JS的早期版本向程序员提供了查询和操控web文档某些实际内容的手段,主要是图像和表单,因为JS预先定义了images和forms等术语,通常把这种试验性质的初级DOM成为0级DOM。

3 浏览器战争

Netscape Navigator 4发布于1997年6月,IE4发布于同年10月,两者都大幅扩展了DOM(但彼此不兼容),使JS功能大大增加,出现一个新名词:DHTML,即动态HTML,是描述HTML、CSS、JS技术组合的术语。

4 制定标准

浏览器制造商们携手W3C于1998年制定完成了新的标准,即第1级DOM。标准化的DOM有远大的抱负。

  • 浏览器以外的考虑。DOM是一种API(应用编程接口),API是一组已经得到有关各方共同认可的基本约定,如国际时区、化学元素周期表等。W3C对DOM的定义是:一个与系统平台和编程语言无关的接口,程序和脚本可以通过这个接口动态地访问和修改文档的内容、结构和样式。
  • 浏览器战争的结局。市场份额大战中,微软战胜了Netscape。下一代浏览器产品对Web标准的支持得到了极大的改善。
  • 崭新的起点。如今,safari(WebKit)、Chrome(WebKit)、Firefox(Gecko)、IE(Trident)和一些智能手机都对DOM有良好的支持。

第2章 JS语法

1 准备工作

编写JS脚本:文本编辑器+脚本,必须通过HTML/XHTML文档执行。
两种方式:

  • 将JS代码放到文档<head>标签中的<script>标签之间
  • 把JS代码存为一个扩展名为.js的独立文件,在<head>部分放入<script>标签,将其src属性指向该文件
  • 最好的做法是把<script>标签放在HTML文档的最后,</body>之前,这样能使浏览器更快地加载页面

程序设计语言分为解释型和编译型两大类。Java或C++等语言需要一个编译器,能把源代码翻译为直接在计算机上执行的文件。解释型语言不需要编译器,仅需要解释器。对于JS解释器,浏览器负责完成有关的解释和执行工作。

2 语法

(1)语句。建议在每条语句后面加分号,这是一种良好的编程习惯。

(2)注释。有效帮助了解代码流程,多种注释方法。建议用“//”来注释单行,用“/*”注释多行。

(3)变量。将值存入变量的操作,叫赋值。提前声明变量是一种良好的编程习惯,虽然JS没有强制要求。最有效率的方法是声明和赋值一次完成:

var mood="happy",age=26;
相当于:
var mood,age;
mood="happy";
age=33;

JS中变量和其他语法元素的名字都区分字母大小写,不允许变量名中包括空格或标点符号(美元符号除外),允许包括字母、数字、美元符号和下划线,第一个字符不允许是数字。如变量名my mood无效,可用my_mood,或myMood,这种驼峰格式是函数名、方法名和对象属性名命名的首选格式。

(4)数据类型。上述代码中,变量mood的值是字符串,变量age的值是数字,类型不同但声明和赋值语法完全相同。有些其他语言还要求声明数据类型,称为强类型语言,而JS属于弱类型语言。JS中的几种数据类型:

  • 字符串。包括(但不限于)字母、数字、标点符号和空格,必须包在引号里,单双均可,但最好在整个文本里保持一致。如果字符串里本身含引号造成一定干扰,可使用反斜线\进行转义,如‘don't ask'。
  • 数值。允许任意位小数,称为浮点数,也可以是负数。
  • 布尔值。布尔数据只有两个可选值:true 或者false。布尔值不是字符串,不要用引号括起来,false和“false”是两码事。

(5)数组。字符串、数值和布尔值都是标量(scalar),它只能有一个值。而数组是指用一个变量表示一个值的集合,集合中的每个值都是这个数组的一个元素。
JS数组可以用关键字Array声明,声明的同时还可以指定初始元素个数,即数组长度。

var beatles=Array(4);或 var beatles=Array()

向数组添加元素的操作称为填充(populating)。填充时,需要给出新元素的值及其在数组中的存放位置,即元素的下标(index),下标必须用方括号括起来:

array[index]=element;
如,var beatles=Array(4);
    beatles[0]="John";(JS规定0作为第一个下标)
    beatles[1]="Paul";
    beatles[2]="George";
    beatles[3]="Ringo";
简便写法:
var beatles=Array("John","Paul","George","Ringo");
或者:
var beatles=["John","Paul","George","Ringo"];

数组元素可以是字符、数字、布尔值:

var lennon=["John",1940,false];

还可以是变量:

var name="John";
var beatles[0]=name;

还可以是另一个数组的元素:

var names=["Ringo","John","George","Paul"];
beatles[1]=names[3];

还可以包含其他的数组:

var lennon=["John",1940,false];
var beatles=[];
beatles[0]lennon;
则beatles[0][1]的值是1940.

但如果要记住每个下标数字的话,编程工作将十分麻烦,幸好还有其他方法可以填充数组:关联数组;将数组保存为对象。
关联数组:
如果在填充数组时只给出了元素的值,这个数组将是一个传统数组,它的各个元素的下标将被自动创建和刷新。在为新元素给出下标时,不必局限于使用整数数字,还可以用字符串:

var lennon=Array();
lennon["name"]="John";
lennon["year"]=1940;
lennon["living"]=false;

这样的数组叫关联数组,代码更具有可读性,但这种用法不是一个好习惯,不推荐使用。本质上,在创建关联数组时,创建的是Array对象的属性。在JS中所有变量实际上都是某种类型的对象。理想情况下,不应该修改Array对象的属性,而应该使用通用的对象(Object)。
(6)对象
与数组类似,对象也是使用一个名字表示一组值。对象的每个值都是对象的一个属性,前一节的lennon数组也可以创建成下面这个对象:

var lennon=Object();
lennon.name="John";
lennon.year=1940;
lennon.living=false;

它不使用方括号和下标来获取元素,而是像任何JS对象一样,使用点号来获取属性。创建对象还有一种更简洁的语法,即花括号语法:

var lennon={name:"John",year:1940,living:false};

用对象来代替传统数组的做法意味着可以通过元素的名字而不是下标数字来引用它们,这大大提高了脚本的可读性。

3 操作

进行计算和处理数据,即操作(operation)。
算术操作符
变量可以包含操作:var total=(1+4)*5;

还可以对变量进行操作:var temp_fahrenheit=95;var temp_celsius=(temp_fahrenheit-32)/1.8;
加号(+)是一个比较特殊的操作符,既可用于数值,也可用于字符串:

var message="I am feeling"+"happy";

这种把多个字符串首尾相连的操作叫拼接(concatenation)。
拼接可通过变量完成:

var mood="happy";
var message="I am feeling"+mood;
还可以把数值和字符串拼接
var year=2005;
var message="The year is"+year;

把字符串和数字拼接在一起,结果是一个更长的字符串,两个数值拼接在一起,结果是两个数值的算术和。
快捷操作符:
++相当于+1;
+=则是一次完成“拼接和赋值”:

var year=2010;
var message="The year is";
message+=year;
结果是变量message的值为“The year is 2005”。

4 条件语句

最常见的条件语句是if语句,基本语法:if (condition) {statements;},其中condition的求值结果永远是一个布尔值。if语句可以有一个else子句,其语句会在给定condition为false时执行。
(1)比较操作符。

  • 等于。注意(=)是赋值,(==)才是等于。(==)不表示严格相等,而(===)表示全等操作符,不仅比较值,还会比较变量类型。
var my_mood="happy";
var your_mood="sad";
if (my_mood==your_mood){
alert("we both feel the same");
}
  • 不等于。(!=),严格不相等需使用(!==)
if (my_mood != your_mood){
alert("We are feeling different moods.");
}

(2)逻辑操作符。把条件语句里的操作组合在一起,需使用逻辑操作符。

  • 逻辑与&&。两个操作数都true才是true。
if (num>=5 && num<=10){
alert("The number is in the right range.");
}
  • 逻辑或||。一个true就是true。
  • 逻辑非!。
if (!(5>10||5<1)){
alert("The number is in the right range.");
}

5 循环语句

if 语句的不足是无法完成重复性操作,如需多次执行同一个代码块,必须使用循环语句,其工作原理是只要给定条件仍能满足,包含在循环语句里的代码就会重复执行,一旦给定条件的求值结果不再是true,循环终止。
(1)while 循环。与if语句的语法几乎完全一样,唯一的区别是只要给定条件求值结果是true,花括号里的代码就将反复执行。

var count=1;
while(count<11){
alert(count);
count++;
}

对循环控制条件的求值发生在每次循环开始之前,如果控制条件首次求值结果是false,那么花括号里的代码将一个也不会被执行。在某些场合,为保证代码至少执行一次,用到do...while循环,其对循环控制条件的求值发生在每次循环结束之后。

var count=1;
do{
alert(count);
count++;
}while(count<11);

(2)for循环。是while循环的一种变体。
for(initial condition;test condition;alter condition){
statements;
} 用for循环来重复执行代码的好处是循环控制结构更加清晰。上例:

for (var count=1; count<11;count++){
alert(count);
}

for循环最常见的用途之一是对某个数组里的全体元素进行遍历处理:

var beatles=Array("John","Paul","George","Ringo");
for(var count=0;count<beatles.length;count++){
alert(beatles[count]);
}

6 函数

每当需要反复做一件事时,都可以利用函数来避免重复键入大量相同内容,不过函数的真正威力体现在,你可以把不同数据传递给它们,完成预定操作。把传递给函数的数据称为参数(argument)。JS提供了很多内建函数,如前面的alert。
定义一个函数的语法:

function name(arguments){
statements;
}
如传递两个参数的函数:
function multiply(num1,num2){
var total=num1*num2;
alert(total);
}
定义了这个函数的脚本,可从任意位置调用
multiply(10,2);

函数不仅能够以参数的形式接收数据,还能够返回数据,需用到return语句。
函数的真正价值体现在,可以把它们当做一种数据类型来使用,变量用下划线,函数用驼峰命名法,可予以区分。
变量的作用域(scope)

  • 全局变量global variable,可以在脚本中的任何位置被引用。
  • 局部变量local variable,只存在于声明它的函数内部。
    如果在某个函数中使用了var,那个变量就是局部变量,否则视为全局变量,如果脚本里已经存在一个与之同名的全局变量,这个函数就会改变那个全局变量的值。所以定义一个函数时,一定要把它内部的变量全都明确地声明为局部变量,避免隐患。

7 对象

包含在对象里的数据可以通过两种形式访问——属性property和方法method。属性是隶属于某个特定对象的变量,方法是只有某个特定对象才能调用的函数。

Person.mood
Person.age
Person.walk()
使用new关键字,为对象创建实例instance:
var jeremy=new Person;
就可以用Person对象的属性来检索关于jeremy的信息了:
jeremy.age
jeremy.mood

(1)内建对象。
数组就是一种,还有Meth、Date等。
(2)宿主对象。
这些对象不是由JS语言本身而是由它的运行环境提供的。具体到Web应用,这个环境就是浏览器,由浏览器提供的预定义对象被称为宿主对象,如Form、Image等。

第3章 DOM

1 文档:DOM中的D

当创建了一个网页并把它加载到Web浏览器中时,你编写的网页文档就转换为一个文档对象了。

2 对象:DOM中的O

JS对象分三种类型:用户自定义对象、内建对象、宿主对象。

3 模型:DOM中的M

就像一个模型火车代表着一列真正的火车,DOM代表着加载到浏览器窗口的当前网页。DOM把文档表示为一棵家谱树,称为“节点树”更准确。

4 节点

节点node表示网络中的一个连接点,一个网络就是由一些节点构成的集合。
DOM里有三种不同类型的节点:元素节点、文本节点、属性节点。
获取元素

  • getElementById.这个方法将返回一个与有着给定id属性值的元素节点对应的对象。如document.getElementById("purchases")。
  • getElementsByTagName.这个方法将返回一个对象数组,每个对象分别对应着文档里有着给定标签的一个元素。为了减少不必要的打字量并改善代码可读性,可把document.getElementsByTagName("")赋值给一个变量。
    getElementsByTagName还允许把通配符作为参数,如alert(document.getElementsByTagName("*").length);
    还可以跟getElementById结合起来运用。
  • getElementsByClassName.

5 获取和设置属性

(1)getAttribute.是只有一个参数的函数,但它不属于document对象,所以不能通过document对象调用,只能通过元素节点对象调用。
(2)setAttribute.它允许我们队属性节点值做出修改,也是只能用于元素节点。
object.setAttribute(attribute,value)

var shopping = document.getElementById("purchases");
alert(shopping.getAttribute("title"));
shopping.setAttribute("title","a list of goods");
alert(shopping.getAttribute("title"));

第一个alert显示null,第二个alert显示a list of goods。这表明setAttribute实际上完成了两项操作:先创建属性,然后设置它的值。
一个细节:通过setAttribute对文档做出修改后,查看源代码时看到的仍是改变前的属性值。这种现象源自DOM的工作模式:先加载文档的静态内容,再动态刷新,动态刷新不影响文档的静态内容。

第4章 案例研究:JS图片库

1 标记

第一项工作是为这些图片创建一个链接清单。如果图片有排序,用<ol>,无排序,用 <ul>。增加一个占位符图片为图片预留一个浏览区域。

    <ul>
    <li>
    <a href="images/1.jpg" onclick="showPic(this);return false;" title="gray">gray</a>
    </li>
    <li>
    <a href="images/2.jpg" onclick="showPic(this);return false;" title="ziont1">ziont</a>
    </li>
    </ul>
    <img id="placeholder" src="" alt="my image gallery">

2 JS

function showPic(whichpic){
var source = whichpic.getAttribute("href");
var placeholder = document.getElementById("placeholder");
placeholder.setAttribute("src",source);
}

函数取名为showPic,其参数取名为whichpic.通过getAttribute获取whichpic对象的href属性,通过getElementById获取占位符图片,通过setAttribute更改占位符图片的src属性。

3 应用这个函数

需要在html文件中添加事件处理函数(event handler).
事件处理函数的作用是,在特定事件发生时调用特定的JS代码。如onmouseover、onmouseout、onclick函数。
当把onclick函数嵌入到一个链接中时,需要把这个链接本身用作showPic函数的参数,可使用this关键字:onclick="showPic(this);"
但是,点击这个链接时,不仅showPic函数被调用,链接被点击的默认行为也会被调用,如何阻止这个默认行为被调用。事件处理函数的工作机制是,一旦事件发生,相应的JS代码就会被执行。被调用的JS代码可以返回给事件函数一个值。如果让返回的是一个布尔值,如该例中return false,则onclick就认为“这个链接没有被点击”。

4 扩展这个函数

(1)childNodes属性。
在一棵节点树上,childNodes属性可以用来获取任何一个元素的所有子元素,它是一个包含这个元素全部子元素的数组:element.childNodes.
如需精确查出body元素一共有多少个子元素:

function countBodyChildren(){
var body_element = document.getElementsByTagName("body")[0];
alert(body_element.childNodes.length);
}
window.onload = countBodyChildren;

(2)nodeType属性。
nodeType共有12种可取值,但其中仅有3种具有实用价值。

  • 元素节点的nodeType属性值是1
  • 属性节点的nodeType属性值是2
  • 文本节点的nodeType属性值是3

(3)为标记里增加一段描述。
首先,为目标文本安排显示位置,设置id值。

<p id="description">Choose an picture.</p>

目的是图片链接被点击时,不仅把占位符图片替换为那个href属性指向的图片,还要把这段文本同时替换为那个图片链接的title属性值。
(4)用JS改变这段描述。
修改showPic()函数:

function showPic(whichpic){
var source = whichpic.getAttribute("href");
var placeholder = document.getElementById("placeholder");
placeholder.setAttribute("src",source);
var text = whichpic.getAttribute("title");
var description = document.getElementById("description");
}

(5)nodeValue属性。
如果想改变一个文本节点的值,要使用DOM提供的nodeValue属性,它用来得到(和设置)一个节点的值。注意:<p>元素本身的nodeValue属性是一个空值,包含在<p>元素里的文本是另一种节点,它是p元素的第一个子节点。要修改p元素的文本值,需要获取的是文本而不是p,因此下面两条语句,第一条返回null,第二条才是文本值。

alert(description.nodeValue);
alert(description.childNodes[0].nodeValue);

(6)firstChild和lastChild属性。
数组元素childNodes[0]有个更直观易读的同义词:firstChild.与之对应的是lastChild.
(7)利用nodeValue属性刷新这段描述。

function showPic(whichpic){
var source = whichpic.getAttribute("href");
var placeholder = document.getElementById("placeholder");
placeholder.setAttribute("src",source);
var text = whichpic.getAttribute("title");
var description = document.getElementById("description");
description.firstChild.nodeValue = text;
}

第5章 最佳实践

1 过去的错误

(1)JS
易学易用的技术是一把双刃剑,容易被广泛应用,但往往缺乏高水平的质量控制。一些现成的JS函数里有很多问题考虑不周全。一旦浏览器不支持或禁用了JS解释功能,那些质量低劣的脚本就会导致用户无法浏览相应的网页甚至整个网站。
(2)flash
(3)质疑
网站对JS的滥用已经持续了相当长的时间。如果要使用JS,就要确认:这么做会对用户浏览体验产生什么影响?用户浏览器不支持JS该怎么办?

2 平稳退化

如果正确地使用了JS脚本,就可以让访问者在他们浏览器不支持JS的情况下仍能顺利地浏览你的网站,这就是所谓的平稳退化,就是说虽然某些功能无法使用,但最基本的操作仍能顺利完成。举例:创建新的浏览器窗口 window.open(url,name,features)
(1)JS伪协议
“真”协议用来在因特网上的计算机之间传输数据包,如HTTP协议、FTP协议等,伪协议则是一种非标准化的协议,伪协议让我们通过一个链接来调用JS函数。
如调用popUp()函数:

<a hret="javascript:popUp('http://www.example.com/');">Example</a>

这条语句在支持“javascript:”伪协议的浏览器中运行正常,较老的浏览器会失败,支持这种伪协议但禁用了JS功能的浏览器什么也不会做。总之在HTML文档中通过“javascript:”伪协议调用JS代码的做法非常不好。
(2)内嵌的事件处理函数

<a href="#" onclick="popUp('http://www.example.com/');
return false;">Example</a>

把href值设置为“#”只是为了创建一个空链接,实际工作全部由onclick属性负责完成。这个方法同样不能平稳退化。
(3)平稳退化的重要性
一个重要的访问者:搜索机器人(searchbot)。目前只有极少数搜索机器人能理解JS代码(?)。如果你的JS网页不能平稳退化,它们在搜索引擎上的排名就可能大受损害。一个解决办法,具体到popUp()函数,把href属性设置为真实存在的URL地址:

<a href="http://www.example.com/"
onclick="popUp('http://www.example.com');return false;">Example</a>
上述代码可简化为
<a href="http://www.example.com/"
onclick="popUp(this.href);return false;">Example</a>

3 向CSS学习

(1)结构与样式的分离
具备CSS支持的浏览器可以把网页呈现得美轮美奂,不支持或禁用了CSS功能的浏览器同样可以把网页的内容按照正确的结构显示出来。
(2)渐进增强
所谓“渐进增强”就是用一些额外的信息层去包裹原始数据,按照渐进增强原则创建出来的网页几乎都符合平稳退化原则。如果说CSS是提供“表示”,则JS是提供“行为”。把CSS代码从HTML文档里分离出来可以让CSS工作得更好,这同样适用于JS行为层。

4 分离JS

<a href="http://www.example.com/" class="popup">Example</a>

如何实现当这个链接被点击时,它将调用popUp()函数:JS语言不要求事件必须在HTML文档里处理,可以在外部JS文件里把一个事件添加到HTML文档中的某个元素上,可以利用class或id属性来解决。具体步骤:
(1)把文档中所有链接全放入一个数组里
(2)遍历数组
(3)如果某个链接的class属性等于popup,就表示这个链接在被点击时应调用popUp()函数。

window.onload = prepareLinks;
function prepareLinks(){
var links = document.getElementsByTagName("a");
for (var i=0;i<links.length;i++){
if (links[i].getAttribute("class")=="popup"){
links[i].onclick=function(){
popUp(this.getAttribute("href"));
return false;
}
}
}
}
function popUp(winURL){
window.open(winURL,"popup",width=320,height=480");
}

以上代码将调用popUp()函数的onclick事件添加到有关链接上,等于把这些操作从HTML文档里分离出来,这就是“分离JavaScript”。另外,为保证HTML文档加载完再加载脚本,可将代码打包到preparelinks()函数,并将其添加到windows对象的onload事件上。

5向后兼容

(1)对象检测
检测浏览器对JS的支持程度。只要把某个方法打包在一个if语句里,就可以根据这条if语句的条件表达式求值结果是true还是false来决定采取怎样的行动。这种检测称为对象检测。

if(method){
statements
}

但如此编写出来的函数会增加一对花括号,如果需要在函数里检测多个DOM方法和/或属性是否存在,这个函数最重要的语句就会深埋在一层又一层的花括号里,这样的代码往往很难阅读和理解。把测试条件改为“如果你不理解这个方法,就离开”则更简单。如:

if(!getElementById || !getElementsByTagName)return false;

(2)浏览器嗅探技术
这是一种风险很大的技术。一是浏览器有时会撒谎,二是为适用于多种浏览器,嗅探脚本会越来越复杂,三是许多嗅探脚本在进行此类测试时,要求浏览器版本号必须得到精确匹配,因此需要一直修改。这种技术正在被更简单更健壮的对象检测技术所取代。

6 性能考虑

(1)尽量少访问DOM和尽量减少标记。在多个函数都会取得一组类似元素的情况下,可以考虑重构代码,把搜索结果保存在一个全局变量里,或者把一组元素直接以参数形式传递给函数。并且要尽量减少文档中的标记数量。
(2)合并和放置脚本。包含脚本的最佳方式就是使用外部文件。减少请求数量通常都是在性能优化时首先要考虑的。脚本在标记中的位置对页面的初次加载时间也有很大影响。传统上放在<head>里,但会导致浏览器无法并行加载其他文件。一般来说根据HTTP规范,浏览器每次从同一域名中最多只能同时下载两个文件。把所有<script>标签都放到文档的末尾,</body>标记前,可以让页面变得更快(?)
(3)压缩脚本。是指把脚本文件中不必要的字节,如空格和注释统统删除。有许多工具可以用来精简代码。多数情况下你应该有两个版本,一个是工作副本,可以修改代码并添加注释,另一个是精简副本,用于放在站点上,为了与非精简版本区分开,可在文件名中加上min字样。

第6章 案例研究:图片库改进

1 图片库代码回顾

2 平稳退化

此代码(见第5章)即使在JS功能被禁用,用户也可以浏览图片库里的图片,所有链接也都正常工作。但如果选用“javascript:”伪协议,或把链接写成#,禁用JS的用户将无法浏览图片。

3 JS与HTML是分离的吗

因为在html里插入了onclick事件,所以JS和HTML是混在在一起的。把JS移出HTML有多种方法,如给每个链接添加class属性。但注意图片清单里各个链接有一个共同点,他们都包含在同一个列表清单元素里,因此给列表清单设置一个id比较简单。
(1)添加事件处理函数。
想要完成的工作:

  • 检查当前浏览器是否理解getElementsByTagName
  • 检查当前浏览器是否理解getElementById
  • 检查当前网页是否有id为imagegallery的元素
  • 遍历imagegallery中的所有链接
  • 设置onclick事件,让它在有关链接被点击时完成以下操作:把这个链接作为参数传给showPic()函数,取消链接被点击时的默认行为。
    ①检查点
    if(!document.getElementsByTagName||!document.getElementById)return false;
    if(!document.getElementById(“imagegallery”))return false;
    出于JS与HTML分离的原则,如果想用JS给某个网页添加一些行为,就不应该让JS代码对这个网页的结构有任何依赖。
    ②变量名里有什么
    创建一个“gallery”变量,选择一些有意义的单词来命名可以让代码更容易阅读和理解。但要注意有些单词在JS语言中有特殊的含义和用途,这些统称为“保留字”的单词不能用作变量名,另外,现有JS函数或方法的名字,如alert、var、if,也不能用来命名变量。
    var galley=document.getElementById("imagegallery");
    var links=gallery.getElementsByTagName("a");
    ③遍历
    把充当循环计数器的变量命名为“i”是一种传统做法,含义是increment(递增)。
    for(var i=0;i<links.length;i++){
    ④改变行为
links[i].onclick=function(){         /*定义匿名函数*/
showPic(this);                            /*this代表links[i]*/
return false;                              /*禁用链接默认行为*/
}                            

⑤完成JS函数

function prepareGallery(){
if(!document.getElementsByTagName||!document.getElementById)return false;
if(!document.getElementById(“imagegallery”))return false;
var galley=document.getElementById("imagegallery");
var links=gallery.getElementsByTagName("a");
for(var i=0;i<links.length;i++){
links[i].onclick=function(){
showPic(this); 
return false; 
}
}
}

(2)共享onload事件。
如果DOM不完整,则测试的准确性就无从谈起。因此应该让这个函数在网页加载完毕之后立刻执行。网页加载完毕会触发onload事件,必须把prepareGallery函数绑定在这个事件上:window.onload=prepareGallery;
但如果想让两个函数都在页面加载完成是执行,分别与onload绑定,只有最后那个函数会执行。一个最简单的解决方法:创建一个匿名函数来容纳这两个函数,再将该匿名函数与onload绑定。一个弹性最佳的解决方法:利用addLoadEvent函数。

function addLoadEvent(func){
var oldonload=window.onload;
if(typeof window.onload!='function'{
window.onload=func;
}else{
window.onload=function(){
oldonload();
func();
}
}
}

4 不要做太多假设

之前的代码里用到了id属性值等于placeholder和description的元素,但未对这些元素是否存在做任何检查。假如我们要实现,只要placeholder图片存在,即使description元素不存在,切换显示新图片的操作也照常进行。
检查placeholder:
if(!document.getElementById("placeholder"))return false;
description:
if(document.getElementById("description"));这样是可选的,有则执行,无则忽略。
添加这两条代码后,即使HTML中没有id=placeholder也不会出现JS错误,但是会出现点击链接没任何响应,这意味着脚本不能平稳退化。问题在于prepareGallery函数做出了这样的假设:showPic函数肯定会正常返回。基于这一假设,prepareGallery函数取消了onclick事件的默认行为。是否返回false值以取消onclick事件的默认行为,应该由showPic函数决定。如果图片切换成功,返回true;如果图片切换不成功,返回false。应该在返回前验证showPic的返回值,以决定是否组织默认行为。如果showPic返回true,则返回false阻止默认行为;如果showPic返回false,则返回true允许默认行为。

function prepareGallery(){
if(!document.getElementsByTagName||!document.getElementById)return false;
if(!document.getElementById("imagegallery"))return false;
var gallery=document.getElementById("imagegallery");
var links=gallery.getElementsByTagName("a");
for(var i=0;i<links.length;i++){
links[i].onclick=function(){
return !showPic(this);  
}
}
}

5 优化

showPic函数里仍存在一些需要处理的假设,如假设每个链接都有title属性。

if(whichpic.getAttribute("title") !=null)

作为一种简单的视觉反馈,把title不存在时的text设置为空字符串:

if(whichpic.getAttribute("title") !=null){
var text=whichpic.getAttribute("title");
} else {
var text="";
}
还可以写成:
var text=whichpic.getAttribute("title")?whichpic.getAttribute("title"):"";
这里的问号叫三元操作符,其后是text的两种取值:
variable=condition?if true:if false;

还可验证placeholder是否为图片:

if(placeholder.nodeName!="IMG") return false;

注意nodeName总是返回一个大写字母的值,即使元素在HTML中是小写。
还可验证description的第一个子元素是文本:

if(description.firstChild.nodeType==3){
description.firstChild.nodeValue=text;
}

在实际工作中,你要自己决定是否需要这些检查,它们针对的是HTML中可能不在你控制范围的内情况。理想情况下不应该对HTML的内容和结构做太多假设。

6 键盘访问

有些用户不使用鼠标,使用键盘,需调用onkeypress事件处理函数。要让onkeypress和onclick触发同样的行为,可复制onclick代码,或者links[i].onkeypress=links[i].onclick;但onkeypress很容易出问题。不过在几乎所有浏览器里,用Tab键移动到某个链接后按回车,也可触发onclick。最好不要使用onkeypress,onclick对键盘访问的支持已很完美。最终代码:

window.onload=prepareGallery;
function prepareGallery(){
if(!document.getElementsByTagName||!document.getElementById) return false;
if(!document.getElementById("imagegallery"))return false;
var gallery=document.getElementById("imagegallery");
var links=gallery.getElementsByTagName("a");
for(var i=0;i<links.length;i++){
links[i].onclick=function(){
return showPic(this)?false:true;
}
}
}
function showPic(whichpic){
    if(!document.getElementById("placeholder")) return false;
    var source = whichpic.getAttribute("href");
    var placeholder = document.getElementById("placeholder");
    if(placeholder.nodeName !="IMG") return false;
    placeholder.setAttribute("src",source);
    if(document.getElementById("description")){
    var text = whichpic.getAttribute("title")? whichpic.getAttribute("title"):"";
    var description = document.getElementById("description");
    if (description.firstChild.nodeType==3){
        description.firstChild.nodeValue = text;
    }
}
return true;
}

7 把JS和CSS结合起来##

DOM Core和HTML-DOM##

至此用到的DOM方法:getElementsByTagName、getElementById、getAttribute、setAttribute都是DOM Core的组成部分,并不专属于JS,支持DOM的任何一种程序语言都可以使用它们。使用JS和DOM为HTML文件编写脚本时,还有许多属性可供选用,如onclick,这些属性属于HTML-DOM。例如HTML-DOM提供了一个forms对象,它可以把document.getElementsByTagName("form")简化成document.forms。HTML-DOM代码通常比DOM Core代码简短,但它们只能用来处理Web文档。可根据个人喜好和具体情况进行选择。

第7章 动态创建标记

网页的结构由标记负责创建,绝大多数JS函数只用来改变某些细节而不改变其底层结构。但JS也可以用来改变网页的结构和内容。

1 传统方法

(1)document.write
HTML:

<body>
<script>
document.write("<p>This is inserted.</p>");
</script>
</body>

其最大缺点是违背了“行为应该与表现分离”的原则。即使把语句挪到外部函数里,也仍然要在<body>部分添加<script>来调用。最好用外部JS文件去控制网页行为,避免在<body>部分乱用<script>,避免使用document.write方法。

(2)innerHTML属性
类似于document.write方法,innerHTML属性也是HTML专有属性,不能用于任何其他标记语言文档。任何时候,标准的DOM都可以替代innerHTML,虽然往往需要多编写一些代码,但DOM提供了更高的精确性和更强大的功能。

2 DOM方法

DOM不仅可以获取文档内容,还可以更新文档内容。如setAttibute,注意它并未改变文档的物理内容,只有用浏览器打开文档是才会看到效果变化,这是因为浏览器实际显示的是那棵DOM节点树,在浏览器看来,DOM节点树才是文档。所以你不是在创建标记,而是在改变DOM节点树。
(1)createElement方法

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="testdiv">
</div>
</body>
</html>

想把一段文本插入到testdiv中,用DOM语言说,就是要创建一个p节点,并将其作为div节点的一个子节点。用createElement创建:

var para = document.createElement("p");

任何时候,只要使用了createElement,把新创建的元素赋给一个变量总是个好主意。虽然p已经存在,但它还不是任何DOM节点树的组成部门,这种情况称为文档碎片,但它已经像其他节点一样,有自己的DOM属性了。

(2)appendChild方法

parent.appendChild(child)  /*child不上引号*/
具体到上面的例子,让p称为testdiv的子节点:
var testdiv=document.getElementById("testdiv");
testdiv.appendChild(para);

虽然使用appendChild方法时,不必非得使用一些变量来引用父节点和子节点,但这样会提高代码的可读性。

(3)createTextNode方法
现在想把一些文本放入p元素,需要用createTextNode,语法与createElement类似。

var para = document.createElement("p");
var testdiv=document.getElementById("testdiv");
testdiv.appendChild(para);
var txt=document.createTextNode("Hello World");
para.createTextNode(txt);

appendChild方法还可以用来连接那些尚未成为文档树的节点,所以可以先创建节点,再使用appendChild连接。

(4)一个更复杂的组合
如果要插入<p>This is <em>my</em> content.</p>,先分析节点树再写代码:

window.onload=function(){
var para=document.createElement("p");
var txt1=document.createTextNode("This is ");
var emphasis=document.createElement("em");
var txt2=document.createTextNode("my ");
var txt3=document.createTextNode("content.");
para.appendChild(txt1);
para.appendChild(emphasis);
para.appendChild(txt3);
emphasis.appendChild(txt2);
var testdiv=document.getElementById("testdiv");
testdiv.appendChild(para);
}

3 重回图片库

之前图片库的HTML中有一个图片和一段文字的存在只是为了让DOM处理,那么用DOM方法来创建它们是最合适的选择。
需要完成的任务:

  • 创建一个img元素节点
  • 设置这个节点的id属性
  • 设置这个节点的src属性
  • 设置这个节点的alt属性
  • 创建一个p元素节点
  • 设置这个节点的id属性
  • 创建一个文本节点
  • 把这个文本节点追加到p元素上
  • 把p元素和img元素插入到gallery.html文档
var placeholder=document.createElement("img");
placeholder.setAttribute("id","placeholder");
placeholder.setAttribute("src","images/show.jpg");
placeholder.setAttribute("alt","my image gallery");
var description=document.createElement("p");
description.setAttribure("id","description");
var desctext=document.createTextNode("Choose an image");
description.appendChild(desctext);
document.body.appendChild(placeholder);
document.body.appendChild(description);

(1)在已有元素前插入一个新元素
原HTML文档中图片清单刚好在文档最后,所以把placeholder和description追加到body节点上,它们会出现在清单后面。如果想把一个新元素插入到一个现有元素的前面,可用insertBefore()方法实现:

parentElement.insertBefore(newElement,targetElement)

其实不用搞清楚parentElement是哪个,因为targetElement的parentNode属性值就是它。

gallery.parentNode.insertBefore(description,gallery);

(2)在现有方法后插入一个新元素
DOM没有提供insertAfter方法,但完全可以用DOM方法编写一个insertAfter方法。思路,查看目标元素是不是parent的lastChild,如果是,直接appendChild;如果不是,就插入到目标元素下一个兄弟元素的前面。

function insertAfter(newElement,targetElement){
var parent=targetElement.parentNode;
if(parent.lastChild==targetElement){
parent.appendChild(newElement);
}else{
parent.insertBefore(newElement,targetElement.nextSibling);
}

(3)图片库二次改进

4 Ajax

2005年发明,用于概括异步加载页面内容的技术。使用Ajax可以做到只更新页面中的一小部分,不必再次加载整个页面。对页面的请求以异步方式发送到服务器,服务器不会用整个页面来响应请求,会在后来处理请求,用户此时仍能继续浏览页面并与页面交互。
(1)XMLHttpRequest对象
它是Ajax技术的核心,充当浏览器的脚本与服务器之间的中间人。JS通过这个对象可以自己发送请求和处理响应。由于不同浏览器实现该对象的方式不太一样,因此需要为同一事情写不同的代码分支。

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

推荐阅读更多精彩内容