JavaScript知识点总结

文章目录


参考Javascript高级程序设计(第3版)

1、JavaScript简介

1.1 DOM

文档对象模型(DOM,Document Object Model)是针对 XML 但经过扩展用于 HTML 的应用程序编程接口(API,Application Programming Interface)。DOM 把整个页面映射为一个多层节点结构。

1.2 BOM

浏览器窗口的浏览器对象模型(BOM,Browser Object Model)。

2、在 HTML 中使用 JavaScript

2.1 < script >元素

script标签定义了下列 6 个属性:

属性说明
async可选。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
charset可选。表示通过 src 属性指定的代码的字符集。
defer可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。IE7 及更早版本对嵌入脚本也支持这个属性。 相当于告诉浏览器立即下载,但延迟执行。 脚本会被延迟到整个页面都解析完毕后再运行。
language已废弃。原来用于表示编写代码使用的脚本语言(如 JavaScript、JavaScript1.2或 VBScript)。大多数浏览器会忽略这个属性,因此也没有必要再用了。
src可选。表示包含要执行代码的外部文件。
type可选。可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(也称为 MIME 类型)。目前 type 属性的值依旧还是text/javascript。不过,这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为text/javascript。

2.1.1 使用< script >元素的方式有两种

(1)直接在页面中嵌入 JavaScript 代码

须为< script >标签指定 type 属性

<script type="text/javascript">     
	function sayHi(){
	      alert("Hi!");     
	}
</script> 

包含在< script >元素内部的 Java Script 代码将被从上至下依次解释。
注意:在使用< script >嵌入 JavaScript 代码时,不要在代码中的任何地方出现"< script >"字符串。通过转义字符“/”,例如:

<script type="text/javascript">     
	function sayScript(){
         alert("<\/script>");     
    } 
</script> 
(2)包含外部 JavaScript文件

通过< script >元素来包含外部 JavaScript 文件,那么 src 属性就是必需的,指向外部 JavaScript 文件的链接。

<script type="text/javascript" src="example.js"></script>

在解析外部 JavaScript 文件(包括下载该文件)时,页面的处理也会暂时停止。
通过< script >元素的 src 属性还可以包含来自外部域的 JavaScript 文件。(跨域要注意安全)

<script type="text/javascript" src="http://www.somewhere.com/afile.js"></script>

2.1.2 标签的位置

现代 Web 应用程序一般都把全部 JavaScript 引用放在< body >元素中页面内容的后面,为了在解析包含的 JavaScript 代码之前,先将页面的内容完全呈现在浏览器中。

2.1.3 延迟脚本

为< script >标签定义了 defer 属性, 相当于告诉浏览器立即下载,但延迟执行。 脚本会被延迟到整个页面都解析完毕后再运行。但是脚本会先于 DOMContentLoaded 事件执行,在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发
前执行,因此最好只包含一个延迟脚本。defer 属性只适用于外部脚本文件。支持
HTML5 的实现会忽略给嵌入脚本设置的 defer 属性。

2.1.4 异步脚本

HTML5 为< script >元素定义了 async 属性。async 只适用于外部脚本文件,并告诉浏览器立即下载文件。标记为 async 的脚本并不保证按照指定它们的先后顺序执行。指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改 DOM。
异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。

3、基本概念

3.1 语法

3.1.1 严格模式

严格模式是为 JavaScript 定义了一种不同的解析与执行模型。在严格模式下,ECMAScript 3 中的一些不确定的行为将得到处理,而且对某些不安全的操作也会抛出错误。“use strict” 指令只允许出现在脚本或函数的开头。要在整个脚本中启用严格模式,可以在顶部添加如下代码:

"use strict";

也可以指定函数在严格模式下执行。

严格模式的限制
  1. 不能使用未声明的变量。(对象也是一个变量)
  2. 不允许删除变量或对象
  3. 不允许删除函数
  4. 不允许变量重名
  5. 不允许使用八进制
  6. 不允许使用转义字符
  7. 不允许对只读属性赋值
  8. 不允许对一个使用getter方法读取的属性进行赋值
  9. 不允许使用 with 语句
"use strict";
var obj = { get x() {return 0} };
obj.x = 3.14;            // 报错
  1. 不允许删除一个不允许删除的属性
  2. 变量名不能使用 “eval” 字符串:eval(String) 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
  3. 变量名不能使用 “arguments” 字符串
  4. 由于一些安全原因,在作用域 eval() 创建的变量不能被调用
  5. 禁止this关键字指向全局对象
function f(){
   return !this;
} 
// 返回false,因为"this"指向全局对象,"!this"就是false

function f(){ 
   "use strict";
   return !this;
} 
// 返回true,因为严格模式下,this的值为undefined,所以"!this"为true。

//因此,使用构造函数时,如果忘了加new,this不再指向全局对象,而是报错。

function f(){
   "use strict";
   this.a = 1;
};
f();// 报错,this未定义
为什么使用严格模式?
  1. 消除代码运行的一些不安全之处,保证代码运行的安全;
  2. 提高编译器效率,增加运行速度;

3.4 数据类型

5 种基本数据类型(按值访问):Undefined、Null、Boolean、Number和 String
1种复杂数据类型(按引用访问):Object,Array,Function,Object 本质上是由一组无序的名值对组成的。

3.4.1 typeof操作符

检测给定变量的数据类型
返回值含义:

返回值说明
undefined这个值未定义;
boolean这个值是布尔值;
string这个值是字符串;
number这个值是数值;
object这个值是对象或nu11;
function这个值是函数。

说明:

  1. 调用 typeof null会返回"object",因为特殊值 null 被认为是一个空的对象引用。
  2. Safari 5 及之前版本、Chrome 7 及之前版本在对正则表达式调用 typeof 操作符时会返回"function",而其他浏览器在这种情况下会返回"object"。
  3. 对未初始化的变量执行 typeof 操作符会返回 undefined 值
  4. 对未声明的变量执行 typeof 操作符同样也会返回 undefined 值。

