本章内容:

引用类型的值(对象)是引用类型的一个实例。在ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。

它也常被称为类,但这种称呼并不妥当。尽管ECMAScript从技术上讲是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构。

引用类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。

虽然引用类型与类看起来相似,但它们并不是相同的概念。

对象是某个特定引用类型的实例。ECMAScript提供了很多原生引用类型(例如Object),以便开发人员用以实现常见的计算任务。

5.1 Object类型

创建Object实例的方式有两种。第一种是使用new操作符后跟Object构造函数。另一种是使用对象字面量表示法

左边的花括号{表示对象字面量的开始,因为它出现在了表达式上下文中。同样的花括号,如果出现在一个语句上下文中,例如跟在if语句条件的后面,则表示一个语句块的开始。

在对象字面量中,使用逗号来分隔不同的属性,最后一个属性后面添加分号。

在通过对象字面量定义对象时,实际上不会调用Object构造函数。对象字面量也是想函数传递大量可选参数的首选方式。

一般来说,访问对象属性时使用的都是点表示法,这也是很对面向对象语言中通用的语法。不过,在JavaScript也可以使用方括号表示法来访问对象的属性。使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。

两种访问对象属性的方法没有任何区别。但方括号语法的主要优点是可以通过变量来访问属性。如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法。例如有空格的情况

通常,除非必须使用变量来访问属性,否则我们建议使用点表示法。

5.2 Array类型

ECMAScript的数组与其他多数语言中的数组有着相当大的区别。与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据。并且大小也是可以动态调整的。

创建数组的的基本方式有两种。第一种是使用Array构造函数。另外,在使用Array构造函数时也可以省略new操作符。第二种基本方式是使用数组字面量表示法。

对于类似这样的数组var values = [1,2,];IE8及之前版本中的ECMAScript实现在数组字面量方面存在bug。由于IE的实现与其他浏览器不一致,因此我们强烈建议不要使用这种语法。

与对象一样,在使用数组字面量表示法时,也不会调用Array构造函数

数组的length属性很有特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。

数组最多可以包含4294967295个项。超过这个上限值,就会发生异常。而创建一个初始大小接近的数组,则可能会导致运行时间超长的脚本错误。

5.2.1 检测数组

自从ES3做出规定以后,就出现了确定某个对象是不是数组的经典问题。

    if(value instanceof Array){}

instanceof操作符的问题在于,它假定只有一个全局执行环境。

为了解决这个问题,ECMAScript新增了Array.isArray()方法。这个方法的目的是最终确定某个值是不是数组,而不管它是在哪个全局执行环境中创建的。支持Array.isArray()方法的浏览器有IE9+

5.2.2 转换方法

如前所述,所有对象都具有toLocaleString(),toString()和valueOf()方法。由于alert()要接受字符串参数,它会在后台调用toString()方法。

数组继承的toLocaleString(),toString()和valueOf()方法,在默认情况下都会以逗号分隔字符串的形式返回数组项。使用join()方法,可以使用不同的分隔符。

5.2.3 栈方法

ECMAScript为数组专门提供了push()和pop()方法。实现类似栈的行为。此方法有返回值。

push()方法可以接受任意数量的参数。可以将栈方法与其他数组方法连用。

5.2.4 队列方法

栈数据结构的访问规则是后进先出,而队列数据结构的访问规则是先进先出。

由于push()是向数组末端添加项的方法,因此要模拟队列只需一个从数组前端取得项的方法。实现这一操作的数组就是shift()。结合使用shift()和push()方法,可以像使用队列一样使用数组。

unshift()和shift()的用途相反:在数组前端添加任意个项并返回新数组的长度。

    var colors = new Array();
    var count = colors.unshift("red","green");
    count = colors.unshift("black");
    console.log(colors);

在调用pop()方法时,移除并返回的是最后一项,即”green”。

5.2.5 重排序方法

数组中已经存在两个可以直接用来重排序的方法:reverse()和sort()。reverse()方法会反转数组项的顺序。

sort()方法会调用每个数组项的toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。

sort()方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值的前面。如果第一个参数应该位于第二个之前则返回一个负数。

5.2.6 操作方法

ECMAScript为操作已经包含在数组中的项提供了很多方法。其中concat()方法可以基于当前数组中的所有项创建一个数组。如果传递给从concat()方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中。

slice()方法可以接收一或两个参数,即要返回项的起始和结束位置。它是基于当前数组中的一或多个项创建一个新数组。比如slice(1,4)包括1但不包括4

splice()方法可以删除,插入,替换等功能。splice()方法始终都会返回一个数组。

5.2.7 位置方法

ES5为数组实例添加了两个位置方法:indexOf()和lastIndexOf()。这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。在比较时会使用全等操作符。IE8不支持。

5.2.8 迭代方法

ES5为数组定义了5个迭代方法。every(),filter(),forEach(),map(),some()。

every()和some()都用于查询数组中的项是否满足某个条件。

map()也返回一个数组,而这个数组的每一项都是在原始数组中的对应项上运行传入函数的结构。适合创建包含的项与另一个数组一一对应的数组。

filter()函数,它利用指定的函数确定是否在返回的数组中包含某一项。

forEach(),它只是对数组中的每一项运行传入的函数。这个方法没有返回值。

这些数组方法通过执行不同的操作,可以大大方便处理数组的任务。IE不支持。

5.2.9 归并方法

ES5还新增了两个归并数组的方法:reduce()和reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。支持这两个归并函数的浏览器有IE 9+,Firefox 3+,Safari 4+,Opera 10.5和Chrome

5.3 Data 类型

要创建一个日期对象,使用new操作符和Date构造函数即可,如下所示:

var now = new Date();

在调用Date()构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象必须传入表示该日期的毫秒数,为了简化这一计算过程,ECMAScript提供了两种方法:Date.parse()和Date.UTC()

Date.parse()方法接受一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。这个方法的行为因实现而异,而且通常是因地区而异。如果传入Date.parse()方法的字符串不能表示日期,那么它会返回NaN。

如果直接将表示日期的字符串传递给Date构造函数,也会在后台调用Date.parse()

Date.UTC()有两个必须的参数,分别是年份,基于0的月份。小时必须设置为17(因为小时以0到23表示)。

如同模仿Date.parse()一样,Date构造函数也会模仿Date.UTC(),但有一点明显不同:日期和时间都基于本地时区而非GMT来创建。

ES5添加了Date.now()方法,返回表示调用这个方法时的日期和时间的毫秒数。IE 9+

5.3.1 继承的方法

与其他引用类型一样,Date()类型也重写了toLocaleString(),toString()和valueOf()方法;

5.3.2 日期格式化方法

Date类型还有一些专门用于将日期格式化为字符串的方法。与toLocaleString和toString()方法一样,以上这些字符串格式方法的输出也是因浏览器而异的,因此没有哪一个方法能够用来在用户界面中显示一致的日期信息。

日期/时间组件方法

剩下还未介绍的Date类型的方法,都是直接取得和设置日期值中特定部分的方法了。需要注意的是UTC日期指的是在没有时区偏差的情况下(将日期转换为GMT时间)的日期值。

5.4 RegExp类型

ECMAScript通过RegExp类型来支持正则表达式。每个正则表达式都可带有一或多个标志,用以标明正则表达式的行为。

正则表达式的匹配模式支持下列3个标志:

g:表示全局模式

i:表示不区分大小写模式

m:表示多行模式

因此,一个正则表达式就是一个模式与上述3个标志的组合体。

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。

一般都是以字面量形式来定义的正则表达式。另一种创建正则表达式的方式是使用RegExp构造函数。如:var pattern2 = new RegExp("at","i");

ECMAScript5明确规定,使用正则表达式字面量必须像直接调用RegExp构造函数一样,每次都创建新的RegExp实例。

5.4.1 RegExp实例属性

RxgExp的每个实例都具有下列属性。通过这些属性可以获知一个正则表达式的各方面信息,但却美欧多大用处,因为这些信息全都包含在模式声明中。

5.4.2 RegExp 实例方法

RegExp对象的主要方法是exec(),该方法是专门为捕获组而设计的。exec()接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回null。

返回的数组虽然是Array的实例,但包含两个额外的属性:index和input。其中,index表示匹配项在字符串中的位置,而input表示应用正则表达式的字符串。

正则表达式的valueOf()方法返回正则表达式本身。

5.4.3 RegExp构造函数属性

RegExp构造函数包含一些属性(这些属性在其他语言中被看成静态属性)。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名。

5.4.4 模式的局限性

尽管ECMAScript中的正则表达式功能还是比较完备的,但仍缺少某些语言(特别是Perl)所支持的高级正则表达式特性。比如正则表达式注释等。

即使存在这些限制,ECMAScript正则表达式仍然是非常强大的,能够帮我们完成绝大多数模式匹配任务。

5.5 Function 类型

说起来ECMAScript中什么最有意思,我想那莫过于函数了——而有意思的根源,则在于函数实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性he方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针。

函数表达式末尾有一个分号,就像声明其他变量时一样。

由于函数名仅仅是指向函数的指针,一个函数可能会有多个名字。注意,使用不带圆括号的函数名是访问函数指针,而非调用函数。

5.5.1 没有重载(深入理解)

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

通过观察重写之后的代码,可知,在创建第二个函数时,实际上覆盖了引用第一个函数的变量

5.5.2 函数声明和函数表达式

解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用;

至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。

5.5.3 作为值的函数

因为ECMAScript中的函数本身就是变量,所以函数也可以作为值来使用。

5.5.4 函数内部属性

在函数内部,有两个特殊的对象:arguments和this。前者包含着传入函数中的所有参数。虽然arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。

请看下面这个非常经典的阶乘函数:

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

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

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

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

函数内部的另一个特殊对象是this,其行为与Java和C#中的this大致类似。换句话说,this引用的是函数据以执行的环境对象——或者也可以说是this值。来看下面的例子:

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

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

    sayColor();//"red"

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

上面这个函数sayColor()是在全局作用域中定义的,它引用了this对象。由于在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中引用不同的对象。当在全局作用域中调用sayColor()时,this引用的是全局对象window;换句话说,对this.color求值会转换成对window.color求值,于是结构就返回”red”。而当把这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o,因此对this.color求值会转换成对o.color求值,结果就返回了”blue”。

ECMAScript5页规范化了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。