JS

本文章是本人在学习JS时所作记录,记录大部分来源于该书:

第3章 基本概念

3.4 数据类型

typeof

鉴于ECMAScript是松散类型的,因此需要有一种手段来检测给定变量的数据类型——typeof就是负责提供者方面信息的操作符。对一个值使用typeof操作符可能返回下列某个字符串:
- “undefined”——如果这个值未定义;
- “boolean”——如果这个值是布尔值;
- “string”——如果这个值是字符串;
- “number”——如果这个值是数值;
- “object”——如果这个值是对象或null;
- “function”——如果这个值是函数;
下面是几个使用typeof操作符的例子:

var message = "some string";
console.log(typeof message);   // string
console.log(typeof(message));  // string
console.log(typeof(95));       // number

Undefined类型

Undefined类型只有一个只,即特殊的undefined。在使用var声明变量但未对其加以初始化时,这个变量的值就是undefined,例如:

var message;
console.log(message == undefined); // true

Null类型

Null类型是第二个只有一个值的数据类型,这个特殊的值是null。从逻辑角度来看,null值表示一个空对象指针,而这也正是使用typeof操作符检测null值时返回”object”的原因,如下示例:

var car = null;
console.log(typeof(car)); // object

实际上,undefined值是派生自null值的,因此ECMA-262规定对它们的相等性测试要返回true:

console.log(null == undefiend); // true

Boolean类型

Boolean类型是ECMAScript中使用得最多的一种类型,该类型只有两个字面值:true和false。这两个值与数字值不是一回事,因此true不一定等于1,而false也不一定等于0。以下是为变量赋值Boolean类型值的例子:

var found = true;
var lost = false;

需要注意的是,Boolean类型的字面值true和false是区分大小写的。
要将一个值转换为其对应的Boolean值,可以调用转型函数Boolean(),如下所示

var message = "Hello world!";
console.log(Boolean(message)); // true;

下表给出了各种类型及其对应的转换规则:

数据类型转换为true的值转换为false的值
Booleantruefalse
String任何非空字符串“”(空字符串)
Number任何非零数字值(包括无穷大)0和NaN
Object任何对象null
Undefinedn/a或N/A(意思是不适用)undefined

Number类型

Number类型应该是ECMAScript中最令人关注的数据类型了,这种类型适用IEEE745格式来表示整数和浮点数值(浮点数值在某些语言中也被称为双精度值)。为支持各种数值类型,ECMA-262定义了不同的数值字面量格式。
最基本的数值字面量格式是十进制,十进制整数可以像下面这样直接在代码中输入:

var intNum = 55; // 整数 

除了以十进制表示外,整数还可以通过八进制(以8为基础)或十六进制(以16为基数)的字面值来表示。其中,八进制字面值的第一位必须是零(0),然后是八进制数字序列(0~7)。如果字面值中的数值超出了范围,那么前导零将会被忽略,后面的数值将被当作十进制数值解析。请看下面的例子:

var octalNum1 = 070;  // 八进制的56
var octalNum2 = 079;  // 无效的八进制数值——解析为79
var octalNum3 = 08;   //  无效的八进制数值——解析为8

十六进制字面值的前两位必须是0x,后跟任何十六进制数字(0~9及A~F)。其中字母A~F可以大写,也可以小写。如下面的例子所示:

var hexNum1 = 0xA;    // 十六进制的10
var hexNum2 = 0x1f;   // 十六进制的31

在进行算数计算时,所有以八进制和十六进制表示的数值最终都将被转换成十进制数值。

浮点数值

所谓浮点数值,就是该数值中必须包含一个小数点,并且小数点后面必须至少有一位数字。

var floatNum1 = 1.1;
var floatNum2 = 0.1;
var floatNum3 = .1;  // 有效,但不推荐

由于保存浮点数值需要的内存空间是保存整数值的两倍,因此ECMAScript会不失时机地将浮点数值转换为整数值。显然,小数点后面没有跟任何数字,那么这个数值就可以作为整数保存,如果浮点数值本身表示的就是一个整数(如1.0),那么该值也会被转换为整数:

var floatNum1 = 1.;    // 小数点后面没有数字——解析为1
var floatNum2 = 10.0;  // 整数——解析为10

对于那些极大或者极小的值,可以用e表示法(科学计数法)表示的浮点数值表示。用e表示法表示的数值等于e前面的数值乘以10的指数次幂:

var floatNum1 = 3.125e7; // 等于31250000
var floatNum2 = 3.125e-7 // 等于0.0000003125
数值范围

由于内存的限制,ECMAScript并不能保存世界上所有的数值。ECMAScript能够表示的最小数值保存在Number.MIN_VALUE中——在大多数浏览器中,这个值是5e-324;能够表示的最大数值保存在Number.MAX_VALUE中——在大多数浏览器中,这个值是1.7976931348623157e+308。如果某次计算的结果得到了一个超出JS数值范围的值,那么这个数值将被自动转换成特殊的Infinity值。具体来说,如果这个数值是负数,则会被转换成-Infinity(负无穷);如果这个数值是正数,则会被转换成Infinity(正无穷)。

NaN

NaN,即非数值是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况。
NaN有两个非同寻常的特点。首先,任何涉及NaN的操作(例如NaN/10)都会返回NaN,这个特点在多步计算中有可能导致问题。其次,NaN与任何值都不相等,包括NaN本身。例如下面的例子:

console.log(NaN == NaN); // false

针对NaN的这两个特点,ECMAScript定义了isNaN()函数,这个函数接收一个参数,该参数可以是任何类型,而函数会帮我们确定这个参数是否“不是数值”。isNaN()在接收到一个值之后,会尝试将这个值转换为数值,某些不是数值的值会被直接转换为数值,请看下面示例:

console.log(isNaN(NaN));    // true
console.log(isNaN(10));     // false(10是一个数值)
console.log(isNaN("10"));   // false(可以被转换成数值10)
console.log(isNaN("blue")); // true (不能转换成数值)
console.log(isNaN(true));   // false(可以被转换成数值1)

在基于对象调用isNaN()函数时,会首先调用对象的valueOf()方法,然后确定该方法返回的值是否可以转换为数值。如果不能,则基于这个返回的值再调用toString()方法,再测试返回值。

数值转换

有3个函数可以把非数值转换为数值:Number()、parseInt()和parseFloat()。第一个函数,即转型函数Number()可以用于任何数据类型,而另两个函数则专门用于把字符串转换成数值。这3个函数对于同样的输入会有返回不同的结果。

Number()函数

Number()函数的转换规则如下:

  • 如果是Boolean值,true和false将分别转换为1和0。
  • 如果是数字值,只是简单的传入和返回。
  • 如果是null值,返回0。
  • 如果是undefined,返回NaN。
  • 如果是字符串,遵循下列规则:
    • 如果字符串中只包含数字(包括前面带正号或负号的情况),则将其转换为十进制数值,即”1”会变成1,”123”会变成123,而”011”会变成11(注意:前导的零被忽略了);
    • 如果字符串中包含有效的浮点格式,如”1.1”,则将其转换为对应的浮点数值(同样,也会忽略前导的零);
    • 如果字符串中包含有效的十六进制格式,如”0xf”,则将其转换为相同大小的十进制整数值;
    • 如果字符串是空的(不包含任何字符),则将其转换为0;
    • 如果字符串中包含除上述格式之外的字符,则将其转换为NaN。
  • 如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值。如果转换的结果是NaN,则调用对象的toString()方法,然后再次依照前面的规则转换返回的字符串值。
var num1 = Number("Hellow world!"); // NaN
var num2 = Number("");              // 0
var num3 = Number("000011");        // 11
var num4 = Number(true);            // 1
parseInt()函数

由于Number()函数在转换字符串时比较复杂而且不够合理,因此在处理整数的时候,更常用的是parseInt()函数。
parseInt()函数的转换规则如下:
- 它会忽略字符串前面的空格,直至找到第一个非空格字符;
- 如果第一个字符不是数字字符或者负号,parseInt()就会返回NaN;也就是说,用parseInt()转换空字符串会返回NaN(Number()对空字符串返回0);
- 如果第一个字符是数字字符,parseInt()会继续解析第二个字符,直到解析完所有后续字符或遇到了一个非数字字符。parseInt()可以识别出各种整型格式(即前面讨论的十进制、八进制、十六进制):
- 如果字符串以”0x”开头且后跟数字字符,就会将其当作一个十六进制整数;
- 如果字符串以”0”开头且后跟数字字符,则会将其当作一个八进制数来解析。

var num1 = parseInt("1234blue");    // 1234
var num2 = parseInt("");            // NaN
var num3 = parseInt("0xA");         // 10(十六进制数)
var num4 = parseInt(22.5);          // 22
var num5 = parseInt("070");         // 56(八进制数)
var num6 = parseInt("70");          // 70(十进制数)
var num7 = parseInt("0xf");         // 15(十六进制数)

在使用parseInt()解析像八进制字面量的字符串时,ECMAScript3和5存在分歧。例如:

// ECMAScript 3认为是56(八进制),ECMAScript 5认为是70
var num = parseInt("070");

为了消除在使用parseInt()函数时可能导致的上述困惑,可以为这个函数提供第二个参数:转换时使用的基数(即多少进制)。例如:

var num = paserInt("0xAF", 16); // 175
var num1 = parseInt("AF", 16);  // 175
var num2 = parseInt("AF");      // NaN
var num3 = parseInt("10", 2);   // 2
var num4 = parseInt("10", 8);   // 8
var num5 = parseInt("10", 10);  // 10
var num6 = parseInt("10", 16);  // 16
parseFloat()函数

parseFloat()的转换规则如下:
- 与parseInt()函数类似,parseFloat()也从第一个字符(位置0)开始解析每个字符,而且也一直解析到字符串末尾,或者解析到遇见一个无效的浮点数字字符位置。也就是说,字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,因此它后面的字符串将被忽略。
- 与parseInt()函数第二个区别在于它始终都会忽略前导的零。它可以识别前面讨论过的所有浮点数值格式,也包括十进制整数格式。但十六进制格式的字符串则始终被转换成0。
- 需要注意一点:如果字符串包含的是一个可解析为整数的数(没有小数点,或者小数点后面都是零),parseFloat()会返回整数。

var num1 = parseFloat("1234blue"); // 1234(整数)
var num2 = parseFloat("0xA");      // 0
var num3 = parseFloat("22.5");     // 22.5
var num4 = parseFloat("22.34.5");  // 22.34
var num5 = parseFloat("0908.5");   // 908.5
var num6 = parseFloat("3.125e7");  // 31250000

String类型

字符字面量

String数据类型包含一些特殊的字符字面量,也叫转移序列,用于表示非打印字符,或者具有其他用途的字符:

字面量含义
\n换行
\t制表
\b退格
\r回车
\f进制
\斜杠
\’单引号(‘),在用单引号表示的字符串中使用。例如:’He said,\\’hey.\’ ‘
\”双引号(“),在用双引号表示的字符串中使用。例如:”He said,\\”hey.\” “
\xnn以十六进制代码nn表示的一个字符串(其中n为0~F)。例如,\x41表示”A”。 nn对应的是转换为十进制数之后在ASCLL码表所对应的字符。
\unnn以十六进制代码nnnn表示的一个Unicode字符(其中n为0~F)。例如\u03a3表示希腊字符∑

这些字符字面量可以出现在字符串中任意的位置,而且也将被作为一个字符来解析:

var text = "This is the letter sigma: \u03a3.";

这个例子中的变量text有28个字符,其中6个字符长的转移序列表示1个字符。
任何字符串的长度都可以通过访问其length属性取得,例如:

console.log(text.length); // 输出28

获取ASLL码外的字符长度,例如:

var text = "你好,世界";
var realLength = 0;
var len = text.length;
var charCode = -1;
for (var i = 0; i < len; i++) {
    charCode = text.charCodeAt(i);
    if (charCode >= 0 && charCode <= 128) realLength += 1;
    else realLength += 2;
}
console.log(realLength); // 输出10,汉字长度为2个字符,而逗号","是中文的逗号,也占两个字符
字符串的特点

ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量,例如:

var lang = "Java";
lang = lang + "Script"; 
console.log(lang); // JavaScript
转换为字符串

要把一个值转换为字符串有两种方式。第一种是使用几乎每个值都有的toString()方法(数值、布尔值、对象和字符串值都有这个方法,但null和undefined没有这个方法):

var age = 11;
var ageAsString = age.toString();     // 字符串"11"
var found = true;
var foundAsString = found.toString(); // 字符串"true"

多数情况下,调用toString()方法不必传递参数。但是,在调用数值的toString()方法时,可以传递一个参数:输出数值的基数。默认情况下,toString()方法以十进制格式返回数值的字符串表示。而通过传递基数,toString()可以输出以二进制、八进制、十六进制,乃至其他任意有效进制格式表示的字符串值:

var num = 10;
console.log(num.toString());    // 10
console.log(num.toString(2));   // 1010
console.log(num.toString(8));   // 12
console.log(num.toString(10));  // 10
console.log(num.toString(16));  // A

通过这个例子可以看出,通过指定基数,toString()方法会改变输出的值。而数值10根据基数的不同,可以在输出时被转换为不同的数值格式。注意,默认的输出值与指定基数10时的输出值相同。
在不知道要转换的值是不是null或undefined的情况下,还可以使用转型函数String(),这个函数能够将任何类型的值转换为字符串。String()函数遵循下列转换规则:
- 如果值有toString()方法,则调用该方法(没有参数)并返回相应的结果;
- 如果值是null,则返回”null”;
- 如果值数undefined,则返回”undefined”

Object类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象可以通过执行new操作符后跟要创建的对象类型的名称来创建。而创建Object类型的实例并为其添加属性和(或)方法,就可以创建自定义对象,如下所示:

var o = new Object();
var o = new Object; // 有效,但不推荐省略圆括号

Object的每个实例都具有下列属性和方法:

  • construcotr:保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是Object()。
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定。
  • isPrototypeOf(object):用于检查传入的对象是否是当前对象的原型。
  • propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举。与hasOwnProperty()方法一样,作为参数的属性名必须以字符串形式指定。
  • toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象的字符串、数值或布尔值表示。通常与toString()方法的返回值相同。

第4章 变量、作用域和内存问题

4.1 基本类型和引用类型的值

检测类型(typeof和instanceof)

typeof用于获取变量的数据类型,而instanceof可用来判断变量是否属于某个类型。

var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();

// typeof返回数据类型
typeof(s);  // string
typeof(b);  // number
typeof(i);  // boolean
typeof(u);  // undefined
typeof(n);  // object
typeof(o);  // object

// instanceof会返回boolean值
person instanceof Object    // 变量person是Object吗 
colors instanceof Array     // 变量colors是Array吗
pattern instanceof RegExp   // 变量ppattern是RegExp吗

第5章 引用类型

5.2 Array类型

所有对象都具有toLocaleString()、toString()和valueOf()方法。数组调用valueOf返回的还是数组本身,而调用toString()方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。