3.6 语句

3.6.8 with语句

with 语句的作用是将代码的作用域设置到一个特定的对象中。with 语句的语法如下:

with (expression) statement;

定义 with 语句的目的主要是为了简化多次编写同一个对象的工作,如:

var qs = location.search.substring(1); 
var hostName = location.hostname; 
var url = location.href;

上面几行代码都包含 location 对象。如果使用 with 语句,可以把上面的代码改写成如下所示:

with(location){ 
 var qs = search.substring(1); 
 var hostName = hostname; 
 var url = href; 
}

注意:
严格模式下不允许使用 with 语句,否则将视为语法错误。因为大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用 with 语句。

4、变量、作用域和内存

4.1 基本类型和引用类型

4.1.4 instanceof检测类型

instanceof操作符判断对象变量是什么类型的对象。

alert(person instanceof Object);   // 变量person是Object吗?
alert(colors instanceof Array);    // 变量colors是Array吗?
alert(pattern instanceof RegExp);  // 变量pattern是RegExp吗?

4.2 执行环境和作用域

当代码在一个环境(全局,函数,代码块等)中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
类似于一套房子,有公共的区域:客厅,卫生间,厨房,还有私人区域:卧室A和卧室B,公共区域即为全局作用域,私人区域即为类似函数、catch块、with语句等的作用域,私人区域可以访问大范围的公共区域,而公共区域不能访问私人区域。私人区域之间也不能相互访问。

4.2.2 没有块级作用域

JavaScript没有块级作用域。由for语句创建的变量 i 即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。
使用var声明的变量会自动被添加到最接近的环境中。

function add(num1, num2) {
     var sum = num1 + num2;
     return sum; 
  } 
var result = add(10, 20); //30 
alert(sum);      //由于sum不是有效的变量,因此会导致错误

4.3 垃圾收集

JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。
垃圾收集机制的原理: 找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

函数中局部变量的正常生命周期

在函数执行时,会在栈(堆)空间上为变量分配空间。然后函数执行过程中使用,函数执行结束后,占用的内存就会被释放。垃圾收集器会对变量进行标记跟踪,用来判断没用的变量,对其进行回收。

4.3.1 标记清除

JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。而当变量离开环境时,则将其标记为“离开环境”。

标记策略: 可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。

标记清除方式回收内存流程: 垃圾收集器在运行的时候会给内存中的所有变量打标,然后去除环境内的变量以及正在被环境内的变量引用的变量的标记,而此后再次被加上标记的变量则会被视为准备删除的变量,最后垃圾收集器完成内存清除工作。

4.3.2 引用计数

引用计数的含义是跟踪记录每个值被引用的次数。
引用计数方式回收内存流程: 当声明一个变量并将一个引用类型的值赋值给该变量,这个值的引用次数就加1,相反,如果这个变量又被赋值另外一个引用类型的值,则减1,当引用次数为0时,垃圾收集器释放该内存。

存在的问题:循环引用。对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。

4.3.3 内存管理

为了提高性能,优化内存,将不再有用的数据值设置为null来释放其引用——解除引用(dereferencing)。
解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

5、引用类型

5.1 Object

5.1.1 创建对象的两种方式

  1. 使用new操作符后跟Object构造函数
var person = new Object(); 
  1. 使用对象字面量
var person = {
     name : "Nicholas",
     age : 29 
}; 

使用对象字面量语法时,如果留空其花括号,则可以定义只包含默认属性和方法的对象,如下所示:

var person = {};         //与new Object()相同

5.1.2 访问对象属性的方法

alert(person.name);         //"Nicholas" 
  1. 方括号(主要优点是可以通过变量来访问属性)
alert(person["name"]);       //"Nicholas" 

//方括号语法的主要优点是可以通过变量来访问属性
var propertyName = "name"; 
alert(person[propertyName]);  //"Nicholas" 

5.2 Array

  1. ECMAScript数组的每一项可以保存任何类型的数据。
  2. ECMAScript数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。

5.2.1 创建Array的两种方式

数组最多可以包含4 294 967 295个项。

  1. 使用Array构造函数(可以省略new操作符,建议使用这种方式)
var colors = new Array(); 
var colors = new Array(20);
var colors = new Array("red", "blue", "green"); 
  1. 使用数组字面量表示法
var colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组var names = [];                      // 创建一个空数组
var values = [1,2,];             // 不要这样!这样会创建一个包含2或3项的数组
var options = [,,,,,];           // 不要这样!这样会创建一个包含5或6项的数组

注意:数组的length属性不是只读的,可以通过设置length的值来从数组的末尾移除项或向数组中添加新项。

5.2.2 数组检测

  1. instanceof
    缺陷:不同的全局执行环境,从而存在两个以上不同版本的Array构造函数,因为其检测的是由构造函数构造的实例对象,不同的全局环境构造函数可能不同。
  2. Array.isArray()方法

5.2.3 栈方法

ECMAScript 为数组专门提供了 push()和 pop()方法

方法说明
push()可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。
pop()从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。

5.2.4 队列方法

栈数据结构的访问规则是 LIFO(后进先出)
队列数据结构的访问规则是 FIFO(First-In-First-Out,先进先出)。
队列在列表的末端添加项,从列表的前端移除项。

方法说明
push()向数组末端添加项
shift()移除数组中的第一个项并返回该项,同时将数组长度减 1
unshift()在数组前端添加任意个项并返回新数组的长度

5.2.5 其他方法

