再看《JavaScript高级程序设计》第13、14、17、20-25章

第十三章 事件          

1、事件流:描述的是从页面中接收事件的顺序

1)事件冒泡:事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点

2)事件捕获:事件开始时由不太具体的节点接收,然后逐级向下传播到最具体的元素

3)DOM事件流:经历三个阶段——事件捕获阶段、处于目标阶段和事件冒泡阶段

2、事件处理程序:事件是用户或浏览器自身执行的某种动作,响应某个事件的函数就是事件处理程序

1)HTML事件处理程序将事件处理程序设置为 null 就可以删除该事件处理程序。

2)DOM0级事件处理程序:为元素的事件处理程序属性设置一个函数;事件处理程序会在其所属元素的作用域内运行将事件处理程序设置为 null 就可以删除该事件处理程序。

3)DOM2级事件处理程序:addEventListener()removeEventListener(),都接收三个参数——要处理的事件名、作为事件处理程序的函数和一个布尔值,布尔值参数如果是true表示在捕获阶段调用事件处理程序,如果是false表示在冒泡阶段调用事件处理程序

4)IE事件处理程序(IEOpera有效):attachEvent()detachEvent(),都接收两个参数——事件处理程序名称与事件处理程序函数;由于IE及更早的版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段;可以为一个元素添加多个事件处理程序,事件处理程序会在全局作用域中运行,因此this等于window

5)跨浏览器的事件处理程序:addHandler()用于使用DOM0级方法、DOM2级方法或IE方法来添加事件,removeHandler()用于默认使用DOM0级方法法来移除之前添加的事件,都接收三个参数——要操作的元素、事件名称和事件处理程序函数;为了保证处理事件的代码能在大多数浏览器下一致地运行,跨浏览器的事件处理程序只处理事件冒泡

3、事件对象:

1)DOM中的事件对象:

兼容DOM的浏览器会将一个event对象传入到事件处理程序中;

preventDefault()用于阻止特定事件的默认行为,前提是cancelable属性设置为truestopPropagation()用于立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡;

currentTarget属性表明其事件处理程序当前正在处理事件的那个元素,始终等于thistarget属性表明事件真正的目标;eventPhase属性表明事件当前正处于事件流的哪个阶段,值为1表明事件处于捕获阶段,值为2表明事件处于目标对象上,值为3表明事件处于冒泡阶段;

2)IE中的事件对象:

如果使用DOM0级方法添加事件处理程序时,可以通过window.event取得event对象;如果使用attachEvent()时,那么会有一个event对象作为参数被传入事件处理程序函数中;如果使用HTML特性添加事件处理程序时,可以通过一个event变量来访问event对象;srcElement属性表明事件真正的目标;returnValue属性用于阻止特定事件的默认行为,值为false表明阻止;cancelBubble属性用于立即停止事件在DOM层次中的传播,即取消进一步的事件冒泡,值为true表明停止;

3)跨浏览器的事件对象:getEvent()返回对event对象的引用;getTarget()返回事件的目标,通过检测event对象的target属性,如果存在则返回该属性的值,否则返回srcElement属性的值;preventDefault()用于取消事件的默认行为,如果不存在该方法,则设置returnValue的值为falsestopPropagation()用于阻止事件流,如果不存在该方法,则设置cancelBubble的值为true

var EventUtil = {

getEvent: function(event){

        return event ? event : window.event;

    },

getTarget: function(event){

        return event.target || event.srcElement;

    },

preventDefault: function(event){

        if (event.preventDefault){

            event.preventDefault();

        } else {

            event.returnValue = false;

        }

    },

stopPropagation: function(event){

        if (event.stopPropagation){

            event.stopPropagation();

        } else {

            event.cancelBubble = true;

        }

    }

 

};

4、事件类型

1UI事件:指的是那些不一定与用户操作有关的事件

DOMactive:表明元素已经被用户操作(通过鼠标或键盘)激活;

load:当页面完全加载在window上触发;

unload:当页面完全卸载在window上触发;

abort:当用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上触发;

error:当发生JavaScript错误时在window上触发;

select:当用户选择文本框(<input><texterea>)中的一或多个字符时触发;

resize:当窗口或框架大小变化时在window上触发;

scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发;

2)焦点事件:会在页面元素获得或失去焦点时触发

blur:在元素失去焦点时触发(通用);

DOMFocusIn:在元素获得焦点时触发,冒泡(Opera支持

DOMFocusOut:在元素失去焦点时触发Opera支持

focus:在元素获得焦点时触发(通用);

focusin:在元素获得焦点时触发,冒泡(IE5.5+Safari5.5+Opera11.5+Chrome支持

focusout:在元素失去焦点时触发IE5.5+Safari5.5+Opera11.5+Chrome支持

3)鼠标和滚轮事件

click:在用户点击主鼠标按钮或者按下回车键时触发;

dblclick:在用户双击主鼠标按钮时触发;

mousedown:在用户按下了任意鼠标按钮时触发;

mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发;

mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发;

mousemove:当鼠标指针在元素内部移动时重复触发;

mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发;

mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发;

mouseup:在用户释放鼠标按钮时触发;

mousewheel:当用户通过鼠标滚轮在垂直方向上滚动页面时触发;当用户向前滚动页面时,wheelDelta120的倍数;当用户向后滚动页面时,wheelDelta-120的倍数

shiftKeyctrlKeyaltKeymetaKey:在用户按下ShiftCtrlAltMeta(在Windows键盘中是Windows键,在苹果机是Cmd键)时布尔值为true

设置bottom属性:

var EventUtil = {

getButton: function(event){

        if (document.implementation.hasFeature("MouseEvents", "2.0")){

            return event.button;

        } else {

            switch(event.button){

                case 0:

                case 1:

                case 3:

                case 5:

                case 7:

                    return 0;

                case 2:

                case 6:

                    return 2;

                case 4: return 1;

        }

    }

}

4)键盘与文本事件

keydown:当用户按下键盘上的任意键时触发,如果按住不放则会重复触发;

keypress:当用户按下键盘上的字符键时触发,如果按住不放则会重复触发;

keyup:当用户释放键盘的键时触发;

textInput:当用户将文本插入文本框之前触发;

5)复合事件