var colors = ["red", "blue", "green"];
alert(colors.toString()); // red,blue,green
alert(colors.valueOf());  // red,blue,green
alert(colors);            // red,blue,green

数组的toLocaleString()、toString()和valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用join()方法,则可以使用不同的分隔符来创建这个字符串。

var colors = ["red", "green", "blue"];
alert(colors.join(","));    // red,green,blue
alert(colors.join("||"));   // red||green||blue 
// 初始化方法
var array = new Array();
var array = ["a", "b", "c"];

栈的操作

/* push、pop */
var variable = "";
variable = array.push("d");         // 将d推入数组,会返回数组长度
console.log("variable = " + variable);  // 4
console.log(array.toString());          // a,b,c,d 

variable = array.pop();                 // 将最后一位推出数组,会返回最后一位
console.log("variable = " + variable);  // d
console.log(array.toString());          // a,b,c

队列方法

/* shift、unshift */
variable = array.shift();               // 将第一位推出数组,会返回第一位
console.log("variable = " + variable);  // a
console.log(array.toString());          // b,c

variable = array.unshift("a","d");      // 往第一位推入所传参数,可以传递多个参数,返回数组长度
console.log("variable = " + variable);  // 4
console.log(array.toString());          // a,d,b,c

重排序方法

/* reverse()、sort() */
variable = array.reverse();             // 将数组反转,返回数组对象(返回的是同一个数组)
console.log("variable = " + variable);  // c,b,d,a

variable = array.sort();                // 对数组进行排序(默认升序)
console.log("variable = " + variable);  // a,b,c,d

// 1降序 -1升序
function compare(value1, value2) {
    if (value1 < value2) {
        return 1;
    } else if (value1 > value2) {
        return -1;
    } else {
        return 0;
    }
}
variable = array.sort(compare);         // 根据compare的排序方式进行排序
console.log("variable = " + variable);  // d,c,b,a

操作方法

/* concat() 基于当前数组中的所有项创建一个新数组 */
var newArray = array.concat("e", "f");  
console.log("concat(\"e\", \"f\") = " + newArray); // d,c,b,a,e,f

/* slice() 基于当前数组中的一个或多个项创建一个新数组。slice()的方法可以接受一或两个参数,即要返回项的起始和结束位置。
 * 当只有一个参数的情况下,会返回从起始位置到数组末尾的所有项
 * 当有两个参数的情况下,会返回起始位置和结束位置之间的项--但不包括结束位置的项
 */
console.log("slice(1) = " + newArray.slice(1));     // c,b,a,e,f
console.log("slice(1,4) = " + newArray.slice(1,4)); // c,b,a

/*
 * splice() 主要用途是向数组的中部插入项,但使用这种方法的方式则有如下3种。
 * 删除:可以删除任意数量的项,只需指定2个参数:要删除的第一个项的位置和要删除的项数。例如splice(0, 2)会删除数组中的前两项。
 * 插入:可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除的项数)和要插入的项。例如splice(2, 0, "red", "green")会从当前数组的位置2开始插入字符串"red"和"green"。
 * 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice(2, 1, "red", "green")会删除当前数组位置2的项,然后再从位置2开始插入字符串"red"和"green"。
 */
var colors = ["red", "green", "blue"];
var removed = colors.splice(0, 1);      // 删除第一项
console.log("\ncolors = " + colors);    // green,blue
console.log("removed = " + removed);    // red

removed = colors.splice(1, 0, "yellow", "orange"); // 从位置1开始插入两项
console.log("\ncolors = " + colors);    // green,yellow,orange,blue
console.log("removed = " + removed);    // 返回的是一个空数组

removed = colors.splice(1, 1, "red", "purple"); // 插入两项,删除一项
console.log("\ncolors = " + colors);    // green,red,purple,blue
console.log("removed = " + removed);    // yellow

位置方法

/*
 * indexOf()和lastIndexOf()。这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。
 * 其中,indexOf()方法从数组的开头开始向后查找,lastIndexOf()方法则从数组的末尾开始向前查找。
 * 这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。
 */
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
console.log("\nnumbers.indexOf(4) = " + numbers.indexOf(4)); // 3
console.log("numbers.lastIndexOf(4) = " + numbers.lastIndexOf(4)); // 5

console.log("numbers.indexOf(4, 4) = " + numbers.indexOf(4, 4)); // 5
console.log("numbers.lastIndexOf(4, 4) = " + numbers.lastIndexOf(4, 4)); // 3

var person = {"name":"Nicholas"};
var people = [{"name":"Nicholas"}];
var morePeople = [people];
console.log(people.indexOf(person));     // -1
console.log(morePeople.indexOf(people)); // 0

迭代方法

/* every() 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true,否则不继续放往下走 */
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
var everyResult = numbers.every(function (item, index, array) {
    return (item > 2);
});
console.log("everyResult = " + everyResult); // everyResult = false

/* some() 对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true。*/
var someResult = numbers.some(function (item, index, array) {
    return (item > 2);
});
console.log("someResult = " + someResult); // someResult = true

/*
 * filter() 对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组
 */
var filterResult = numbers.filter(function (item, index, array) {
    return (item > 2);
});
console.log("filterResult = " + filterResult); // filterResult = 3,4,5,4,3

/*
 * forEach() 对数组中的每一项运行给定函数。这个方法没有返回值
 */
var eachResult = numbers.forEach(function (item, index, array) {

});

/*
 * map() 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
 */
var mapResult = numbers.map(function (item, index, array) {
    return item * 2;
});
console.log("mapResult = " + mapResult); // mapResult = 2,4,6,8,10,8,6,4,2

归并方法

/* 
 * reduce()和reduceRight() 都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。
 * 函数接收4个参数:前一个指、当前值、项的索引、数组对象。
 * 这个函数返回的任何值都会作为第一个参数自动传给下一项。
 * 第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。
 */

/* reduce()方法从数组的第一项开始,逐个遍历到最后。*/
var value = [1,2,3,4,5];
var sum = value.reduce(function (prev, cur, index, array){
    return prev + cur;
}, 5);
console.log(sum); // 20

/* reduceRight()方法从数组的最后一项开始,逐个遍历到第一项。*/
var sum = value.reduceRight(function (prev, cur, index, array){
    return prev + cur;
}, 5);
console.log(sum); // 20

5.3 Date类型

/*
 * 构造函数
 * 在调用Date构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。
 * 如果想根据特定的日期和时间创建对象,必须传入表示该日期的毫秒数(即从UTC时间1970年1月1日午夜 至 该日期止经过的毫秒数)
 */
var now = new Date();

/*
 * Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数
 * 日期格式:
 * "月/日/年",如6/13/2004
 * "英文月名 日, 年",如January 12,2004
 * "英文星期几 英文月名 日 年 时:分:秒 时区",如Tue May 25 2004 00:00:00 GMT-0700
 * ISO 8601扩展格式 YYYY-MM-DDTHH:mm:ss.sssZ(例如2004-05-25T00:00:00)
 */

// 为2004年5月25日创建一个日起对象。如果传入Date.parse()方法的字符串不能表示日期,那么它会返回NaN。
var someDate = new Date(Date.parse("May 25, 2004"));
console.log("someDate = " + someDate); // Tue May 25 2004 00:00:00 GMT+0800 (CST)
var someDate = new Date("May 25, 2004");
console.log("someDate = " + someDate); // Tue May 25 2004 00:00:00 GMT+0800 (CST)

/*
 * Data.UTC()同样也返回表示日期的毫秒数,但它与Date.parse()在构建值时使用不同的信息。
 * Data.UTC()的参数分别是年份、基于0的月份(一月是0)、月中的哪一天(1到31)、小时数(0到23)、分钟、秒以及毫秒数。
 * 前两个参数是必须的,如果没有提供月中的天数,则假设为1;如果其他参数省略,则统统假设为0.
 */
// GMT时间2000年1月1日午夜零时
var y2k = new Date(Date.UTC(2000, 0));
console.log(y2k); // Sat Jan 01 2000 08:00:00 GMT+0800 (CST)

// GMT时间2005年5月5日下午5:55:55
var y2k = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));
console.log(y2k); // Fri May 06 2005 01:55:55 GMT+0800 (CST)

// 本地时间2000年1月1日午夜零时
var y2k = new Date(2000, 0);
console.log(y2k); // Sat Jan 01 2000 00:00:00 GMT+0800 (CST)

// 本地时间2005年5月5日下午5:55:55
var y2k = new Date(2005, 4, 5, 17, 55, 55);
console.log(y2k); // Thu May 05 2005 17:55:55 GMT+0800 (CST)

// 取得当前时间
var now = Date.now();
console.log("now = " + now); // 返回毫秒数

// Date类型可以使用比较操作符
var date1 = new Date(2007, 0, 1);
var date2 = new Date(2007, 1, 1);
console.log("date1 > date2 = " + (date1 > date2)); // date1 > date2 = false

日期格式化方法

toDateString()        以特定于实现的格式显示星期几、月、日和年;
toTimeString()        以特定于实现的格式显示时、分、秒和时区;
toLocaleDateString()  以特定于地区的格式显示星期几、月、日和年;
toLocaleTimeString()  以特定于实现的格式显示时、分、秒;
toUTCString()         以特定于实现的格式完整的UTC日期。

/**
 * 时间转换
* @param time 时间戳
*/
Date.createFromStamp(time).format("yyyy/MM/dd")

日期/时间组件方法

getTime()                  // 返回表示日期的毫秒数;与valueOf()方法返回的值相同
setTime(毫秒)              // 以毫秒数设置日期,会改变整个日期 
getFullYear()              // 取得4位数的年份(如2007而非07)
setFullYear(年)            // 设置日期的年份。传入的年份值必须是4位数(如2007年而非07年)
getUTCFullYear()           // 返回UTC日期的4位数年份
setUTCFullYear(年)         // 设置UTC日期的年份。传入年份的值必须是4位数数字(如2007年而非07年)
getMonth()                 // 返回日期中的月份,其中0表示一月,11表示十二月
setMonth(月)               // 设置日期的月份。传入的月份必须大于0,超过11则增加年份
getUTCMonth()              // 返回UTC日期中的月份,其中0表示一月,11表示十二月
setUTCMonth(月)            // 设置UTC日期的月份。传入的月份必须大于0,超过11则增加年份
getDate()                  // 返回日期中月份中的天数(1到31)
setDate(日)                // 设置日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份。
getUTCDate()               // 返回UTC日期中月份中的天数(1到31)
setUTCDate(日)             // 设置日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份。
getDay()                   // 返回日期中星期的星期几(0表示星期日,6表示星期6)
getUTCDay()                // 返回UTC日期中星期的星期几(0表示星期日,6表示星期6)
getHours()                 // 返回日期中的小时数(0到23)
setHours(时)               // 设置日期中的小时数。传入的值超过了23则增加月份中的天数               
getUTCHours()              // 返回UTC日期中的小时数(0到23)
setUTCHours(时)            // 设置UTC日期中的小时数。传入的值超过了23则增加月份中的天数
getMinutes()               // 返回日期中的分钟数(0到59)               
setMinutes(分)             // 设置日期中的分钟数。传入的值超过59则增加小时数
getUTCMinutes()            // 返回设置UTC日期中的分钟数(0到59)
setUTCMinutes(分)          // 设置UTC日期中的分钟数。传入的值超过59则增加小时数
getSeconds()               // 返回日期中的秒数(0到59)
setSeconds(秒)             // 设置日期中的秒数。传入的值超过59则增加分钟数
getUTCSeconds()            // 返回UTC日期中的秒数(0到59)
setUTCSeconds(秒)          // 设置UTC日期中的秒数。传入的值超过59则增加分钟数         
getMilliseconds()          // 返回日期中的毫秒数
setMilliseconds(毫秒)      // 设置日期中的毫秒数   
getUTCMilliseconds()       // 返回UTC日期中的毫秒数
setUTCMilliseconds(毫秒)   // 设置UTC日期中的毫秒数
getTimezoneOffset()        // 返回本地时间与UTC时间相差的分钟数。

5.4 RegExp类型

RegExp用来支持正则表达式,使用下面类似perl的语法,就可以创建一个正则表达式:
var expression = / pattern / flags ;
其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标识(flags),用以标明正则表达式的行为。
正则表达式的匹配模式支持下列3个标志。
- g:表示全局模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
- i:表示不区分大小写模式,即在确定匹配项时忽略模式与字符串的大小写;
- m:表示多行模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

因此,一个正则表达式就是一个模式与上述3个标志的组合体,不同组合产生不同的结果,如下面的例子所示:

/* 
 * 匹配字符串中所有"at"的实例
 */
 var pattern1 = /at/g;

/* 
 * 匹配第一个"bat"或者"cat",不区分大小写
 */
 var pattern2 = /[bc]at/i;

/* 
 * 匹配所有以"at"结尾的3个字符的组合,不区分大小写
 */
 var pattern3 = /.at/gi;

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。元字符包括:( [ { \ ^ $ | ) ? * + . ] }
这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对他们进行转义。