方法说明
reverse()反转
sort()按升序排列数组项
sort(compare)通过比较函数产生排序,需要实现一个compare函数,比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数。function compare(value1, value2){ return value2 - value1; }
concat()基于当前数组中的所有项创建一个新数组。在没有给concat()方法传递参数的情况下,它只是复制当前数组并返回副本。如果传递给concat()方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中。如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。
slice()基于当前数组中的一或多个项创建一个新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。注意,slice()方法不会影响原始数组。 如果slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。
splice()主要用途是向数组的中部插入项,splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。
indexOf()接收两个参数:查找项和查找起点索引(可选),从数组开头开始向后查找,返回要查找的项在数组中的位置,或者在没找到的情况下返回-1
lastIndexOf()接收两个参数:查找项和查找起点索引(可选),从数组末尾开始向前查找,返回要查找的项在数组中的位置,或者在没找到的情况下返回-1
every()对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true。
filter()对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组。
forEach()对数组中的每一项运行给定函数。这个方法没有返回值。
map()对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
some()对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true。
reduce()从数组的第一项开始,逐个遍历到最后,迭代数组的所有项,然后构建一个最终返回的值。接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给reduce()和reduceRight()的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。
reduceRight()从数组的最后一项开始,向前遍历到第一项。,迭代数组的所有项,然后构建一个最终返回的值。接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给reduce()和reduceRight()的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。

splice()方法使用说明:

示例说明
splice(0,2)删除数组中的前两项。删除: 可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。
splice(2,0,“red”,“green”)从当前数组的位置2开始插入字符串"red"和"green"。插入: 可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。
splice (2,1,“red”,“green”)删除当前数组位置2的项,然后再从位置2开始插入字符串"red"和"green"。替换: 可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。

5.5 Function

5.5.1 没有重载

如果出现两个同名函数,后面的函数会覆盖前面的函数。

5.5.2 函数声明与函数表达式

解析器在向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码才会真正被解释执行

函数声明提升:对代码求值时,JavaScript 引擎在第一遍会声明函数并将它们放到源代码树的顶部。

不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

访问函数的指针而不执行函数的话,必须去掉函数名后面的那对圆括号。

5.5.4 函数内部属性

函数内部,有两个特殊的对象:arguments和this。

  1. arguments对象有一个callee属性,该属性是一个指针,指向拥有这个arguments对象的函数。arguments的主要用途是保存函数参数。
    当函数在严格模式下运行时,访问arguments.callee会导致错误。
  2. 另一个函数对象的属性:caller。保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。通过arguments.callee.caller 来访问
    为了分清arguments.caller和函数的caller属性,定义了arguments.caller属性。当函数在严格模式下运行时,访问arguments.caller会导致错误,且不能为函数的caller属性赋值,否则会导致错误。

5.5.5 函数属性和方法

每个函数都包含两个属性:length和prototype。

  • length属性表示函数希望接收的命名参数的个数
  • prototype是保存它们所有实例方法的真正所在,prototype属性是不可枚举的,使用for-in无法发现。

每个函数都包含两个非继承而来的方法:apply()和call()。在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

  • apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。
  • call() 方法,第一个参数是this值没有变化,变化的是其余参数都直接传递给函数。在使用call()方法时,传递给函数的参数必须逐个列举出来,而不是使用数组
  • bind() 会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。

apply()和call()真正强大的地方是能够扩充函数赖以运行的作用域。使用call()(或apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。

window.color = "red"; 
var o = { color: "blue" }; 
function sayColor(){
   alert(this.color); 
} 
sayColor();              //red 
sayColor.call(this);     //red 
sayColor.call(window);   //red 
sayColor.call(o);        //blue

var objectSayColor = sayColor.bind(o); 
objectSayColor();    //blue 

5.6 基本包装类型

引用类型与基本包装类型的主要区别就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着不能在运行时为基本类型值添加属性和方法。

5.6.1 Boolean

基本类型与引用类型的布尔值还有两个区别。首先,typeof操作符对基本类型返回"boolean",而对引用类型返回"object"。其次,由于Boolean对象是Boolean类型的实例,所以使用instanceof 操作符测试Boolean对象会返回true,而测试基本类型的布尔值则返回false。

5.6.2 Number

可以为toString()方法传递一个表示基数的参数,告诉它返回几进制数值的字符串形式

var num = 10; 
alert(num.toString());    //"10" 
alert(num.toString(2));   //"1010" 
alert(num.toString(8));   //"12" 
alert(num.toString(10));  //"10" 
alert(num.toString(16));  //"a" 

toFixed()方法会按照指定的小数位返回数值的字符串

var num = 10; 
alert(num.toFixed(2));     //"10.00" 

5.6.3 String

5.7 单体内置对象

5.7.1 Global对象

5.7.1.1 URI编码方法

对URI(Uniform Resource Identifiers,通用资源标识符)进行编码,以便发送给浏览器。有效的URI中不能包含某些字符,例如空格。而这两个URI编码方法就可以对URI进行编码,它们用特殊的UTF-8编码替换所有无效的字符,从而让浏览器能够接受和理解。

方法说明
encodeURI()主要用于整个URI(例如,http://www.wrox.com/illegal value.htm),不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;
encodeURIComponent()主要用于对URI中的某一段(例如前面URI中的illegal value.htm)进行编码。会对它发现的任何非标准字符进行编码。
decodeURI()只能对使用encodeURI()替换的字符进行解码。
decodeURIComponent()能够解码使用encodeURIComponent()编码,的所有字符,即它可以解码任何特殊字符的编码。
var uri = "http://www.wrox.com/illegal value.htm#start"; 

//"http://www.wrox.com/illegal%20value.htm#start" 
alert(encodeURI(uri)); 	

//"http%3A%2F%2Fwww.wrox.com%2Fillegal%20v
alert(encodeURIComponent(uri));alue.htm%23start" 

使用encodeURI()编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了%20。而encodeURIComponent()方法则会使用对应的编码替换所有非字母数字字符。这也正是可以对整个URI使用encodeURI(),而只能对附加在现有URI后面的字符串使用encodeURIComponent()的原因所在。

5.7.1.2 eval()方法

eval()方法就像是一个完整的ECMAScript解析器,它只接受一个参数,即要执行的ECMAScrip(t或JavaScript)字符串。
当解析器发现代码中调用eval()方法时,它会将传入的参数当作实际的ECMAScript语句来解析,然后把执行结果插入到原位置。通过eval()执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过eval()执行的代码可以引用在包含环境中定义的变量,举个例子:

var msg = "hello world"; 
eval("alert(msg)");    //"hello world" 

eval("function sayHi() { alert('hi'); }"); 
sayHi(); 

严格模式下,在外部访问不到eval()中创建的任何变量或函数,因此前面两个例子都会导致错误。同样,在严格模式下,为eval赋值也会导致错误:

注意:eval()中创建的任何变量或函数都不会被提升,因为在解析代码的时候,它们被包含在一个字符串中;它们只在eval()执行的时候创建。

能够解释代码字符串的能力非常强大,但也非常危险。因此在使用eval()时必须极为谨慎,特别是在用它执行用户输入数据的情况下。否则,可能会有恶意用户输入威胁你的站点或应用程序安全的代码(即所谓的代码注入)。

5.7.1.4 window对象

另一种取得Global对象的方法是使用以下代码:

var global = function(){
     return this;
}(); 

6、面向对象的程序设计

6.1 理解对象

6.1.1 属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

6.1.1.2 数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

特性说明
[[Configurable]]表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Enumerable]]表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Writable]]表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Value]]包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined。