compositionstart:在IME(输入法编辑器)的文本复合系统打开时触发,包含正在编辑的文本;

compositionupdate:在想输入字段中插入新字符时触发,包含正插入的新字符;

compositionend:在IME的文本复合系统关闭时触发,包含此次输入会话中插入的所有字符;

6)变动事件

DOMSubtreeModified:在DOM结构中发生任何变化时触发;

DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发,在使用appendChild()replaceChild()insertBefore()时首先触发;

DOMNodeRemoved:在节点从其父节点中被移除时触发,在使用removeChild()replaceChild()时首先触发;

DOMNodeInsertedIntoDocument:在一个节点被直接插入文档或通过子树间接插入文档之后触发;

DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发;

DOMAttrModified:在特性被修改之后触发;

DOMCharacterDataModified:在文本节点的值发生变化时触发;

7)HTML5事件

contexmenu事件:用于表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单;

beforeunload事件:在浏览器卸载页面之前触发;

DOMContentLoaded:在形成完整的DOM树之后触发,不管当时图像、JavaScript文件、CSS文件或其他资源是否已经下载完毕;

readystatechange事件:提供与文档或元素的加载状态有关的信息,值为uninitialized(未初始化)、loading(正在加载)、loaded(加载完毕)、interactive(交互)、complete(完成);

pageshow事件:在页面显示时触发,而且是在load事件之后触发;pagehide事件:在浏览器卸载页面时触发,而且是在unload事件之前触发;

hashchange事件:在URL的参数列表(及URL中“#”号后面的所有字符串)发生变化时通知开发人员

8)设备事件

orientationchange事件:当用户将设备由横向查看模式切换为纵向查看模式时触发,window.orientationchange值为0(表示肖像模式)、90(表示向左旋转的横向模式)、-90(表示向右旋转的模式);

MozOrientation事件:当设备的加速计检测到设备方向改变(移动)时触发,event.xevent.yevent.z的值介于1-1之间,表示不同坐标轴上的方向;

deviceorientation事件:当设备的加速计检测到设备方向改变(朝向)时触发;

devicemotion事件::当用户将设备移动时触发;

9)触摸与手势事件

touchstart事件:当手指触摸屏幕时触发;

touchmove:当手指在屏幕上滑动时连续触发;

touchend:当手指从屏幕上移开时触发;

touchcancel:当系统停止跟踪触摸时触发;

gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发;

gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发;

gestureend:当任何一个手指从屏幕上面移开时触发;

5、内存和性能

1)事件委托:利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件

2)移除事件处理程序

6、模拟事件

1)模拟鼠标事件

var btn = document.getElementById("myBtn");

//创建事件对象

var event = document.createEvent("MouseEvents");

//初始化事件对象

event.initMouseEvent("click", true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0,null);

//触发事件

btn.dispatchEvent(event);

2)模拟键盘事件

3)模拟其他事件

4)自定义DOM事件

5)IE中的事件模拟

第十四章 表单脚本

1、表单的基础知识

1)HTMLFormElement类型的属性和方法

acceptCharset:服务器能够处理的字符集

action:接收请求的URL,等价于HTML中的action特性;

elements:表单中所有控件的集合;

encype:请求的编码类型,等价于HTML中的enctype特性;

length:表单中控件的数量;

method:要发送的HTTP请求类型,通常是“get”或“post”,等价于HTML中的method特性;

name:表单的名称,等价于HTML中的name特性;

reset():将所有表单域重置为默认值;

submit():提交表单;

target:用于发送请求接收响应的窗口名称,等价于HTML中的target特性;

2)提交表单

 <!-- 通用提交按钮 -->

<input type=”submit” value=”Submit Form”>

 <!-- 自定义提交按钮 -->

<bottom type=”submit” >Submit Form</bottom>

 <!-- 图像按钮 -->

<input type=”submit” src=”submit-form.gif”>

3)重置表单

 <!-- 通用重置按钮 -->

<input type=”reset” value=”Reset Form”>

 <!-- 自定义重置按钮 -->

<bottom type=”reset” >Reset Form</bottom>

4)表单字段

取得表单字段

var form=document.getElementById(“form1”);

//取得表单中的第一个字段

var field1=form.elements[0];

//取得名为“textbox1”的字段

var field2=form.elements[“textbox1”];

//取得表单中包含的字段的数量

var fieldCount=form.elements.length;

 

共有的表单字段属性(<fieldset>除外)

disable:布尔值,表示当前字段是否被禁用;

form:表示当前字段所属表单的指针,只读;

name:当前字段的名称;

readOnly:布尔值,表示当前字段是否只读;

tabIndex:表示当前字段的切换(tab)序号;

type:当前字段的类型;

value:当前字段将被提交给服务器的值;

 