字符描述
\将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\’ 匹配 “\” 而 “(” 则匹配 “(“。
^匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
|RegExpMultiline | 匹 配 输 入 字 符 串 的 结 束 位 置 。 如 果 设 置 了 R e g E x p 对 象 的 M u l t i l i n e 属 性 , 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
?当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。
.匹配除 “\n” 之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像”(.|\n)”的模式。
(pattern)匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 0 0 … 9 属性。要匹配圆括号字符,请使用 ‘(’ 或 ‘)’。
(?:pattern)匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。
(?=pattern)正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95|98|NT|2000)”能匹配”Windows2000”中的”Windows”,但不能匹配”Windows3.1”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95|98|NT|2000)”能匹配”Windows3.1”中的”Windows”,但不能匹配”Windows2000”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?<=pattern)反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”(?<=95|98|NT|2000)Windows”能匹配”2000Windows”中的”Windows”,但不能匹配”3.1Windows”中的”Windows”。
(?反向否定预查,与正向否定预查类似,只是方向相反。例如”(?
x|y匹配 x 或 y。例如,’z|food’ 能匹配 “z” 或 “food”。’(z|f)ood’ 则匹配 “zood” 或 “food”。
[xyz]字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
[^xyz]负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’、’l’、’i’、’n’。
[a-z]字符范围。匹配指定范围内的任意字符。例如,’[a-z]’ 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。
[^a-z]负值字符范围。匹配任何不在指定范围内的任意字符。例如,’[^a-z]’ 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。
\b匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
\B匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
\cx匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\d匹配一个数字字符。等价于 [0-9]。
\D匹配一个非数字字符。等价于 [^0-9]。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。
\w匹配字母、数字、下划线。等价于’[A-Za-z0-9_]’。
\W匹配非字母、数字、下划线。等价于 ‘[^A-Za-z0-9_]’。
\xn匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,’\x41’ 匹配 “A”。’\x041’ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。
\num匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,’(.)\1’ 匹配两个连续的相同字符。
\n标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
\nm标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
\nml如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
\un匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。
/* 
 * 匹配第一个"[bc]at",不区分大小写
 */
var pattern4 = /\[bc\]at/i;

/* 
 * 匹配所有".at",不区分大小写
 */
var pattern5 = /\.at/gi;

/* 
 * 与pattern5相同,只不过是使用构造函数创建的
 */
var pattern6 = new RegExp(".at", "gi");

由于传递给RegExp构造函数的模式参数数字符串,所以在某些情况下要对字符进行双重转义。所有元字符都必须双重转义,那些已经转义过的字符也是如此,例如\n。

模式字面量形式等价的字符串(RegExp)
/[bc]at/
bc b c
at”
/.at/“/\.at”
/name\/age/“name\/age”
/\d.\d{1,2}“\d.\d{1,2}”
/\w\hello\123“\w\\hello\\123”

RegExp实例属性

  • global:布尔值,表示是否设置了g标志。
  • ignoreCase:布尔值,表示是否设置了i。
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从0算起。
  • multiline:布尔值,表示是否设置了m标志。
  • source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。
    通过这些属性可以获知正则表达式的各方面信息,但却没有多大用处,因为这些信息全都包含在模式声明中。例如:
var pattern1 = /\[bc\]at/i;
console.log("pattern1.global = " + pattern1.global);            // false
console.log("pattern1.ignoreCase = " + pattern1.ignoreCase);    // true
console.log("pattern1.multiline = " + pattern1.multiline);      // false
console.log("pattern1.lastIndex = " + pattern1.lastIndex);      // 0
console.log("pattern1.source = " + pattern1.source + "\n\n");   // "\[bc\]at"

var pattern2 = new RegExp("\\[bc\\]at", "i");
console.log("pattern2.global = " + pattern2.global);            // false
console.log("pattern2.ignoreCase = " + pattern2.ignoreCase);    // true
console.log("pattern2.multiline = " + pattern2.multiline);      // false
console.log("pattern2.lastIndex = " + pattern2.lastIndex);      // 0
console.log("pattern2.source = " + pattern2.source);            // "\[bc\]at"

RegExp实例方法

RegExp对象的主要方法是exec(),该方法是专门为捕获组而设计的。exec()接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回null。
返回的数组虽然是Array的实例,但包含两个额外的属性:index和input。其中,index表示匹配项在字符串中的位置,而input表示应用正则表达式的字符串。
在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。

var text = "mom and dad and baby";
var pattern = /mom( and dad( and baby)?)?/gi;

var matches = pattern.exec(text);
console.log("\n" + matches.index); // 0
console.log(matches.input); // mom and dad and baby
console.log(matches[0]);    // mom and dad and baby
console.log(matches[1]);    // and dad an baby
console.log(matches[2]);    // and baby

这个例子中的模式包含两个捕获组。最内部的捕获组匹配"and baby",而包含它的捕获组匹配"and dad"或者"and dad and baby"。当把字符串传入exec()方法之后,发现了一个匹配项。因为整个字符串本身与模式匹配,所以返回的数组matches的index属性值为0。数组中的第一项是匹配的整个字符串,第二项包含与第一个捕获组匹配的内容,第三项包含与第二个捕获组匹配的内容。
对于exec()方法而言,即使在模式中设置了全局标志(g),它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用exec()将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用exec()则都会在字符串中继续查找新匹配项,如下面的例子所示。
var text = "cat, bat, sat, fat";

var pattern1 = /.at/;
var matches = pattern1.exec(text);
console.log("matches.index = " + matches.index); // 0
console.log("matches[0] = " + matches[0]); // cat
console.log("pattern1.lastIndex = " + pattern1.lastIndex); // 0

matches = pattern1.exec(text);
console.log("matches.index = " + matches.index); // 0
console.log("matches[0] = " + matches[0]); // cat
console.log("pattern1.lastIndex = " + pattern1.lastIndex); // 0

var pattern2 = /.at/g;
matches = pattern2.exec(text);
console.log("matches.index = " + matches.index); // 0
console.log("matches[0] = " + matches[0]); // cat
console.log("pattern2.lastIndex = " + pattern2.lastIndex); // 3

matches = pattern2.exec(text);
console.log("matches.index = " + matches.index); // 5
console.log("matches[0] = " + matches[0]); // bat
console.log("pattern2.lastIndex = " + pattern2.lastIndex); // 8

RegExp构造函数属性

RegExp构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问他们。换句话说,这些属性分别有一个长属性名和一个短属性名(opera是例外,它不支持短属性名)。

长属性名短属性名说明
input$_最近一次要匹配的字符串。Opera未实现此属性
lastMatch$&最近一次的匹配项。Opera未实现此属性
lastParen$+最近一次匹配的捕捉组。Opera未实现此属性
leftContext$`input字符串中lastMatch之前的文本
multiline$*布尔值,表示是否所有表达式都使用多行模式。 IE和Opera未实现此属性
rightContext$’input字符串中lastMatch之后的文本

使用这些属性可以从exec()和test()执行的操作中提取出更具体的信息。

var text = "this has benn a short summer";
var pattern = /(.)hort/g;

/*
 * 注意:Opera不支持input、lastMatch、lastParen和multiline属性
 * Internet Explorer不支持multiline属性
 */
if (pattern.test(text)) {
    console.log("RegExp.input = " + RegExp.input);               // this has benn a short summer
    console.log("RegExp.leftContext = " + RegExp.leftContext);   // this has benn a 
    console.log("RegExp.rightContext = " + RegExp.rightContext); // summer
    console.log("RegExp.lastMatch = " + RegExp.lastMatch);       // short
    console.log("RegExp.lastParen = " + RegExp.lastParen);       // s
    console.log("RegExp.multiline = " + RegExp.multiline);       // undefined
}

var text = "this has benn a short summer";
var pattern = /(.)hort/g;
if (pattern.test(text)) {
    console.log("\nRegExp.$_ = " + RegExp.$_);   // this has benn a short summer
    console.log("RegExp[$`] = " + RegExp["$`"]); // this has benn a 
    console.log("RegExp[$'] = " + RegExp["$'"]); // summer
    console.log("RegExp[$&] = " + RegExp["$&"]); // short
    console.log("RegExp[$+] = " + RegExp["$+"]); // s
    console.log("RegExp[$*] = " + RegExp["$*"]); // undefined
}

除了上述介绍的几个属性之外,还有多达9个用于存储捕获组的构造函数属性。访问这些属性的语法RegExp. 1RegExp. 1 、 R e g E x p . 2…RegExp.$9,分别存储第一、第二…第九个匹配的捕获组。在调用exec()或test()方法时,这些属性会被自动填充。然后,我们就可以像下面这样来使用它们。

var text = "this has been a short summer";
var pattern = /(..)or(.)/g;
if (pattern.test(text)) {
    console.log("\nRegExp.$1 = " + RegExp.$1); // sh
    console.log("RegExp.$2 = " + RegExp.$2);   // t
}

5.5 Function类型

每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函数声明语法定义的,如下面的例子所示:

function sum (number1, number2) {
    return num1 + number2;
}

这与下面使用函数表达式定义函数的方式几乎相差无几。

var sum = function(number1, number2) {
    return number1 + number2;
}

以上代码定义了变量sum并将其初始化为一个函数。在这段代码中,函数名后没有函数名,这是因为在使用函数表达式定义函数时,没有必要使用函数名——通过变量sum即可饮用函数。另外还要注意函数末尾有一个分好,就像声明其他变量时一样。
最后一种定义函数的方式是使用Function构造函数。Function构造函数可以接受任意数量的参数,但最后一个参数始终被看成是函数体,而前面的参数则枚举出了新函数的参数:

var sum = new Function("number1", "number2", "return number1 + number2"); // 不推荐
console.log(sum(1, 2)); // 3

从技术角度讲,这是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规ECMAScript代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。

一个函数可能会有多个名字:

function sum(num1, num2) {
    return num1 + num2;
}
console.log(sum(10, 10)); // 20

var anotherSum = sum;
console.log(anotherSum(10, 10)); // 20

sum = null;
console.log(anotherSum(10, 10)); // 20

以上代码首先定义了一个名为sum()的函数,然后又声明了变量anotherSum,并将其设置为与sum相等(将sum的值赋值给anotherSum)。注意,使用不带圆括号的函数名是访问其函数指针,而非调用函数。此时,anotherSumsum就都指向同一个函数,因此anotherSum()也可以被调用并返回结果。即时将sum设置为null,让它与函数失去关联,但仍可以调用anotherSum()

没有重载(深入理解)

将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念:

function addSomeNumber(num) {
    return num + 100;
}

function addSomeNumber(num) {
    return num + 200;
}

var result = addSomeNumber(100); // 300

显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。以上代码与下面的代码没有区别:

var addSomeNumber = function(num) {
    return num + 100;
}

addSomeNumber = function(num) {
    return num + 200;
}

var result = addSomeNumber(100); // 300

函数声明与函数表达式

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

alert(sum(10, 10)); // 20
function sum(num1, num2) {
    return num1 + num2;
}

如果像下面例子所示的,把上面的函数声明改为函数表达式,就会在执行期间导致错误:

alert(sum(10, 10)); // 20
var sum = function(num1, num2) {
    return num1 + num2;
}

以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中不会保存有对函数的引用;而且,由于第一行代码就会导致”unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。

也可以同时使用函数声明和函数表达式,例如var sum = function sum() {}。不过,这种语法在Safari中会导致错误

作为值的函数

因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function callSomeFunction(someFunction, someArgumemt) {
    return someFunction(someArgumemt);
}

这个函数接受两个参数:第一个参数是一个函数,第二个参数是要传递给该函数的一个值。然后就可以像下面一样传递参数了:

function add10(num) {
    return num + 10;
}

var result1 = callSomeFunction(add10, 10);
console.log(result1); // 20

function getGreeting(name) {
    return "Hellow, " + name;
}
var result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hellow, Nicholas"

当然也可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。例如,假设有一个对象组,我们想要根据某个对象属性对数组进行排序。而传递给数组sort()方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数,下面就是这个函数的定义:

function createComparisonFunction(propertyName) {
    return function(object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}

上面的函数可以像下面例子中这样使用。

var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nicholas

data.sort(createComparisonFunction("age"));
console.log(data[0].name); // Zachary

函数内部属性

在函数内部,有两个特殊对象:argumentsthis。其中,arguments它是一个类数组对象,包含着传入函数的所有参数。虽然arguments的主要用途是保存函数参数,但这个对象还有一个名词叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。请看下面这个非常经典的阶乘函数:

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}
console.log(factorial(3));

定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名factorial紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}
console.log(factorial(3));  // 6

在这个重写后的factorial()函数的函数体内,没有再引用函数名factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常递归调用。例如:

var trueFactorial = factorial;
factorial = function() {
    return 0;
}
console.log(factorial(5));      // 0
console.log(trueFactorial(5));  // 120

函数内部的另一个特殊对象是this,其行为与Java和C#中的this大致类似。换句话说,this引用的是函数执行的环境对象——或者也可以说是true值(当在网页的全局作用域中调用函数时,this对象引用的就是window):

window.color = "red";
var o = {color : "blue"};

function sayColor() {
    console.log(this.color);
}

sayColor();   // red
o.sayColor = sayColor;
o.sayColor(); // blue

函数名仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。

函数属性和方法

ECMASript中的函数都是对象,因此函数也有属性和方法。每个函数都包含两个属性:lengthprototype。其中length属性表示希望接收的命名参数的个数,如下面的例子:

function sayName(name) {
}

function sum(num1, num2) {
}

function sayHi() {
}

console.log(sayName.length);    // 1
console.log(sum.length);        // 2
console.log(sayHi.length);      // 0

在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了。对于ECMAScript中的引用类型而言,prototype是保存它们所有实例方法的真正所在。换句话说,诸如toString()valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的。在ECMAScript 5中,prototype属性是不可枚举的,因此for-in无法发现。
每个函数都包含两个非集成而来的方法:apply()call()。这两个方法的用途都是在特定的作用域调用函数,实际上等于设置函数体内this对象的值。首先,apply()方法接收两个参数:一个是在其运行函数的作用域,另外一个是参数数组。其中,第二个参数可以是Array的实例,也可以使arguments对象。例如:

function sum(num1, num2) {
    return num1 + num2;
}

function callSum1(num1, num2) {
    return sum.apply(this, arguments); // 传入arguments
}

function callSum2(num1, num2) {
    return sum.apply(this, [num1, num2]); // 传入数组
}

console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20

在上面这个例子中,callSum1()在执行sum()函数时传入了this作为this值(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而callSum2()同样也调用了sum()函数,但它传入的则是this和一个参数数组。这两个函数都会正常执行并返回正确的结果。

call()方法与apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是this值没有变化,变化的是其余参数都直接传递给函数:

function sum(num1, num2) {
    return num1 + num2;
}

function callSum(num1, num2) {
    return sum.call(this, num1, num2); 
}

console.log(callSum(10, 10)); // 20

事实上,传递参数并非apply()call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域:

window.color = "red";
var o = {color : "blue" };

function sayColor(){
    console.log(this.color);
}

sayColor();             // red
sayColor.call(this);    // red
sayColor.call(window);  // red
sayColor.call(o);       // blue

使用call()(或apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。

ECMAScript 5还定义了一个方法:bind()。这个方法会创建一个函数的实例,其this值会被绑定到传递给bind()函数的值。例如:

window.color = "red";
var o = {color : "blue" };

function sayColor(){
    console.log(this.color);
}

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

在这里,sayColor()调用bind()并传入对象o,创建了objectSayColor()函数。objectSayColor()函数的this值等于o,因此即使在全局作用域中调用这个函数,也会看到”blue”。

5.6 基本包装类型

为了便于操作基本类型,ECMAScript还提供了3个特殊的引用类型:BooleanNumberString。他们与上面的其他引用类型相似,但同时也具有各自的基本类型相应的特殊行为。

Object构造函数像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。把字符串传给Object构造函数,就会创建String的实例;而传入数值参数会得到Number的实例,传入布尔值参数就会得到Boolean的实例。

要注意的是,使用new调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。例如:

var value = "25";
var number = Number(value); // 转型函数
console.log(typeof number); // number

var obj = new Number(value); // 构造函数
console.log(typeof obj);     // Object

Boolean类型

Boolean类型是与布尔值对应的引用类型。要创建Boolean对象,可以像下面这样调用Boolean构造函数并传入truefalse值。

var booleanOjbect = new Boolean(true);

Boolean类型的实例重写了valueOf()方法和toString()方法
- valueOf() 返回基本类型值truefalse
- toString()返回字符串”true”和”false”。

可是,Boolean对象在ECMAScript中的用处不大,因为它已经造成人们的误解。其中最常见的问题就是在布尔表达式中使用Boolean对象,例如:

var falseObject = new Boolean(false);
var result = falseObject && true;
console.log(result); // true

var falseValue = false;
result = falseValue && true;
console.log(result); // false

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

console.log(typeof falseObject); // object
console.log(typeof falseValue);  // boolean

console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean);  // false

理解基本类型的布尔值与Boolean对象之间的区别非常重要,当然,建议永远不要使用Boolean对象。

Number类型

Number是与数字值对应的引用类型。要创建Number对象,对象可以调用Number构造函数时向其中传递相应的数值。

var numberObject = new Number(10);

Boolean类型一样,Number类型也重写了valueOf()toLocaleString()toString()方法。
- valueOf()返回对象表示的基本类型的数值。
- toString(hex)返回对象表示的数值的字符串形式。其中参数”hex”是要展示的进制数格式。
- toLocaleString()返回对象表示的数值的字符串形式(会根据本地文字返回不同字符)。

除了继承的方法外,Number类还提供了一些用于将数值格式化为字符串的方法。
- toFixed(),该方法会按照给定的小数位返回数值的字符串表示(如果数值本身包含的小数多余指定的小数位,那么将会舍入)
例如:

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

var num1 = 10.005;
console.log(num1.toFiexed(2)); // "10.01"

toFixed()方法可以表示带有0到20个小数位的数值。但这只是标准实现的范围,有些浏览器可能支持更多位数。

  • toExponential(),该方法返回以指数表示法(也称e表示法)表示的数值的字符串形式。与toFixed()一样,toExponential()也接收一个指定小数位数的参数。
var num = 1000;
console.log(num.toExponential(5)); // 1.00000e+3
  • toPrecision(),该方法可能会返回固定大小格式,也可能返回指数格式;具体规则看哪种格式最合适。这个方法接收一个参数,即表示数值的所有数字的位数(不包括指数部分)。
var num = 99;
console.log(num.toPrecision(1));    // "1e+2",因为一位数无法准确地表示99,因此进行了舍入。
console.log(num.toPrecision(2));    // "99"
console.log(num.toPrecision(3));    // "99.0"

实际上,toPrecision()会根据要处理的数值决定到底调用toFixed()还是调用toExponential()

toPrecision()方法可以表现1到21位小数。某些浏览器支持的范围更大,但这是典型实现的范围。

String类型

String类型是字符串的对象包装类型,可以像下面这样使用String构造函数来创建。

var stringObject = new String("hellow world");

String对象的方法也可以在所有基本的字符串值中访问到。其中,继承的valueOf()toLocaleString()toString()方法,都返回对象所表示的基本字符串值。

String类型的每一个实例都有一个length属性,表示字符串中包含多个字符。

var stringValue = "hello world";
console.log(stringValue.length); // "11"

String类型提供了很多方法,用于辅助完成对ECMAScript中字符串的解析和操作:

字符方法
  • charAt(),以单字符串的形式返回给定位置的那个字符。
var stringValue = "hello world";
console.log(stringValue.charAt(1)); // "e"
  • charCodeAt(),以单字符串的形式返回给定位置的那个字符的字符编码。
var stringValue = "hello world";
console.log(stringValue.charCodeAt(1)); // "101"

ECMAScript 5还定义了另一个访问个别字符的方法。在支持该方法的浏览器中,可以使用方括号加数字索引来访问字符串中的特定字符:

var stringValue = "hello world";
console.log(stringValue[1]); // "e"
字符串操作方法
  • concat(),用于将一或多个字符串拼接起来,返回拼接得到的新字符串(可接收任意多个参数),该方法不会对原字符串有任何影响。
var stringValue = "hello ";

var result = stringValue.concat("world");
console.log(stringValue); // "hello "
console.log(result);      // "hello world"

var result1 = stringValue.concat("world", "!"); 
console.log(result1);     // "hello world!"

虽然concat()是专门用来拼接字符串的方法,但实践中使用更多的还是加号操作符(+)。

  • slice()substring()substr()。字符截取,第一个参数指定字符串的开始位置,第二个参数(在指定的情况下,不指定则默认全部字符)表示字符串最后一个字符后面的位置(substr()表示返回的字符个数)。三个方法都不会修改字符串本身的值。
var stringValue = "hello world";
console.log(stringValue.slice(3));          // "lo world"
console.log(stringValue.substring(3));      // "lo world"
console.log(stringValue.substr(3));         // "lo world"
console.log(stringValue.slice(3, 7));       // "lo w"
console.log(stringValue.substring(3, 7));   // "lo w"
console.log(stringValue.substr(3, 7));      // "lo worl"
字符串位置方法
  • indexOf(),从字符串开头向后搜索子字符串。
  • lastIndexOf(),从字符串末尾向前搜索子字符串。
    这两个方法都用于搜索子字符串,如果未搜索到则返回-1。它们都可以接收可选的第二个参数,表示从字符串中哪个位置开始搜索。
var stringValue = "hello world";
console.log(stringValue.indexOf("o"));      // 4
console.log(stringValue.lastIndexOf("o"));  // 7

console.log(stringValue.indexOf("o", 6));       // 7,从w开始向后搜索。
console.log(stringValue.lastIndexOf("o", 6));   // 4,从w开始向前搜索。

在使用第二个参数的情况下,可以通过循环调用indexOf()lastIndexOf()来找到所有匹配的子字符串:

var stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
var positions = new Array();
var pos = stringValue.indexOf("e");

while(pos > -1) {
    positions.push(pos);
    pos = stringValue.indexOf("e", pos + 1);
}

console.log(positions); // "3,24,32,35,52"
trim()方法

ECMAScript5为所有字符串定义了trim()方法。这个方法会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。部分浏览器还支持非标准的trimLeft()trimRight()方法,分别删除字符串开头和末尾的空格。

var stringValue = "    hello world    ";
var trimmedStringValue = stringValue.trim();
var trimLeftStringValue = stringValue.trimLeft();
var trimRightStringValue = stringValue.trimRight();

console.log(stringValue);           // "    hello world   "   
console.log(trimmedStringValue);    // "hello world"
console.log(trimLeftStringValue);   // "hello world    "
console.log(trimRightStringValue);  // "    hello world"
字符串大小写转换方法

ECMAScript中涉及字符串大小写转换的方法有4个:toLowerCase()toLocaleLowerCase()toUpperCase()toLocaleUpperCase()。其中toLowerCase()toUpperCase()是两个经典的方法,借鉴自java.lang.String中的同名方法。而toLocaleLowerCase()toLocaleUpperCase()方法则是针对特定地区的实现。对有些地区来说,针对地区的方法与其通用方法得到的结果相同,但少数语言(如土耳其语)会为Unicode大小写转换应用特殊的规则,这时候就必须使用针对地区的方法来保证实现正确的转换:

var stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase());   // "HELLO WORLD"
console.log(stringValue.toUpperCase());         // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase());   // "hello world"
console.log(stringValue.toLowerCase());         // "hello world"

在不知道自己的代码将在哪种语言环境中运行的情况下,还是使用针对地区的方法更稳妥一些。

字符串的模式匹配方法

String类型定义了几个用于在字符串中匹配模式的方法。

  • match()
    在字符串上调用这个方法,本质上与调用RegExp的exec()方法相同。match()方法只接受一个参数,要么是一个正则表达式,要么是一个RegExp对象。
var text = "cat, bat, sat, fat";
var pattern = /.at/;

// 与pattern.exec(text)相同
var matches = text.match(pattern);
consolg.log(matches.index);         // 0
consolg.log(matches[0]);            // "cat"
consolg.log(pattern.lastIndex);     // 0

本例中的match()方法返回了一个数组;如果是调用RegExp对象的exec()方法并传递本例中的字符串作为参数,那么也会得到与此相同的数组:数组第一项是与整个模式匹配的字符串,之后的每一项(如果有)保存着与正则表达式中的捕获组匹配的字符串。

  • search()
    这个方法唯一参数与match()方法的参数相同,search()方法返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回-1。而且,search()方法是中是从字符串开头向后查找。
var text = "cat, bat, sat, fat";
var pos = text.search(/at/);
console.log(pos); // 1 表示"at"在字符串中第一次出现的位置。
  • replace()
    这个方法接收两个参数:第一个参数可以是一个RegExp对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以是字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,唯一的办法就是提供一个正则表达式,而且要指定全局(g)标识:
var text = "cat, bat, sat, fat";
var result = text.replace("at", "ond"); 
console.log(result);    // "cond, bat, sat, fat"

result = text.replace(/at/g, "ond");
console.log(result);    // "cond, bond, soud, fond"

在这个例子中,首先传入replace()方法的是字符串”at”和替换用的字符串”ond”。替换的结果是把”cat”变成了”cond”,但字符串中的其他字符并没有受到影响。然后,通过将第一个参数修改为带有全局标志的正则表达式,就将全部”at”都替换成了”ond”。

字符序列替换文本
$$$
$&匹配整个模式的子字符串。与RegExp.lastMatch的值相同
&’匹配的子字符串之前的子字符串。与RegExp.leftContext的值相同
&`匹配的子字符串之后的子字符串。与RegExp.rightContext的值相同
&n匹配第n个捕获组的子字符串,其中n等于0~9。例如,$1是匹配第一个捕获组的子字符串,$2是匹配第二个捕获组的子字符串,以此类推。如果正则表达式中没有定义捕捉组,则使用空字符串
&nn匹配第nn个捕获组的子字符串,其中nn等于01~99。例如,$01是匹配第一个捕获组的子字符串,$02是匹配第二个捕获组的子字符串,以此类推。如果正则表达式中没有定义捕捉组,则使用空字符串

通过这些特殊的字符序列,可以使用最近一次匹配结果中的内容,如下面的例子所示。

var text = "cat, bat, sat, fat";
result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)

