JavaScript高级程序设计-7-引用类型1-Object/Array/Date

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

注:虽然引用类型和类看起来相似,但不是一个概念。

对象是某个特定引用类型的实例,新对象是使用new 操作符后跟一个构造函数来创建的,构造函数本身是一个函数,只不过该函数的目的是创建对象。

代码var person = new Object();创建了Object引用类型的一个新实例,然后把该实例保持在变量person中,使用的构造函数是Object,构造函数为新对象定义了默认的属性和方法。ES提供了很多原生引用类型以便开发者使用。

Object类型

Object是ES中使用最多的一个类型,大多数引用类型值都是Object类型的实例。Object实例具备的功能不多,但非常利于存储和传输数据。

创建Object实例的方式有两种,第一个是使用new操作符后跟Object构造函数:

1
2
3
var person = new Object();
person.name = "king";
person.age = 26;

另一个方式是使用对象字面量表示法,对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程,也是日常中使用最多的一种方式:

1
2
3
4
var person = {
name: "king",
age : 26
};

使用对象字面量语法时,属性名可以是数字(会自动转换为字符串),访问时需要用[]语法。同时若留空其花括号,则可以定义只包含默认属性和方法的对象,与使用new Object()相同,但通过对象字面量创建的对象不会调用Object构造函数。

Array类型

除了Object之外,Array数组类型算是最常用的类型了,ES的数组可以存放任意类型的数据,同时大小可以动态调整。创建数组也有两种方式:使用Array构造函数方式和数组字面量方式。

当使用构造函数方式时,需要注意,若只传入一个数字时,其实是指定数组的大小。其他情况下则参数自动被初始化为数组的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a1 = new Array(); // 初始化一个空的数组
console.log(a1.length, a1);

var a2 = new Array(3); // 初始化一个大小为3的数组,3个元素都被初始化为undefined
console.log(a2.length, a2);

var a3 = new Array('king'); // 初始化一个有1个元素的数组,该元素是字符串"king"
console.log(a3.length, a3);

var a4 = new Array(1,2,3); // 初始化一个有三个元素的数组,元素分别为1,2,3
console.log(a4.length, a4);

var a5 = Array(); // 可以省略new操作符
console.log(a5.length, a5);

使用数组字面量的方式则简单很多:

1
2
3
4
5
6
7
8
9
10
11
var a1 = []; // 初始化一个空的数组
console.log(a1.length, a1);

var a2 = [,,,]; // 初始化一个大小为3的数组,3个元素都被初始化为undefined
console.log(a2.length, a2);

var a3 = ['king']; // 初始化一个有1个元素的数组,该元素是字符串"king"
console.log(a3.length, a3);

var a4 = [1,2,3]; // 初始化一个有三个元素的数组,元素分别为1,2,3
console.log(a4.length, a4);

注意:若需要一个确定大小的数组时,不建议使用字面量的方式预设大小,因为不利于阅读,同时浏览器中会有实现差异,有的浏览器会忽略最后一个逗号,有的不会。

数组的length属性是可读写的,通过设置这个属性,可以从末尾移除已有项或详述组中添加新的项,新添加的项用undefined初始化。

1
2
3
4
5
6
7
8
9
10
11
var a1 = [1,2,3]; // 初始化一个有三个元素的数组,元素分别为1,2,3
a1.length = 2;
console.log(a1.length, a1[2]); // 2, undefined

var a2 = [1,2,3]; // 初始化一个有三个元素的数组,元素分别为1,2,3
a2.length = 4;
console.log(a2.length, a2[3]); // 4, undefined

var a3 = [1,2,3]; // 初始化一个有三个元素的数组,元素分别为1,2,3
a3[99] = 99;
console.log(a3.length, a1[98]); // 100, 索引3-98 都为undefined

检测数组

ES3有一个确定某对象是否是数组的经典问题:
对一个页面或全局作用域而言,使用instanceof操作符即可value instanceof Array.但这存在一个问题,即instanceof假定单一的全局执行环境。当页面包含多个iframe时,实际上存在两个以上的不同的全局执行环境,此时就存在了两个以上不同版本的Array构造函数,所以,当从一个框架向另一个传入数组,那么传入的数组与第二个框架中原生创建的数组分别具有不同的构造函数,导致判断错误。

ES5为了解决这个问题,新增了Array.isArray(value)方法,用于检测一个值到底是不是数组,不论在哪个执行环境。