共有的表单字段方法(<fieldset>除外)

focus():用于将浏览器的焦点设置到表单字段,使其可以响应键盘事件;

blur():用于从元素中移走焦点;

 

共有的表单字段事件

blur事件:当前字段失去焦点时触发;

change事件:对于<input><textarea>元素,在它们失去焦点且value值改变时触发,对于<select>元素,在其选项改变时触发;

focus事件:当前字段获得焦点时触发;

2、文本框脚本

1)<input>元素的单行文本框:<input type=”text” size=”25” maxlength=”50” value=”initial value”>

2)<inputarea>元素的多行文本框:<textarea rows=”25” cols=”5”>initial value</textarea>

3)选择文本:

select()方法:选择文本框中的所有文本

select事件

取得选择的文本:属性selectionStartselectionEnd,值为基于0的数值,表示所选择文本的范围(即文本选区开头和结尾的偏移量)

Function getSelectedText(textbox){

return textbox.value.subString(textbox.selectionStart,textbox.selectionEnd);

}

选择部分文本:setSelectionRange()方法,接收两个参数——要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引

4)过滤输入

屏蔽字符

EventUtil.addHandler(textbox, "keypress", function(event){

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

var charCode = EventUtil.getCharCode(event);

if (!/\d/.test(String.fromCharCode(charCode)) && charCode > 9 && !event.ctrlKey){

EventUtil.preventDefault(event);

}

});

 

操作剪切板

beforecopy事件:在发生复制操作前触发;

copy事件:在发生复制操作时触发;

beforecut事件:在发生剪切操作前触发;

cut事件:在发生剪切操作时触发;

beforepaste事件:在发生粘贴操作前触发;

paste事件:在发生粘贴操作时触发;

clipboardData对象:用于访问剪切板的数据,getData()setData()clearData()方法<TextboxClipboardExample01.htm>

var EventUtil = {

   getClipboardText: function(event){

        var clipboardData =  (event.clipboardData || window.clipboardData);

        return clipboardData.getData("text");

    },

setClipboardText: function(event, value){

        if (event.clipboardData){

            event.clipboardData.setData("text/plain", value);

        } else if (window.clipboardData){

            window.clipboardData.setData("text", value);

        }

    }

};

5)自动切换焦点:<TextboxTabForwardExample01.htm>

function(){

function tabForward(event){            

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

if (target.value.length == target.maxLength){

var form = target.form;

for (var i=0, len=form.elements.length; i < len; i++) {

if (form.elements[i] == target) {

if (form.elements[i+1]){

form.elements[i+1].focus();

}

return;

}

}

}

}

6)HTML5约束验证API

必填字段:required属性

其他输入类型:type=”email”、type=”url”

数值范围:可以指定min属性(最小的可能值)、max属性(最大的可能性)和step属性(从minmax的两个刻度间的差值)

输入模式:pattern属性,值为正则表达式,用于匹配文本框中的值

检测有效性:checkValidity()方法可以检测表单中的某个字段是否有效

禁用验证:novalidate属性

3、选择框脚本

1)HTMLSelectElement类型的属性和方法

add(newOption,relOption):向控件中插入新<option>元素,其位置在相关项relOption之前;

multiple:布尔值,表示是否允许多项选择,等价于HTML中的multiple特性;

options:控件中所有<option>元素的HTMLCollection

remove(index):移除给定位置的选项;

selectedIndex:基于0的选中项的索引,如果没有选中项,则为-1

size:选择框中可见的行数,等价于HTML中的size特性;

2)HTMLOptionElement对象的属性

index:当前选项在options集合中的索引;

label:当前选项的标签,等价于HTML中的label特性;

selected:布尔值,表示当前选项是否被选中;

text:选项的文本;

value:选项的值,等价于HTML中的value特性;

3)选择选项:第一种方法使用选择框的selectedIndex属性,不过只允许选择一项;第二种方法是将要取得的那一项的selected属性设置为true,而且允许动态选择任意多项;

4)添加选项:

第一种方法是使用DOM方法<SelectboxExample04.htm>

var newOption = document.createElement("option");

newOption.appendChild(document.createTextNode(textTextbox.value));

newOption.setAttribute("value", valueTextbox.value);                 selectbox.appendChild(newOption);

第二种方法是使用Option构造函数来创建新选项,接收两个参数——文本和值(可选)

var newOption = new Option(“Option text”,”Option value”);

selectbox.appendChild(newOption);

第三种方法是使用选择框的add()方法,接收两个参数——要添加的新选项和将位于新选项之后的选项(可选)

var newOption = new Option(“Option text”,”Option value”);

selectbox.add(newOption,undefined);

5)移除选项

第一种方法是使用DOM方法:selectbox.removeChild(selectbox.options[0]);

第二种方法是使用remove()方法,接收一个参数——要移除选项的索引:selectbox.remove(0);

第三种方法是将相应选项设置为nullselectbox.option[0]=null;

6)移动和重排选项

将第一个选择框中的第一个选项移动到第二个选择框中

var selectbox1 = document.getElementById("selLocations1");

var selectbox2 = document.getElementById("selLocations2");

selectbox2.appendChild(selectbox1.options[0]);

在选择框中向前移动一个选项的位置

var optionToMove = selectbox.options[1];

selectbox.insertBefore(optionToMove, selectbox.options[optionToMove.index-1]);

4、表单序列化<FormSerializationExample01.htm>