直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为 true,而[[Value]]特性被设置为指定的值。

Object.defineProperty()方法修改属性默认的特性值。接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,可以修改
对应的特性值。例如:

var person = {}; 
Object.defineProperty(person, "name", { 
 writable: false, 
 value: "Nicholas" 
}); 
alert(person.name); //"Nicholas" 
person.name = "Greg"; 
alert(person.name); //"Nicholas"
6.1.1.2 访问器属性

它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

特性说明
[[Configurable]]表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。
[[Enumerable]]表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true。
[[Get]]在读取属性时调用的函数。默认值为 undefined。
[[Set]]在写入属性时调用的函数。默认值为 undefined。

访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。

var book = { 
	 _year: 2004, 
	 edition: 1 
}; 

Object.defineProperty(book, "year", { 
	 get: function(){ 
	 	return this._year; 
	 }, 
	 set: function(newValue){ 
	 	if (newValue > 2004) { 
	 	this._year = newValue; 
	 	this.edition += newValue - 2004; 
	 } 
 } 
}); 
book.year = 2005; 
alert(book.edition); //2

_year 前面
的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。

6.2 创建对象

6.2.1 工厂模式

用函数来封装以特定接口创建对象的细节,例如:

function createPerson(name, age, job){ 
	 var o = new Object(); 
	 o.name = name; 
	 o.age = age; 
	 o.job = job; 
	 o.sayName = function(){ 
	 alert(this.name); 
 }; 
 return o; 
} 
var person1 = createPerson("Nicholas", 29, "Software Engineer"); 
var person2 = createPerson("Greg", 27, "Doctor");

但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

6.2.2 构造函数模式

任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而

function Person(name, age, job){ 
	 this.name = name; 
	 this.age = age; 
	 this.job = job; 
	 this.sayName = function(){ 
	 alert(this.name); 
 }; 
} 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor");

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。而实际上每个实例上的方法没必要都重新创建。通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = sayName; 
} 
function sayName(){ 
 alert(this.name); 
} 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor");

新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

6.2.3 原型模式

6.2.3.1 原型对象

每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。 换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:

function Person(){ 
} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
 alert(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"Nicholas" 
var person2 = new Person();
person2.sayName(); //"Nicholas" 
alert(person1.sayName == person2.sayName); //true

虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;使用 delete 操作符可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

6.2.3.2 原型与 in 操作符

用于判断给定属性是否存在于对象中,无论该属性存在于实例中还是原型中。

“属性名” in 对象实例名

同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。

6.2.3.3. 更简单的原型语法
function Person(){ 
} 
Person.prototype = { 
 name : "Nicholas", 
 age : 29, 
 job: "Software Engineer", 
 sayName : function () { 
 alert(this.name); 
 } 
};

注意:constructor 属性不再指向 Person 了,constructor 属性就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。

6.2.4 组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = { 
 constructor : Person, 
 sayName : function(){ 
 alert(this.name); 
 } 
} 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor"); 
person1.friends.push("Van"); 
alert(person1.friends); //"Shelby,Count,Van" 
alert(person2.friends); //"Shelby,Count" 
alert(person1.friends === person2.friends); //false 
alert(person1.sayName === person2.sayName); //true

6.2.5 动态原型模式

function Person(name, age, job){ 
	 //属性
	 this.name = name; 
	 this.age = age; 
	 this.job = job;
	 //方法
	 if (typeof this.sayName != "function"){ 
		 Person.prototype.sayName = function(){ alert(this.name); }; 
	 } 
} 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName();

6.3 继承

实现继承主要是依靠原型链。

6.3.1 原型链

基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。让原型对象等于另一个类型的实例。

原型链的问题(实践中很少会单独使用原型链。):

  1. 包含引用类型值的原型在通过原型来实现继承时,原型属性会被所有实例共享。
  2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

6.3.3 组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function SuperType(name){ 
 this.name = name; 
 this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){ 
 alert(this.name);
}; 
function SubType(name, age){ 
 //继承属性
 SuperType.call(this, name); 
 
 this.age = age; 
} 
//继承方法
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){ 
 alert(this.age); 
}; 
var instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black" 
instance1.sayName(); //"Nicholas"; 
instance1.sayAge(); //29 
var instance2 = new SubType("Greg", 27); 
alert(instance2.colors); //"red,blue,green" 
instance2.sayName(); //"Greg"; 
instance2.sayAge(); //27

