> For the complete documentation index, see [llms.txt](https://lewiszlw.gitbook.io/notebooks/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lewiszlw.gitbook.io/notebooks/blogs/history/an-li-yan-jiu-tu-pian-ku-gai-jin-ban.md).

# 案例研究：图片库改进版

在[最佳实践](http://zlw.poker/2016/11/02/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/)学到如何平稳退化、分离JavaScript、向后兼容和提高性能，在这里，将运用这些知识来改进图片库。

### 快速回顾

在[案例研究：JavaScript图片库](http://zlw.poker/2016/11/01/%E6%A1%88%E4%BE%8B%E7%A0%94%E7%A9%B6%EF%BC%9AJavaScript%E5%9B%BE%E7%89%87%E5%BA%93/)里，我编写了一个用来替换“占位符”图片的src属性的脚本，只用一个网页就建立起了图片库。

### 它支持平稳退化吗

第一个问题是：“如果JavaScript功能被禁用，会怎样？” 分析可知：即使JavaScript功能被禁用，用户也可以浏览图片库里的所有内容，网页里的所有连接也都可以正常工作：

```html
<li>
  <a href="images/fireworks.jpg" onclick="showPic(this);return false;" title="a fireworks display">fireworks</a>
</li>
```

在JavaScript功能被禁用的情况下，浏览器将沿着href属性给出的链接前进，用户将看到一张图片而不是“该页无法显示”之类的错误信息。

### 它的JavaScript与HTML标记是分离的吗

由代码知显然不是分离的。 把JavaScript代码移出HTML文档不是难事，但为了让浏览器知道页面里都有哪些链接有着不一样的行为，我们必须找到一种“挂钩”把JavaScript代码与HTML文档中的有关标记关联起来。有多种办法可以做到这点： 可以给每个链接分别添加一个class属性：

```html
<li>
  <a href="images/fireworks.jpg" class="gallerypic" title="a fireworks display">fireworks</a>
</li>
```

但这种方法不够理想，这与给它们分别添加事件处理函数同样麻烦。 第二种办法：图片清单里的每个链接有一个共同点：它们都包含在同一个列表清单元素里。给整个清单设置一个独一无二的id的办法要简单得多：

```html
<ul id="imagegallery">
  <li>...</li>
</ul>
```

#### 添加事件处理函数

现在，需要编写一个简短的函数把有关操作关联到onclick事件上。我将其命名为prepareGallery。 下面是这个函数需要完成的工作：

* 检查当前浏览器是否理解getElementsByTagName和getElementById；
* 检查当前网页是否存在一个id为imagegallery的元素；（这项测试是一个预防性措施。现在我知道调用这个JavaScript函数的文档里有一个id属性值等于imagegallery的列表清单元素，但我不确定这在将来会不会发生变化。有了这个预防性措施，即使以后从网页上删掉图片库，也用不着这个网页的JavaScript代码会突然出错。作为一条原则，**如果想用JavaScript给某个网页添加一些行为，就不应该让JavaScript代码对这个网页的结构有任何依赖。**）
* 遍历imagegallery元素中的所有连接；
* 设置onclick事件，让它在有关链接被点击时完成以下操作： 1.把这个链接作为参数传递给showPic函数； 2.取消链接被点击时的默认行为，不让浏览器打开这个链接。

```javascript
function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!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(){       //定义了一个匿名函数。这是一种在代码执行时创建函数的办法。
					showPic(this);
					return false;
				}
		}
}
```

调用此函数，就会把onclick事件绑定到“id”等于“imagegallery”的元素内的各个链接元素上。

#### 共享onload事件

必须执行prepareGallery函数才能对onclick事件进行绑定。 如果马上执行这个函数，它就无法完成其工作。在HTML文档完成加载之前执行脚本，此时DOM是不完整的。 应该让这个函数在网页加载完毕后立刻执行。网页加载完毕时会触发一个onload事件，这个事件与window对象相关联。所以我们须把prepareGallery函数绑定到这个事件上：

```javascript
window.onload=prepareGallery;
```

但如果有多个函数，如果像下面这样逐一绑定，它们中**只有最后那个函数才会得到实际执行**：

```javascript
window.onload=firstFunction;
window.onload=secondFunction;
```

有种办法可以解决这个问题：可以先创建一个匿名函数来容纳这两个函数，然后把那个匿名函数绑定到onload事件上：

```javascript
window.onload=function(){
		firstFunction();
		secondFunction();
	}
```

这里还有一个弹性最佳的解决方案——使用addloadEvent函数。它只有一个参数：打算在页面加载完毕时执行的函数的名字。 下面是addloadEvent函数将要完成的操作：

* 把现有的window\.onload事件处理函数的值存入变量oldonload。
* 如果在这个处理函数上还没有绑定任何函数，就像平时那样把新函数添加给它。
* 如果在这个处理函数上已经绑定了一些函数，就把新函数追加到现有指令的末尾。

下面是addloadEvent函数的代码清单：

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

这将把那些在页面加载完毕时执行的函数创建为一个队列。如果想把刚才那两个函数添加到这个队列里去，只需要写出以下代码就行了：

```javascript
addloadEvent(firstFunction);
addloadEvent(secondFunction);
```

### 不要做太多假设

在showPic函数里发现的一个问题是，没有让它进行任何测试和检查。 showPic函数将由prepareGallery函数调用，而我已经在后者的开头对getElementById和getElementsByTagName等DOM方法是否存在进行过检查，所以我确切地知道用户的浏览器不会因为不理解这个方法而出问题。不过并未对id值等于placeholder和description的元素进行任何检查。

```javascript
function showPic(whichpic){
	if(!document.getElementById("placeholder")) return false;
	var source = whichpic.getAttribute("href");
	var placeholder = document.getElementById("placeholder");
	placeholder.setAttribute("src",source);

	if(document.getElementById("description")){	
	var text=whichpic.getAttribute("title");
	var description=document.getElementById("description");
	description.firstChild.nodeValue=text;
	}
	return true;                 //即使description元素不存在，切换显示新图片的操作也照常进行
}
```

改进后的showPic()函数不再假设有关标记文档里肯定存在着placeholder图片和description元素。即使文档里没有placeholder图片，也不会发生任何JavaScript错误。 可是还有一个问题：如果把placeholder图片从标记文档里删除并在浏览器里刷新这页面，就会出现这种情况，无论点击imagegallery清单里的哪个链接，都没有任何反应。这意味着我们的脚本不能平稳退化。问题在于prepareGallery函数做出了这样一个假设：showPic函数肯定会正常返回。基于这一假设，prepareGallery函数取消了onclick事件的默认行为。

showPic函数应该返回两个可能的值：

* 如果图片切换成功，返回true。
* 如果图片切换不成功，返回false。

为修正这个问题，应该在返回前验证showPic的返回值，以便决定是否阻止默认行为。showPic返回true，那么更新placeholder。在onclick事件处理函数中，我们可以利用“!”对showPic的返回值进行取反：

```javascript
links[i].onclick=function(){
		return !showPic(this);
	}
```

现在，如果showPic返回true，我们就返回false，浏览器不会打开那个链接。如果showPic返回false，那么我们认为图片没有更新，于是返回true以允许默认行为发生。下面是prepareGallery函数现在的代码清单：

```javascript
function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!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);
				}
		}
}
```

### 优化

这个函数已经很完善了。尽管如此，在showPic函数里仍存在一些需要处理的假设。 例如，假设每个链接都有一个title属性：

```javascript
var text=whichpic.getAttribute("title");
```

检查方法：

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

if(whichpic.getAttribute("title"))
```

在title属性不存在时把text的值设置为空字符串：

```javascript
if(whichpic.getAttribute("title")){
		var text=whichpic.getAttribute("title");
	}else{
			var text="";
		}


var text=whichpic.getAttribute("title") ? whichpic.getAttribute("title") : "";    //另一种方法
```

其他检查：

```javascript
if(placeholder.nodeName!="IMG") return false;  //检查placeholder。nodeName返回一个大写字母的值。

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

最终prepareGallery()和showPic()代码清单：

```javascript
function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!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");
		description.firstChild.nodeValue=text;
	}
	return true;               
}
```

### 把JavaScript与CSS结合起来

把JavaScript代码从HTML文档里分离出去还带来另一个好处。在把内嵌型事件处理函数移出标记文档时，我在文档里为JavaScript代码留下了一个“挂钩”：

```html
<ul id="imagegallery">
```

这个挂钩完全可以用在CSS样式表里。

还可以把图片链接换成缩略图：

```html
<li>
  <a href="images/fireworks.jpg" title="a fireworks display">
    <img src="images/thumbnail_fireworks.jpg" alt="fireworks" />
  </a>
</li>
```

### DOM Core和HTML-DOM

至此，在编写JavaScript代码时只用到了一下几个DOM方法：

* getElementById
* getElementsByTagName
* getAttribute
* setAttribute

这些方法都是DOM Core的组成部分。它们并不专属于JavaScript，支持DOM的任何一种程序设计语言都可以使用它们。它们的用途也并非仅限于处理网页，它们可以用来处理用任何一种标记语言（比如XML）编写出来的文档。 在使用JavaScript语言和DOM为HTML文件编写脚本时，还有许多属性可供选择。例如属性onclick，用于图片库的事件管理。这些属性属于HTML-DOM，它们在DOM Core出现之前很久就已经为人们所熟悉了。 比如说，HTML-DOM提供了一个forms对象。这个对象可以把下面这样的语句：

```javascript
document.getElementsByTagName("form")
```

简化为：

```javascript
document.forms
```

类似地，HTML-DOM还提供了许多描述各种HTML元素的属性。比如说，HTML-DOM图片提供的src属性可以把下面这样的语句：

```javascript
element.getAttribute("src")
```

简化为：

```javascript
element.src
```

这些方法和属性可以相互替换。同样的操作既可以只使用DOM Core来实现，也可以使用HTML-DOM来实现。通常HTML-DOM代码更短，必须提醒一点的是，它们只能用来处理web文档。

### 效果预览

![Snapshots](http://ofolh8dcq.bkt.clouddn.com/snapshots.PNG)