转换方法

数组是一个对象,所以也有toString、toLocalString、valueOf等方法,其中,valueOf返回数组本身,toString返回一个字符串,将元素的字符串形式串联起来,用逗号分割。实际上数组的toString方法会调用数组每一项的toString方法。toLocalString与toString类似。

可以用数组的join()方法来达到同样的效果,其中join接收一个参数作为分隔符。join默认逗号作为分割符,即传入undefined与不传的效果一样。

1
2
3
4
5
var a = [1,2,3,4];
a.join(':'); // "1:2:3:4"
a.join(); //"1,2,3,4"
a.join(undefined); // "1,2,3,4"
a.join(null); // "1null2null3null4"

若数组中的某一项是null或undefined,则在join、toString、toLocalString、valueOf方法的返回结果中以空字符串表示。

栈方法

通过push、pop方法,可以将数组当做栈使用,这期间数组的大小是动态改变的。

队列方法

通过push、pop、shift、unshift方法,可以将数组当做队列使用,这期间数组的大小是动态改变的。

重排序方法

通过reverse、sort方法可以对数组序列重新排列。

其中sort默认是升序排列数组项,sort方法会调用每个数组项的toString方法,然后比较得到的字符串,所以这种策略在对比纯数值、或字符串存在大小写时会出问题。

所以,sort可以接受一个比较函数作为参数,以便指定哪个值放在前面。比较函数接收两个参数,若第一个参数应该放在前面,则返回一个负数,若第一个参数应该放在后面,则返回一个正数,若两者相等,则返回0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function compare(v1, v2){
if(v1 < v2){
return -1;
}else if( v1 > v2){
return 1;
}else {
return 0;
}
}

var a = [0,1,5,10,3,15];
// a.sort(); // [0, 1, 10, 15, 3, 5]
a.sort(compare);
console.log(a); // [0, 1, 3, 5, 10, 15]

reverse和sort的返回值都是进过排序后的数组,若数组元素是数值类型或其valueOf返回的是数值类型的对象类型,则直接返回两数相减即可:

1
2
3
function compare(v1, v2){
return v1 - v2; // 降序
}

操作方法

concat方法

concat方法能连接2个数组,其实是创建一个新数组。具体来说,这个方法会先创建当前数组的一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。

在没有给concat方法传递参数的情况下,它仅仅复制当前数组并返回副本。若给concat传入一个或多个数组,则将这些数组的每一项都添加到新数组中,而非数组参数也会被添加到新数组中。

slice方法

slice方法能基于当前数组的一个或多个项创建新数组,slice方法接受一个或两个参数,分别表示返回项的起始和结束位置。slice方法不会影响到原数组。

在只有一个参数的情况下,slice方法返回从该参数指定位置开始到当前数组末尾的所有项。若有2个参数则返回起始和结束位置之间的项,但不包括结束位置项。

但参数为负数时,则从后算起(用数组长度加上该负数)。若结束位置小于起始位置,则返回空数组。

splice方法

splice方法非常强大,有多种用法,主要用途是向数组中插入项,但也能做到其他效果:

  • 删除,可以删除任意数量的项,指定2个参数,要删除的第一项的位置和要删除的数量,如splice(0,2)会删除数组的头两项。
  • 插入,可以向指定位置插入任意数量的项,指定3个参数,起始位置、0(表示不删除)、插入的新项(若有多个则依次排列即可),如splice(2,0,'red','green')会从数组的位置2开始插入字符串red和green
  • 替换,向指定位置插入任意数量的项,且同时删除任意数量的项,如splice(2,1,'red','green')会从数组的位置2开始删除1项,然后再从位置2开始插入字符串red和green

splice方法始终返回数组,该数组中宝哈你从元素数组中删除的项(若没有删除则返回一个空数组)。

位置方法

ES5中添加了2个数组位置方法,indexOf和lastIndexOf,这两个方法接收两个参数,要查找的项和表示查找起点位置的索引,省略第二个参数时,则indexOf默认从数组开头向后查找,而lastIndexOf反之。

indexOf和lastIndexOf方法使用全等比较,若没有找到则返回-1,否则返回第一个参数所在索引。

迭代方法