7、函数表达式

定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。
函数声明的语法:

function functionName(arg0, arg1, arg2) { 
 //函数体
}

函数表达式的语法:

var functionName = function(arg0, arg1, arg2){ 
 //函数体 
};

这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。函数表达式在使用前必须先赋值。

7.2 闭包

闭包就是函数里的函数,能让外部的变量访问到函数内部的变量,函数作为返回值。闭包中的变量没有被销毁。

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

使用闭包创建计数器:

function count(count){
	return function(){
		count ++;
	}
};

闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

即使 JavaScript 中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过
度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭
包。

7.3 模仿块级作用域

匿名函数模仿块级作用域,定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。

(function(){ 
 //这里是块级作用域
})();

//下面这段代码会导致语法错误,是因为 JavaScript 将 function 关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。
function(){ 
 //这里是块级作用域
}(); //出错!

7.4 私有变量

7.4.2 模块模式

JavaScript 是以对象字面量的方式来创建单例对象的。

9、客户端检测

9.1 能力检测

能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。采用这种方式不必顾及特定的浏览器如何如何,只要确定浏览器支持特定的能力,就可以给出解决方案。

13、事件

13.1 事件流

13.1.3 DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

13.2 事件处理程序

13.2.2 DOM0 级事件处理程序

每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写,例如 onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:

var btn = document.getElementById("myBtn"); 
btn.onclick = function(){ 
 alert("Clicked"); 
};

删除通过 DOM0 级方法指定的事件处理程序:

btn.onclick = null; //删除事件处理程序

DOM0 级对每个事件只支持一个事件处理程序。

13.2.3 DOM2 级事件处理程序

用于处理指定和删除事件处理程序的操作:addEventListener()和 removeEventListener()。
它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。

var btn = document.getElementById("myBtn"); 
btn.addEventListener("click", function(){ 
 alert(this.id); 
}, false);

使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。

var btn = document.getElementById("myBtn"); 
btn.addEventListener("click", function(){ 
 alert(this.id); 
}, false); 
btn.addEventListener("click", function(){ 
 alert("Hello world!"); 
}, false);

这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的 ID,其次会显示"Hello world!"消息。

通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。通过 addEventListener()添加的匿名函数将无法移除

13.2.5 跨浏览器的事件处理程序

第一个要创建的方法是 addHandler(),它的职责是视情况分别使用 DOM0 级方法、DOM2 级方法或 IE 方法来添加事件。这个方法属于一个名叫 EventUtil 的对象,本书将使用这个对象来处理浏览器间的差异。addHandler()方法接受 3 个参数:要操作的元素、事件名称和事件处理程序函数。

移除之前添加的事件处理程序对应的方法是 removeHandler(),它也接受相同的参数。

EventUtil 的用法如下所示:

var EventUtil = { 
 
	 addHandler: function(element, type, handler){ 
		 if (element.addEventListener){ //如果存在 DOM2 级方法
		 	element.addEventListener(type, handler, false); 
		 } else if (element.attachEvent){ //如果存在的是 IE 的
方法
		 	element.attachEvent("on" + type, handler); //为了在 IE8 及更早版本中运行,此时的事件类型必须加上"on"前缀。
		 } else { //是使用 DOM0 级方法
		 	element["on" + type] = handler; 
		 } 
	 }, 
	 removeHandler: function(element, type, handler){ 
		 if (element.removeEventListener){ //如果存在 DOM2 级方法
		 	element.removeEventListener(type, handler, false); 
		 } else if (element.detachEvent){ //如果存在的是 IE 的
方法
		 	element.detachEvent("on" + type, handler); //为了在 IE8 及更早版本中运行,此时的事件类型必须加上"on"前缀。
		 } else { //是使用 DOM0 级方法
		 	element["on" + type] = null; 
		 } 
	 } 
};

使用 EventUtil 对象:

var btn = document.getElementById("myBtn"); 
var handler = function(){ 
 alert("Clicked"); 
}; 
EventUtil.addHandler(btn, "click", handler); 
//这里省略了其他代码
EventUtil.removeHandler(btn, "click", handler);

13.5 内存和性能

13.5.1 事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

使用事件委托,只需在DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示:

<ul id="myLinks"> 
 <li id="goSomewhere">Go somewhere</li> 
 <li id="doSomething">Do something</li> 
 <li id="sayHi">Say hi</li> 
</ul>
var list = document.getElementById("myLinks"); 
EventUtil.addHandler(list, "click", function(event){ 
	 event = EventUtil.getEvent(event); 
	 var target = EventUtil.getTarget(event); 
	 switch(target.id){ 
		 case "doSomething": 
			 document.title = "I changed the document's title"; 
			 break; 
		 case "goSomewhere": 
			 location.href = "http://www.wrox.com"; 
			 break; 
		 case "sayHi": 
			 alert("hi"); 
			 break; 
	 } 
});

最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。(因为当鼠标从一个元素移到其子节点时,或者当鼠标移出该元素时,都会触发 mouseout 事件。)

13.5.2 移除事件处理程序

设置< div >的 innerHTML 属性之前,先移除按钮的事件处理程序。

<div id="myDiv"> 
 <input type="button" value="Click Me" id="myBtn"> 
</div> 
<script type="text/javascript"> 
 var btn = document.getElementById("myBtn");
 btn.onclick = function(){ 
 //先执行某些操作
 btn.onclick = null; //移除事件处理程序
 document.getElementById("myDiv").innerHTML = "Processing..."; 
 }; 
</script>

14、表单脚本

14.1 表单基础知识

14.1.1 提交表单

