第5章:引用类型(上)

引用类型的值(对象)是引用类型的一个实例。在ECMAScript中,引用类型是一种数据结构,用于将功能和数据结合在一起。它也称为类,但是这种称呼并不妥当。尽管ECMAScript从技术上是一门面向对象的语言,但是它不具备传统的面向对象语句所支持的类和接口等基本结构。引用类型有时候也称为对象定义,因为它们的描述是一类对象所具有的属性和方法。

虽然引用类型与类看起来很像,但是并不是相同的概念。为了避免混淆,本书将不使用这个概念。

如前所述,对象是某个特定引用类型的实例。新对象使用new操作符后跟一个构造函数来创建的。构造函数本身就时Object,它只为新定义了默认的属性和方法。ECMAScript提供了很对原生引用类型(例如Object),以便开发人员用以实现常见的计算任务。

var person = new Object();

这行代码创建了Object引用类型的一个新的实例,然后将实例保存在了变量person中。使用的构函数是Object,它只为新对象定义了默认的属性和方法。ECMAScript提供很对原生引用类型(例如Object),以便开发人员用以实现常见的计算任务。

5.1 Object类型

到目前为止,我们看到的大多数引用类型的值都是Object类型的实例;而且,Object也是ECMAScript中使用最多的一种类型。虽然Object的实例不具备多少功能,但是对于在应用程序中存储和传输数据而言,它们确实是非常理想的选择。

创建Object实例对象的方式有两种。第一种是使用new操作符后跟new操作符后跟Object构造函数,如下面所示:

var person = new Object();
person.name = "Nicholas";
person.age = 20;

另一种方式是使用对象字面量的表示方法。对象字面量是对象定义的一种简写形式,目前在于简化创建包含大量属性的对象的过程。

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