replace()方法的第二个参数可以是一个函数。在只有一个匹配项(即与模式匹配的字符串)的情况下,会向这个函数传递3个参数:模式的匹配项、模式匹配项在字符串中的位置和原始字符串。在正则表达式中定义了多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,但最后两个参数仍然分别是模式的匹配项在字符串中的位置和原始字符串。这个函数应该返回一个字符串,表示应该被替代的匹配项。使用函数作为replace()方法的第二个参数可以实现更加精细的替换操作:

function htmlEscape (text) {
    return text.replace(/[<>"&]/g, function (match, pos, originalText) {
        switch (match) {
            case "<":
                return "&lt;";
            case ">":
                return "&gt;";
            case "&":
                return "&amp;";
            case "\":
                return "&quot;";
        }
    });
}
  • split()
    这个方法可以基于指定的分隔符将一个字符串分隔成多个子字符串,并将结果放在一个数组中。分隔符可以是字符串,也可以是一个RegExp对象(这个方法不会将字符串看成正则表达式)。split()方法可以接受可选的第二个参数,用于指定数组的大小,以便确保返回的数组不会超过既定大小。
var colorText = "red,blue,green,yellow";
var color1 = colorText.split(",");      // ["red", "blue", "green", "yellow"]
var color2 = colorText.split(",", 2);   // ["red", "blue"]
var color3 = colorText.split(/[^\,]+/); // ["", ",", ",", ",", ""]
console.log(color1);
console.log(color2);
console.log(color3);

最后一项通过使用正则表达式,可以取得包含逗号字符的数组。需要注意的是,在最后一次调用split()返回的数组中,第一项和最后一项是两个空字符串。之所以会这样,是因为通过正则表达式指定的分隔符出现在了字符串的开头(即子字符串”red”)和末尾(即子字符串”yellow”)。

localeCompare()方法

用于比较两个字符串,并返回下面值中的一个:

  • 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是-1,具体的值要视实现而定)
  • 如果字符串等于字符串参数,则返回0
  • 如果字符串在字母表中应该排在字符串参数之后,则返回一个整数(大多数情况下是1,具体同样要视实现而定)
var stringValue = "yellow";
console.log(stringValue.localeCompare("brick"));    // 1
console.log(stringValue.localeCompare("yellow"));   // 0
console.log(stringValue.localeCompare("zoo"));      // -1
fromCharCode()方法

这个方法的任务是接收一或多个字符编码,然后将它们转换成一个字符串。

console.log(String.fromCharCode(104, 101, 108, 108, 111)); // "hello"
HTML方法
方法输出结果
anchor(name)<a name= "name">string</a>
big()<big>string</big>
bold()<b>string</b>
fixed()<tt>string</tt>
fontcolor(color)<font color="color">string</font>
fontsize(size)<font size="size">string</font>
italics()<i>string</i>
link(url)<a href="url">string</a>
small()<small>string</small>
strike()<strike>string</strike>
sub()<sub>string</sub>
sup()<sup>string</sup>

5.7 单体内置对象

Global对象

Global(全局)对象可以说是ECMAScript中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。所有在全局作用域定义的属性和函数,都是Global对象的属性。