function serialize(form){        

var parts = [],

field = null,

i,

        en,

        j,

        optLen,

        option,

        optValue;  

        for (i=0, len=form.elements.length; i < len; i++){

         field = form.elements[i];

                switch(field.type){

                 case "select-one":

                    case "select-multiple":

                    

if (field.name.length){

                 for (j=0, optLen = field.options.length; j < optLen; j++){

                     option = field.options[j];

                        if (option.selected){

                         optValue = "";

                            if (option.hasAttribute){

                             optValue = (option.hasAttribute("value") ? option.value : option.text);

                             } else {

optValue = (option.attributes["value"].specified ? option.value : option.text);

                              }

                              parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(optValue));

}

}

}

                break;      

case undefined:     //fieldset

            case "file":        //file input

            case "submit":      //submit button

            case "reset":       //reset button

            case "button":      //custom button

             break;     

            case "radio":       //radio button

case "checkbox":    //checkbox

             if (!field.checked){

                 break;

                 }

                 /* falls through */                

default:

             //don't include form fields without names

                if (field.name.length){

                 parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(field.value));

                }

}

}        

return parts.join("&");

}

5、富文本编辑

1)使用designMode属性:首先在页面中嵌入一个包含空HTML页面的iframe,其次设置designMode属性(默认值为off)为on时,整个文档就会变得可以编辑;

2)使用contenteditable属性:通过将contenteditable属性应用给页面中的任何元素,该元素就可以立即被编辑;通过设置contenteditate属性打开或关闭编辑模式,值为true(打开)、false(关闭)和inherit(从父元素那里继承)

3)操作富文本:document.execCommand()用于对文档执行预定义的命令,接收三个参数——要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值(始终为false)和执行命令必须一个值(如果不需要值,则传递null

4)富文本选区:使用iframe的getSelection()方法,返回一个表示当前选择文本的Selection对象

5)表单与富文本<RichTextEditingExample01.htm>

EventUtil.addHandler(form, "submit", function(){

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);                 target.elements["comments"].value = frames["richedit"].document.body.innerHTML;

});

第十七章 错误处理与检测

1、浏览器报告的错误

1)IEIE 是唯一一个在浏览器的界面窗体(chrome)中显示 JavaScript 错误信息的浏览器。在发生 JavaScript错误时,浏览器左下角会出现一个黄色的图标,图标旁边则显示着 "Error on page" (页面中有错误)。打开“Tools”(工具)菜单中的“Internet Options”(Internet 选项)对话框,切换到“Advanced”(高级)选项卡,选中“Display a notification about every script error”(显示每个脚本错误的通知)复选框。单击“OK”(确定)按钮保存设置。

2)Firefox:默认情况下,Firefox JavaScript 发生错误时不会通过浏览器界面给出提示。但它会在后台将错误记录到错误控制台中。单击“Tools”(工具)菜单中的“Error Console”(错误控制台)可以显示错误控制台。

3)SafariWindows Mac OS 平台的 Safari 在默认情况下都会隐藏全部 JavaScript 错误。为了访问到这些信息,必须启用“Develop”(开发)菜单。为此,需要单击“Edit”(编辑)菜单中的“Preferences”(偏好设置),然后在“Advanced”(高级)选项卡中,选中“Show develop menu in menubar”(在菜单栏中显示“开发”菜单)。启用此项设置之后,就会在 Safari 的菜单栏中看到一个“Develop”菜单。

4)OperaOpera 在默认情况下也会隐藏 JavaScript 错误,所有错误都会被记录到错误控制台中。要打开错误控制台,需要单击“Tools”(工具)菜单,在“Advanced”(高级)子菜单项下面再单击“Error Console”(错误控制台)。

5)Chrome:与 Safari Opera 一样,Chrome 在默认情况下也会隐藏 JavaScript 错误。所有错误都将被记录到Web Inspector 控制台中。要查看错误消息,必须打开 Web Inspector。为此,要单击位于地址栏右侧的“Control this page”(控制当前页)按钮,选择“Developer”(开发人员)、“JavaScript console”(JavaScript控制台)。

2、错误处理

1)try-catch语句

try{

// 可能会导致错误的代码

} catch(error){

// 在错误发生时怎么处理

}

finally语句:虽然在 try-catch 语句中是可选的,但是finally 子句一经使用,其代码无论如何都会执行。只要代码中包含 finally 子句,则无论 try catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。

function testFinally(){

try {

return 2;

} catch (error){

return 1;

} finally {

return 0;

}

}

2)抛出错误:throw操作符,用于随时抛出自定义操作符。抛出错误时,必须要给 throw 操作符指定一个值。

3)error(错误)事件:在任何 Web 浏览器中, onerror 事件处理程序都不会创建 event 对象,但它可以接收三个参数:错误消息、错误所在的 URL 和行号。

4)常见的错误类型

类型转化错误:类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时,在使用相等(==)和不相等(!=)操作符,或者在 if for while 等流控制语句中使用非布尔值时。

数据类型错误:在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。

通信错误:第一种通信错误与格式不正确的 URL 或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent() 对数据进行编码;另外,在服务器响应的数据不正确时,也会发生通信错误。

5)区分致命错误和非致命错误:对于非致命错误,可以根据下列一或多个条件来确定:不影响用户的主要任务;只影响页面的一部分;可以恢复;重复相同操作可以消除错误。对于致命错误,可以根据下列一或多个条件来确定:应用程序根本无法继续运行;错误明显影响到了用户的主要操作;会导致其他连带错误。

6)把错误记录到服务器:要建立这样一种 JavaScript 错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。

