第五章
引用类型的值(对象)是引用类型的一个实例。在 ECMAScript 中,引用类型是一种数据结构, 用于将数据和功能组织在一起。它也常被称为类,但这种称呼并不妥当。尽管 ECMAScript 从技术上讲是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构。引用类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。
对象是某个特定引用类型的实例。新对象是使用 new 操作符后跟一个构造函数来创建的。 构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。
ECMAScript 提供了很多原生引用类 型(例如 Object),以便开发人员用以实现常见的计算任务。
5.1 Object类型
到目前为止,我们看到的大多数引用类型值都是 Object 类型的实例;而且,Object 也是 ECMAScript 中使用最多的一个类型。
创建 Object 实例的方式有两种。第一种是使用 new 操作符后跟 Object 构造函数。
eg:
var person = new Object(); person.name = "Nicholas"; person.age = 29;
另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建 包含大量属性的对象的过程。
eg:
var person = { name : "Nicholas", age : 29 };
在最后一个属性后面添加逗号,会在 IE7 及更早版本和Opera 中导致错误。
在这个例子中,左边的花括号({)表示对象字面量的开始,因为它出现在了表达式上下文 (expression context)中。ECMAScript 中的表达式上下文指的是能够返回一个值(表达式)。赋值操作 符表示后面是一个值,所以左花括号在这里表示一个表达式的开始。同样的花括号,如果出现在一个语句上下文(statement context)中,例如跟在 if 语句条件的后面,则表示一个语句块的开始。
在使用对象字面量语法时,属性名也可以使用字符串。
eg:
var person = { "name" : "Nicholas", "age" : 29, 5 : true };
这个例子会创建一个对象,包含三个属性:name、age 和 5。但这里的数值属性名会自动转换为字符串。
对象字面量也是向函数传递大量可选参 数的首选方式。
eg:
这种传递参数的模式最适合需要向函数传入大量可选参数的情形。一般来讲,命名参数虽然容易处理,但在有多个可选参数的情况下就会显示不够灵活。最好的做法 是对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数。
一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。不过, 在 JavaScript 也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性 以字符串的形式放在方括号中。
eg:
alert(person["name"]); //"Nicholas"alert(person.name); //"Nicholas"
从功能上看,这两种访问对象属性的方法没有任何区别。但方括号语法的主要优点是可以通过变量 来访问属性。
eg:
var propertyName = "name";alert(person[propertyName]); //"Nicholas"
如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括 号表示法。
eg:
person["first name"] = "Nicholas";
由于"first name"中包含一个空格,所以不能使用点表示法来访问它。然而,属性名中是可以包 含非字母非数字的,这时候就可以使用方括号表示法来访问它们。
通常,除非必须使用变量来访问属性,否则我们建议使用点表示法。
5.2 Array类型
数据的有序列表,但与其他语言不同的是,ECMAScript 数组的每一项可以保存任何类型的数据。而且,ECMAScript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容 纳新增数据。
创建数组的基本方式有两种。第一种是使用 Array构造函数。
eg:
var colors = new Array();
如果预先知道数组要保存的项目数量,也可以给构造函数传递该数量,而该数量会自动变成 length 属性的值。
eg:
var colors = new Array(20);
也可以向 Array 构造函数传递数组中应该包含的项。以下代码创建了一个包含 3 个字符串值的数组:
var colors = new Array("red", "blue", "green");
给构造函数传递值:如果传递的是数 值,则会按照该数值创建包含给定项数的数组;而如果传递的是其他类型的参数,则会创建包含那个值 的只有一项的数组。
另外,在使用 Array 构造函数时也可以省略 new 操作符。如下面的例子所示,省略 new 操作符的 结果相同:
var colors = Array(3); // 创建一个包含 3 项的数组var names = Array("Greg"); // 创建一个包含 1 项,即字符串"Greg"的数组
创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表 示,多个数组项之间以逗号隔开。
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组var names = []; // 创建一个空数组 var values = [1,2,]; // 不要这样!这样会创建一个包含 2 或 3 项的数组 var options = [,,,,,]; // 不要这样!这样会创建一个包含 5 或 6 项的数组
分析:
在 IE 中,values 会成为一个包含 3 个项且每 项的值分别为 1、2 和 undefined 的数组;在其他浏览器中,values 会成为一个包含 2 项且值分别为 1 和 2 的数组。原因是 IE8 及之前版本中的 ECMAScript 实现在数组字面量方面存在 bug。由于这个 bug导致的另一种情况如最后一行代码所示,该行代码可能会创建包含 5 项的数组(在 IE9+、Firefox、Opera、Safari 和 Chrome 中),也可能会创建包含 6 项的数组(在 IE8 及更早版本中)。在像这种省略值的情况下, 每一项都将获得undefined值;这个结果与调用Array构造函数时传递项数在逻辑上是相同的。但是 由于 IE 的实现与其他浏览器不一致,因此我们强烈建议不要使用这种语法。
在读取和设置数组的值时,要使用方括号并提供相应值的基于 0 的数字索引,如下所示: var colors = ["red", "blue", "green"]; // 定义一个字符串数组
alert(colors[0]); // 显示第一项 colors[2] = "black"; // 修改第三项 colors[3] = "brown"; // 新增第四项
如果设 置某个值的索引超过了数组现有项数,如这个例子中的 colors[3]所示,数组就会自动增加到该索引 值加 1 的长度(就这个例子而言,索引是 3,因此数组长度就是 4)。
数组的 length 属性很有特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移 除项或向数组中添加新项。请看下面的例子:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组 colors.length = 2;alert(colors[2]); //undefined
如果将其 length属性设置为大于数组 项数的值,则新增的每一项都会取得 undefined 值,如下所示:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组 colors.length = 4;alert(colors[3]); //undefined
利用 length 属性也可以方便地在数组末尾添加新项,如下所示:
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组 colors[colors.length] = "black"; //(在位置3)添加一种颜色 colors[colors.length] = "brown"; //(在位置4)再添加一种颜色
数组最多可以包含 4 294 967 295 个项.
5.2.1 检测数组
对于一个网页,或者一个全局作用域而言,使用 instanceof 操作符就能得到满意的结果:
if (value instanceof Array){ //对数组执行某些操作}
instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实 际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array构造函数。如果你从 一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自 不同的构造函数。
为了解决这个问题,ECMAScript 5 新增了 Array.isArray()方法。这个方法的目的是最终确定某 个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。这个方法的用法如下。
if (Array.isArray(value)){ //对数组执行某些操作}
支持 Array.isArray()方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 10.5+和 Chrome。
5.2.2 转换方法
所有对象都具有 toLocaleString()、toString()和 valueOf()方法。其中,调用 数组的 toString()方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。而 调用 valueOf()返回的还是数组。实际上,为了创建这个字符串会调用数组每一项的 toString()方 法。
当调用数组的 toLocaleString()方法时,它也会创建一个数组值的以逗号分隔的字符 串。而与前两个方法唯一的不同之处在于,这一次为了取得每一项的值,调用的是每一项的 toLocale- String()方法,而不是 toString()方法。
eg:
var person1 = { toLocaleString : function () { return "Nikolaos"; }, toString : function() { return "Nicho"; } };var person2 = { toLocaleString : function () { return "Grigorios"; }, toString : function() { return "Greg"; } };var people = [person1, person2];alert(people); //Nicho,Greg alert(people.toString()); //Nicho,Greg alert(people.toLocaleString()); //Nikolaos,Grigorios
数组继承的 toLocaleString()、toString()和 valueOf()方法,在默认情况下都会以逗号分隔的字 符串的形式返回数组项。而如果使用 join()方法,则可以使用不同的分隔符来构建这个字符串。join()方 法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。
eg:
var colors = [1, "green", "blue"]; colors.join("||"); // "1||green||blue"
我们使用 join()方法重现了 toString()方法的输出。在传递逗号的情况下,得到了以 逗号分隔的数组值。
var colors = [1, "green", "blue"];colors.toString(); //"1,green,blue"//colors.join(",") //"1,green,blue"
如果不给 join()方法传入任何值,或者给它传入 undefined,则使用逗号作为分隔 符。IE7 及更早版本会错误的使用字符串"undefined"作为分隔符。
eg:
var colors = [1, "blue","red"]; //colors.join() //"1,blue,red"//colors.join("undefined") //"1undefinedblueundefinedred"colors.join(undefined) //"1,blue,red"//colors.join(null) //"1nullbluenullred"
如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、 toLocaleString()、toString()和 valueOf()方法返回的结果中以空字符串表示。
eg:
var colors = [1, undefined, "blue",null]; //colors.join(",") //"1,,blue,"//colors.toString() //"1,,blue,"colors.valueOf() //[1, undefined, "blue", null]//colors.toLocaleString() //"1,,blue,"
colors.valueOf()结果并未预想的以空字符串表示???
eg:
var nobody = [1,,"red"];var color = [1,2,"red"];var nullArr = [1,null,"red"];var undefinedArr = [1,undefined,"red"]; //nobody.valueOf() //[1, undefined, "red"]//nobody.join(",") //"1,,red"//nobody.toString() //"1,,red"//nobody instanceof Array //true //color.valueOf() //[1, 2, "red"]//color.join(",") //"1,2,red"//color.toString() //"1,2,red"//color instanceof Array //true //nullArr.valueOf() //[1, null, "red"]//nullArr.join(",") //"1,,red"//nullArr.toString() //"1,,red"//nullArr instanceof Array //true //undefinedArr.valueOf() //[1, undefined, "red"]//undefinedArr.join(",") //"1,,red"undefinedArr.toString() //"1,,red"//undefinedArr instanceof Array //true
分析:当数组某一项为空时,valueOf结果为undefined,其他情况都返回原来值(null的valueOf结果为null,undefined的valueOf结果为undefined);
而当数组某项为空或null或nudefined:join(",")和toString()的结果都为空。
5.2.3 栈方法
栈是一种 LIFO(Last-In-First-Out, 后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做 弹出),只发生在一个位置——栈的顶部。ECMAScript 为数组专门提供了 push()和 pop()方法,以便 实现类似栈的行为。
push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而 pop()方法则从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。
eg:
var colors = new Array();var count = colors.push("red", "green"); console.log(count); //2 count = colors.push("black"); console.log(count); //3console.log(colors); // ["red", "green", "black"] var item = colors.pop(); console.log(item); //blackconsole.log(colors.length); //2console.log(colors); // ["red", "green"]
5.2.4 队列方法
队列数据结构的访问规则是 FIFO(First-In-First-Out, 先进先出)。队列在列表的末端添加项,从列表的前端移除项。shift(),它能够移除数组中的第一个项并返回该项,同时将数组长度减 1。结合使用 shift()和 push()方法,可以像使 用队列一样使用数组。
eg:
var colors = new Array();var count = colors.push("red", "green"); console.log(count); //2 count = colors.push("black"); console.log(count); //3console.log(colors); // ["red", "green", "black"] var item = colors.shift(); console.log(item); //redconsole.log(colors.length); //2console.log(colors); // ["green", "black"]
unshift()与 shift()的用途相反: 它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用 unshift()和 pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项。
eg:
var colors = new Array();var count = colors.unshift("red", "green"); console.log(count); //2 count = colors.unshift("black"); console.log(count); //3console.log(colors); // ["black", "red", "green"] var item = colors.pop(); console.log(item); //greenconsole.log(colors.length); //2console.log(colors); // ["black", "red"]
5.2.5 重排序方法
数组中已经存在两个可以直接用来重排序的方法:reverse()和 sort()。有读者可能猜到了,
reverse()方法会反转数组项的顺序。
eg:
var values = [1, 2, 3, 4, 5]; values.reverse();alert(values); //5,4,3,2,1
在默认情况下,sort()方法按升序排列数组项——即最小的值位于最前面,最大的值排在最后面。 为了实现排序,sort()方法会调用每个数组项的 toString()转型方法,然后比较得到的字符串,以 确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。
eg:
var values = [0, 1, 5, 10, 15]; values.sort(); alert(values); //0,1,10,15,5
sort()方法也会根据测试字符串的结果改变原来的顺序。 因为数值 5 虽然小于 10,但在进行字符串比较时,"10"则位于"5"的前面,于是数组的顺序就被修改了。
sort()方法可以接收一个比较函数作为参 数,以便我们指定哪个值位于哪个值的前面。
eg:
function compare(){ return -1; } var values = [0, 10,2,15]; values.sort(compare); console.log(values); // [0, 10, 2, 15]
函数返回-1时,顺序不变。
eg2:
function compare(){ return 1; } var values = [0, 10,2,15]; values.sort(compare); console.log(values); // [15, 2, 10, 0]
函数返回1时,顺序相反。
eg3:
function compare(){ return 0; } var values = [0, 10,2,15]; values.sort(compare); console.log(values); // [0, 10, 2, 15]
函数返回0时,顺序不变。
eg4:
function compare(value1, value2) { if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } var values = [0, 10,2,15]; values.sort(compare); console.log(values); // [0, 2, 10, 15]
在将比较函数传递到 sort()方法之后,数值仍然保持了正确的升序。
eg5:
function compare(value1, value2) { if (value1 < value2) { return 1; } else if (value1 > value2) { return -1; } else { return 0; } } var values = [0, 10,2,15]; values.sort(compare); console.log(values); // [15, 10, 2, 0]
通过比较函数产生降序排序的结果,只要交换比较函数返回的值即可。
对于数值类型或者其 valueOf()方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。这个函数只要用第二个值减第一个值即可
function compare(value1, value2){ return value2 - value1; }
由于比较函数通过返回一个小于零、等于零或大于零的值来影响排序结果,因此减法操作就可以适
当地处理所有这些情况。
5.2.6 操作方法
concat()方法可以基于当前数 组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数 添加到这个副本的末尾,最后返回新构建的数组。在没有给 concat()方法传递参数的情况下,它只是 复制当前数组并返回副本。如果传递给 concat()方法的是一或多个数组,则该方法会将这些数组中的 每一项都添加到结果数组中。如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。
eg:
var colors = ["red", "green", "blue"]; var colors2 = colors.concat("yellow", ["black", "brown"]); alert(colors); //red,green,blue alert(colors2); //red,green,blue,yellow,black,brown
slice(),它能够基于当前数组中的一或多个项创建一个新数组。slice()方法可以 接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice()方法返回从该 参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项— —但不包括结束位置的项。注意,slice()方法不会影响原始数组。
eg:
var colors = ["red", "green", "blue", "yellow", "purple"]; var colors2 = colors.slice(1); var colors3 = colors.slice(1,4); alert(colors2); //green,blue,yellow,purple alert(colors3); //green,blue,yellow
如果 slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位 置。例如,在一个包含 5 项的数组上调用 slice(-2,-1)与调用 slice(3,4)得到的 结果相同。如果结束位置小于起始位置,则返回空数组。
splice()方法,这个方法恐怕要算是最强大的数组方法了,它有很多种用法。 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"。
- splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何 项,则返回一个空数组)。
eg:
var colors = ["red", "green", "blue"]; //var removed = colors.splice(0,1); //console.log(colors); // ["green", "blue"] //console.log(removed); // ["red"],返回的数组中只包含一项 var removed = colors.splice(1, 0, "yellow", "orange"); console.log(colors); // ["red", "yellow", "orange", "green", "blue"]console.log(removed); // [],返回的是一个空数组 //var removed = colors.splice(1, 1, "red", "purple");//console.log(colors); // ["red", "red", "purple", "blue"]//console.log(removed); // ["green"],返回的数组中只包含一项
5.2.7 位置方法
ECMAScript 5 为数组实例添加了两个位置方法:indexOf()和 lastIndexOf()。这两个方法都接收 两个参数:要查找的项和(可选的)表示查找起点位置的索引。其中,indexOf()方法从数组的开头(位 置 0)开始向后查找,lastIndexOf()方法则从数组的末尾开始向前查找。
这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回1。在比较第一个参数与数组中的每一项时,会使用全等操作符;也就是说,要求查找的项必须严格相等(就像使用===一样)。
eg1:
var numbers = [1,2,3,4,5,4,3,2,1]; console.log(numbers.indexOf(4)); //3console.log(numbers.lastIndexOf(4)); //5console.log(numbers.indexOf(4, 4)); //5console.log(numbers.lastIndexOf(4, 4)); //3console.log(numbers.indexOf("4")); //-1
eg2:
var person = { name: "Nicholas" };var morePeople = [person];var people = [{ name: "Nicholas" }]; console.log(people); // [Object { name="Nicholas"}]console.log(morePeople); //[Object { name="Nicholas"}]console.log(people==morePeople); //false ????console.log(people.indexOf(person)); //-1 ???console.log(morePeople.indexOf(person)); //0
为什么people==morePeople结果为false??? 数组是对象,不能直接比较的。
使用 indexOf()和 lastIndexOf()方法查找特定项在数组中的位置非常简单,支持它们的浏览器包 括 IE9+、Firefox 2+、Safari 3+、Opera 9.5+和 Chrome。
5.2.8 迭代方法
ECMAScript 5 为数组定义了 5 个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和 (可选的)运行该函数的作用域对象——影响 this 的值。传入这些方法中的函数会接收三个参数:数 组项的值、该项在数组中的位置和数组对象本身。根据使用的方法不同,这个函数执行后的返回值可能
会也可能不会影响方法的返回值。以下是这 5 个迭代方法的作用。
- every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
- filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
- forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
- map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
- some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。
以上方法都不会修改数组中的包含的值。
在这些方法中,最相似的是 every()和 some(),它们都用于查询数组中的项是否满足某个条件。
对 every()来说,传入的函数必须对每一项都返回 true,这个方法才返回 true;否则,它就返回
false。而 some()方法则是只要传入的函数对数组中的某一项返回 true,就会返回 true。
eg:
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); //false var someResult = numbers.some(function(item, index, array){ return (item > 2); });console.log(someResult); //true var filterResult = numbers.filter(function(item, index, array){ return (item > 2); });console.log(filterResult); // [3, 4, 5, 4, 3] var mapResult = numbers.map(function(item, index, array){ return item * 2; });console.log(mapResult); // [2, 4, 6, 8, 10, 8, 6, 4, 2]
filter()方法对查询符合某些条件的所有数组项非常有用。
map()也返回一个数组,而这个数组的每一项都是在原始数组中的对应项上运行传入函数的结果。这个方法适合创建包含的项与另一个数组 一一对应的数组。
最后一个方法是 forEach(),它只是对数组中的每一项运行传入的函数。这个方法没有返回值, 本质上与使用 for 循环迭代数组一样。
eg:
var numbers = [1,2,3,4,5,4,3,2,1]; numbers.forEach(function(item, index, array){//执行某些操作 });
这些数组方法通过执行不同的操作,可以大大方便处理数组的任务。支持这些迭代方法的浏览器有 IE9+、Firefox 2+、Safari 3+、Opera 9.5+和 Chrome。
5.2.9 归并方法
ECMAScript 5 还新增了两个归并数组的方法:reduce()和 reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中,reduce()方法从数组的第一项开始,逐个遍历 到最后。而 reduceRight()则从数组的最后一项开始,向前遍历到第一项。
这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给 reduce()和 reduceRight()的函数接收 4 个参数:前一个值、当前值、项的索引和数组对象。这 个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第 一个参数是数组的第一项,第二个参数就是数组的第二项。
使用 reduce()方法可以执行求数组中所有值之和的操作。
eg:
reduceRight()的作用类似,只不过方向相反而已。
eg:
使用 reduce()还是 reduceRight(),主要取决于要从哪头开始遍历数组。除此之外,它们完全 相同。
支持这两个归并函数的浏览器有 IE9+、Firefox 3+、Safari 4+、Opera 10.5 和 Chrome。
5.3 Date类型
Date 类型使用自 UTC(Coordinated Universal Time,国际协调时间)1970 年 1 月 1 日午夜(零时)开始经过 的毫秒数来保存日期。在使用这种数据存储格式的条件下,Date 类型保存的日期能够精确到 1970 年 1 月 1 日之前或之后的 285 616 年。
要创建一个日期对象。
eg:
var now = new Date();
在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根 据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从 UTC 时间 1970 年 1 月 1 日午 夜起至该日期止经过的毫秒数)。为了简化这一计算过程,ECMAScript 提供了两个方法:Date.parse() 和 Date.UTC()。
(1)Date.parse()
其中,Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日 期的毫秒数。ECMA-262 没有定义 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)。只有兼容
ECMAScript 5 的实现支持这种格式。
例如,要为 2004 年 5 月 25 日创建一个日期对象,可以使用下面的代码:
var someDate = new Date(Date.parse("May 25, 2004"));
如果传入 Date.parse()方法的字符串不能表示日期,那么它会返回 NaN。
实际上,如果直接将表 示日期的字符串传递给 Date 构造函数,也会在后台调用 Date.parse()。
var someDate = new Date("May 25, 2004");
这行代码将会得到与前面相同的日期对象。
(2)Date.UTC()
Date.UTC()方法同样也返回表示日期的毫秒数,但它与 Date.parse()在构建值时使用不同的信 息。Date.UTC()的参数分别是年份、基于 0 的月份(一月是 0,二月是 1,以此类推)、月中的哪一天 (1 到 31)、小时数(0 到 23)、分钟、秒以及毫秒数。在这些参数中,只有前两个参数(年和月)是必 需的。如果没有提供月中的天数,则假设天数为 1;如果省略其他参数,则统统假设为 0。
eg:
// GMT时间2000年1月1日午夜零时var y2k = new Date(Date.UTC(2000, 0));console.log(y2k); // Date {Sat Jan 01 2000 08:00:00 GMT+0800 (CST)}
// GMT时间2005年5月5日 下午5:55:55var allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));console.log(allFives); //Date {Fri May 06 2005 01:55:55 GMT+0800 (CST)}
如同模仿 Date.parse()一样,Date 构造函数也会模仿 Date.UTC(),但有一点明显不同:日期 和时间都基于本地时区而非 GMT 来创建。不过,Date 构造函数接收的参数仍然与 Date.UTC()相同。
因此,如果第一个参数是数值,Date 构造函数就会假设该值是日期中的年份,而第二个参数是月份,
以此类推。据此,可以将前面的例子重写如下。
eg:
// GMT时间2000年1月1日午夜零时var y2k = new Date(2000, 0);console.log(y2k); // Date {Sat Jan 01 2000 08:00:00 GMT+0800 (CST)}
// GMT时间2005年5月5日 下午5:55:55var allFives = new Date(2005, 4, 5, 17, 55, 55);console.log(allFives); // Date {Thu May 05 2005 17:55:55 GMT+0800 (CST)}
以上代码创建了与前面例子中相同的两个日期对象,只不过这次的日期都是基于系统设置的本地时 区创建的。
eg:
var newDate = new Date(); //本地时间var dateparse = new Date(Date.parse("11/20/2016"))var dateP = new Date("11/20/2016");var dateUTC = new Date(Date.UTC(2016,10,20,15,55,55)); //UTC时间,UTC时间+8=本地时间var dateU = new Date(2016,10,20,15,55,55); //本地时间console.log(newDate);console.log(dateparse);console.log(dateP);console.log(dateUTC);console.log(dateU);
(3)Data.now()
ECMAScript 5 添加了 Data.now()方法,返回表示调用这个方法时的日期和时间的毫秒数。这个方 法简化了使用 Data 对象分析代码的工作。例如:
//取得开始时间var start = Date.now();//调用函数 doSomething();//取得停止时间var stop = Date.now(),result = stop – start;
支持 Data.now()方法的浏览器包括 IE9+、Firefox 3+、Safari 3+、Opera 10.5 和 Chrome。在不支
持它的浏览器中,使用+操作符把 Data 对象转换成字符串,也可以达到同样的目的。
//取得开始时间 var start = +new Date(); //调用函数 doSomething(); //取得停止时间var stop = +new Date(), result = stop - start;
5.3.1 继承的方法
与其他引用类型一样,Date 类型也重写了 toLocaleString()、toString()和 valueOf()方法; 但这些方法返回的值与其他类型中的方法不同。
Date 类型的 toLocaleString()方法会按照与浏览器 设置的地区相适应的格式返回日期和时间。
toString()方法则通常返回带有时区信息的日期和 时间,其中时间一般以军用时间(即小时的范围是 0 到 23)表示。
事实上, toLocaleString()和 toString()的这一差别仅在调试代码时比较有用,而在显示日期和时间时没有 什么价值。
至于 Date 类型的 valueOf()方法,则根本不返回字符串,而是返回日期的毫秒表示。因此,可以 方便使用比较操作符(小于或大于)来比较日期值。
eg:
var date1 = new Date(2007, 0, 1);var date2 = new Date(2007, 1, 1);console.log(date1 < date2); //trueconsole.log(date1 > date2); //false
5.3.2 日期格式化方法
Date 类型还有一些专门用于将日期格式化为字符串的方法,这些方法如下。
- toDateString()——以特定于实现的格式显示星期几、月、日和年;
- toTimeString()——以特定于实现的格式显示时、分、秒和时区;
- toLocaleDateString()——以特定于地区的格式显示星期几、月、日和年;
- toLocaleTimeString()——以特定于实现的格式显示时、分、秒;
- toUTCString()——以特定于实现的格式完整的 UTC 日期。
与 toLocaleString()和 toString()方法一样,以上这些字符串格式方法的输出也是因浏览器 而异的,因此没有哪一个方法能够用来在用户界面中显示一致的日期信息。
5.3.3 日期/时间组件方法
UTC 日期指的是在没有时区偏差的情况下(将日期转换为 GMT 时间) 的日期值。
5.4 RegExp类型
ECMAScript 通过 RegExp 类型来支持正则表达式。使用下面类似 Perl 的语法,就可以创建一个正则表达式。
var expression = / pattern / flags ;
其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。正则表达式的匹配模式支持下列 3 个标志。
- g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即 停止;
- i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
- m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。
因此,一个正则表达式就是一个模式与上述 3 个标志的组合体。不同组合产生不同结果。
eg:
/* 匹配字符串中所有"at"的实例 */ var pattern1 = /at/g;/* 匹配第一个"bat"或"cat",不区分大小写 */ var pattern2 = /[bc]at/i;/* 匹配所有以"at"结尾的 3 个字符的组合,不区分大小写*/ var pattern3 = /.at/gi;
与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:
( [ { \ ^ $ | ) ? * + .]}
这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。
eg:
/* 匹配第一个"bat"或"cat",不区分大小写 */ var pattern1 = /[bc]at/i;/* 匹配第一个" [bc]at",不区分大小写 */ var pattern2 = /\[bc\]at/i;/* 匹配所有以"at"结尾的 3 个字符的组合,不区分大小写 */ var pattern3 = /.at/gi;/* 匹配所有".at",不区分大小写 */ var pattern4 = /\.at/gi;
这些例子都是以字面量形式来定义的正则表达式。另一种创建正则表达式的方式是使用 RegExp 构造函数,它接收两个参数:一个是要匹配的字符串模式,另一个是可选的标志字符串。可以 使用字面量定义的任何表达式,都可以使用构造函数来定义。
eg:
/* 匹配第一个"bat"或"cat",不区分大小写 */ var pattern1 = /[bc]at/i;/* 与 pattern1 相同,只不过是使用构造函数创建的 */ var pattern2 = new RegExp("[bc]at", "i");
在此,pattern1 和 pattern2 是两个完全等价的正则表达式。要注意的是,传递给 RegExp 构造 函数的两个参数都是字符串(不能把正则表达式字面量传递给 RegExp 构造函数)。由于 RegExp 构造 函数的模式参数是字符串,所以在某些情况下要对字符进行双重转义。所有元字符都必须双重转义,那 些已经转义过的字符也是如此,例如\n(字符\在字符串中通常被转义为\\,而在正则表达式字符串中就 会变成\\\\)。
使用正则表达式字面量和使用 RegExp 构造函数创建的正则表达式不一样。在 ECMAScript 3 中, 正则表达式字面量始终会共享同一个RegExp实例,而使用构造函数创建的每一个新RegExp实例都是一个新实例。
ECMAScript 5 明确规定,使用正则表达式字面量必须像直接调用 RegExp 构造函数一样,每次都创 建新的 RegExp 实例。IE9+、Firefox 4+和 Chrome 都据此做出了修改。
5.4.1 RegExp实例属性
RegExp 的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。
- global:布尔值,表示是否设置了 g 标志。
- ignoreCase:布尔值,表示是否设置了 i 标志。
- lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从 0 算起。
- multiline:布尔值,表示是否设置了 m 标志。
- source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。
source 属性保存的是规范形式的字符串,即字面量形式所用的字符串。
5.4.2 RegExp实例方法
(1)exec()
RegExp 对象的主要方法是 exec(),该方法是专门为捕获组而设计的。exec()接受一个参数,即 要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回 null。 返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。其中,index 表示匹配 项在字符串中的位置,而 input 表示应用正则表达式的字符串。在数组中,第一项是与整个模式匹配 的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。
eg:
var text = "mom and dad and baby";var pattern = /mom( and dad( and baby)?)?/gi;var matches = pattern.exec(text);console.log(matches.index);//0console.log(matches.input);// "mom and dad and baby" console.log(matches[0]);// "mom and dad and baby"console.log(matches[1]);// " and dad and baby"console.log(matches[2]);// " and baby"
对于 exec()方法而言,即使在模式中设置了全局标志(g),它每次也只会返回一个匹配项。在不 设置全局标志的情况下,在同一个字符串上多次调用 exec()将始终返回第一个匹配项的信息。而在设 置全局标志的情况下,每次调用 exec()则都会在字符串中继续查找新匹配项。
eg:
var text = "cat, bat, sat, fat";var pattern1 = /.at/;var matches = pattern1.exec(text); console.log(matches.index); //0console.log(matches[0]); //catconsole.log(pattern1.lastIndex); //0 matches = pattern1.exec(text); console.log(matches.index); //0 console.log(matches[0]); //cat console.log(pattern1.lastIndex); //0 var pattern2 = /.at/g;var matches = pattern2.exec(text); console.log(matches.index); //0 console.log(matches[0]); //cat console.log(pattern2.lastIndex); //3
IE 的 JavaScript 实现在 lastIndex 属性上存在偏差,即使在非全局模式下, lastIndex 属性每次也会变化。
(2)test()
test(),它接受一个字符串参数。在模式与该参数匹配的情况下返回 true;否则,返回 false。在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的 情况下,使用这个方法非常方便。因此,test()方法经常被用在 if 语句中
eg:
这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效。
(3)
RegExp 实例继承的 toLocaleString()和 toString()方法都会返回正则表达式的字面量,与创 建正则表达式的方式无关。
eg:
var pattern = new RegExp("\\[bc\\]at", "gi"); alert(pattern.toString()); // /\[bc\]at/gi alert(pattern.toLocaleString()); // /\[bc\]at/gi
即使上例中的模式是通过调用 RegExp 构造函数创建的,但 toLocaleString()和 toString() 方法仍然会像它是以字面量形式创建的一样显示其字符串表示。
正则表达式的 valueOf()方法返回正则表达式本身。
5.4.3 RegExp构造函数属性
RegExp 构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名 (Opera 是例外,它不支持短属性名)。下表列出了 RegExp 构造函数的属性。
RegExp 构造函数的各个属性返回了下列值:
- input 属性返回了原始字符串;
- leftContext 属性返回了单词 short 之前的字符串,而 rightContext 属性则返回了 short
之后的字符串;
- lastMatch 属性返回最近一次与整个正则表达式匹配的字符串,即 short;
- lastParen 属性返回最近一次匹配的捕获组,即例子中的 s。
eg1:
var text = "this has been a short summer";var pattern = /(.)hort/g;/* 注意:Opera 不支持 input、lastMatch、lastParen 和 multiline 属性 ; Internet Explorer 不支持 multiline 属性*/ if (pattern.test(text)){ console.log(RegExp.input); //this has been a short summer console.log(RegExp.leftContext); //this has been a console.log(RegExp.rightContext); // summer console.log(RegExp.lastMatch); //short console.log(RegExp.lastParen); //s console.log(RegExp.multiline); //false}
eg2:
var text = "this has been a short summer";var pattern = /(.)hort/g;/* 注意:Opera 不支持 input、lastMatch、lastParen 和 multiline 属性 ; Internet Explorer 不支持 multiline 属性*/ if (pattern.test(text)){ console.log(RegExp.$_); //this has been a short summer console.log(RegExp["$`"]); //this has been a console.log(RegExp["$'"]); // summer console.log(RegExp["$&"]); //short console.log(RegExp["$+"]); //s console.log(RegExp["$*"]); //false}
除了上面介绍的几个属性之外,还有多达 9 个用于存储捕获组的构造函数属性。访问这些属性的语 法是 RegExp.$1、RegExp.$2...RegExp.$9,分别用于存储第一、第二......第九个匹配的捕获组。在 调用 exec()或 test()方法时,这些属性会被自动填充。
eg:
var text = "this has been a short summer"; var pattern = /(..)or(.)/g; if (pattern.test(text)){ alert(RegExp.$1); //sh alert(RegExp.$2); //t }
5.4.4 模式的局限性
尽管 ECMAScript 中的正则表达式功能还是比较完备的,但仍然缺少某些语言(特别是 Perl)所支 持的高级正则表达式特性。下面列出了 ECMAScript 正则表达式不支持的特性(要了解更多相关信息, 请访问 www.regular-expressions.info)。
- 匹配字符串开始和结尾的\A 和\Z 锚;但支持以插入符号(^)和美元符号($)来匹配字符串的开始和结尾。
- 向后查找(lookbehind);但完全支持向前查找(lookahead)。
- 并集和交集类
- 原子组(atomic grouping)
- Unicode 支持(单个字符除外,如\uFFFF)
- 命名的捕获组;但支持编号的捕获组。
- s(single,单行)和 x(free-spacing,无间隔)匹配模式
- 条件匹配
- 正则表达式注释
5.5 Function类型
函数实际 上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函 数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
1.函 数声明语法定义
function sum (num1, num2) { return num1 + num2; }
2.函数表达式定义函数
var sum = function(num1, num2){ return num1 + num2; };
通过变量 sum 即可以引 用函数。另外,还要注意函数末尾有一个分号,就像声明其他变量时一样。
3.Function 构造函数
var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
Function 构造函数可以接收任意数量的参数, 但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。
从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语 法会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数中的字符串), 从而影响性能。
5.5.1 没有重载(深入理解)
声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。
eg:
var sum = function(a,b){return a+b;}; var othersum = sum; var sum = function(c,d){return c+2*d;}; console.log(sum(10,10)); //30console.log(othersum(10,10));//20
5.5.2 函数声明与函数表达式
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真 正被解释执行。
eg1:
console.log(sum(10,10));function sum(num1, num2){return num1 + num2;}//20
分析:因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升 (function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript 引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后 面,JavaScript 引擎也能把函数声明提升到顶部。
eg2:
console.log(sumn(10,10));var sumn = function(num1, num2){return num1 + num2;};//Uncaught TypeError: sumn is not a function(…)
分析:在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一 行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。
也可以同时使用函数声明和函数表达式,例如 var sum = function sum(){}。 不过,这种语法在 Safari 中会导致错误。
5.5.3 作为值的函数
因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以 像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
eg:
function callSomeFunction(someFunction, someArgument){return someFunction(someArgument);}function add10(num){return num + 10;}var result1 = callSomeFunction(add10, 10);console.log(result1); //20function getGreeting(name){return "Hello, " + name;}var result2 = callSomeFunction(getGreeting, "Nicholas");console.log(result2); //"Hello, Nicholas"
这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。
要访问函数的指针而不执行函数的话,必须去掉函数名后 面的那对圆括号。
假设有一个 对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组 sort()方法的比较函数要接收 两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题, 可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数。
eg:
//从小到大排序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;}};}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
5.5.4 函数内部属性
1.arguments
在函数内部,有两个特殊的对象:arguments 和 this。其中,arguments是一个类数组对象,包含着传入函数中的所有参数。虽然 arguments 的主要用途是保存函数参数, 但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个arguments 对象的函数。
eg:
//经典阶乘函数function factorial(num){if (num <=1) {return 1;} else {return num * factorial(num-1)}}console.log(factorial(5)); //120
定义阶乘函数一般都要用到递归算法;这个函数的执行与函数名 factorial 紧紧耦合在了一起。为 了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee。
eg:
function factorial(num){if (num <=1) {return 1;} else {return num * arguments.callee(num-1)}};console.log(factorial(5)); //120
在这个重写后的 factorial()函数的函数体内,没有再引用函数名 factorial。这样,无论引用 函数时使用的是什么名字,都可以保证正常完成递归调用。
eg1:
function factorial(num){if (num <=1) {return 1;} else {return num * arguments.callee(num-1)}}; var trueFactorial = factorial; factorial = function(){return 0;}; console.log(trueFactorial(5)); //120console.log(factorial(5)); //0
分析:
var trueFactorial = factorial,实际上是在另一个位置上保存了一个函数 的指针。
eg2:
function factorial(num){ if (num <=1) { return 1; } else { return num * factorial(num-1) }}; var trueFactorial = factorial; factorial = function(){return 0;}; console.log(trueFactorial(5)); //0console.log(factorial(5)); //0
2.this
函数内部的另一个特殊对象是 this,其行为与 Java 和 C#中的 this 大致类似。换句话说,this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时, this 对象引用的就是 window)。
eg:
window.color = "red";var o = { color: "blue" };function sayColor(){console.log(this.color);}sayColor(); // redo.sayColor = sayColor;o.sayColor(); // blue
分析:函数 sayColor()是在全局作用域中定义的,它引用了 this 对象。当在全局作用域中调用 sayColor()时,this 引用的是全局对象 window;而当把这个函数赋给对象 o 并调用 o.sayColor() 2 时,this 引用的是对象 o。
函数的名字仅仅是一个包含指针的变量而已。因此,即使是 在不同的环境中执行,全局的 sayColor()函数与 o.sayColor()指向的仍然是同一 个函数。
ECMAScript 5 也规范化了另一个函数对象的属性:caller。除了 Opera 的早期版本不支持,其他 4 浏览器都支持这个 ECMAScript 3 并没有定义的属性。这个属性中保存着调用当前函数的函数的引用, 如果是在全局作用域中调用当前函数,它的值为 null。
eg:
function outer(){inner();};function inner(){alert(inner.caller);};outer();//function outer(){ inner(); }
分析:因为 outer()调用了 inter(),所以inner.caller 就指向 outer()。为了实现更松散的耦合,也可以通过 arguments.callee.caller来访问相同的信息。
eg:
function outer(){ inner();} function inner(){alert(arguments.callee.caller);}outer();// function outer(){ inner(); }
IE、Firefox、Chrome 和 Safari 的所有版本以及 Opera 9.6 都支持 caller 属性。
当函数在严格模式下运行时,访问 arguments.callee 会导致错误。ECMAScript 5 还定义了 arguments.caller 属性,但在严格模式下访问它也会导致错误,而在非严格模式下这个属性始终是 undefined。定义这个属性是为了分清 arguments.caller 和函数的 caller 属性。以上变化都是为 了加强这门语言的安全性,这样第三方代码就不能在相同的环境里窥视其他代码了。
严格模式还有一个限制:不能为函数的 caller 属性赋值,否则会导致错误。
5.5.5 函数属性和方法