在这个例子中,左边的花括号表示({)表示对象字面量的开始,因为它出现在表达式上下文中。ECMAScript中的表达式上下文指示能够返回一个值(表达式)。赋值操作符表示后面是一个值,所以左边花括号在这里在这里表示表达式的开始。同样的花括号,如果出现在一个语句上下文,例如if语句条件的后面,则表示语句块的开始。

在使用对象字面量的语法时候,属性名也可以使用字符串,如下面这个例子所示:

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

这个例子会创建一个对象,包含三个属性:name,age,5;但是这里的的数值属性会被转换为字符串。

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

var person = {};
person.name = "Nicholas";
person.age = 29;

在通过对象字面量定义对象时,实际上不会调用Object()构造函数。

虽然可以使用前面介绍的任何一种方法来定义对象,但是开发人员更加青睐字面量语法,因为这种语法要求代码量很少,而且能够给人封装数据的感觉。实际上,对象字面量也是像函数传递大量可选参数的首选方式,例如;

function displayInofo(args){
    var output = "";
    if(typeof args.name == "string"){
        output += "Name: " + args.name + "\n";
    }
    if(typeof args.age == "number"){
        output += "Age: " + args.age + "\n";
    }
    alert(output);
}
displayInfo({
    name : "Nicholas",
    age : 29
});

在这个例子中,函数displayInfo()接受一个名为args的参数。这个参数可能带有一个名为name或者age属性,也可能这个两个属性都有或者都没有。在这个函数内部,我们通过typeof运算符来检测每个属性是否存在,然后再基于相应的属性来构建一条要显示的信息。然后,饿哦们调用了两次这个函数,每次都使用一个对象字面量来执行不同的数据。这两次调用传递的参数虽然不同,但是函数都能正常执行。

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

alert(person["name"]);  // "Nicholas"
alert(person.name); // "Nicholas"

从功能上来看,这两种访问对象属性的方法没有任何区别。但是方括号语法的主要优点是可以通过变量来访问属性,例如:

var propertyName = "name";
alert(person[propertyName]);

如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或者保留字,也可以使用方括号表示法。例如:

person["first name"] = "Nicholas";

由于“first name”中包含一个空格,所以不能使用点好表示法来访问它。然而属性名中可以包含非字母非数字的,这时候可以使用方括号来访问它们。

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

5.2 Array类型

除了Object之外,Array类型恐怕是ECMAScript中最常用的类型了。而且,ECMAScript数组与其他语言的数组有相当大的区别。虽然ECMAScript数组与其他语言中的数组都是数据的有序列表,但是与其他语言不同的是,ECMAscript数组的每一项可以保存任何类型的数据。也就是说,可以用数组的第一个位置来保存字符串,用第二个位置来保存数值,用第三个位置来保存对象,一次类推。而且ECMAScript中数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。

创建数组的基本方法有两种。第一种是使用Array构造函数,如下面的代码所示:

var colors = new Array();

如果要知道数组要保存的项目数量,也可以给构造函数传递数量,而该数量会自动变成length属性的值。例如,下面的代码将创建length的值为20的数组。

var colors = new Array(20);

当然,给构造函数传递一个值也可以创建数组。但是这时候问题就复杂一点了。因为如果传递的是数值,则会按照数值创建包含给定项数的数组;而传递的是其他类型的参数,则会创建包含那个值的只有一项的数组。下面就两个例子:

var colors = Array(3);      // 创建一个包含3项的数组
var names = Array("Greg");  // 创建一个包含一项,即字符串"Greg"的数组

另外,在使用Array构造函数时也会省略new操作符结果相同:

var colors = Array(3);
var names = Array("Greg");

数组创建的第二种方式是使用数组字面量表示法。数组字面量是由一对方括号表示,多个数组之间用逗号隔开,如下所示:

var colors = ["red","blue","green"];
var names = [];
var values = [1,2,] // 不建议这样使用,这样会创建一个包含2或者3项数组
var options = [,,,,,];

以上代码的第一行创建了一个包含3个字符串数组。第二行使用一对方括号创建了一个空数组。第三行展示了在数组字面量最后一项添加逗号的结果:在IE中,values会成为一个包含3个项且值分别为1和2的数组。原因是IE8以及之前版本中的ECMAScript实现在数组字面量存在bug。由于这个bug导致另外一种情况如最后一行代码所示,该行代码可能会创建5项的数组,也可能创建包含6项的数组。在像这种情况下,每一项都将获得undefined值;这个结果与调用Array构造函数时创建项数在逻辑上是相同的。但是由于IE的实现和其他浏览器不一致,因此我们强烈建议不要使用这种语法。

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

在读取和设置数组的值时,要使用方括号并提供相应值的基于0的数字索引,如下所示:

var colors = ["red","blue","green"];
alert(colors[0]);
colors[2] = "black";
colors[3] = "brown";

方括号中的所用表示要访问的值。如果索引小于数组中的项数,则返回对象项目的值,就像这个例子中的colors[0]会显示”red”值。设置数组的值也使用相同的语法,但是会替代指定位置的值。如果设置某个值的索引超过了数组现有的项数,如这个例子中的colors[3]所示,数组就会自动增加到该索引值加1的长度。

数组的项数保存在其length属性中,这个属性始终会返回0或者更大的值,如下面这个例子所示:

var colors = ["red","blue","green"];
var names = [];
alert(colors.length);       // 3
alert(names.lneght);        // 0

数组的length属性很有特点–它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或者项数组中添加新的项。请看下面这个例子:

var colors = ["red","blue","green"];
colors.length = 2;
alert(colors[3]);   // undefined

这个例子中的数组colors一开始有3个值。将其length属性设置为2会移除最后一项,结果再访问colors[2]就会显示undefined了。如果将其length属性设置为大于数组项的数,则新增加的每一项都会取得undefined值,如下所示:

var colors = ["red","blue","green"];
colors.length = 4;
alert(colors[3]);

在此,虽然colors数组中包含3项,但是把它的length属性设置为4。这个数组不存在位置3,所以访问这个位置的值就会得到undefined。

利用length属性也可以方便在数组尾部添加新项,如下所示:

var colors = ["red","blue","green"];
colors[colors.length] = "black";
colors[colors.length] = "brown";

由于数组最后一项的索引是length -1,因此下一个新项位置就是length。每当数组末尾添加一项后,其length属性会自动更新反映这一变化。换句话说,上面的例子中的第二行中的colors[colors.length]为位置3添加一个值,最后一行的colors[colors.length]则为位置4添加了一个值。当把一个值放在超出当前数组大小的位置上时,数组就冲重新计算其长度值,即长度值等于最后一项的索引添加1,如下面的例子所示:

var colors = ["red","blue","green"];
colors[99] = "black";
alert(colors.length); // 110

在这个例子中,我们向colors数组的位置99插入一个值,结果数组新长度就是100。而位置3到位置98实际上是不存在的,所以访问它们都将返回undefined。

5.2.1 检测数组

自从ECMAScript3做出规定之后,就出现了确定某个对象是不是数组的经典问题。对于一个网页,或者一个全局作用域来说,使用instanceof操作符就能得到满意的结果:

if(value instanceof Array){
    // 对数组执行某些操作
}

instanceof操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架实际上就存在两个以上不同版本的Array构造函数。如果你从一个框架向另外一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

为了解决这个问题,ECMAScript5新增加Array.isArray()方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它在哪个全局执行环境中创建的。这个方法用法如下:

if(Array.isArray(value)){
    // 对数组执行某些操作
}

支持Array.isArray()方法的浏览器有IE9+、FireFox 4+、Safari5+、Oper10.5+和chrome。

5.2.2 转换方法

如前面所述,所有对象都具有toString()、toString()和valueOf()方法。其中,调用数组的toString()方法会返回素组中每个值的字符串形式连接而成的一个逗号分隔符。而调用valueOf()返回的还是数组。实际上,为了创建整个字符串会调用数组每一项的toString()方法。来看下面的例子:

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

在这里,我们首先显式的调用了toString()方法,以便返回数组的字符串表示,每个值的字符串表示拼接成了一个字符串,中间以逗号分隔。接着调用valueOf()方法。而最后一行代码直接将数组传递给了alert()。由于alert()要接受字符串参数,所以它会在后台调用toString()方法,由此会得到与直接调用toString()方法相同的结果。

另外,toLocaleString()方法经常也会返回与toString()和valueOf()方法相同的值,但是也不总是如此。当调用数组的toLocaleString()方法时,它也会创建一个数组值的以逗号分隔的字符串。而与前面两个方法唯一的不同之处在于,这一次为了取得每一项的值,调用的每一项的toLocaleString()方法,而不是toString()方法。请看下面的例子:

var person1 = {
    toLocaleString : function(){
        return "Nicholas";
    },
    toString : function(){
        return "Nicholas";
    }
};
var person2 = {
    toLocaleString : function(){
        return "grigorios";
    },
    toString : function(){
        return "Greg";
    }
};
var person = [person1,person2];
alert(person);      // Nicholas,Greg
alert(person.toString());   // Nicholas,Greg
alert(person.toLocaleString()); // Nicholas,Grigorios

我们在这里定义了两个对象:person1和person2。而且还分别为每个对象定义了一个toString()方法和toLocaleString()方法,这两个方法返回不同的值。然后,创建一个包含前面定义的两个对象的数组。在将数组传递给alert()时,输出结果是”Nicholas,Greg”,因为调用了数组每一项的toString()方法。而当调用数组的toLocaleString()方法的时候,输出结果是”Nicholas,Grigorios”,原因是调用数组每一项的toLocaleString()方法。

数组继承toLocaleString()、toString()和valueOf()方法,在默认情况下都会以逗号分隔符的字符串形式返回。而如果使用join()方法,则可以使用不同分隔符来构建这个字符串。join()方法只接受一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串,请看下面的例子:

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

在这里,我们使用join()方法重现了toString()方法输出。在传递逗号的情况下,得到了一个逗号分隔的值。在最后一行代码中,我们传递了双竖线符号,结果就得到了字符串”red||green||blue”。如果不给join()方法传入任何值,或者给他传入undefined,则会使用逗号分隔符。

5.2.3 栈方法

ECMAScript数组提供了一种让数组的行为类似于其他数据结构的方法。具体来说,数据可以像栈一样,后者是一种可以限制插入和删除项的数据结构。栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构。也就是最新添加的项最早被移除。而栈中的插入(叫做推入)和移除(叫做弹出),只发生在一个位置–栈的顶部。ECMAScript为数组专门提供了push()和pop(0方法,以便实现类似栈的行为。

push()方法可以接受任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而pop()方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项。请看下面的例子:

var colors = new Array();
var count = colors.push("red","green");
alert(count);   // 2

count = colors.push("black");
alert(count);   // 3

var item = colors.pop();
alert(item);    // "black"
alert(colors.length);   // 2

以上代码中的数组可以看成是栈。首先,我们使用push()两个字符串推入数组末尾,并将返回的结果保存在变量count中。然后再推入一个值,而结果仍然保存在count中。因此此时的数组中包含3项,所以push()返回3。在调用pop()时,它会返回数组的最后一项,即字符串”black”。此后数组仅剩下两项。

可以将栈方法与其他方法连用,像下面的例子:

var colors = ["red","blue"];
colors.push("brown");
colors[3] = "black";
alert(colors.length);   // 4
var item = colors.pop();    //取得最后一项
alert(item);

在此,我们首先用两个值来初始化一个数组。然后使用push()添加第三个值,再通过直接在位置3上赋值来添加第四个值。而在调用pop()时候,该方法返回了字符串”black”,即最后一个添加到数组的值。

5.2.4 队列方法

栈数据结构的访问规则是LIFO(后进先出),而队列数据结构的访问规则是FIFO(First-In-First-Out,先进先出)。队列在列表的前端移项。由于push()是想数组末端添加项的方法,因此要模拟队列只需要一个从数组前端取得项的方法。实现这一操作的数组方法是shift(),它能够移除数组中的第一个项并返回该项,同时将数组长度减1。结合使用shift()和push()方法,就可以像使用队列一样使用数组。

var colors = new Array();   // 创建一个数组
var count = colors.push("red","green"); // 推入两项
alert(count);   // 2

count = colors.push("black");   // 推入另外一项
alert(count);   // 3

var item = colors.shift();
alert(item);    // "red"
alert(colors.length);   // 2

这个例子首先使用了push()方法创建了一个包含3中颜色名称的数组。代码中加粗的哪一行使用了shift()方法从数组中取得第一项,即”red”。在移除第一项之后,”green”即变成了第一项,而”black”则变成了第二项,数组也只包含两项了。

ECMAScript还为数组提供了一个unshift()方法。顾名思义,unshift()与shift()的用途相反:它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用unshift()和pop()方法,可以从相反的方向模拟队列,即在数组的前端添加项,从数组末端移除项。如下面的例子所示:

var colors = new Array();   // 创建一个数组
var count = colors.unshift("red","green");
alert(count);   // 2

count = colors.unshift("black");    // 推入另一项
alert(count);   // 3

var item = colors.pop();    // 取得数组最后一项
alert(item);    // "green"
alert(colors.length);   // 2

这个例子创建了一个数组并使用unshift()方法先后推入了3个值。首先是“red”和“green”,然后是”black”,数组中各个项的顺序为”black”,“red”,“green”。在调用pop()方法时,移除并返回的是最后一项,即”green”。

5.2.5 重排序方法

数组中已经存在两个可以直接用来重排序的方法:reverse()和sort()。有读者可能猜到了,reverse()方法会翻转数组项的顺序。请看下面的例子。

var values = [1,2,3,4,5];
values.reverse();
alert(values);  // 5,4,3,2,1

这里数组的初始值顺序是1,2,3,4,5。而调用数组的reverse()方法后,其值的顺序变成了5,4,3,2,1。这个方法的作用相当直观明了,但是不够灵活,因此才有sort()方法。

在默认情况下,sort()方法按照升序排列数组项–即最小的值位于最前面,最大值排在最后面。为了实现排序,sort()方法会调用每个数组项的toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串,如下所示:

var values = [0,1,5,10,15];
values.sort();
alert(values);  // 0,1,10,15,5

可见,即使例子中的值的顺序没有问题,但是sort()方法也会根据测试字符串的结果改变原来的顺序。因为5虽然小于10,但是在进行字符串比较的时候,“10”则位于”5”的前面,于是数组的顺序就被修改了。不用说,这种排序方式在很多情况下都不是最佳方案。因此,sort()方法可以接受一个比较函数作为参数,以便我们指定那个值位于那个值的前面。

比较函数接受两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等,则返回0,如果第一个参数应该位于第二个之后则返回一个正数。以下是一个简单的比较 函数:

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

这个比较函数可以适用于大多数数据类型,只要将其作为参数传递给sort()方法即可,如下面这个例子所示:

var values = [0,1,5,10,15];
values.sort(compare);
alert(values);

在将比较函数传递到sort()方法之后,数值仍然保持了正确的升序。当然,也可以通过比较函数降序排序的结果,只要交换比较 函数的值即可。

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

var values = [0,1,5,10,15];
values.sort(compare);
alert(values);  // 15,10,5,1,0

在这个修改后的例子中,比较函数在第一个值应该位于第二个之后的情况下返回1,而在第一个值应该在第二个之前的情况下返回-1。交换返回值的意思是让更大的值排位更靠前,也就是对数组按照降序排序。当然,如果只想翻转数组原来的顺序,使用reverse()方法更快一些。

reverse()和sort()方法的返回值是经过排序后的数组。

对于数值类型或者valueOf()方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。这个函数只要用第二个值减第一个值即可。

function compare(value1,value2){
    return value1 -value2;
}

由于比较函数通过返回一个小于零、等于零或者大于零的值来影响排序结果,因此减法操作就可以适用当地处理所有这些情况。

猜你喜欢

转载自blog.csdn.net/s_a_g_e/article/details/80335420