3、调试技术

1)将消息记录到控制台

2)将消息记录到当前页面

3)抛出错误

4、常见的IE错误

1)操作中止:在修改尚未加载完成的页面时,就会发生操作终止错误。发生错误时,会出现一个模态对话框,告诉你“操作终止。”单击确定(OK)按钮,则卸载整个页面,继而显示一张空白屏幕。要避免这个问题,可以等到目标元素加载完毕后再对它进行操作,或者使用其他操作方法。

2)无效字符:所谓无效字符,就是 JavaScript 语法中未定义的字符。在 JavaScript 文件中存在无效字符时,IE 会抛出无效字符(invalid character)错误。

3)未找到成员错误:IE 中的所有 DOM 对象都是以 COM 对象,而非原生 JavaScript 对象的形式实现的。IE 中的未找到成员(Member not found)错误,就是由于垃圾收集例程配合错误所直接导致的。

4)未知运行时错误:当使用 innerHTML outerHTML 以下列方式指定 HTML 时,就会发生未知运行时错误(Unknown runtime error):一是把块元素插入到行内元素时,二是访问表格任意部分( <table> <tbody> 等)的任意属性时。

5)语法错误:只要 IE 一报告发生了语法错误(syntax error),原因可能是代码中少了一个分号,或者花括号前后不对应。如果引用了外部的 JavaScript 文件,而该文件最终并没有返回 JavaScript 代码,IE 也会抛出语法错误。

6)系统无法找到指定资源:在使用 JavaScript请求某个资源 URL,而该 URL的长度超过了 IEURL最长不能超过 2083 个字符的限制时,就会发生这个错误。IE 不仅限制JavaScript 中使用的 URL 的长度,而且也限制用户在浏览器自身中使用的 URL 长度。IE URL路径还有一个不能超过 2048 个字符的限制。

第二十章 JSON

1、语法

1)简单值:使用与 JavaScript 相同的语法,可以在 JSON 中表示字符串、数值、布尔值和 null。但 JSON 不支持 JavaScript 中的特殊值 undefinedJavaScript 字符串与 JSON 字符串的最大区别在于,JSON 字符串必须使用双引号(单引号会导致语法错误)。

2)对象:与 JavaScript 的对象字面量相比,JSON 对象有三个地方不一样。首先,没有声明变量(JSON 中没有变量的概念)。其次,没有末尾的分号(因为这不是 JavaScript 语句,所以不需要分号)。最后,对象的属性必须加双引号,属性的值可以是c简单值也可以是复杂类型值。

3)数组:JSON 数组也没有变量和分号。把数组和对象结合起来,可以构成更复杂的数据集合。

2、解析与序列化

1)JSON对象:JSON 对象有两个方法: stringify() parse() 。在最简单的情况下,这两个方法分别用于把JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。在序列化 JavaScript 对象时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined 的任何属性也都会被跳过。结果中最终都是值为有效 JSON 数据类型的实例属性。

2)序列化选项:

JSON.stringify() 还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化 JavaScript 对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在 JSON 字符串中保留缩进。如果过滤器参数是数组,那么 JSON.stringify() 的结果中将只包含数组中列出的属性;如果过滤器参数是函数,传入的函数接收两个参数,属性(键)名和属性值。如果函数返回了undefined ,那么相应的属性会被忽略。JSON.stringify() 方法的第三个参数用于控制结果中的缩进和空白符。如果这个参数是一个数值,那它表示的是每个级别缩进的空格数。

 toJSON() 方法用于返回其自身的 JSON 数据格式。假设把一个对象传入 JSON.stringify() ,序列化该对象的顺序如下:

(1) 如果存在 toJSON() 方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身。

(2) 如果提供了第二个参数,应用这个函数过滤器。传入函数过滤器的值是第(1)步返回的值。

(3) 对第(2)步返回的每个值进行相应的序列化。

(4) 如果提供了第三个参数,执行相应的格式化。

3)解析选项:JSON.parse() 方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用。如果还原函数返回 undefined ,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中。

第二十一章 Ajax Comet

1、XMLHttpRequest对象

1)创建XMLHttpRequest对象

function createXHR(){

if (typeof XMLHttpRequest != "undefined"){

return new XMLHttpRequest();

} else if (typeof ActiveXObject != "undefined"){

if (typeof arguments.callee.activeXString != "string"){

var versions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",

"MSXML2.XMLHttp"],

i, len;

for (i=0,len=versions.length; i < len; i++){

try {

new ActiveXObject(versions[i]);

arguments.callee.activeXString = versions[i];

break;

} catch (ex){

//跳过

}

}

}

return new ActiveXObject(arguments.callee.activeXString);

} else {

throw new Error("No XHR object available.");

}

}

2)使用XMLHttpRequest对象:

open() 方法接收 3 个参数——要发送的请求的类型( "get" "post" 等)、请求的 URL 和表示是否异步发送请求的布尔值。调用 open() 方法并不会真正发送请求,而只是启动一个请求以备发送。

send()方法接收一个参数——要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null。调用 send() 之后,请求就会被分派到服务器。

abort() 方法用于取消异步请求。

3HTTP头部信息

默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息。

Accept :浏览器能够处理的内容类型。

Accept-Charset :浏览器能够显示的字符集。

Accept-Encoding :浏览器能够处理的压缩编码。

Accept-Language :浏览器当前设置的语言。

Connection :浏览器与服务器之间连接的类型。

Cookie :当前页面设置的任何 Cookie

Host :发出请求的页面所在的域。