提交表单时可能出现的最大问题,就是重复提交表单。在第一次提交表单后,如果长时间没有反应,用户可能会变得不耐烦。解决这一问题的办法有两个:

  1. 在第一次提交表单后就禁用提交按钮
  2. 利用 onsubmit 事件处理程序取消后续的表单提交操作。

15、使用Canvas绘图

15.1 基本用法

  1. 创建< canvas >标签

    <canvas id="drawing" width=" 200" height="200">A drawing of something.</canvas>
    
  2. 取得绘图上下文
    在使用元素之前,首先要检测 getContext()方法是否存在,这一步非常重要。

var drawing = document.getElementById("drawing");

//确定浏览器支持<canvas>元素
if (drawing.getContext){
	var context = drawing.getContext("2d"); 
	//更多代码
}

15.2 2D 上下文

2D 上下文的坐标开始于元素的左上角,原点坐标是(0,0)。所有坐标值都基于这个原点计算,x 值越大表示越靠右,y 值越大表示越靠下。

15.2.1 填充和描边

fillStyle 和 strokeStyle

var drawing = document.getElementById("drawing"); 
//确定浏览器支持<canvas>元素
if (drawing.getContext){ 
 var context = drawing.getContext("2d"); 
 context.strokeStyle = "red"; 
 context.fillStyle = "#0000ff"; 
}

15.2.2 绘制矩形

矩形是唯一一种可以直接在 2D 上下文中绘制的形状。
与矩形有关的方法包括 :
fillRect()、strokeRect()和 clearRect()。

15.2.3 绘制路径

要绘制路径,首先必须调用 beginPath()方法,表示要开始绘制新路径。

方法说明
arc(x, y, radius, startAngle, endAngle, counterclockwise)以(x,y)为圆心绘制一条弧线,弧线半径为 radius,起始和结束角度(用弧度表示)分别为 startAngle 和endAngle。最后一个参数表示 startAngle 和 endAngle 是否按逆时针方向计算,值为 false表示按顺时针方向计算。
arcTo(x1, y1, x2, y2, radius)从上一点开始绘制一条弧线,到(x2,y2)为止,并且以给定的半径 radius 穿过(x1,y1)。  bezierCurveTo(c1x, c1y, c2x, c2y, x, y):从上一点开始绘制一条曲线,到(x,y)为止,并且以(c1x,c1y)和(c2x,c2y)为控制点。
lineTo(x, y)从上一点开始绘制一条直线,到(x,y)为止。
moveTo(x, y)将绘图游标移动到(x,y),不画线。
quadraticCurveTo(cx, cy, x, y)从上一点开始绘制一条二次曲线,到(x,y)为止,并且以(cx,cy)作为控制点。
rect(x, y, width, height)从点(x,y)开始绘制一个矩形,宽度和高度分别由 width 和height 指定。这个方法绘制的是矩形路径,而不是 strokeRect()和 fillRect()所绘制的独立的形状。
closePath()绘制一条连接到路径起点的线条
fill()用 fillStyle 填充它,先指定fillStyle
stroke()对路径描边,先指定fillStyle
clip()在路径上创建一个剪切区域

15.2.4 绘制文本

fillText()和 strokeText(),都可以接收 4 个参数:要绘制的文本字符串、x 坐标、y 坐标和可选的最大像素宽度。这两个方法都以下列 3 个属性为基础。

方法说明
font表示文本样式、大小及字体,用 CSS 中指定字体的格式来指定,例如"10px Arial"。
textAlign表示文本对齐方式。可能的值有"start"、“end”、“left”、“right"和"center”。建议使用"start"和"end",不要使用"left"和"right",因为前两者的意思更稳妥,能同时适合从左到右和从右到左显示(阅读)的语言。
textBaseline表示文本的基线。可能的值有"top"、“hanging”、“middle”、“alphabetic”、“ideographic"和"bottom”。

15.2.5 变换

如下方法来修改变换矩阵。

方法说明
rotate(angle)围绕原点旋转图像 angle 弧度。
scale(scaleX, scaleY)缩放图像,在 x 方向乘以 scaleX,在 y 方向乘以 scaleY。scaleX和 scaleY 的默认值都是 1.0。
translate(x, y)将坐标原点移动到(x,y)。执行这个变换之后,坐标(0,0)会变成之前由(x,y)表示的点。
setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy)将变换矩阵重置为默认状态,然后再调用 transform()。
transform(m1_1, m1_2, m2_1, m2_2, dx, dy)直接修改变换矩阵,方式是乘以如下矩阵。

m1_1       m1_2       dx
m2_1       m2_2       dy
0               0             1

有两个方法可以跟踪上下文的状态变化:

方法说明
save()把设置保存到栈结构,save()方法保存的只是对绘图上下文的设置和变换,不会保存绘图上下文的内容。
restore()调用 restore()则可以一级一级返回。

15.2.10 使用图像数据

通过 getImageData()取得原始图像数据。这个方法接收4 个参数:要取得其数据的画面区域的 x 和 y 坐标以及该区域的像素宽度和高度。例如,要取得左上角坐标为(10,5)、大小为 50×50 像素的区域的图像数据,可以使用以下代码:

var imageData = context.getImageData(10, 5, 50, 50);

返回的对象是 ImageData 的实例。每个 ImageData 对象都有三个属性:width、height 和
data。其中 data 属性是一个数组,保存着图像中每一个像素的数据。在 data 数组中,每一个像素用4 个元素来保存,分别表示红、绿、蓝和透明度值。

17、错误处理与调试

17.2 错误处理

17.2.1 try-catch语句

try-catch 语句,作为 JavaScript 中处理异常的一种标准方式。

try{ 
 // 可能会导致错误的代码
} catch(error){ 
 // 在错误发生时怎么处理
}

如果 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。

在发生错误时,就可以像下面这样实事求是地显示浏览器给出的消息。

try { 
 window.someNonexistentFunction(); 
} catch (error){ 
 alert(error.message); 
}