URI编码方法

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

  • encodeURI()
    主要用于对整个URI(例如,http://www.wrox.com/illegal value.htm)进行编码。

  • encodeURIComponent()
    主要用于对URI中的某一段(例如上面的illegal value.htm)进行编码。

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

// http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start
console.log(encodeURIComponet(uri));

使用encodeURI()编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了%20。而encodeURIComponent()方法则会使用对应的编码替换所有非字母数字字符。

一般来说,我们使用encodeURIComponet()方法的时候要比使用encodeURI()更多,因为在实践中更常见的是对查询字符串参数而不是对基础URI进行编码。

encodeURI()encodeURIComponent()方法对应的两个方法分别是decodeURI()decodeURIComponent()。其中,decodeURI()只能对使用encodeURI()替换的字符进行编码。例如,它可将%20替换成一个空格,但不会对%23做任何处理,因为%23表示井字号(#),而井字号不是使用encodeURI()替换的。同样的,decodeURIComponent()能够解码使用encodeURIComponent()编码的所有字符,即它可以解码任何特殊字符的编码。来看下面的例子:

var uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.html%23start";

// http%3A%2F%2Fwww.wrox.com%2Fillegal value.html%23start
console.log(decodeURI(uri));

// http://www.wrox.com/illegal value.html#start
console.log(decodeURIComponent(uri));
eval()方法

现在,介绍最后一个——大概是整个ECMAScript语言中最强大的一个方法:eval()。它像是一个完整的ECMAScript解析器,它只接受一个参数,即要执行的ECMAScript(或JavaScript字符串)。请看下面的例子:
eval("alert('hi')");等价于alert("hi")

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

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

可见,变量msg是在eval()调用的环境之外定义的,但其中调用的alert()仍然能够显示”hello world”。这是因为上面第二行代码最终被替换成了一行真正的代码。同样地,我们也可以在eval()调用中定义一个函数,然后再在调用的外部代码中引用这个函数:

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

显然,函数sayHi()是在eval()内部定义的。但由于对eval()的调用最终会被替换成定义函数的实际代码,因此可以在下一行调用sayHi()。对于变量也一样:

eval("var msg = 'hello world'; ");
console.log(msg); // hello world

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

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

Global对象的属性
属性说明属性说明
undefined特殊值undefinedDate构造函数
NaN特殊值NaNRegExp构造函数RegExp
Infinity特殊值InfinityError构造函数Error
Object构造函数ObjectEvalError构造函数EvalError
Array构造函数ArrayRangeError构造函数RangeError
Function构造函数FunctionReferenceError构造函数ReferenceError
Boolean构造函数BooleanSynTaxError构造函数SynTaxError
String构造函数StringTypeError构造函数TypeError
Number构造函数NumberURIError构造函数URIError
window 对象

ECMAScript虽然没有指出如何直接访问Global对象,但Web浏览器都是将这个全局对象作为window对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了window对象的属性。来看下面的例子:

var color = "red";

function sayColor() {
    alert(window.color);
}

window.sayColor(); // "red"

这里定义了一个名为color的全局变量和一个名为sayColor()的全局函数。在sayColor()内部,我们通过window.color来访问color变量,以说明全局变量是window对象的属性。然后,又使用window.sayColor()来直接通过window对象调用这个函数,结果显示在了警告框中。

JavaScript中的window对象除了扮演ECMAScript规定的Global对象的角色外,还承担了很多别的任务。

Math对象

ECMAScript还为保存数据公式和信息提供了一个公共位置,即Math对象。与我们在JavaScript直接编写的计算功能相比,Math对象提供的计算功能执行起来要快得多。Math对象中还提供了辅助完成这些计算的属性和方法。

Math对象的属性
属性说明
Math.E自然对数的底数,即常量e的值
Math.LN1010的自然对数
Math.LN22的自然对数
Math.LOG2E以2为底e的对数
Math.LOG10E以10为底e的对数
Math.PIπ的值
Math.SQRT1_21/2的平方根(即2的平方根的倒数)
Math.SQRT22的平方根
min()和max()方法

Math对象还包含许多方法,用以辅助完成简单和复杂的数学计算。
其中,min()max()方法用于确定一组数值中最小值和最大值。这两个方法都可以接收任意多个数值参数,如下面的例子所示:

var max = Math.max(3, 54, 32, 16);
console.log(max); // 54

var min = Math.min(3, 54, 32, 16);
console.log(min);   // 3
舍入方法

下面来介绍将小数值舍入为整数的几个方法:Math.ceil()Math.floor()Math.round()

  • Math.ceil() 执行向上舍入,即它总是将数值向上舍入为最接近的整数
  • Math.floor() 执行向下舍入,即它总是将数值向下舍入为最接近的整数
  • Math.round()执行标准的舍入,即它总是将数值四舍五入为最接近的整数(这也是我们在数学课上学到的舍入规则)

下面是使用示例:

console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26

console.log(Math.round(25.9)); // 26
console.log(Math.round(25.5)); // 26
console.log(Math.round(25.1)); // 25

console.log(Math.floor(25.9)); // 25
console.log(Math.floor(25.5)); // 25
console.log(Math.floor(25.1)); // 25
random()方法

Math.random()方法返回大于等于0小于1的一个随机数。对于某些站点来说,这个方法非常实用,因为可以利用它来随机显示一些名人名言和新闻事件。套用下面的公式,就可以利用Math.random()从某个整数范围内随机选择一个值。

值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)

例如想选择一个1到10的值,可以像下面这样编写代码:

var num = Math.floor(Math.random() * 10 + 1);

如果想选择一个2到10的值,可以像下面这样编写代码:

var num = Math.floor(Math.random() * 9 + 2);
其他方法
方法说明
Math.abs(num)返回num的绝对值
Math.exp(num)返回Math.E的num次幂
Math.log(num)返回num的自对数
Math.pow(num, power)返回num的power次幂
Math.sqrt(num)返回num的平方根
Math.acos(x)返回x的反余弦值
Math.asin(x)返回x的反正弦值
Math.atan(x)返回x的反正切值
Math.atan2(y, x)返回y/x的反正切值
Math.cos(x)返回x的余弦值
Math.sin(x)返回x的正弦值
Math.tan(x)返回x的正切值

第6章 面向对象的程序设计

6.1 理解对象

6.1.1 属性类型

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

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对于直接在对象上定义的属性,这个特性的默认值为true。。
  • [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true
  • [[Writable]]:表示能否修改属性的值。对于直接在对象上定义的属性,这个特性的默认值为true
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined
var person = {
    name: "Nicholas"
};

这里创建了一个名为name的属性,为它指定的值是”Nicholas”。也就是说,[[Value]]特性将被设置为”Nicholas”,而对这个值的任何修改都将反映在这个位置。
要修改属性默认的特性,必须使用 ECMAScript5 的 Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurableenumerablewritablevalue。设置其中的一或多个值,可以修改 对应的特性值。例如:

var person = {};

Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas"
});

alert(person.name); //"Nicholas" 
person.name = "Greg"; 
alert(person.name); //"Nicholas"

这个例子创建了一个名为 name 的属性,它的值”Nicholas”是只读的。这个属性的值是不可修改的,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导致抛出错误。类似的规则也适用于不可配置的属性。例如:

var person = {};

Object.defineProperty(person, "name", {
    configurable: false,
    value: "Nicholas"
});

alert(person.name); //"Nicholas"
delete person.name;
alert(person.name); //"Nicholas"

configurable设置为false,表示不能从对象中删除属性。如果对这个属性调用delete,则在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误:

var person = {};

Object.defineProperty(person, "name", {
    configurable: false,
    value: "Nicholas"
});

//抛出错误
Object.defineProperty(person, "name", {
    configurable: true,
    value: "Nicholas"
});

在调用Object.defineProperty()方法时,如果不指定,configurableenumerablewritable特性的默认值都是false。多数情况下,可能都没有必要利用Object.defineProperty()方法提供的这些高级功能。不过,理解这些概念对理解JavaScript对象却非常有用。

2.访问器属性
访问器属性不包含数据值;它们包含一对儿gettersetter函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用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

不一定非要同时指定gettersetter。只指定getter意味着属性是不能写,尝试写入属性会被忽略。 在严格模式下,尝试写入只指定了getter函数的属性会抛出错误。类似地,只指定setter函数的属性也不能读,否则在非严格模式下会返回undefined,而在严格模式下会抛出错误。

6.1.2 定义多个属性

由于为对象定义多个属性的可能性很大,ECMAScript 5又定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例如:

var book = {};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },

    edition: {
        value: 1
    },

    year: {
        get: function(){
            return this._year;
        }

        set: function(newValue) {
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    },
});

以上代码在book对象上定义了两个数据属性(_year 和 edition)和一个访问属性(year)。最终的对象与上一节中定义的对象相同。唯一的区别是这里的属性都是在同一个时间创建的。

6.1.3 读取属性的特性

var book = {};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },

    edition: {
        value: 1
    },

    year: {
        get: function(){
            return this._year;
        }

        set: function(newValue) {
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    },
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value); // 2004
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "function"

对于数据属性_year,value等于最初的值,configurable是false,而get等于undefined。
对于访问器属性year,value等于undefined,enumerable是false,而get是一个指向getter函数的指针。

创建对象

6.2.1 工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。例子:

function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。随着JavaScript的发展,又一个新模式出现了。

6.2.2 构造函数模式

前几章介绍过,ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型和属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    jobthis.sayName = function() {
        console.log(this.name);
    };
}

var person1 = Person("Nicholas", 29, "Software Engineer");
var person2 = Person("Greg", 27, "Doctor");

在这个例子中,Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

此外,还应该注意到函数名Person使用的是大写字母P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此this就只指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象

在前面的例子的最后,person1person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person,如下所示。

console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true

对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象的类型,还是instanceof操作符要可靠一些。

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

创建自定义的构造函数意味着将来可以将它的实际标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

1. 将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。

// 当做构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"

// 作为普通函数
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); // "Greg"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // Kristen

2.构造函数的问题
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建以便。在前面的粒子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Functon的实例。不要忘了——ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}

从这个角度上看构造函数,更容易明白每个Person实例都包含一个不同的Function实例(以显示name属性)的本质。说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,以下代码可以证明这一点。

console.log(person1.sayName == person2.sayName); // false

然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用再执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通常把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName() {
    console.log(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

在这个例子中,我们把sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName()属性设置成等于全局的sayName()函数。这样一来,由于sayName包含的是同一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

6.2.3 原型模式

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

function Person() {

}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var person1 = new Person();
person1.sayName(); // "Nicholas"

var person2 = new Person();
person2.sayName(); //"Nicholas"
console.log(person1.sayName == person2.sayName);  //true

在此,我们将sayName()方法和所有属性直接添加到了Personprototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。要理解原型模式的工作原理,必须先理解ECMAScript中原型对象的性质。

1.理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor指向Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome 在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

以前面使用Person构造函数和Person.prototype创建实例的代码为例,图 6-1展示了各个对象之间的关系。
图 6-1

图 6-1 展示了Person构造函数Person的原型属性以及Person现有的两个实例之间的关系。在此,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例——person1person2都包含一个内部属性,该属性仅仅指向了Person.prototype;换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。

虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true,如下所示:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Person.prototype.isPrototypeOf(person2)); //true

这里,我们用原型对象的isPrototypeOf()方法测试了person1person2。因为它们内部都有一个指向Person.prototype的指针,因此都返回了true

ECMAScript 5 增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。例如:

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true 
console.log(Object.getPrototypeOf(person1).name); //"Nicholas"

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例person1sayName属性吗?”答:“没有。”然后,它继续搜索,再问:“person1的原型有sayName属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

前面提到过,原型最初只包含 constructor 属性,而该属性也是共享的,因此可以通过对象实例访问。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。来看下面的例子。

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();
var person2 = new Person();

person1.name = "Greg";
alert(person1.name);    // "Greg"——来自实例  
alert(person2.name);    // "Nicholas"——来自原型

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。

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(); 
var person2 = new Person();

person1.name = "Greg";
alert(person1.name);    //"Greg"——来自实例  
alert(person2.name);    //"Nicholas"——来自原型

delete(person1.name);
alert(person1.name);    //"Nicholas"——来自原型

使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true。来看下面这个例子。

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(); 
var person2 = new Person();

alert(person1.hasOwnProperty("name")); //false

person1.name = "Greg";
alert(person1.name);    //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true

alert(person2.name);    //"Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false

delete(person1.name);
alert(person1.name);    //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false

图 6-2 展示了上面例子在不同情况下的实现与原型的关系(为了简单起见,图中省略了与Person构造函数的关系)。
图 6-2

ECMAScript 5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法。

2.原型与 in 操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。看一看下面的例子。

function Person(){
}

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person(); 
var person2 = new Person();

console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); // true

person1.name = "Greg"; 
console.log(person1.name);  // "Greg" ——来自实例
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true

console.log(person2.name);  // "Nicholas" ——来自原型 
console.log(person2.hasOwnProperty("name")); //false 
console.log("name" in person2); // true

delete person1.name;
console.log(person1.name);  // "Nicholas" ——来自原型 
console.log(person1.hasOwnProperty("name")); //false 
console.log("name" in person1); // true

在以上代码执行的整个过程中,name属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用"name" in person1始终都返回true,无论该属性存在于实例中还是存在于原型中。同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。

function hasPrototypeProperty(object, name){
    // 属性是否在原型中
    return !object.hasOwnProperty(name) && (name in object);
}

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。
IE早期版本的实现中存在一个 bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。例如:

var o = {
    toString : function(){ 
        return "My Object";
    }
};

for (var prop in o){
    if (prop == "toString"){
        alert("Found toString");    //在 IE 中不会显示
    }
}

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如:

function Person(){

}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){
    alert(this.name);
};

var keys = Object.keys(Person.prototype); 
alert(keys);    //"name,age,job,sayName"

var p1 = new Person(); 
p1.name = "Rob"; 
p1.age = 31;

var p1keys = Object.keys(p1); 
alert(p1keys);  //"name,age"

这里,变量keys中将保存一个数组,数组中是字符串”name”、”age”、”job”和”sayName”。这个顺序也是它们在for-in循环中出现的顺序。如果是通过Person的实例调用,则Object.keys()返回的数组只包含”name”和”age”这两个实例属性。如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person.prototype); 
alert(keys);    // "constructor,name,age,job,sayName"

注意结果中包含了不可枚举的constructor属性。Object.keys()Object.getOwnPropertyNames()方法都可以用来替代for-in循环。支持这两个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。

3.更简单的原型语法
读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示。

function Person(){

}

Person.prototype = { 
    name : "Nicholas",
    age  : 29,
    job  : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

在上面的代码中,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor属性不再指向Person了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person 函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了,如下所示。

var friend = new Person();

alert(friend instanceof Object);        //true 
alert(friend instanceof Person);        //true 
alert(friend.constructor == Person);    //false 
alert(friend.constructor == Object);    //true

在此,用instanceof操作符测试ObjectPerson仍然返回true,但constructor属性则等于Object而不等于Person了。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

function Person(){

}

Person.prototype = { 
    constructor : Person,
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

以上代码特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值。
注意,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容ECMAScript5的JavaScript引擎,可以试一试Object.defineProperty()

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job : "Software Engineer", 
    sayName : function () {
        alert(this.name);
    }
};

//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(
    Person.prototype, 
    "constructor", 
    { 
        enumerable: false,
        value: Person
    }
);

4.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。

var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};

friend.sayHi(); //"hi"(没有问题!)

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子。

function Person(){
}

var friend = new Person();

Person.prototype = {
    constructor: Person, 
    name : "Nicholas", 
    age : 29,
    job : "Software Engineer", 
    sayName : function () {
        alert(this.name);
    }
};

friend.sayName();   //error

图6-3 展示了以上过程的内幕。
图6-3

5.原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(ObjectArrayString,等等)都在其构造函数的原型上定义了方法。例如,在Array.prototype 中可以找到sort()方法,而在String.prototype中可以找到substring()方法,如下所示。

console.log(typeof Array.prototype.sort);       // "function"
console.log(typeof String.prototype.substring); // "function"

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。下面的代码就给基本包装类型String添加了一个名为startsWith()的方法。

String.prototype.startsWith = function (text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));   // true

这里新定义的startsWith()方法会在传入的文本位于一个字符串开始时返回true。既然方法被添加给了String.prototype,那么当前环境中的所有字符串就都可以调用它。由于msg是字符串,而且后台会调用String基本包装函数创建这个字符串,因此通过msg就可以调用startsWith(方法。

尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支 持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。

6.原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。

function Person(){

}

Person.prototype = { constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

假如我们的初衷就是像这样在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

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");

console.log(person1.friends);   //"Shelby,Count,Van"
console.log(person2.friends);   //"Shelby,Count"

console.log(person1.friends === person2.friends);   //false
console.log(person1.sayName === person2.sayName);   //true

6.2.5 动态原型模式

有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子。

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();

注意构造函数代码中加粗的部分。这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中,if语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。

使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

6.2.6 寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。下面是一个例子。

function Person(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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

在这个例子中,Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray(){

    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); // "red|blue|green"

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

6.2.7 稳妥构造函数模式

道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript 中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用thisnew),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下。

function Person(name, age, job){

    //创建要返回的对象
    var o = new Object();



    // 可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function(){
        alert(name);
    };

    //返回对象
    return o;
}

注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。

6.3 继承

继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

6.3.1 原型链

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式,其代码大致如下。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了 SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());    //true

以上代码定义了两个类型:SuperTypeSubType。每个类型分别有一个属性和一个方法。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确立了继承关系之后,我们给SubType.prototype添加了一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。这个例子中的实例以及构造函数和原型之间的关系如图 6-4 所示。