Referer :发出请求的页面的 URI

User-Agent :浏览器的用户代理字符串。

setRequestHeader() 方法用于设置自定义的请求头部信息,接收两个参数——头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用 open() 方法之后且调用 send() 方法之前调用 setRequestHeader()

调用 XHR 对象的 getResponseHeader() 方法并传入头部字段名称,可以取得相应的响应头部信息。而调用 getAllResponseHeaders() 方法则可以取得一个包含所有头部信息的长字符串。

4)GET请求:最常用于向服务器查询某些信息。

5)POST请求:通常用于向服务器发送应该被保存的数据。

2、XMLHttpRequest2

1)FormData:用于序列化表单以及创建与表单格式相同的数据(用于通过 XHR 传输)

2)超时设定:

var xhr = createXHR();

xhr.onreadystatechange = function(){

if (xhr.readyState == 4){

try {

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){

alert(xhr.responseText);

} else {

alert("Request was unsuccessful: " + xhr.status);

}

} catch (ex){

//假设由 ontimeout 事件处理程序处理

}

}

};

xhr.open("get", "timeout.php", true);

xhr.timeout = 1000; // 将超时设置为 1秒钟(仅适用于 IE8+

xhr.ontimeout = function(){

alert("Request did not return in a second.");

};

xhr.send(null);

3)overrideMimeType() 方法:用于重写 XHR 响应的 MIME 类型

3、进度事件

1)load事件:响应接收完毕后将触发 load 事件,因此也就没有必要去检查 readyState 属性了。而 onload 事件处理程序会接收到一个 event 对象,其 target 属性就指向 XHR 对象实例,因而可以访问到 XHR 对象的所有方法和属性。

var xhr = createXHR();

xhr.onload = function(){

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){

alert(xhr.responseText);

} else {

alert("Request was unsuccessful: " + xhr.status);

}

};

xhr.open("get", "altevents.php", true);

xhr.send(null);

2)progress事件: onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但包含着三个额外的属性: lengthComputable position totalSize 。其中, lengthComputable是一个表示进度信息是否可用的布尔值, position 表示已经接收的字节数, totalSize 表示根据Content-Length 响应头部确定的预期字节数。

进度指示器实例:

var xhr = createXHR();

xhr.onprogress = function(event){

var divStatus = document.getElementById("status");

if (event.lengthComputable){

divStatus.innerHTML = "Received " + event.position + " of " +

event.totalSize +" bytes";

}

};

xhr.open("get", "altevents.php", true);

xhr.send(null);

4、跨源资源共享:ORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

5、其他跨域技术

1)图像Ping:请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load error 事件,它能知道响应是什么时候接收到的。图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。图像 Ping 有两个主要的缺点,一是只能发送 GET 请求,二是无法访问服务器的响应文本。

2)JSONPJSONP 是被包含在函数调用中的 JSONJSONP 由两部分组成——回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。与图像 Ping 相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过,JSONP 也有两点不足,首先,JSONP 是从其他域中加载代码执行,其次,要确定 JSONP 请求是否失败并不容易。

3)CometAjax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。有两种实现 Comet 的方式:长轮询和HTTP流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。HTTP流就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。

4)服务器发送事件(SSE):是围绕只读 Comet 交互推出的 API 或者模式。SSE 支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接。

SSE API:用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。

事件流:服务器事件会通过一个持久的 HTTP 响应发送,这个响应的 MIME 类型为 text/event-stream 。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀 data:

5)Web SocketsWeb Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 Web Socket 协议。

6、安全

1)要求以 SSL 连接来访问可以通过 XHR 请求的资源。

2)要求每一次请求都要附带经过相应算法计算得到的验证码。

请注意,下列措施对防范 CSRF 攻击不起作用。

3)要求发送 POST 而不是 GET 请求——很容易改变。

4)检查来源 URL 以确定是否可信——来源记录很容易伪造。

5)基于 cookie 信息进行验证——同样很容易伪造。

第二十二章 高级技巧

1、高级函数

1)安全类型检测

function isFunction(value){

return Object.prototype.toString.call(value) == "[object Function]";

}

2)作用域安全的构造函数:作用域安全的构造函数在进行任何更改前,首先确认 this 对象是正确类型的实例。如果不是,那么会创建新的实例并返回。

3)惰性载入函数:惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。

4)函数绑定:函数绑定要创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。

5)函数柯里化:用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

2、防篡改对象:一旦把对象定义为防篡改,就无法撤销了。

1)不可扩展对象:在调用了 Object.preventExtensions() 方法后,就不能给对象添加新属性和方法了。在非严格模式下,给对象添加新成员会导致静默失败。

2)密封的对象:密封对象不可扩展,而且已有成员的 [[Configurable]] 特性将被设置为 false ,这就意味着不能删除属性和方法,因为不能使用 Object.defineProperty() 把数据属性修改为访问器属性,或者相反。属性值是可以修改的。使用 Object.isSealed() 方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用Object.isExtensible() 检测密封的对象也会返回 false

3)冻结的对象:冻结的对象既不可扩展,又是密封的,而且对象数据属性的 [[Writable]] 特性会被设置为 false 。如果定义 [[Set]] 函数,访问器属性仍然是可写的。 Object.freeze() 方法可以用来冻结对象。因为冻结对象既是密封的又是不可扩展的,所以用 Object.isExtensible() Object.isSealed() 检测冻结对象将分别返回 falsetrue