只要代码中包含 finally 子句,则无论 try 或 catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。

function testFinally(){ 
 try { 
 return 2; 
 } catch (error){ 
 return 1; 
 } finally { 
 return 0; 
 } 
}
//返回 0

17.2.7 把错误记录到服务器

参考:21.5.1 图像Ping
首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:

function logError(sev, msg){ 
 var img = new Image(); 
 img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + 
 encodeURIComponent(msg); 
}

这个 logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了 Image 对象来发送请求,这样做非常灵活,主要表现如下几方面:

  • 所有浏览器都支持 Image 对象,包括那些不支持 XMLHttpRequest 对象的浏览器。
  • 可以避免跨域限制。通常都是一台服务器要负责处理多台服务器的错误,而这种情况下使用XMLHttpRequest 是不行的。
  • 在记录错误的过程中出问题的概率比较低。大多数 Ajax 通信都是由 JavaScript 库提供的包装函数来处理的,如果库代码本身有问题,而你还在依赖该库记录错误,可想而知,错误消息是不可能得到记录的。

只要是使用 try-catch 语句,就应该把相应错误记录到日志中。来看下面的例子:

for (var i=0, len=mods.length; i < len; i++){ 
 try { 
 mods[i].init(); 
 } catch (ex){ 
 logError("nonfatal", "Module init failed: " + ex.message); 
 } 
}

17.3 调试技术

17.3.3 抛出错误

这个简单的函数计算两个数的除法,但如果有一个参数不是数值,它会返回 NaN。类似这样简单的计算如果返回 NaN,就会在 Web 应用程序中导致问题。对此,可以在计算之前,先检测每个参数是否都是数值。例如:

function divide(num1, num2){ 
 if (typeof num1 != "number" || typeof num2 != "number"){ 
 throw new Error("divide(): Both arguments must be numbers."); 
 } 
 return num1 / num2; 
}

对于大型应用程序来说,自定义的错误通常都使用 assert()函数抛出。这个函数接受两个参数,一个是求值结果应该为 true 的条件,另一个是条件为 false 时要抛出的错误。以下就是一个非常基本的 assert()函数。

function assert(condition, message){ 
 if (!condition){ 
 throw new Error(message); 
 } 
}

可以用这个 assert()函数代替某些函数中需要调试的 if 语句,以便输出错误消息。下面是使用
这个函数的例子:

function divide(num1, num2){ 
 assert(typeof num1 == "number" && typeof num2 == "number", 
 "divide(): Both arguments must be numbers."); 
 return num1 / num2; 
}

20、JSON

20.1 语法

JSON 的语法可以表示以下三种类型的值:

  • 简单值:可以在 JSON 中表示字符串、数值、布尔值和 null。 但 JSON 不支持 JavaScript 中的特殊值 undefined。
  • 对象:对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。
  • 数组:数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型——简单值、对象或数组。

20.1.1 简单值

JavaScript 字符串与 JSON 字符串的最大区别在于,JSON 字符串必须使用双引号(单引号会导致语法错误)。

布尔值和 null 也是有效的 JSON 形式。

20.1.2 对象

JSON 中的对象与 JavaScript 字面量稍微有一些不同。

JavaScript 中创建对象字面量的标准方式,但 JSON 中的对象要求给属性加引号。(对象字面量也可以给属性加引号)

对象字面量:

var person = { 
 name: "Nicholas", 
 age: 29 
};

JSON 表示上述对象的方式如下:

{ 
 "name": "Nicholas", 
 "age": 29 
}

JSON 对象 JavaScript 的对象字面量区别:

  1. 没有声明变量(JSON 中没有变量的概念)
  2. 没有末尾的分号(因为这不是 JavaScript 语句,所以不需要分号)

手工编写 JSON 时,忘了给对象属性名加双引号或者把双引号写成单引号都是常见的错误。

20.1.3 数组

JSON 数组采用的就是 JavaScript 中的数组字面量形式。对象和数组通常是 JSON 数据结构的最外层形式。

[ 
	{ 
		 "title": "Professional JavaScript", 
		 "authors": [ 
		 "Nicholas C. Zakas" 
		 ], 
		 edition: 3, 
		 year: 2011 
	 }, 
	 { 
		 "title": "Professional JavaScript", 
		 "authors": [ 
		 "Nicholas C. Zakas" 
		 ], 
		 edition: 2, 
		 year: 2009 
	 }, 
	 { 
		 "title": "Professional Ajax", 
		 "authors": [ 
		 "Nicholas C. Zakas", 
		 "Jeremy McPeak", 
		 "Joe Fawcett" 
		 ], 
		 edition: 2, 
		 year: 2008 
	 }
]

20.2 解析与序列化

20.2.1 JSON对象

JSON 对象有两个方法:

方法说明
stringify()把JavaScript 对象序列化为 JSON 字符串
parse()和把 JSON 字符串解析为原生 JavaScript 值

在序列化 JavaScript 对象时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined 的任何属性也都会被跳过。结果中最终都是值为有效 JSON 数据类型的实例属性。

21、Ajax与Comet

21.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."); 
	 } 
}

创建 XHR 对象:

var xhr = createXHR();

21.1.1 XHR的用法

  1. 调用open(),它接受 3 个参数:要发送的请求的类型(“get”、"post"等)、请求的 URL 和表示是否异步发送请求的布尔值。需要说明两点:一是 URL相对于执行代码的当前页面(当然也可以使用绝对路径);二是调用 open()方法并不会真正发送请求,而只是启动一个请求以备发送。
xhr.open("get", "example.php", false);
  1. 要发送特定的请求,必须像下面这样调用 send()方法,send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null,因为这个参数对有些浏览器来说是必需的。调用 send()之后,请求就会被分派到服务器。
xhr.open("get", "example.txt", false); 
xhr.send(null);