6-4

1.别忘记默认的原型
事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()valueOf()等默认方法的根本原因。所以,我们说上面例子展示的原型链中还应该包括另外一个继承层次。图 6-5 为我们展示了该例子中完整的原型链。

6-5

2.确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。
第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。以下几行代码就说明了这一点。

alert(instance instanceof Object);      //true
alert(instance instanceof SuperType);   //true
alert(instance instanceof SubType);     //true

第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true,如下所示。

alert(Object.prototype.isPrototypeOf(instance));    //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance));   //true

3.谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。来看下面的例子。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function (){
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue());    //false

还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这 样做就会重写原型链,如下面的例子所示。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了 SuperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },

    someOtherMethod : function (){
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue());    //error!

4.原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。下列代码可以用来说明这个问题。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){

}

//继承了 SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green,black"

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上, 应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。

6.3.2 借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示:

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //继承了 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green"

通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。

1.传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。看下面这个例子。

function SuperType(name){
    this.name = name;
}

function SubType(){
    //继承了 SuperType,同时还传递了参数
    SuperType.call(this, "Nicholas");

    //实例属性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name);   //"Nicholas";
alert(instance.age);    //29

2.借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

6.3.3 组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。下面来看一个例子。

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

在这个例子中,SuperType构造函数定义了两个属性:namecolorsSuperType的原型定义了一个方法sayName()SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己属性——包括 colors属性,又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

6.3.4 原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,有了如下函数。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。来看下面的例子。

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(anotherPerson.name);    // "Greg"
console.log(yetAnotherPerson.name); // "Linda"
console.log(person.friends);        // "Shelby,Court,Van,Rob,Barbie"

这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是person对象,于是我们把它传入到object()函数中,然后该函数就会返回一个新对象。这个新对象将person作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。
ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()object()方法的行为相同。

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); // "Shelby,Court,Van,Rob,Barbie"

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    },
    age: {
        value: 19
    }
});

console.log(anotherPerson.name); // "Greg"
console.log(anotherPerson.age);  // 19

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

6.3.5 寄生式继承

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄生式继承模式。

function createAnother(original){
    var clone = object(original);   // 通过调用函数创建一个新对象
    clone.sayHi = function(){       // 以某种方式来增强这个对象
        alert("hi");
    };
    return clone;   // 返回这个对象
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

6.3.6 寄生组合式继承

前面说过,组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。再来看一看下面组合继承的例子。

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); // 第二次调用 SuperType()
    this.age = age;
}

SubType.prototype = new SuperType();    // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:namecolors;它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性namecolors。于是,这两个属性就屏蔽了原型中的两个同名属性。图 6-6 展示了上述过程。

6-6

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);//创建对象
    prototype.constructor = subType;            //增强对象
    subType.prototype = prototype;              //指定对象
}

这个示例中的inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了,例如:

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;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
    alert(this.age);
};

这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

YUI 的 YAHOO.lang.extend()方法采用了寄生组合继承,从而让这种模式首次出现在了一个应用非常广泛的 JavaScript 库中。要了解有关 YUI 的更多信息,请访问http://developer. yahoo.com/yui/。

第7章 函数表达式

函数表达式是JavaScript中的一个既强大又容易令人困惑的特性。第5章曾介绍过,定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。

  • 函数声明:
function functionName(arg0, arg1, arg2) {
    //函数体
}

首先是function关键字,然后是函数的名字,这就是指定函数名的方式。Firefox、Safari、Chrome和Opera都给函数定义了一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于跟在function关键字后面的标识符。

//只在 Firefox、Safari、Chrome 和 Opera 有效
alert(functionName.name); // "functionName"

关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。

sayHi();
function sayHi(){
    alert("Hi!");
}
  • 函数表达式:
var functionName = function(arg0, arg1, arg2){
    //函数体
};

这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName。这种情况下创建的函数叫做匿名函数(anonymous function),因为function关键字后面没有标识符。(匿名函数有时候也叫拉姆达函数。)匿名函数的name属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误。

sayHi();    //错误:函数还不存在
var sayHi = function(){
    alert("Hi!");
};

理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果可能会让人意想不到。

// 不要这样做!
if(condition){
    function sayHi(){
        alert("Hi!");
    }
} else {
    function sayHi(){
        alert("Yo!");
    }
}

表面上看,以上代码表示在conditiontrue时,使用一个sayHi()的定义;否则,就使用另一个定义。实际上,这在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理的状态。但问题是浏览器尝试修正错误的做法并不一致。大多数浏览器会返回第二个声明,忽略condition;Firefox会在conditiontrue时返回第一个声明。因此这种使用方式很危险,不应该出现在你的代码中。不过,如果是使用函数表达式,那就没有什么问题了。

// 可以这样做
var sayHi;

if(condition){
    sayHi = function(){
        alert("Hi!");
    };
} else {
    sayHi = function(){
        alert("Yo!");
    };
}

能够创建函数再赋值给变量,也就能够把函数作为其他函数的值返回。还记得第5章中的那个createComparisonFunction()函数吗:

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}

createComparisonFunction()就返回了一个匿名函数。返回的函数可能会被赋值给一个变量, 或者以其他方式被调用;不过,在createComparisonFunction()函数内部,它是匿名的。在把函数当成值来使用的情况下,都可以使用匿名函数。不过,这并不是匿名函数唯一的用途。

7.1 递归

递归函数是在一个函数通过名字调用自身的情况下构成的,如下所示。

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

这是一个经典的递归阶乘函数。虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错。

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错!

以上代码先把factorial()函数保存在变量anotherFactorial中,然后将factorial变量设置为null,结果指向原始函数的引用只剩下一个。但在接下来调用anotherFactorial()时,由于必须执行factorial(),而factorial已经不再是函数,所以就会导致错误。在这种情况下,使用arguments.callee可以解决这个问题。

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * arguments.callee(num-1);
    }
}

在严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。这种方式在严格模式和非严格模式下都行得通。例如:

var factorial = (function f(num){
    if (num <= 1){
        return 1;
    } else {
        return num * f(num-1);
    }
});

7.2 闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,仍以前面的createComparisonFunction()函数为例,注意加粗的代码。

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName]; // 加粗部分
        var value2 = object2[propertyName]; // 加粗部分

        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}

在这个例子中,突出的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。之所以还能够访问这个变量, 是因为内部函数的作用域链中包含createComparisonFunction()的作用域。要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。来看下面的例子。

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含argumentsvalue1value2的活动对象。全局执行环境的变量对象(包含resultcompare)在compare()执行环境的作用域链中则处于第二位。图7-1展示了包含上述关系的compare()函数执行时的作用域链。

7-1

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。图7-2展示了当下列代码执行时,包含函数与内部匿名函数的作用域链。

var compare = createComparisonFunction("name");
var result = compare(
    { name: "Nicholas" },
    { name: "Greg" }
);

在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:

// 创建函数
var compareNames = createComparisonFunction("name");

// 调用函数
var result = compareNames(
    { name: "Nicholas" },
    { name: "Greg" }
);

// 解除对匿名函数的引用(以便释放内存)
compareNames = null;

首先,创建的比较函数被保存在变量compareNames中。而通过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。图7-2展示了调用compareNames()的过程中产生的作用域链之间的关系。

7-2

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像V8等优化后的JavaScript引擎会尝试回收被闭包占用的内存,但请大家还是要慎重使用闭包。

7.2.1 闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子可以清晰地说明这个问题。

var array = createFunctions();
console.log(array[0]()); // 10

function createFunctions(){
    var result = new Array();

    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }

    return result;
}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中都保存着createFunctions()函数的活动对象, 所以它们引用的都是同一个变量i 。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。

function createFunctions(){
    var result = new Array();

    for (var i=0; i < 10; i++){
        result[i] = function(num){
            return function(){
                return num;
            };
        }(i);
    }

    return result;
}

7.2.2 关于this对象

在闭包中使用this对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于 window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向 window(当然,在通过call()apply()改变函数执行环境的情况下,this就会指向其他对象。)。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。下面来看一个例子。

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name; // "My Object"
            return this.name; // "The Window"
        };
    }
};

alert(object.getNameFunc()());

thisarguments也存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。

在几种特殊情况下,this的值可能会意外地改变。比如,下面的代码是修改前面例子的结果。

var name = "The Window";
var object = {
    name : "My Object",
    getName: function(){
        return this.name;
    }
};

这里的getName()方法只简单地返回this.name的值。以下是几种调用object.getName()的方式以及各自的结果。

object.getName();   //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window",在非严格模式下

7.2.3 内存泄漏

由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。来看下面的例子。

function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function() {
        alert(element.id);
    };
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用(事件将在第13章讨论)。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

function assignHandler(){
    var element = document.getElementById("someElement");
    var id = element.id;

    element.onclick = function(){
        alert(id);
    };

    element = null;
}

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

7.3 模仿块级作用域

JavaScript没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的,来看下面的例子。

function outputNumbers(count){
    for (var i = 0; i < count; i++){
        alert(i);
    }
    alert(i); // count
}

这个函数中定义了一个for循环,而变量i的初始值被设置为0。在Java、C++等语言中,变量i只会在for循环的语句块中有定义,循环一旦结束,变量i就会被销毁。可是在JavaScrip中,变量i是定义在ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即使像下面这样错误地重新声明同一个变量,也不会改变它的值。

function outputNumbers(count){
    for (var i=0; i < count; i++){
        alert(i);
    }

    var i;  // 重新声明变量
    alert(i);   // count
}

JavaScript从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。

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

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:

function outputNumbers(count){
    (function () {
        for (var i=0; i < count; i++){
            alert(i);
        }
    })();

    alert(i);   // 导致一个错误!
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。例如:

(function(){
    var now = new Date();
    if (now.getMonth() == 0 && now.getDate() == 1){
        alert("Happy new year!");
    }
})();

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

7.4 私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。来看下面的例子:

function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}

在这个函数内部,有3个私有变量:num1num2sum。在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

function MyObject(){
    //私有变量和私有函数
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    //特权方法
    this.publicMethod = function (){
        privateVariable++;
        return privateFunction();
    };
}

利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例如:

function Person(name){
    this.getName = function(){
        return name;
    };

    this.setName = function (value) {
        name = value;
    };
}

var person = new Person("Nicholas");
alert(person.getName());    // "Nicholas"

person.setName("Greg");
alert(person.getName());    // "Greg"

不过,在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。第6章曾经讨论过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

7.4.1 静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下所示。

(function(){
    // 私有变量和私有函数
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    // 构造函数
    MyObject = function(){

    };

    // 公有/特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
})();

这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明MyObject时使用var关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下给未经声明的变量赋值会导致错误。

这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。来看一看下面的代码。

(function(){

    var name = "";

    Person = function(value){
        name = value;
    };

    Person.prototype.getName = function(){
        return name;
    };

    Person.prototype.setName = function (value){
        name = value;
    };
})();

var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"

var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"

这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用setName()会影响所有实例。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个显明的不足之处。