ES5为数组添加了5个迭代方法,每个方法都接受2个参数,分别是要在每一项运行的函数和运行该函数的作用域对象(影响this的值,默认为当前调用数组为运行环境),而传入的函数会在运行时接收三个参数:当前数组项的值,该项所在位置索引,数组本身。以下分别是5个迭代方法的作用:

  • every方法,对数组中的每一项运行给定函数,若该函数对每一项都返回true,则返回true
  • filter方法,对数组中的每一项运行给定函数,返回新的数组,新数组的项为该函数会返回true的项
  • forEach方法,对数组中的每一项运行给定函数,无返回值
  • map方法,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
  • some方法,对数组中的每一项运行给定函数,若只要对任意一项函数返回true,则返回true

以上方法不会修改原数组。

缩小方法

ES5还添加了2个缩小数组的方法:reduce和reduceRight,这两个方法也会迭代数组的所有项,然后构建一个最终返回的值(不是数组,而是一个值)。其中,reduce从数组的第一项开始,逐个遍历到最后。而reduceRight则从后向前遍历。

这两个方法都接收2个参数,一个在每一项上调用的函数和初始值(若不指定初始值则默认为数组的第一/最后项)。第一个函数参数在运行接受四个参数,分别是前一项,当前项,当前项的索引,原数组对象。函数的返回值会作为下一次函数运行的第一参数。

对reduce方法,若不指定初始值则第一次迭代发生在数组的第二项上,因为第一个参数是数组的第一项,第二个参数就是数组的第二项。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不指定初始值
var a = [1,2,3,4,5];
var sum = a.reduce(function(prev, crt, idx, array){
return prev + crt;
});
console.log(sum); // 15

// 指定初始值
var a = [1,2,3,4,5];
var sum = a.reduce(function(prev, crt, idx, array){
return prev + crt;
}, 10);
console.log(sum); // 25

Date类型

ES中的Date类型是在早期的java.util.Date类基础上构建的,因此,Date类型基于UTC(Coordinated Universal Time 国际协调时间),从1970年1月1日0时开始经过的毫秒数来保存日期。在这种情况下,Date类型保存的日期能够精确到以1970年1月1日0时为中分前后的285616年。

调用Date构造函数在不传入参数的情况下,新对象默认获取当前的时间和日期,若想根据特定的日期和时间创建日期对象,则必须传入该日期的毫秒数(从1970年1月1日0时开始计算),为了简化计算过程,ES提供Date.parse和Date.UTC方法。

Date.parse方法接收一个表示日期的字符串参数,然后尝试根据字符串返回相应日期的毫秒数,ES并没有定义parse应该支持那种日期格式,因此该方法因实现而异,通常接收如下几种日期格式:

  • 月/日/年
  • 英文月名 日,年
  • 年-月-日T时:分:秒 (ISO 8601扩展格式)
    若传入的字符串不能表示日期,则返回NaN,事实上,若直接将日期字符串传入Date构造函数中后,Date构造函数会在内部调用Date.parse方法。即如下调用方式等价:
    1
    2
    var date = new Date(Date.parse("May 25, 2014"));
    var date = new Date("May 25, 2014");

Date.UTC方法同样返回表示日期的毫秒数,但它与Date.parse方法在构建值时基于不同的信息。Date.UTC的参数分别是年、月(从0开始)、日(从1开始)、时(从0开始)、分、秒、毫秒。这些参数中,年和月参数是必须的,其他默认为最小值。

Date构造函数也可接收Date.UTC的参数,但日期和时间是基于本地时区而非GMT。

ES5添加了Date.now方法,返回表示调用时的时间毫秒数。

1
2
3
4
var start = Date.now();
// ...
var end = Date.now();
var during = end - start;

继承的方法

Date类型重写了toLocalString、toString、valueOf方法,其中toLocalString和toString的结果因浏览器实现不同而异,但总的来说是返回日期的字符串格式,仅在调试时有用,用于显示还是有差异,慎用。valueOf返回日期的毫秒数。

日期格式化方法

Date类型有一些专门将日期格式化为字符串的方法(因浏览器实现不同而异):

  • toDateString 显示星期、月、日、年
  • toTimeString 显示时、分、秒、时区
  • toLocalDateString 显示某地区的星期、月、日、年
  • toLocalTimeString 显示时、分、秒
  • toUTCString 显示完整的UTC日期

日期/时间组件方法

Date还提供了一些获取/设置日期值中特定部分的方法: 日期/时间组件方法