3、高级定时器: setTimeout() setInterval()window.setTimeout(function(){Time();setTimeout(arguments.callee, 1000);}, 1000);

1)Yielding Processes:chunk() 方法接受三个参数:要处理的项目的数组,用于处理项目的函数,以及可选的运行该函数的环境。

function chunk(array, process, context){

setTimeout(function(){

var item = array.shift();

process.call(context, item);

if (array.length > 0){

setTimeout(arguments.callee, 100);

}

}, 100);

}

2)函数节流:基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

function throttle(method, context) {

clearTimeout(method.tId);

method.tId= setTimeout(function(){

method.call(context);

}, 100);

}

4、自定义事件

function EventTarget(){

this.handlers = {};

}

EventTarget.prototype = {

constructor: EventTarget,

addHandler: function(type, handler){

if (typeof this.handlers[type] == "undefined"){

this.handlers[type] = [];

}

this.handlers[type].push(handler);

},

fire: function(event){

if (!event.target){

event.target = this;

}

if (this.handlers[event.type] instanceof Array){

var handlers = this.handlers[event.type];

for (var i=0, len=handlers.length; i < len; i++){

handlers[i](event);

}

}

},

removeHandler: function(type, handler){

if (this.handlers[type] instanceof Array){

var handlers = this.handlers[type];

for (var i=0, len=handlers.length; i < len; i++){

if (handlers[i] === handler){

break;

}

}

handlers.splice(i, 1);

}

}

};

5、拖放

var DragDrop = function(){

var dragging = null;

diffX = 0;

diffY = 0;

function handleEvent(event){

//获取事件和目标

event = EventUtil.getEvent(event);

var target = EventUtil.getTarget(event);

//确定事件类型

switch(event.type){

case "mousedown":

if (target.className.indexOf("draggable") > -1){

dragging = target;

diffX = event.clientX - target.offsetLeft;

diffY = event.clientY - target.offsetTop;

}

break;

case "mousemove":

if (dragging !== null){

//指定位置

dragging.style.left = (event.clientX - diffX) + "px";

dragging.style.top = (event.clientY - diffY) + "px";

}

break;

case "mouseup":

dragging = null;

break;

}

};

//公共接口

return {

enable: function(){

EventUtil.addHandler(document, "mousedown", handleEvent);

EventUtil.addHandler(document, "mousemove", handleEvent);

EventUtil.addHandler(document, "mouseup", handleEvent);

},

disable: function(){

EventUtil.removeHandler(document, "mousedown", handleEvent);

EventUtil.removeHandler(document, "mousemove", handleEvent);

EventUtil.removeHandler(document, "mouseup", handleEvent);

}

}

}();

第二十三章 离线应用与客户端存储

1、离线检测: navigator.onLine属性,这个属性值为 true 表示设备能上网,值为 false 表示设备离线。事件online offline ,当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件,这两个事件在 window 对象上触发。

2、应用缓存

3、数据存储

1)Cookie:最初是在客户端用于存储会话信息的。该标准要求服务器对任意 HTTP 请求发送 Set-Cookie HTTP 头作为响应的一部分,其中包含会话信息。通过为每个请求添加 Cookie HTTP 头将信息发送回服务器,发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。

2)JavaScript中的cookie:当用来获取属性值时,document.cookie 返回当前页面可用的(根据 cookie 的域、路径、失效时间和安全设置)所有 cookie的字符串,一系列由分号隔开的名值对儿。基本的cookie 操作有三种:读取、写入和删除。

var CookieUtil = {

get: function (name){

var cookieName = encodeURIComponent(name) + "=",

   cookieStart = document.cookie.indexOf(cookieName),

   cookieValue = null;

if (cookieStart > -1){

var cookieEnd = document.cookie.indexOf(";", cookieStart);

if (cookieEnd == -1){

cookieEnd = document.cookie.length;

}

cookieValue=decodeURIComponent(document.cookie. substring(cookieStart+ cookieName.length, cookieEnd));

}

return cookieValue;

},

set: function (name, value, expires, path, domain, secure) {

var cookieText = encodeURIComponent(name) + "=" +

encodeURIComponent(value);

if (expires instanceof Date) {

cookieText += "; expires=" + expires.toGMTString();

}

if (path) {

cookieText += "; path=" + path;

}

if (domain) {

cookieText += "; domain=" + domain;

}

if (secure) {

cookieText += "; secure";

}

document.cookie = cookieText;

},

unset: function (name, path, domain, secure){

this.set(name, "", new Date(0), path, domain, secure);

}

};

3IE用户数据:用户数据允许每个文档最多128KB 数据,每个域名最多 1MB 数据。要使用持久化用户数据,首先必须如下所示,使用 CSS 在某个元素上指定 userData 行为:

<div style="behavior:url(#default#userData)" id="dataStore"></div>

4)Web存储机制:Web Storage 的目的是克服由 cookie 带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。Web Storage 的两个主要目标是:提供一种在 cookie 之外存储会话数据的途径;提供一种存储大量可以跨会话存在的数据的机制。

5)IndexedDBIndexedDB 的思想是创建一套 API,方便保存和读取 JavaScript 对象,同时还支持查询及搜索。

IndexedDB大多数操作会以请求方式异步进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。

数据库:IndexedDB最大的特色是使用对象保存数据,而不是使用表来保存数据。一个 IndexedDB 数据库,就是一组位于相同命名空间下的对象的集合。

对象存储空间:如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。

事务:在数据库对象上调用 transaction() 方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。