7.4.2 模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式(module
pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。

var singleton = {
    name : value,
    method : function () {
        //这里是方法的代码
    }
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

var singleton = function(){

    //私有变量和私有函数
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    //特权/公有方法和属性
    return {
        publicProperty: true,
        publicMethod : function(){
            privateVariable++;
            return privateFunction();
        }
    };
}();

由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:

var application = function(){

    // 私有变量和函数
    var components = new Array();

    // 初始化
    components.push(new BaseComponent());

    // 公共
    return {
        getComponentCount : function(){
            return components.length;
        },

        registerComponent : function(component){
            if (typeof component == "object"){
                components.push(component);
            }
        }
    };
}();

简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没有什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用instanceof操作符来检查其对象类型了。

7.4.3 增强的模块模式

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。来看下面的例子。

var singleton = function(){

    // 私有变量和私有函数
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    // 创建对象
    var object = new CustomType();

    // 添加特权/公有属性和方法
    object.publicProperty = true;

    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };

    // 返回这个对象
    return object;
}();

如果前面演示模块模式的例子中的 application 对象必须是 BaseComponent 的实例,那么就可以使用以下代码。

var application = function(){

    //私有变量和函数
    var components = new Array();

    //初始化
    components.push(new BaseComponent());

    //创建 application 的一个局部副本
    var app = new BaseComponent();

    //公共接口
    app.getComponentCount = function(){
        return components.length;
    };

    app.registerComponent = function(component){
        if (typeof component == "object"){
            components.push(component);
        }
    };

    //返回这个副本
    return app;
}();

第8章 BOM

8.1 window对象

8.1.1 全局作用域

定义全局变量与在window对象上直接定义属性还是有一点差别:全局变量不能通过delete操作符删除,而直接在window对象上的定义的属性可以。例如:

var age = 29;
window.color = "red";
//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 false
delete window.age;

//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 true
delete window.color; //returns true

alert(window.age);  //29
alert(window.color); //undefined

刚才使用var语句添加的window属性有一个名为[[Configurable]]的特性,这个特性的值被设置为false,因此这样定义的属性不可以通过delete操作符删除。

另外,还要记住一件事:尝试访问未声明的变量会抛出错误,但是通过查询window对象,可以知道某个可能未声明的变量是否存在。例如:

//这里会抛出错误,因为 oldValue 未定义
var newValue = oldValue;

//这里不会抛出错误,因为这是一次属性查询
//newValue 的值是 undefined
var newValue = window.oldValue;

Windows Mobile 平台的 IE 浏览器不允许通过 window.property = value 之类的形式,直接在 window 对象上创建新的属性或方法。可是,在全局作用域中声明的所有变量和函数,照样会变成 window 对象的成员。

8.1.2 窗口关系及框架

如果页面中包含框架,则每个框架都拥有自己的window对象,并且保存在frames集合中。在frames集合中,可以通过数值索引(从 0 开始,从左至右,从上到下)或者框架名称来访问相应的window对象。每个 window对象都有一个name属性,其中包含框架的名称。下面是一个包含框架的页面:

<html>
    <head>
        <title>Frameset Example</title>
    </head>
    <frameset rows="160,*">
        <frame src="frame.htm" name="topFrame">
        <frameset cols="50%,50%">
        <frame src="anotherframe.htm" name="leftFrame">
        <frame src="yetanotherframe.htm" name="rightFrame">
        </frameset>
    </frameset>
</html>

对于这个例子可以通过 window.frames[0] 或者 window.frames["topFrame"] 来引用上方的框架。不过,恐怕你最好使用 top 引用这些框架(例如,通过 top.frames[0])。
top对象始终指向最高(最外)层的框架,也就是浏览器窗口。使用它可以确保在一个框架中正确地访问另一个框架。因为对于在一个框架中编写的任何代码来说,其中的 window 对象指向的都是那个框架的特定实例,而非最高层的框架。图 8-1 展示了在最高层窗口中,通过代码来访问前面例子中每个框架的不同方式。

8-1

top相对的另一个window对象是parentparent(父)对象始终指向当前框架的直接上层框架。在某些情况下,parent有可能等于top;但在没有框架的情况下,parent一定等于top(此时它们都等于window)。再看下面的例子。

<html>
    <head>
        <title>Frameset Example</title>
    </head>
    <frameset rows="100,*">
        <frame src="frame.htm" name="topFrame">
        <frameset cols="50%,50%">
        <frame src="anotherframe.htm" name="leftFrame">
        <frame src="anotherframeset.htm" name="rightFrame">
        </frameset>
    </frameset>
</html>

这个框架集中的一个框架包含了另一个框架集,该框架集的代码如下所示。

<html>
    <head>
        <title>Frameset Example</title>
    </head>
    <frameset cols="50%,50%">
        <frame src="red.htm" name="redFrame">
        <frame src="blue.htm" name="blueFrame">
    </frameset>
</html>

浏览器在加载完第一个框架集以后,会继续将第二个框架集加载到rightFrame中。如果代码位于redFrame(或blueFrame)中,那么parent对象指向的就是rightFrame。可是,如果代码位于topFrame中,则parent指向的是top,因为topFrame的直接上层框架就是最外层框架。图 8-2 展示了在将前面例子加载到浏览器之后,不同window对象的值。

8-2

注意,除非最高层窗口是通过window.open()打开的(本章后面将会讨论),否则其window对象的name属性不会包含任何值。
与框架有关的最后一个对象是self,它始终指向window;实际上,selfwindow对象可以互换使用。引入self对象的目的只是为了与topparent对象对应起来,因此它不格外包含其他值。
所有这些对象都是window对象的属性,可以通过window.parentwindow.top等形式来访问。同时,这也意味着可以将不同层次的window对象连缀起来,例如window.parent.parent.frames[0]

在使用框架的情况下,浏览器中会存在多个 Global 对象。在每个框架中定义的全局变量会自动成为框架中 window 对象的属性。由于每个 window 对象都包含原生类型的构造函数,因此每个框架都有一套自己的构造函数,这些构造函数一一对应, 但并不相等。例如,top.Object 并不等于 top.frames[0].Object。这个问题会影响到对跨框架传递的对象使用 instanceof 操作符。

8.1.3 窗口位置

因浏览器差异问题,使用下列代码可以跨浏览器取得窗口左边和上边的位置。

var leftPos = (typeof window.screenLeft == "number") ? window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ? window.screenTop : window.screenY;

在使用这些值的过程中,还必须注意一些小问题。在 IE、Opera 中,screenLeftscreenTop中保存的是从屏幕左边和上边到由window对象表示的页面可见区域的距离。换句话说,如果window对象是最外层对象,而且浏览器窗口紧贴屏幕最上端——即 y 轴坐标为 0,那么screenTop的值就是位于页面可见区域上方的浏览器工具栏的像素高度。但是,在 Chrome、Firefox 和 Safari 中,screenYscreenTop中保存的是整个浏览器窗口相对于屏幕的坐标值,即在窗口的 y 轴坐标为 0 时返回 0。

使用moveTo()moveBy()方法能将窗口精确地移动到一个新位置。这两个方法都接收两个参数,其中moveTo()接收的是新位置的 x 和 y 坐标值,而moveBy()接收的是在水平和垂直方向上移动的像素数。

//将窗口移动到屏幕左上角
window.moveTo(0,0);

//将窗向下移动 100 像素
window.moveBy(0,100);

//将窗口移动到(200,300) 
window.moveTo(200,300);

//将窗口向左移动 50 像素
window.moveBy(-50,0);

需要注意的是,这两个方法可能会被浏览器禁用;而且,在 Opera 和 IE 7(及更高版本)中默认就是禁用的。另外,这两个方法都不适用于框架,只能对最外层的 window 对象使用。

8.1.4 窗口大小

跨浏览器确定一个窗口的大小不是一件简单的事。IE9+、Firefox、Safari、Opera和Chrome均为此提供了4个属性:innerWidthinnerHeightouterWidthouterHeight。在 IE9+、Safari 和 Firefox 中,outerWidthouterHeight返回浏览器窗口本身的尺寸(无论是从最外层的window对象还是从某个框架访问)。在 Opera 中,这两个属性的值表示页面视图容器的大小。而innerWidthinnerHeight则表示该容器中页面视图区的大小(减去边框宽度)。在Chrome 中,outerWidthouterHeightinnerWidthinnerHeight返回相同的值,即视口(viewport)大小而非浏览器窗口大小。

在 IE、Firefox、Safari、Opera 和 Chrome 中,document.documentElement.clientWidth
document.documentElement.clientHeight
中保存了页面视口的信息。在 IE6 中,这些属性必须在标准模式下才有效;如果是混杂模式,就必须通过document.body.clientWidthdocument.body.clientHeight取得相同信息。而对于混杂模式下的 Chrome,则无论通过document.documentElement还是document.body中的clientWidthclientHeight属性,都可以取得视口的大小。虽然最终无法确定浏览器窗口本身的大小,但却可以取得页面视口的大小,如下所示。

var pageWidth = window.innerWidth, 
    pageHeight = window.innerHeight;
if (typeof pageWidth != "number"){
    if (document.compatMode == "CSS1Compat"){
        pageWidth = document.documentElement.clientWidth; 
        pageHeight = document.documentElement.clientHeight;
    } else {
        pageWidth = document.body.clientWidth;
        pageHeight = document.body.clientHeight;
    }
}

对于移动设备,window.innerWidthwindow.innerHeight保存着可见视口,也就是屏幕上可见页面区域的大小。移动IE浏览器不支持这些属性,但通过document.documentElement.clientWidthdocument.documentElement.clientHeihgt提供了相同的信息。随着页面的缩放,这些值也会相应变化。

在其他移动浏览器中,document.documentElement度量的是布局视口,即渲染后页面的实际大小(与可见视口不同,可见视口只是整个页面中的一小部分)。移动IE浏览器把布局视口的信息保存在document.body.clientWidthdocument.body.clientHeight中。这些值不会随着页面缩放变化。

另外,使用resizeTo()resizeBy()方法可以调整浏览器窗口的大小。这两个方法都接收两个参数,其中resizeTo()接收浏览器窗口的新宽度和新高度,而resizeBy()接收新窗口与原窗口的宽度和高度之差。来看下面的例子。

// 调整到 100×100
window.resizeTo(100, 100);
// 调整到 200×150
window.resizeBy(100, 50);

// 调整到 300×300
window.resizeTo(300, 300);

8.1.5 导航和打开窗口

使用window.open()方法既可以导航到一个特定的URL,也可以打开一个新的浏览器窗口。这个方法可以接收 4 个参数:要加载的URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览器历史记录中当前加载页面的布尔值。通常只须传递第一个参数,最后一个参数只在不打开新窗口的情况下使用。

如果为window.open()传递了第二个参数,而且该参数是已有窗口或框架的名称,那么就会在具有该名称的窗口或框架中加载第一个参数指定的URL。看下面的例子。

// 等同于<a href="http://www.wrox.com" target="topFrame"></a>
window.open("http://www.wrox.com/", "topFrame");

1. 弹出窗口
如果给window.open()传递的第二个参数并不是一个已经存在的窗口或框架,那么该方法就会根据在第三个参数位置上传入的字符串创建一个新窗口或新标签页。如果没有传入第三个参数,那么就会 打开一个带有全部默认设置(工具栏、地址栏和状态栏等)的新浏览器窗口(或者打开一个新标签页——根据浏览器设置)。在不打开新窗口的情况下,会忽略第三个参数。

第三个参数是一个逗号分隔的设置字符串,表示在新窗口中都显示哪些特性。下表列出了可以出现在这个字符串中的设置选项。

设置说明
fullscreenyes或no表示浏览器窗口是否最大化。仅限IE
height数值表示新窗口的高度。不能小于100
left数值表示新窗口的左坐标。不能是负值
locationyes或no表示是否在浏览器窗口中显示地址栏。不同浏览器的默认值不同。如果设置为no,地址栏可能会隐藏,也可能会被禁用(取决于浏览器)
menubaryes或no表示是否在浏览器窗口中显示菜单栏。默认值为no
resizableyes或no表示如果内容在视口中显示不下,是否允许滚动。默认值为no
scrollbarsyes或no表示如果内容在视口中显示不下,是否允许滚动。默认值为no
statusyes或no表示是否在浏览器窗口中显示状态栏。默认值为no
toolbaryes或no表示是否在浏览器窗口中显示工具栏。默认值为no
top数值表示新窗口的上坐标。不能是负值
width数值表示新窗口的宽度。不能小于100

表中所列的部分或全部设置选项,都可以通过逗号分隔的名值对列表来指定。其中,名值对以等号表示(注意,整个特性字符串中不允许出现空格),如下面的例子所示。

window.open("http://www.wrox.com/","wroxWindow","height=400,width=400,top=10,left=10,resizable=yes");

window.open()方法会返回一个指向新窗口的引用。引用的对象与其他window对象大致相似,但我们可以对其进行更多控制。如下所示。

var wroxWin = window.open("http://www.wrox.com/","wroxWindow","height=400,width=400,top=10,left=10,resizable=yes");

//调整大小
wroxWin.resizeTo(500,500);

//移动位置
wroxWin.moveTo(100,100);

//调用 close()方法还可以关闭新打开的窗口。
wroxWin.close();

新创建的window对象有一个opener属性,其中保存着打开它的原始窗口对象。这个属性只在弹出窗口中的最外层window对象(top)中有定义,而且指向调用window.open()的窗口或框架。例如:

var wroxWin = window.open("http://www.wrox.com/","wroxWindow","height=400,width=400,top=10,left=10,resizable=yes");

alert(wroxWin.opener == window);    //true

有些浏览器(如 IE8 和 Chrome)会在独立的进程中运行每个标签页。当一个标签页打开另一个标签页时,如果两个 window 对象之间需要彼此通信,那么新标签页就不能运行在独立的进程中。
opener 属性设置为null就是告诉浏览器新创建的标签页不需要与打开它的标签页通信,因此可以在独立的进程中运行。标签页之间的联系一旦切断,将没有办法恢复。

2. 弹出窗口屏蔽程序
如果是浏览器内置的屏蔽程序阻止的弹出窗口,那么window.open()很可能会返回null。此时,只要检测这个返回的值就可以确定弹出窗口是否被屏蔽了,如下面的例子所示。

var wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null){
    alert("The popup was blocked!");
}

如果是浏览器扩展或其他程序阻止的弹出窗口,那么window.open()通常会抛出一个错误。因此, 要想准确地检测出弹出窗口是否被屏蔽,必须在检测返回值的同时,将对window.open()的调用封装在一个try-catch块中,如下所示。

var blocked = false;

try {
    var wroxWin = window.open("http://www.wrox.com", "_blank"); 
    if (wroxWin == null){
        blocked = true;
    }   
} catch (ex){
    blocked = true;
}

if (blocked){
    alert("The popup was blocked!");
}

在任何情况下,以上代码都可以检测出调用window.open()打开的弹出窗口是不是被屏蔽了。但要注意的是,检测弹出窗口是否被屏蔽只是一方面,它并不会阻止浏览器显示与被屏蔽的弹出窗口有关 的消息。

8.1.6 间歇调用和超时调用

JavaScript是单线程语言,但它允许通过设置超时值和间歇时间值来调度代码在特定的时刻执行。前者是在指定的时间过后执行代码,而后者则是每隔指定的时间就执行一次代码。

超时调用需要使用window对象的setTimeout()方法,它接受两个参数:要执行的代码和以毫秒表示的时间(即在执行代码前需要等待多少毫秒)。其中,第一个参数可以是一个包含JavaScript代码的字符串(就和在eval()函数中使用的字符串一样),也可以是一个函数。例如,下面对setTimeout()的两次调用都会在一秒钟后显示一个警告框。

//不建议传递字符串!
setTimeout("alert('Hello world!') ", 1000);

//推荐的调用方式
setTimeout(function() {
    alert("Hello world!");
}, 1000);

虽然这两种调用方式都没有问题,但由于传递字符串可能导致性能损失,因此不建议以字符串作为第一个参数。

第二个参数是一个表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定会执行。JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就有一个 JavaScript 任务队列。这些任务会按照将它们添加到队列的顺序执行。setTimeout()的第二个参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。

调用setTimeout()之后,该方法会返回一个数值 ID,表示超时调用。这个超时调用 ID 是计划执行代码的唯一标识符,可以通过它来取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用 ID 作为参数传递给它,如下所示。

//设置超时调用
var timeoutId = setTimeout(function() { 
    alert("Hello world!");
}, 1000);

//注意:把它取消
clearTimeout(timeoutId);

超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined

间歇调用会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。设置间歇调用的方法是setInterval(),它接受的参数与setTimeout()相同:要执行的代码(字符串或函数)和每次执行之前需要等待的毫秒数。下面来看一个例子。

//不建议传递字符串!
setInterval ("alert('Hello world!') ", 10000);

// 推荐的调用方式 
setInterval (function() {
    alert("Hello world!");
}, 10000);

调用setInterval()方法同样也会返回一个间歇调用ID,该ID可用于在将来某个时刻取消间歇调用。要取消尚未执行的间歇调用,可以使用clearInterval()方法并传入相应的间歇调用ID。取消间歇调用的重要性要远远高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。以下是一个常见的使用间歇调用的例子。

var num = 0; 
var max = 10;
var intervalId = null;

function incrementNumber() { num++;
    //如果执行次数达到了 max 设定的值,则取消后续尚未执行的调用
    if (num == max) { 
     alert("Done");
    }
}

intervalId = setInterval(incrementNumber, 500);

一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。

8.1.7 系统对话框

浏览器通过alert()confirm()prompt()方法可以调用系统对话框向用户显示消息。

通常使用alert()生成的“警告”对话框向用户显示一些他们无法控制的消息,例如错误消息。而用户只能在看完消息后关闭对话框。例如,alert("Hello world!")会生成如图8-3所示的对话框。
这里写图片描述

第二种对话框是调用confirm()方法生成的。从向用户显示消息的方面来看,这种“确认”对话框很像是一个“警告”对话框。但二者的主要区别在于“确认”对话框除了显示 OK 按钮外,还会显示一个 Cancel(“取消”)按钮,两个按钮可以让用户决定是否执行给定的操作。例如,confirm("Are yousure?")会显示如图8-4所示的确认对话框。

if (confirm("Are you sure?")) { 
    alert("I'm so glad you're sure! ");
} else {
    alert("I'm sorry to hear you're not sure. ");
}

这里写图片描述

最后一种对话框是通过调用prompt()方法生成的,这是一个“提示”框,用于提示用户输入一些文本。提示框中除了显示 OK 和 Cancel 按钮之外,还会显示一个文本输入域,以供用户在其中输入内容。prompt()方法接受两个参数:要显示给用户的文本提示和文本输入域的默认值(可以是一个空字符串)。调用prompt("What's your name?","Michael")会得到如图 8-5 所示的对话框。

var result = prompt("What is your name? ", "");
if (result !== null) {
    alert("Welcome, " + result);
}

这里写图片描述