由于这次请求是同步的,JavaScript 代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充 XHR 对象的属性,相关的属性简介如下。

属性说明
responseText作为响应主体被返回的文本。
responseXML如果响应的内容类型是"text/xml"或"application/xml",这个属性中将保存包含着响应数据的 XML DOM 文档。
status响应的 HTTP 状态。
statusTextHTTP 状态的说明。

多数情况下,我们还是要发送异步请求,才能让JavaScript 继续执行而不必等待响应。此时,可以检测 XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下:

取值说明
0未初始化。尚未调用 open()方法。
1启动。已经调用 open()方法,但尚未调用 send()方法。
2发送。已经调用 send()方法,但尚未接收到响应。
3接收。已经接收到部分响应数据。
4完成。已经接收到全部响应数据,而且已经可以在客户端使用了。

只要 readyState 属性的值由一个值变成另一个值,都会触发一次 readystatechange 事件。

var xhr = createXHR(); 
xhr.onreadystatechange = function(){ 
	 if (xhr.readyState == 4){ 
		 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ 
		 	alert(xhr.responseText); 
		 } else { 
		 	alert("Request was unsuccessful: " + xhr.status); 
		 } 
	 } 
}; 
xhr.open("get", "example.txt", true); 
xhr.send(null);

21.1.4 POST请求

使用 XHR 来模仿表单提交:首先将 Content-Type 头部信息设置为 application/x-www-form-urlencoded,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。

function submitData(){ 
	 var xhr = createXHR(); 
	 xhr.onreadystatechange = function(){ 
		 if (xhr.readyState == 4){ 
		 	if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ 
		 		alert(xhr.responseText); 
			 } else { 
			 	alert("Request was unsuccessful: " + xhr.status); 
			 } 
		 } 
	 }; 
	 xhr.open("post", "postexample.php", true);
	 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 
	 var form = document.getElementById("user-info"); 
	 xhr.send(serialize(form)); 
}

这个函数可以将 ID 为"user-info"的表单中的数据序列化之后发送给服务器。而下面的示例 PHP文件 postexample.php 就可以通过$_POST 取得提交的数据了:

<?php 
 header("Content-Type: text/plain"); 
 echo <<<EOF 
Name: {$_POST[‘user-name’]} 
Email: {$_POST[‘user-email’]} 
EOF; 
?>

21.4 跨源资源共享

CORS(Cross-Origin Resource Sharing,跨源资源共享)实现方法:
一个简单的使用 GET 或 POST 发送的请求,发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:

Origin: http://www.nczonline.net

如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源
信息(如果是公共资源,可以回发"*")。例如:

Access-Control-Allow-Origin: http://www.nczonline.net

如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息。

由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL。这样做能消除歧义,避免出现限制访问头部或本地 cookie 信息等问题。

21.5 其他跨域技术

21.5.1 图像Ping

第一种跨域请求技术是使用标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。

var img = new Image(); 
img.onload = img.onerror = function(){ 
 alert("Done!"); 
}; 
img.src = "http://www.example.com/test?name=Nicholas";

这里创建了一个 Image 的实例,然后将 onload 和 onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置 src 属性那一刻开始,而这个例子在请求中发送了一个 name 参数。

图像 Ping 有两个主要的缺点:

  1. 一是只能发送 GET 请求,
  2. 二是无法访问服务器的响应文本。

因此,图像 Ping 只能用于浏览器与服务器间的单向通信。

21.5.3 Comet

Ajax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技
术。

有两种实现 Comet 的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。

  1. 长轮询把短轮询颠倒了一下。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。
  2. 第二种流行的 Comet 实现是 HTTP 流,它在页面的整个生命周期内只使用一个 HTTP 连接。浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。

Firefox、Safari、Opera 和 Chrome 中,通过侦听 readystatechange 事件及检测 readyState
的值是否为 3,就可以利用 XHR 对象实现 HTTP 流。随着不断从服务器接收数据,eadyState 的值会周期性地变为 3。当 readyState 值变为 3 时,responseText 属性中就会保存接收到的所有数据。此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据。
使用 XHR 对象实现 HTTP 流的典型代码如下所示:

function createStreamingClient(url, progress, finished){ 
	 var xhr = new XMLHttpRequest(), received = 0; 
	 xhr.open("get", url, true); 
	 xhr.onreadystatechange = function(){ 
		 var result; 
		 if (xhr.readyState == 3){ 
			 //只取得最新数据并调整计数器
			 result = xhr.responseText.substring(received); 
			 received += result.length; 
			 //调用 progress 回调函数
			 progress(result); 
		 } else if (xhr.readyState == 4){ 
		 	finished(xhr.responseText); 
		 } 
	 }; 
	 xhr.send(null); 
	 return xhr; 
} 
var client = createStreamingClient("streaming.php"
	, function(data){alert("Received: " + data); }
	, function(data){alert("Done!"); } );

这个 createStreamingClient()函数接收三个参数:要连接的 URL、在接收到数据时调用的函
数以及关闭连接时调用的函数。

21.5.5 Web Sockets

在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 Web Socket 协议。

由于 Web Sockets 使用了自定义的协议,所以 URL 模式也略有不同。未加密的连接不再是 http://,而是 ws://;加密的连接也不是 https://,而是 wss://。

好处:能够在客户端和服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。
问题:存在一致性和安全性的问题。

21.7 安全

为确保通过 XHR 访问的 URL 安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。

  1. 要求以 SSL 连接来访问可以通过 XHR 请求的资源。
  2. 要求每一次请求都要附带经过相应算法计算得到的验证码。

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

  1. 要求发送 POST 而不是 GET 请求——很容易改变。
  2. 检查来源 URL 以确定是否可信——来源记录很容易伪造。
  3. 基于 cookie 信息进行验证——同样很容易伪造。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值