使用游标查询:使用事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是一指向结果集的指针。与传统数据库查询不同,游标并不提前收集结果。游标指针会先指向结果中的第一项,在接到查找下一项的指令时,才会指向下一项。

键范围:键范围为使用游标增添了一些灵活性。键范围由 IDBKeyRange 的实例表示。

设定游标方向:

索引:

并发问题:如果浏览器的两个不同的标签页打开了同一个页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库,问题就有可能发生。只有当浏览器中仅有一个标签页使用数据库的情况下,调用 setVersion() 才能完成操作。

限制:首先,IndexedDB 数据库只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。其次,每个来源的数据库占用的磁盘空间也有限制。

第二十四章 最佳实践

1、可维护性

1)可维护性代码:

可理解性——其他人可以接手代码并理解它的意图和一般途径,而无需原开发人员的完整解释。

q  直观性——代码中的东西一看就能明白,不管其操作过程多么复杂。

q  可适应性——代码以一种数据上的变化不要求完全重写的方法撰写。

q  可扩展性——在代码架构上已考虑到在未来允许对核心功能进行扩展。

2)代码约定

可读性:首先可读性与代码作为文本文件的格式化方式有关,其次是注释。

变量和函数命名:变量名应为名词; 函数名应该以动词开始,返回布尔类型值的函数一般以 is 开头;变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩来缓解。

变量类型透明:第一种方式是初始化。第二种方法是使用匈牙利标记法来指定变量类型, "o" 代表对象, "s" 代表字符串, "i"代表整数, "f" 代表浮点数, "b" 代表布尔型。最后一种指定变量类型的方式是使用类型注释。

3)松散耦合

解耦 HTML/JavaScriptHTML 是数据,JavaScript 是行为。理想情况是,HTML JavaScript 应该完全分离,并通过外部文件和使用 DOM 附加行为来包含 JavaScript

解耦 CSS/JavaScript:通过只修改某个元素的 CSS 类,就可以让大部分样式信息严格保留在 CSS 中。JavaScript 可以更改样式类,但并不会直接影响到元素的样式。

解耦应用逻辑/事件处理程序:勿将 event 对象传给其他方法;只传来自 event 对象中所需的数据;任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。

4)编程实践

尊重对象所有权:不要为实例或原型添加属性;不要为实例或原型添加方法;不要重定义已存在的方法。

避免全局量

避免与null进行比较: 如果值应为一个引用类型,使用 instanceof 操作符检查其构造函数;如果值应为一个基本类型,使用 typeof 检查其类型;如果是希望对象包含某个特定的方法名,则使用 typeof 操作符确保指定名字的方法存在于对象上。

使用常量:重复值;用户界面字符串;URLs;任意可能会更改的值

2、性能

1)注意作用域:避免全局查找;避免 with 语句

2)选择正确方法:避免不必要的属性查找;优化循环——减值迭代、简化终止条件、简化循环体、使用后测试循环;展开循环;避免双重解释;原生方法较快; Switch 语句较快; 位运算符较快

3)最小化语句数

多个变量声明:在 JavaScript 中所有的变量都可以使用单个 var 语句来声明。

插入迭代值:当使用迭代值(也就是在不同的位置进行增加或减少的值)的时候,尽可能合并语句。

使用数组和对象字面量

4)优化DOM交互

最小化现场更新:第一种是将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。第二个方法是使用文档片段来构建 DOM 结构,接着将其添加到 List 元素中。

使用innerHTML

使用事件代理

注意 HTMLCollection

第二十五章 新兴的API

1、requestAnimationFrame()

1)mozRequestAnimationFrame() :通过mozRequestAnimationFrame() 告诉浏览器某些 JavaScript 代码将要执行动画,接收一个参数——即在重绘屏幕前调用的一个函数,这个函数用于改变下一次重绘时的 DOM样式;接收一个参数——时间码(从197011日起至今的毫秒数),表示下一次重绘的实际发生时间。

2)webkitRequestAnimationFrame msRequestAnimationFrame:这两个版本与 Mozilla 的版本有两个方面的微小差异。首先,不会给回调函数传递时间码,因此你无法知道下一次重绘将发生在什么时间。其次,Chrome 又增加了第二个可选的参数,即将要发生变化的 DOM 元素。知道了重绘将发生在页面中哪个特定元素的区域内,就可以将重绘限定在该区域中。

2、Page Visibility API:用于让开发人员知道页面是否对用户可见

document.hidden :表示页面是否隐藏的布尔值。页面隐藏包括页面在后台标签页中或者浏览器最小化。

document.visibilityState :表示下列 4 个可能状态的值。

n  页面在后台标签页中或浏览器最小化。

n  页面在前台标签页中。

n  实际的页面已经隐藏,但用户可以看到页面的预览(就像在 Windows 7 中,用户把鼠标移动到任务栏的图标上,就可以显示浏览器中当前页面的预览)。

n  页面在屏幕外执行预渲染处理。

visibilitychange 事件:当文档从可见变为不可见或从不可见变为可见时,触发该事件。

3、Geolocation API:用于访问到用户的当前位置信息。

4、File API:用于在客户端访问用户计算机中的文件,并更好地对这些文件执行操作。

5、Web Timing API:用于让开发人员通过 JavaScript 就能使用浏览器内部的度量结果,通过直接读取这些信息可以做任何想做的分析。

6、Web Workers:解决JavaScript 进程会导致浏览器冻结用户界面的问题。

posted @ 2016-03-31 23:10  吴萍  阅读(266)  评论(0编辑  收藏  举报