还有两个可以通过 JavaScript 打开的对话框,即“查找”和“打印”。这两个对话框都是异步显示的,能够将控制权立即交还给脚本。

//显示“打印”对话框
window.print();

//显示“查找”对话框
window.find();

8.2 location对象

location是最有用的 BOM 对象之一,它提供了与当前窗口中加载的文档有关的信息,还提供了一些导航功能。location对象是很特别的一个对象,因为它既是window对象的属性,也是document对象的属性。location对象的用处不只表现在它保存着当前文档的信息,还表现在它将URL解析为独立的片段,让开发人员可以通过不同的属性访问这些片段。下表列出了location对象的所有属性(注:省略了每个属性前面的location前缀)。

属性名例子说明
hash“#contents”返回URL中的hash(#号后跟零或多个字符),如果URL中不包含散列,则返回空字符串
host“www.wrox.com:80”返回服务器名称和端口号(如果有)
hostname“www.wrox.com”返回不带端口号的服务器名称
href“http:/www.wrox.com”返回当前加载页面的完整URL。而location对象的toString()方法也返回这个值
pathname“/WileyCDA/”返回URL中的目录和(或)文件名
port“8080”返回URL中指定的端口号。如果URL中不包含端口号,则这个属性返回空字符串
protocol“http:”返回页面使用的协议。通常是http:或https:
search“?q=javascript”返回URL的查询字符串。这个字符串以问号开头

8.2.1 查询字符串参数

虽然通过上面的属性可以访问到location对象的大多数信息,但其中访问 URL 包含的查询字符串的属性并不方便。尽管location.search返回从问号到 URL 末尾的所有内容,但却没有办法逐个访问其中的每个查询字符串参数。为此,可以像下面这样创建一个函数,用以解析查询字符串,然后返回包含所有参数的一个对象:

function getQueryStringArgs(){
    //取得查询字符串并去掉开头的问号
    var qs = (location.search.length > 0 ? location.search.substring(1): ""),

    //保存数据的对象
    args = {},

    //取得每一项
    items = qs.length ? qs.split("&") : [], 
    item = null,
    name = null,
    value = null,

    //在 for 循环中使用
    i = 0,
    len = items.length;

    //逐个将每一项添加到 args 对象中
    for (i=0; i < len; i++){
        item = items[i].split("=");
        name = decodeURIComponent(item[0]); 
        value = decodeURIComponent(item[1]);
        if (name.length) { 
            args[name] = value;
        }
    }
    return args;
}

下面给出了使用这个函数的示例。

//假设查询字符串是?q=javascript&num=10
var args = getQueryStringArgs();

alert(args["q"]);   //"javascript" 
alert(args["num"]); //"10"

8.2.2 位置操作

使用location对象可以通过很多方式来改变浏览器的位置。首先,也是最常用的方式,就是使用assign()方法并为其传递一个URL,如下所示。

location.assign("http://www.wrox.com");

这样,就可以立即打开新 URL 并在浏览器的历史记录中生成一条记录。如果是将location.hrefwindow.location设置为一个 URL 值,也会以该值调用assign()方法。例如,下列两行代码与显式调用assign()方法的效果完全一样。

window.location = "http://www.wrox.com"; 
location.href = "http://www.wrox.com";

在这些改变浏览器位置的方法中,最常用的是设置location.href属性。

另外,修改location对象的其他属性也可以改变当前加载的页面。下面的例子展示了通过将hashsearchhostnamepathnameport 属性设置为新值来改变 URL。

//假设初始 URL 为 http://www.wrox.com/WileyCDA/

//URL 修改为"http://www.wrox.com/WileyCDA/#section1" 
location.hash = "#section1";

//URL 修改为"http://www.wrox.com/WileyCDA/?q=javascript" 
location.search = "?q=javascript";

//URL 修改为"http://www.yahoo.com/WileyCDA/" 
location.hostname = "www.yahoo.com";

//URL 修改为"http://www.yahoo.com/mydir/" 
location.pathname = "mydir";

//URL 修改为"http://www.yahoo.com:8080/WileyCDA/" 
location.port = 8080;

上述任何一种方式修改URL之后,浏览器的历史记录中就会生成一条新记录,因此用户通过单击“后退”按钮都会导航到前一个页面。要禁用这种行为,可以使用 replace()方法。这个方法只接受一个参数,即要导航到的 URL;结果虽然会导致浏览器位置改变,但不会在历史记录中生成新记录。在调用replace()方法之后,用户不能回到前一个页面,来看下面的例子:

<!DOCTYPE html>
<html>
    <head>
        <title>You won't be able to get back here</title>
    </head>
    <body>
        <p>Enjoy this page for a second, because you won't be coming back here.</p>
        <script type="text/javascript"> 
            setTimeout(function () {
                location.replace("http://www.wrox.com/");
            }, 1000);
        </script>
    </body>
</html>

与位置有关的最后一个方法是reload(),作用是重新加载当前显示的页面。如果调用reload()时不传递任何参数,页面就会以最有效的方式重新加载。也就是说,如果页面自上次请求以来并没有改变过,页面就会从浏览器缓存中重新加载。如果要强制从服务器重新加载,则需要像下面这样为该方法传递参数true

location.reload();  //重新加载(有可能从缓存中加载)   
location.reload(true);  //重新加载(从服务器重新加载)

位于reload()调用之后的代码可能会也可能不会执行,这要取决于网络延迟或系统资源等因素。为此,最好将reload()放在代码的最后一行。

8.3 navigator 对象

navigator 对象是所有支持 JavaScript 的浏览器所共有的。与其他 BOM 对象的情况一样,每个浏览器中的 navigator 对象也都有一套自己的属性。下表列出了存在于所有浏览器中的属性和方法,以及支持它们的浏览器版本

属性或方法说 明
appCodeName浏览器的名称。通常都是 Mozilla ,即使在非Mozilla浏览器中也是如此
appMinorVersion次版本信息
appName完整的浏览器名称
appVersion浏览器的版本。一般不与实际的浏览器版本对应
buildID浏览器编译版本
cookieEnabled表示cookie是否启用
cpuClass客户端计算机中使用的CPU类型( x86 、68K 、 Alpha 、 PPC 或 Other )
javaEnabled()表示当前浏览器中是否启用了Java
language浏览器的主语言
mimeTypes在浏览器中注册的MIME类型数组
onLine表示浏览器是否连接到了因特网
opsProfile似乎早就不用了。查不到相关文档
oscpu客户端计算机的操作系统或使用的CPU
platform浏览器所在的系统平台
plugins浏览器中安装的插件信息的数组
preference()设置用户的首选项
product产品名称(如 Gecko )
productSub关于产品的次要信息(如Gecko的版本)
register-ContentHandler()针对特定的MIME类型将一个站点注册为处理程序
register-ProtocolHandler()针对特定的协议将一个站点注册为处理程
securityPolicy已经废弃。安全策略的名称。为了与Netscape Navigator 4向后兼容而保留下来
systemLanguage操作系统的语言
taintEnabled()已经废弃。表示是否允许变量被修改(taint)。为了与Netscape Navigator 3向后兼容而保留下来
userAgent浏览器的用户代理字符串
userLanguage操作系统的默认语言
userProfile借以访问用户个人信息的对象
vendor浏览器的品牌
vendorSub有关供应商的次要信息

表中的这些 navigator 对象的属性通常用于检测显示网页的浏览器类型

8.3.1 检测插件

检测浏览器中是否安装了特定的插件是一种最常见的检测例程。对于非 IE 浏览器,可以使用plugins数组来达到这个目的。该数组中的每一项都包含下列属性。
- name :插件的名字。
- description :插件的描述。
- filename :插件的文件名。
- length :插件所处理的 MIME 类型数量。

但有时候也不完全如此。在检测插件时,需要像下面这样循环迭代每个插件并将插件的 name 与给定的名字进行比较。

//检测插件(在 IE 中无效)
function hasPlugin(name){
    name = name.toLowerCase();
    for (var i=0; i < navigator.plugins.length; i++){
        if (navigator. plugins [i].name.toLowerCase().indexOf(name) > -1){
            return true;
        }
    }
    return false;
}

//检测 Flash
alert(hasPlugin("Flash"));

//检测 QuickTime
alert(hasPlugin("QuickTime"));

这个hasPlugin() 函数接受一个参数:要检测的插件名。第一步是将传入的名称转换为小写形式,
以便于比较。然后,迭代 plugins 数组,通过 indexOf() 检测每个 name 属性,以确定传入的名称是
否出现在字符串的某个地方。

每个插件对象本身也是一个MimeType对象的数组,这些对象可以通过方括号语
法来访问。每个MimeType对象有 4 个属性:包含 MIME 类型描述的description
回指插件对象的enabledPlugin 、表示与 MIME 类型对应的文件扩展名的字符串
suffixes (以逗号分隔)和表示完整 MIME 类型字符串的 type

检测 IE 中的插件比较麻烦,因为 IE 不支持Netscape式的插件。在 IE 中检测插件的唯一方式就是
使用专有的ActiveXObject类型,并尝试创建一个特定插件的实例。IE 是以 COM对象的方式实现插
件的,而 COM对象使用唯一标识符来标识。

//检测 IE 中的插件
function hasIEPlugin(name){
    try {
        new ActiveXObject(name);
        return true;
    } catch (ex){
        return false;
    }
}

//检测 Flash
alert(hasIEPlugin("ShockwaveFlash.ShockwaveFlash"));

//检测 QuickTime
alert(hasIEPlugin("QuickTime.QuickTime"));

在这个例子中,函数 hasIEPlugin()只接收一个 COM 标识符作为参数。在函数内部,首先会尝
试创建一个 COM对象的实例。之所以要在 try-catch 语句中进行实例化,是因为创建未知 COM对象会导致抛出错误。这样,如果实例化成功,则函数返回true ;否则,如果抛出了错误,则执行catch块,结果就会返回 false

鉴于检测这两种插件的方法差别太大,因此典型的做法是针对每个插件分别创建检测函数,而不是
使用前面介绍的通用检测方法。来看下面的例子。

//检测所有浏览器中的 Flash
function hasFlash(){
    var result = hasPlugin("Flash");
    if (!result){
        result = hasIEPlugin("ShockwaveFlash.ShockwaveFlash");
    }
    return result;
}

//检测所有浏览器中的 QuickTime
function hasQuickTime(){
    var result = hasPlugin("QuickTime");
    if (!result){
        result = hasIEPlugin("QuickTime.QuickTime");
    }
    return result;
}

//检测 Flash
alert(hasFlash());

//检测 QuickTime
alert(hasQuickTime());

plugins集合有一个名叫refresh() 的方法,用于刷新plugins以反映最新安
装的插件。这个方法接收一个参数:表示是否应该重新加载页面的一个布尔值。如果
将这个值设置为true,则会重新加载包含插件的所有页面;否则,只更新plugins
集合,不重新加载页面。

8.3.2 注册处理程序

Firefox 2为navigator对象新增了registerContentHandler()registerProtocolHandler()方法(这两个方法是在 HTML5 中定义的)。这两个方法可以让一个站点指明它可以处理特定类型的信息。

其中, registerContentHandler() 方法接收三个参数:要处理的MIME 类型、可以处理该MIME类型的页面的URL以及应用程序的名称。举个例子,要将一个站点注册为处理 RSS 源的处理程序,可
以使用如下代码。

navigator.registerContentHandler("application/rss+xml", "http://www.somereader.com?feed=%s", "Some Reader");

第一个参数是 RSS 源的 MIME 类型。第二个参数是应该接收 RSSURLURL,其中的 %s 表示RSSURL,由浏览器自动插入。当下一次请求RSS源时,浏览器就会打开指定的URL,而相应的Web应用程序将以适当方式来处理该请求。

类似的调用方式也适用于registerProtocolHandler()方法,它也接收三个参数:要处理的协议(例如, mailto 或 ftp )、处理该协议的页面的URL和应用程序的名称。例如,要想将一个应用程序注册为默认的邮件客户端,可以使用如下代码。

navigator.registerProtocolHandler("mailto", "http://www.somemailclient.com?cmd=%s", "Some Mail Client");

这个例子注册了一个mailto 协议的处理程序,该程序指向一个基于 Web 的电子邮件客户端。

8.4 screen 对象

身份证验证

//身份证正则表达式(18位)
var isIdCard2 = /^[1-9]\d{5}(19\d{2}|[2-9]\d{3})((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])(\d{4}|\d{3}X)$/i;
var stard = "10X98765432"; // 最后一位身份证的号码
var first = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; //1-17系数
var sum = 0;
if (!isIdCard2.test(cardid)) {
    return false;
}
var year = cardid.substr(6, 4);
var month = cardid.substr(10, 2);
var day = cardid.substr(12, 2);
var birthday = cardid.substr(6, 8);
var dateString = this.dateToString(new Date(year + '/' + month + '/' + day)); 
if (birthday != dateString) { //校验日期是否合法
    return false;
}
for (var i = 0; i < cardid.length - 1; i++) {
    sum += cardid[i] * first[i];
} 
var result = sum % 11;
var last = stard[result]; //计算出来的最后一位身份证号码
if (cardid[cardid.length - 1].toUpperCase() == last) {
    return true;
} else {
    return false;
}

// 日期转字符串 返回日期格式20080808
game.Utils.Identity.dateToString = function (date) {
    if (date instanceof Date) {
        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        month = month < 10 ? '0' + month: month;
        var day = date.getDate();
        day = day < 10 ? '0' + day: day;
        return year.toString() + month.toString() + day.toString();
    }
    return '';
}

带emoji表情文本截取

function getStirngForNoEmoji (str, n) {
    str = str +'';
    var end = (typeof(n) == undefined || n == null) ? 6 : n ;

    for(var i=0 ; i< str.length;i++){
        if(i == end-1) {
            var hs = str.charCodeAt(i);
            if (0xd800 <= hs && hs <= 0xdbff) {
                if (str.length > 1) {
                    var ls = str.charCodeAt(i + 1);
                    var uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
                    if (0x1d000 <= uc && uc <= 0x1f77f) {
                        end += 1;
                    }
                }
            } else if (str.length > 1) {
                var ls = str.charCodeAt(i + 1);
                if (ls == 0x20e3) {
                    end += 1;
                }
            } else {
                if (0x2100 <= hs && hs <= 0x27ff) {
                    end += 1;
                } else if (0x2B05 <= hs && hs <= 0x2b07) {
                    end += 1;
                } else if (0x2934 <= hs && hs <= 0x2935) {
                    end += 1;
                } else if (0x3297 <= hs && hs <= 0x3299) {
                    end += 1;
                } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030
                    || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b
                    || hs == 0x2b50) {
                    end += 1;
                }
            }
        }
    }

    if(str.length > end){
        return str.substring(0,end) ;
    }
    return str;
};

var name = "陌上花开 ?(一个玉米的emoji,CSDN无法展示)";
console.log(name.substring(0, 6));         // 陌上花开 �(乱码)
console.log(getStirngForNoEmoji(name, 6)); // 陌上花开 ?(emoji表情,CSDN无法展示出来)
  • 24
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值