JavaScript 引用类型

2021-02-19 06:20

阅读:434

标签:自动   pre   some   default   模式   创建过程   pop   形式   排列   

Attitude is everything.

引用类型

在 ECMAScript 中,引用类型是一种数据结构,用于将数据和功能组织在一起。在其他面向对象的语言中,这种数据结构通常被称为类,但 ECMAScript 不具备类和接口等传统的面向对象语言的基本结构。
引用类型也称为对象定义,它描述了一类对象所具有的属性和方法,引用类型的值(对象)是引用类型的一个实例。新对象由 new 操作符后跟一个构造函数创建。
ECMAScript 提供了很多原生引用类型。

1. Object 类型

创建 Object 实例的方法有两种:第一种是使用 Object 构造函数 new Object(),第二种是使用对象字面量表示法。

var person = {
    name : "tom",
    age : 20
}

这种表示法简化了包含大量属性的对象的创建过程,使用这种方法定义对象不会调用 Object 构造函数。

一般访问对象属性使用的是点表示法,在 JavaScript 中也可以使用方括号表示法来访问,使用方括号语法的优点是可以通过变量来访问属性,并且属性名可以包含特殊字符、关键字或者保留字,例如:

var propertyName = "name";
alert (person[propertyName]);
person["last name"] = "Ben";
person["default"] = "hello";

2. Array 类型

Array 类型是除了 Object 之外 ECMAScript 中最常用的类型,并且它与其它语言的数组区别很大。ECMAScript 的数组每一项可以保存任意类型的数值,且数组的大小是可以动态调整的。
创建数组的方法有两种:第一种是使用 Array 构造函数,new 操作符可以省略

new Array()
new Array(20) // 创建 length 为 20 的数组
new Array("a","b","c") // 创建包含三个字符串的数组

第二种是使用数组字面量表示法,这种方法不会调用 Array 构造函数

var color = ["red","blue","green"];
var values = [1,2,]  // 这样会创建包含 2 或 3 项的数组,因浏览器版本不同而不同,不建议这样写

在读取或者写入数组的值时,使用方括号并提供数字索引,如果索引值超过了数组现有项数,数组会自动增加到索引值 + 1 的长度,新增的项都是 undefined 值。
数组的项数保存在其 length 属性中,这个属性是可读写的而非只读的,因此可以通过修改它来移除末尾项,或在末尾新增项,新增的项都是 undefined 值。
因为 arr.length 总是比数组最后一项的索引大 1 ,所以总是可以使用 arr[arr.length] 来给数组末尾添加一个新项。
数组最多可以保存 2^32 - 1 个项。

2.1 检测数组

可以使用 instanceof 操作符来确定某个对象是不是数组,例如:arr instanceof Array。但如果网页中包含多个框架,从而存在多个 Array 构造函数,那么从另一个框架传入的数组与当前框架原生的数组就具有不同的构造函数。
为此,ECMAScript 5 新增了 Array.isArray() 方法,不管数组在哪个全局执行环境中创建的,都能确定某个对象是不是数组。

2.2 转换方法

所有对象都具有 toLocaleString()、toString() 和 valueOf() 方法:

  • toLocaleString():以当地的语言返回对象的字符串表示;
  • toString():返回对象的字符串表示;
  • valueOf():返回对象最合适的原始值(字符串、数值或布尔值)表示。

调用 valueOf() 返回的还是数组本身,而调用 toString() 方法通过调用数组每一项的 toString() 方法,得到数组中每个值的字符串形式,最后返回由这些字符串拼接而成的,一个以逗号分隔的长字符串。
toLocaleString() 与 toString() 方法的方法唯一不同之处是,在获取数组每一项的值时,调用的是 toLocaleString() 方法,而不是 toString() 方法。
以上三种方法都会以逗号为分隔符返回数组项,如果使用 join() 方法,则可以使用不同的分隔符来构建返回的字符串。join() 方法只接受一个用作分隔符的字符串,返回包含所有数组项的字符串。
如果数组的某一项是 null 或者 undefined,那么以上方法的返回结果中以空字符串表示。

2.3 栈方法

ECMAScript 数组提供一种让数组的行为类似于栈的方法。栈是一种后进先出的数据结构,ECMAScript 为数组提供了 push() 和 pop() 方法来实现类似栈的行为。
push() 方法可以接收任意数量的参数,把它们逐个添加到数组末尾,返回修改后数组的长度。pop() 方法则从数组末尾移除最后一项,减少数组 length 值,然后返回移除的项。

2.4 队列方法

栈的访问规则是后进先出,而队列的访问规则是先进先出。由于 push() 是向数组末端添加项的方法,因此要模拟队列只需一个从数组前端取得项的方法,这个方法就是 shift(),它能移除数组中的第一个项并返回该项,同时将数组长度减 1。
ECMAScript 还为数组提供了一个 unshift() 方法,与 shift() 方法相反,它能在数组前端添加任意个项并返回新数组的长度。

2.5 重排序方法

reverse() 方法和 sort() 方法可以直接用来重排序。reverse() 方法会反转数组项的顺序,而 sort() 方法默认按升序排列数组项。
sort() 方法会调用每一项的 toString() 方法,然后比较得到的字符串。在使用 sort() 方法排序数字时,如果不传入参数,则采用默认的升序排序策略,此时"10"会排在"5"的前面,这显然不符合人们的思维习惯。因此,sort() 方法可以接收一个比较函数作为参数,用于指定排序策略。
通过比较函数,我们可以让 sort() 方法返回符合思维习惯的数值升序或降序排序。比较函数需要接收两个参数,如果第一个参数应该排序在第二个参数之前,则比较函数返回一个负数,两参数相等返回 0,否则返回一个正数。

2.6 操作方法

ECMAScript 为操作已经包含在数组中的项提供了很多方法。
concat() 方法可以基于当前数组创建一个新数组,它会先创建一个当前数组的副本,将接受到的项添加到新数组的末尾,并返回新数组。
如果没有给 concat() 方法传递参数,则返回一个副本。如果传递的是一个或多个数组,则会把每一项都添加到新数组中。

slice() 方法能基于当前数组的一个或多个项创建一个新数组,它接受一个或两个参数,即返回项的开始和结束位置。它不会影响原始数组。
splice() 方法非常强大,它的主要用途是向数组中间插入项,使用方式有以下三种:

  • 删除:可以删除任意个项,指定 2 个参数,起始位置和删除项数;
  • 插入:可以向指定位置插入任意个项,指定 3 个参数,起始位置、0(要删除的项数)和要插入的项,如果要插入多个项,则继续传入第 4 个,第 5 个参数即可;
  • 替换:可以向指定位置插入日益个项,并且删除任意个项,指定 3 个参数,起始位置、要删除的项数和要插入的项,删除的项数不必与插入的项数相等。
    splice() 方法返回一个包含被删除的项的数组,如果没有删除,则返回空数组。

2.7 位置方法

ECMAScript 5 为数组实例添加了两个位置方法:indexOf() 和 lastIndexOf(),这两个方法都接收两个参数:要查找的项和查找起点位置的索引(可选)。indexOf() 方法从数组的开头开始查找,lastIndexOf() 从数组末尾开始查找。
他们都返回要查找的项在数组中的位置,没找到则返回 -1。在比较时使用全等操作符,因此要求查找的项严格相等。

2.8 迭代方法

ECMAScript 5 为数组定义了 5 个迭代方法,每个方法都接受两个参数:要在每一项上运行的函数,和运行该函数的作用域对象(可选),这个传入的作用域对象会影响 this 的值。
这些迭代方法中传入的函数,会接收三个参数:该数组项的值,该项在数组中的位置,和数组对象本身。对于某些迭代方法,其传入函数的返回值会影响方法的返回值。
以下是 5 个迭代方法的作用:

  • every():对数组中的每一项运行给定函数,如果给定函数对每一项都返回 true,则返回 true;
  • filter():对数组中的每一项运行给定函数,返回给定函数返回 true 的项组成的数组;
  • forEach():对数组中的每一项运行给定函数,无返回值;
  • map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组;
  • some():对数组中的每一项运行给定函数,如果函数对任意一项返回 true,则返回 true。
    这些方法都不会修改原始数组的值。
var number = [1,2,3];
var result = number.map(function(item,index,array){
    return (item*2);
});
alert(result);  //[2,4,6]

2.9 归并方法

ECMAScript 5 还新增了两个归并数组的方法:reduce() 和 reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中 reduce() 方法从数组的第一项开始逐个遍历,而 reduceRight() 则从数组的最后一项开始向前遍历。
这两个方法都接收两个参数:一个在每一项上调用的函数,和作为归并基础的初始值(可选)。
这个传给归并方法的函数,会接收 4 个参数:前一个数组项的函数返回值、当前数组项的值、项的索引、和数组对象。每次执行完一次函数,其返回值都会自动传给下一项,作为下一次执行函数的第一个参数。第一次迭代发生在数组的第二项,此时数组的第一项就是函数的第一个参数。

var number = [1,2,3];
var result = number.reduce(function(prev,cur,index,array){
    return (prev*cur);
});
alert(result);  //6

reduceRight() 的作用类似,只不过方向相反而已。

3. Date类型

ECMAScript 中的 Date 类型是在早期 Java 中的 java,util.Date 类基础上构建的。其使用 UTC 1970 年 1 月 1 日零时至今经过的毫秒数来保存日期。
要创建一个日期对象,使用 new 操作符和 Date 构造函数即可。
var now = new Date();
不传递参数时,Date 构造函数返回当前日期和时间,要创建特定日期和时间的日期对象,必须传入该日期的毫秒数。ECMAScript 提供了两个方法:Date.parse() 和 Date.UTC(),用于简化这一计算过程。
Date.parse() 方法接收一个表示日期的字符串参数,返回相应的毫秒数。不同地区,其支持的日期格式也不同。如果传入的字符串无效,方法返回 NaN。

var someDate = new Date(Date.parse("May 25 2004"));  //这是 GMT 时间 2004 年 5 月 25 日 零时

如果直接将字符串传递给 Date 构造函数,也会自动调用 Date.parse()。

Date.UTC() 方法同样返回相应的毫秒数,但接收的参数有所不同,其参数分别为年份、基于 0 的月份、天数、24 进制小时数、分钟、秒和毫秒数。只有前两个参数是必需的。

var someDate = new Date(Date.UTC(2004,4,25));
//这是当地时间 2004 年 5 月 25 日 零时

如果直接将日期参数传递给 Date 构造函数,也会自动调用 Date.UTC()。不同的是,Date 构造函数所调用的 Date.UTC() 方法,创建的日期是基于当地时区的当地时间,而非基于格林威治时区的 GMT 时间。

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

var today = Date.now();
var today = +new Date();  //使用 + 操作符获取 Date 对象的时间戳,可以到达同样的目的

3.1 继承的方法

所有对象都具有 toLocaleString()、toString() 和 valueOf() 方法,Date 类型也不例外,但是其返回值与其他类型不同。
toLocaleString() 方法会按照与浏览器设置的时区相适应的格式返回日期和时间,通常是 12 小时制,不含时区信息。而 toString() 方法则通常返回带时区信息的日期和时间,24 小时制。
这两个方法在不同浏览器中返回值差别很大。
至于日期的 valueOf() 方法,则根本不返回字符串,而是返回日期的毫秒表示,因此日期之间可以比较大小。

3.2 日期格式化方法

Date 类型还有一些专门用于将日期格式化为字符串的方法,如下:

  • toDateString():以特定格式显示星期、月、日、年;
  • toTimeString():以特定格式显示时、分、秒、时区;
  • toLoacleDateString():以特定的当地日期格式显示星期、月、日、年;
  • toLocaleTimeString():以特定格式显示时、分、秒;
  • toUTCString():以特定格式显示完整的 UTC 日期。
    这些方法也是因浏览器而异。toGMTString() 方法是一个与 toUTCString() 等价的方法,不过 GMT 时间已经逐渐淘汰了,UTC 时间成为了主流。

3.3 日期/时间组件方法

见书 P102。

4. RegExp 类型

ECMAScript 通过 RegExp 类型来支持正则表达式。
var expression = /pattern/flags;
其中的模式(pattern)部分可以是任何正则表达式,每个正则表达式都可带任意个标志(flags),用以标明正则表达式的行为。
模式支持下列 3 个标志:

  • g:表示全局(global)模式,即模式应用于所有字符串,发现第一个匹配项后不会立即停止;
  • i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
  • m:表示多行(multiline)模式,到达一行文本末尾不停止寻找。

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

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

var pattern = /at/g;    //匹配所有的"at"
var pattern = /.at/g;    //匹配所有以"at"结尾的三个字符的组合
var pattern = /\.at/g;    //匹配所有的".at",因为在正则表达式中,小数点"."表示匹配一个任意字符,所以这里使用"\."转义,表示匹配真正的小数点。
var pattern = /[bd]at/i;    //匹配第一个"bat"或者"dat",不区分大小写
var pattern = /\[bd\]at/i;    //匹配第一个"[bd]at",不区分大小写,因为在正则表达式中,方括号表示匹配任意一个待选字符,所以这里使用"\["和"\]"转义,表示匹配真正的方括号。
var pattern = /ba?t/i;    //匹配第一个"bat"或者"bt",不区分大小写,在正则表达式中,"?"表示匹配前面的子表达式零次或一次

除了上面这种以字面量形式来定义正则表达式,还可以用 RegExp 构造函数创建正则表达式。它接收两个参数:要匹配的字符串模式,和可选的标志字符串。

var pattern1 = /[bd]at/i;    //通过字面量形式来定义
var pattern2 = new RegExp("[bd]at","i");    //通过 RegExp 构造函数形式来定义,与上面的正则表达式等价(实际上略有不同,下面会讲)

传递给 RegExp 构造函数的两个参数都是字符串,而在模式字面量中出现的 \ 是不能直接出现在字符串中的,因此有时要对模式参数进行双重转义。所有元字符都有 \ 的前缀,因此它们都必须双重转义。

var pattern = /\[bd\]at/i;    //匹配第一个"[bd]at",不区分大小写
var pattern = new RegExp("\\[bd\\]at","i");    //对反斜杠进行双重转义,含义同上
var pattern = /\\at/g;    //匹配所有包含"\at"的字符的组合
var pattern = new RegExp("\\\\at","g");    //对连续两个反斜杠都进行双重转义,含义同上

使用正则表达式字面量和使用 RegExp 构造函数创建的正则表达式不一样,在 ECMAScript 3 中,正则表达式字面量始终会共享同一个 RegExp 实例,而使用构造函数创建的每一个新 RegExp 实例都是新实例。

var re1 = /cat/g;
re1.test("catabcdefg");  //true
var re2 = /cat/g;
re2.test("catabcdefg");  //false
var re3 = /cat/g;
re3.test("catabcdefg");  //true

由于正则表达式 re1、re2 和 re3 都是使用字面量形式创建的,它们会共享同一个 RegExp 实例,而这个 RegExp 实例的属性不会重置。RegExp 实例有一个 lastIndex 属性,它会记录上一次匹配的结束位置,初始值为 0。
第一次调用 test() 找到了"cat",此时的 lastIndex 值变成 3,所以第二次调用 test() 是从索引 3 的字符开始的(上一次匹配的末尾),所以就找不到了,再下一次调用 test() 又从头开始,所以又能找到了。


我不确定是不是因为 re1、re2 和 re3 的字面量都是 /cat/g 而导致它们共享同一个 RegExp 实例,书上并没有说清楚共享的条件。也许只要是通过字面量形式创建的正则表达式都会共享,也许是字面量相同的才会共享。


在 ECMAScript 5 中,明确规定使用正则表达式字面量时,每次要都创建新的 RegExp 实例。IE9+、Firefox 4+ 和 Chrome 都据此作出了修改。

4.1 RegExp 实例属性

RegExp 的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。

  • global:布尔值,表示是否设置看 g 标志;
  • inoreCase:布尔值,表示是否设置看 i 标志;
  • multiline:布尔值,表示是否设置看 m 标志;
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从 0 开始;
  • source:正则表达式的字符串表示,按照字面量形式,而非传入构造函数中的字符串模式返回。

不管是字面量形式创建的,还是构造函数创建的实例,其 source 属性都是规范形式的字符串,即字面量形式所用的字符串。

4.2 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);
alert(matches.index);  // 0,表示匹配项在字符串中的位置
alert(matches.input);  // "mom and dad and baby",表示即将要应用正则表达式的字符串
alert(matches[0]]);  // "mom and dad and baby",数组第一项是与整个模式匹配的字符串
alert(matches[1]);  // " and dad and baby",与模式中的第一个捕获组匹配的字符串
alert(matches[2]);  // " and baby",与模式中的第二个捕获组匹配的字符串

上面的例子包含两个捕获组。数组的第一项是匹配的整个字符串,第二项是与第一个捕获组匹配的内容,第三项是与第二个捕获组匹配的内容。
exec() 方法每次只会返回一个匹配项,即使你在模式中设置了全局标志(g)。如果没有设置全局标志,则始终返回第一个匹配项(lastIndex 属性的值不变),如果设置了全局标志,则每次调用 exec() 方法都会在字符串中继续查找新匹配项(lastIndex 属性的值增加)。


IE 比较特殊,就算你没有设置全局标志,lastIndex 属性也会持续变化。


正则表达式的第二个方法是 test(),它接收一个字符串参数,即要应用模式的字符串,若模式与参数匹配返回 true,否则返回 false。这个方法适合验证用户输入格式这样的场景,我们只关心输入是否有效,而不关心其文本内容。

4.3 RegExp 构造函数属性

RegExp 构造函数包含一些属性,这些属性适用于作用域中所有正则表达式,且基于最近执行的正则表达式操作而发生变化。这些属性分别有一个长属性名和一个短属性名,所以你有两种方式来访问它们,使用这些属性可以从 exec() 或 test() 执行的操作中提取出更具体的信息。

  • input($_):最近一次要匹配的字符串;
  • lastMatch($&):最近一次的匹配项;
  • lastParen($+):最近一次匹配的捕获组;
  • leftContext($`):input 字符串中 lastMatch 之前的文本;
  • rightContext($‘):input 字符串中 lastMatch 之后的文本;
  • multiline($*):布尔值,表示是否所有表达式都使用多行模式。

Opera 仅支持 leftContext 和 rightContext 属性,不支持其他长属性名和短属性名。
IE 不支持 multiline 属性。

由于这些短属性名不是有效的 ECMAScript 标识符,因此必须通过方括号来访问它们。

alert(RegExp.input);
alert(RegExp.$_);  //这个是特例,无需加方括号
alert(RegExp.lastMatch);
alert(RegExp["$&"]);
alert(RegExp.lastParen);
alert(RegExp["$+"]);

此外,还有 9 个用于存储捕获组的构造函数属性,访问这些属性的语法是:RegExp.$1、RegExp.$2···RegExp.$9,分别用于存储第一、第二······第九个匹配的捕获组。

4.4 模式的局限性

ECMAScript 缺少某些语言所支持的高级正则表达式特性,但它仍然非常强大,足够帮我们完成绝大多数模式匹配任务。详见 P109。

5. Function 类型

ECMAScript 中的函数是对象,每个函数都是 Function 类型的实例,都具有属性和方法(和其他引用类型一样)。函数名实际上是一个指向函数对象的指针,不会与某个函数绑定。

function sum (num1, num2) {
    return num1 + num2;
}
var sum = function(num1, num2){
    return num1 + num2;
};
var sum = new Function("num1","num2","return num1 + num2");  //不推荐这种使用 Function 构造函数定义函数的方法

通过函数表达式定义函数时,无需在 function 后使用函数名,因为这个函数实例马上就被赋给了等式左边。
使用 Function 构造函数定义函数会导致解析两次代码,影响性能,但是它有助于我们理解“函数是对象,函数名是指针”这一概念。
因为函数名是指向函数的指针,所以同一个函数可能有多个名字。
使用不带圆括号的函数名,是访问函数指针,而不是调用函数。

5.1 没有重载

因为函数名是指向函数的指针,所以 ECMAScript 中没有函数重载的概念。

function add(num) {
    return num + 100;
}
function add(num) {
    return num + 200;
}
add(100); //300

声明两个同名函数,后者会覆盖前者,函数名指针指向新的函数。

var add = function(num){return num + 100;};
add = function(num){return num + 200;};
add(100); //300

使用函数表达式定义函数,很容易看明白发生了什么事,后面的函数覆盖了前面的函数。

5.2 函数声明与函数表达式

实际上,解析器在向执行环境加载数据时,对函数声明和函数表达式并非一视同仁,解析器会先读取函数声明,并使其在执行任何代码之前可访问,而函数表达式,则要等到解析器执行到它所在行,才会真正被解释执行。

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

这段代码可以正常运行,正是因为在代码开始执行前,解析器已经把函数声明添加到执行环境中了(这个过程称之为函数声明提升)。
除了这个解析时间的差异,函数声明与函数表达式的语法其实是等价的。

5.3 作为值的函数

因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。不仅可以像传递参数一样传递一个函数给另一个函数,还可以一个函数作为另一个函数的返回值。注意,把函数作为值传递的时候,不要在其后面加一对圆括号,否则就会变成传递函数的执行结果了。

5.4 函数内部属性

函数内部有两个特殊的对象:arguments 和 this。
其中的 arguments 是一个类似数组的对象,保存着传入函数的所有参数。它有一个 callee 属性,该属性是一个指针,指向拥有 arguments 对象的函数。

function factorial(num){  //阶乘
    if (num 

这个函数的执行与函数名 factorial 紧紧耦合在一起了,为了消除耦合,可以这样写:

function factorial(num){
    if (num 

ECMAScript 函数中的 this 对象与 Java 和 C# 中的 this 大致类似。this 引用的是函数执行的环境对象,当在网页的全局作用域中调用函数时,this 引用的就是 window。

window.color = ‘red‘;
var o = { color: ‘blue‘ };
function getColor() {
	return this.color;
}
getColor();  //red,相当于对 window.color 求值
o.getColor = getColor;
o.getColor();  //blue,相当于对 o.color 求值

注意,函数名仅仅是一个包含指针的变量,就算在不同的执行环境中,在全局作用域中调用的 getColor() 函数与在局部作用域中调用的 o.getColor() 函数,他们执行的是依然的同一段代码。

ECMAScript 5 添加了一个函数对象的属性:caller,它保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,则为 null。

function outer() {
	inner();
}
function inner() {
	alert(inner.caller);  //会显示 outer() 函数的源码
    alert(arguments.callee.caller);  //这是松散耦合的写法
}
outer();

注意,callee 是函数的 arguments 对象的一个属性,而 caller 是函数本身的一个属性。
实际上,arguments 对象还有一个 caller 属性,这个属性的值始终是 undefined。
在严格模式下,访问 arguments.callee 会导致错误,这样做可以防止第三方代码窥视相同环境里的其他代码。
在严格模式下,访问 arguments.caller 也会导致错误。
在严格模式下,为函数本身的 caller 属性赋值也会导致错误。

5.5 函数属性和方法

因为 ECMAScript 中函数也是对象,所以函数也有属性和方法,每个函数都有两个属性:length 和 prototype,length 表示函数希望接收的参数的个数,而 prototype 则保存了函数的所有实例方法。

对于 ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正地方。prototype 无法枚举,因此不能使用 for-in 来遍历。
每个函数都有两个非继承而来的方法:apply() 和 call(),它们都用于在特定作用域中调用函数,实际上等于设置函数体内 this 对象的值。
apply() 方法接收两个参数:函数作用域(this),参数数组。参数数组可以是 Array 实例,也可以是 arguments 对象。

function sum(num1, num2){
    return num1 + num2;
}
function f1(num1, num2){
    return sum.apply(this, arguments);
}
function f2(num1, num2){
    return sum.apply(this, [num1, num2]);
}
alert(f1(1,2));  //3
alert(f2(1,2));  //3

call() 方法与 apply() 方法作用相同,但接收参数的方式不同,其第一个参数依然是 this,其后的参数不再是一个数组,而是多个逐一列举出来的参数。

function sum(num1, num2){
    return num1 + num2;
}
function f1(num1, num2){
    return sum.call(this, num1, num2);
}
alert(f1(1,2));  //3

假如你不打算给函数从传递参数,使用 call() 方法和 apply() 方法没有区别。这两个方法真正强大的地方不是传递参数,而是传递作用域,它能扩充函数的作用域。

window.color = ‘red‘;
var o = { color: ‘blue‘ };
function getColor() {
	return this.color;
}
getColor();  //red
getColor.call(this);  //red
getColor.call(window);  //red
getColor.call(o);  //blue

使用 call() 方法或 apply() 方法扩充作用域的好处在于,getColor() 方法与 o 对象之间没有任何耦合关系了,你不再需要类似 o.getColor = getColor; 这样的步骤,来把 getColor() 函数放到对象 o 中了。
使用 bind() 方法会创建一个函数的实例,传给 bind() 的参数值会变成新建函数的 this 值。

var sayColor = getColor.bind(o); //新建了一个 getColor 的实例,对象 o 变成此实例的 this 值
sayColor();  //blue

每个函数继承的 valueOf() 方法、toString() 方法和 toLocaleString() 方法都始终返回函数的源码,

6. 基本包装类型

ECMAScript 提供 3 个特殊的引用类型:Boolean、Number 和 String,用于操作基本类型值。这些类型与前面介绍的其他引用类型(RegExp 类型、Function 类型、Date 类型、Array 类型、Object 类型)相似,也有一些特殊行为。
每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能通过一些方法操作这些基本类型数据。

var s1 = "abcde";
var s2 = s1.subString(2);  //cde

s1 显然是一个属于基本类型的字符串,但却可以调用 subString() 方法,正是因为后台自动完成了一系列操作:

  1. 创建 String 类型的一个实例;
  2. 在实例上调用指定的 subString() 方法;
  3. 销毁创建的实例。

这三个步骤可以想象成如下代码:

var s1 = new String("abcde");
var s2 = s1.subString(2);
s1 = null;

现在,基本的字符串也能像对象一样进行操作了,布尔值和数字值同理。
使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前一直保存在内存中,可是上述过程中自动创建的基本包装类型的对象,只存在于一行代码执行的瞬间,然后立即销毁。
因此,我们不能在代码执行时为基本类型值添加属性和方法。例如:

var s1 = "abcde";
s1.name = "tom";
alert(s1.name);  //undefined,上一行创建的 String 对象已经销毁,这一行又创建了自己的 String 对象

你可以显式调用 Boolean、Number 和 String 来创建基本包装类型的对象。
但是,这种做法很容易让人分不清自己处理的究竟是基本类型还是引用类型的值。
对基本包装类型的实例调用 typeof 会返回"object",而且基本包装类型的对象在转换为布尔类型时,值都是 true。

Object 构造函数会根据传入值的类型返回相应基本包装类型的实例(就像工厂方法一样)。例如:

var obj = new Object("some text");
alert(obj instanceof String);  //true

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

var value = "25";

var obj = new Number(value);  //基本包装类型的构造函数
alert(typeof obj);  //"object"
var number = Number(value);  //转型函数
alert(typeof number);  //"number"

每个基本包装类型都提供了操作相应值的便捷方法。

6.1 Boolean 类型

Boolean 类型是与布尔值对应的引用类型。要创建 Boolean 对象,可以像下面这样调用 Boolean 构造函数并传入 true 或 false 值。
var booleanObject = new Boolean(true);
Boolean 类型的实例重写了 valueof() 方法,返回基本类型值 true 或 false;重写了 toString() 方法,返回字符串 "true" 或 "false"。
因为在布尔表达式中,所有对象都会被转换为 true(在第三章布尔操作符讲到过),所以 Boolean 对象会导致一些误解。例如:

var b = new Boolean(false);
var result = b && true;  //在这里,b 这个对象被转换为 true 了。
alert(result);  //true

事实上 Boolean 类型会导致非常多的误解,如果按照书中所说,在布尔表达式中,所有对象都会被转换为 true,你无法解释所有的特殊情况,下面我补充两点:

  1. 如果布尔操作符 || 或者 && 的两边都是 Boolean 引用类型的值,那么它的某些运行结果会出人意料。
var b1 = new Boolean(false);
var b2 = new Boolean(true);

alert(b1 || b1);  //Boolean(false),如果所有对象真的转换成 true 了,为什么结果会是一个引用类型值呢?
alert(b1 || b2);  //Boolean(false)
alert(b2 || b1);  //Boolean(true)
alert(b2 || b2);  //Boolean(true)
//我猜想是直接返回了左边的值

alert(b1 && b1);  //Boolean(false)
alert(b1 && b2);  //Boolean(true)
alert(b2 && b1);  //Boolean(false)
alert(b2 && b2);  //Boolean(true)
//我猜想是直接返回了右边的值
  1. 如果布尔操作符 || 或者 && 的一边是 Boolean 引用类型的值,另一半是 Boolean 基础类型值,那么它的某些运行结果同样会出人意料。
alert(b1 || true);  //Boolean(false),如果所有对象真的转换成 true 了,为什么结果会是一个引用类型值呢?
alert(b1 || false);  //Boolean(false)
alert(b2 || true);  //Boolean(true)
alert(b2 || false);  //Boolean(true)
//我猜想是直接返回了左边的值

alert(true || b1);  //true
alert(true || b2);  //true
alert(false || b1);  //Boolean(false),为什么在同一种模式下,还会有两种类型的返回值?
alert(false || b2);  //Boolean(true)
//虽然操作符的返回结果符合常理,但为什么 true 在前就返回基础类型值,false 在前就返回引用类型值,我无法解释这一点

alert(b1 && true);  //true
alert(b1 && false);  //false
alert(b2 && true);  //true
alert(b2 && false);  //false
//这就是书中举例的那种模式,所有对象都会被转换为 true,看上去就像直接返回右边的值

alert(true && b1);  //Boolean(false)
alert(true && b2);  //Boolean(true)
alert(false && b1);  //false,为什么在同一种模式下,还会有两种类型的返回值?
alert(false && b2);  //false
//虽然操作符的返回结果符合常理,但为什么 true 在前就返回引用类型值,false 在前就返回基础类型值,我无法解释这一点

由此可见,永远不要使用 Boolean 对象。

6.2 Number 类型

Number 类型是与数字值对应的引用类型。要创建 Number 对象,可以在调用 Number 构造函数时传入数值。
var numberObject = new Number(10);
Number 类型的实例重写了 valueof() 方法,返回基本类型数值;重写了 toLocaleString() 和 toString() 方法,返回字符串形式的数值。

  • 可以为 toString() 方法传递一个参数,告诉它返回几进制数值的字符串表示。
  • 可以给 toFixed() 方法传递一个参数,告诉它返回一个保留小数点后多少位的数值的字符串表示。
  • 可以给 toExponential() 方法传递一个参数,告诉它返回一个保留小数点后多少位的,用指数表示法表示的数值的字符串表示。
  • 可以给 toPrecision() 方法传递一个参数,告诉它你想返回表示数值的精确位数,它会根据具体情况选择最合适的格式。

不建议使用 Number 对象,原因与 Boolean 对象一样,引用类型值与对应的基础类型值混在一起会产生很多麻烦。

6.3 String 类型

String 类型是与字符串对应的引用类型。要创建 String 对象,可以在调用 String 构造函数时传入字符串。
var stringObject = new String("hello world");
String 对象的方法也可以在基础类型的字符串值中访问到。
String 对象的 valueof() 方法,toLocaleString() 方法和 toString() 方法,都返回基础类型的字符串值。
String 对象都有一个 length 属性,表示其包含多少个字符。

  1. 字符方法

charAt() 方法接收一个参数,以单字符字符串形式返回给定位置的那个字符。
如果你想得到的不是字符,而是那个字符的二进制编码,你可以使用 charCodeAt() 方法。
ECMAScript 5 还定义了另一个方法,在支持此方法的浏览器中,可以用方括号加数字索引来访问字符串中指定位置的字符。(IE8及以上支持)

var stringValue = "helloworld";
alert(stringValue[1]);  //"e"
  1. 字符串操作方法

concat() 用于将一个或多个字符串拼接起来,返回拼接得到的新字符串。

var stringValue = "hello";
var result = stringValue.concat("world");
alert(result);  //"helloworld"
alert(stringValue);  //"hello"

concat() 方法不会改变字符串对象本身的值,而且它可以接受任意多个参数,拼接任意多个字符串。
虽然 concat() 方法是专门用来拼接字符串的,但实践中使用更多的还是加号操作符(+)。

slice()、substr() 和 substring() 方法都不会改变原始值,都会返回一个基本类型的字符串值。

var stringValue = "helloworld";

alert(stringValue.slice(3));  //"loworld",不传入第二个参数时,默认将字符串末尾作为结束位置
alert(stringValue.slice(3,7));  //"lowo",从第 3 个到第 7 个,不含第 7 个
alert(stringValue.slice(-3));  //"rld",第一个参数传入负值时会与字符串长度相加,等价于 slice(7),当你需要返回倒数 N 个字符时非常适合这种用法
alert(stringValue.slice(3,-4));  //"low",第二个参数传入负值时会与字符串长度相加,等价于 slice(3,6)

alert(stringValue.substring(3));  //"loworld"
alert(stringValue.substring(3,7));  //"lowo"
alert(stringValue.substring(-3));  //"helloworld",它把负数转化成了 0
alert(stringValue.substring(3,-4));  //"hel",因为负数转换成了 0,等价于 substring(3,0),又因为substring() 方法会将较小的数作为开始位置,所以又等价于 substring(0,3)

alert(stringValue.substr(3));  //"loworld"
alert(stringValue.substr(3,7));  //"loworld",第二个参数指定返回的字符个数,而不是结束位置
alert(stringValue.substr(-3));  //"rld",第一个参数传入负值时会与字符串长度相加
alert(stringValue.substr(3,-4));  //"",空字符串,因为第二个参数传入负值时会转换成 0,即返回包含零个字符的字符串
  1. 字符串位置方法

有两个从一个字符串中查找给定子字符串的方法:indexOf() 和 lastIndexOf()。它们会返回子字符串的位置,如果没找到则返回 -1。
indexOf() 方法从字符串的开头先后搜索,lastIndexOf() 方法则从字符串末尾向前搜索。
它们都可以接收第二个参数,表示从字符串的哪个位置开始搜索。
可以通过循环调用字符串位置方法找到所有的匹配子字符串:

var stringValue = "weekend";
var positions = new Array();
var pos = stringValue.indexOf("e");

while(pos > -1){
    positions.push(pos);
    pos = stringValue.indexOf("e",pos + 1);  //每次都给pos + 1,确保每次新搜索都是从上一次的后面开始
}
alert(position);  //"1,2,4"
  1. trim() 方法

ECMAScript 5 为所有字符串定义了 trim() 方法,它会创建一个字符串的副本(不影响原始值),删除前置及后缀的所有空格,然后返回结果。
IE9+、Firefox 3.5+、Safari 5+、Opera 10.5+ 和 Chrome 支持此方法,Chrome 8+ 还支持非标准的 trimLeft() 和 trimRight() 方法。

  1. 字符串的大小写转换方法

toLowerCase() 和 toUpperCase() 是两个经典的方法,借鉴自 java.lang.String 中的同名方法。而 toLocaleLowerCase() 和 toLocaleUpperCase() 方法则是针对特定地区的实现。
如果你不知道自己的代码将在哪种语言环境中运行的话,使用针对地区的方法更稳妥。

  1. 字符串的模式匹配方法

String 类型定义了几个用于字符串中匹配模式的方法。
第一个查找模式的方法是 match(),在字符串上调用这个方法,本质上与调用 RegExp 的 exec() 方法相同。match() 方法只接受一个参数,要么是一个正则表达式,要么是一个 RegExp 对象。

var text = "cat,bat,fat"
var pattern = /.at/;

var matches = text.match(pattern);  //与 pattern.exec(text) 结果相同,详见本章第 4 节的 RegExp 实例方法

另一个查找模式的方法是 search(),这个方法的唯一参数与 match() 方法一样,由字符串或者 RegExp 对象指定的一个正则表达式。它返回字符串中第一个匹配项的索引,如果没有匹配项,则返回 -1。
search() 方法始终是从字符串开头向后查找模式。


有没有让除 search() 以外其他几个匹配模式方法从后往前查找模式的办法?


replace() 方法用于替换子字符串,它接受两个参数,第一个参数表示需要被替换的文本,可以是一个正则表达式或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数表示你需要替换成什么样的文本,可以是一个字符串或者一个函数。
如果第一个参数是字符串,那么只会替换第一个子字符串。想要替换所有子字符串,第一个参数必须是一个正则表达式(RegExp 对象或者字面量形式的正则表达式),且该正则表达式要指定全局标志(g)。

var text = "cat,bat,dat";
var result = text.replace("at","ond");
alert(result);  //"coond,bat,dat",只替换了第一个子字符串

result = text.replace(/at/g,"ond");
alert(result);  //"cond,bond,dond",所有匹配的子字符串都被替换了

如果第二个参数是字符串,那么可以使用一些特殊的字符序列(类似于 RegExp 构造函数的短属性名),将正则表达式操作得到的值插入到结果字符串中。通过这些特殊的字符序列可以使用最近一次匹配结果中的内容。

  • $$:将 $ 用作替换文本;
  • $&:匹配整个模式的子字符串,等价于 RegExp.lastMatch;
  • $`:$& 之前的文本,等价于 RegExp.leftContext;
  • $‘:$& 之后的文本,等价于 RegExp.rightContext;
  • $n: 匹配第 n 个捕获组的子字符串,其中 n 等于 0~9,若正则表达式中没有定义捕获组则使用空字符串
  • $nn: 匹配第 nn 个捕获组的子字符串,其中 n 等于 01~99

勘误:$n 的范围并不是 0~9 而是 1~9,因为 $1 表示匹配第一个捕获组的子字符串;而 $0 并没有实际含义,它会被当做一个普通的字符串。


var text = "cat,bat,dat";
var result = text.replace(/(.at)/g,"-$1-");
alert(result);  //"-cat-,-bat-,-dat-"
//显然这里触发了三次替换,从结果来看,这三次替换发生时的 $1 的值分别为:"cat"、"bat"和"dat"
//我明明只执行了一次 replace() 方法,为何 $1 会连续变化三次?
//因为 RegExp 构造函数的属性 $n 是动态的,每次发生模式匹配行为,它都会被相应的捕获组捕获到的字符串自动填充;又因为我给 replace() 方法的第一个参数设置了全局标志(g),使其自动发生了多次替换;所以 $1 会变化三次。

replace() 方法的第二个参数也可以是一个函数,以实现更加精细的替换操作。
在只有一个匹配项的情况下,replace() 方法会向这个函数传递三个参数:模式的匹配项,该模式的匹配项在字符串中的位置,和原始字符串。
如果有多个匹配项(当正则表达式定义了多个捕获组),则 replace() 方法会向这个函数传递若干个参数:模式的匹配项,第一个捕获组的匹配项,第二个捕获组的匹配项······最后一个捕获组的匹配项,该模式的匹配项在字符串中的位置,和原始字符串。

function htmlEscape(text){  //这个函数能转义四个字符
    return text.replace(/["&]/g, function(match, pos, originalText){
        switch(match){
            case "":return ">";
            case "\"":return """;
            case "&":return "&";
        }
    });
}

最后一个与模式匹配有关的方法是 split(),这个方法会接收一个分隔符,然后基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。分隔符可以是一个 RegExp 对象,也可以是字符串 (split() 方法不会把字符串看成正则表达式)。
spilt() 方法可以接收第二个参数,用于指定数组的大小,以确保返回的数组不会太大。

var colorText = "red,blue,green,yellow";
var colors1 = colorText.split(",");     //["red", "blue", "green", "yellow"]
var colors2 = colorText.split(",", 2);  //["red", "blue"]
  1. localeCompare() 方法

这个方法比较两个字符串,并返回下列值中的一个:

  • 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数,例如 -1;
  • 如果字符串等于字符串参数,则返回 0;
  • 如果字符串在字母表中应该排在字符串参数之后,则返回一个正数,例如 1。
var stringValue = "y";
alert(stringValue.localeCompare("b"));   //-1
alert(stringValue.localeCompare("y"));   //0
alert(stringValue.localeCompare("z"));   //1
  1. fromCharCode() 方法

它是 String 构造函数的静态方法,这个方法接收一个或多个字符编码,然后将他们转换成字符串,它与charCodeAt() 执行的是相反的操作。
alert(String.fromCharCode(104, 104, 108, 108, 111)); //"hello"

  1. HTML 方法

早期的 Web 浏览器提供商扩展了标准,实现了一些专门用于简化 HTML 格式化任务的方法,应该尽量避免使用这些非标准方法。详见 P130。

7. 单体内置对象

内置对象:由 ECMAScript 实现提供的,不依赖于宿主环境的对象,在ECMAScript 程序执行之前就已经存在了。开发人员不必显式地实例化内置对象,因为它们已经实例化了。
内置对象有:Object、Array、String、Global 和 Math。

7.1 Global 对象

Global(全局)对象可以说是 ECMAScript 中最特别的一个对象了,因为不管你从什么角度看,它都是不存在的。
不属于任何其他对象的属性和方法,都是它的属性和方法,例如 isNaN()、isFinite()、parseInt()以及 parseFloat()。
除此之外,它还包括其他一些方法。

  1. URI 编码方法

Global 对象的 encodeURI() 和 encodeURICompoment() 方法可以对 URI 进行编码,以便发送给浏览器。


URI:Uniform Resource Identifiers,统一资源标识符;
URL:Uniform Resource Locator,统一资源定位符。


有效的 URI 中不能包含某些字符,例如空格。而这两个 URI 编码方法就可以对 URI 进行编码,它们用特殊的 UTF-8 编码替换所有无效的字符,从而让浏览器能够接受和理解。
其中,encodeURI() 主要用于整个 URI,而 encodeURIComponent() 主要用于对 URI 中的某一段进行编码。encodeURI() 不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井号,而 encodeURIComponent() 则会对它发现的任何非标准字符进行编码。
与上面两个编码方法相对应的解码方法分别是 decodeURI() 和 decodeURIComponent()。其中,decodeURI() 只能对使用 encodeURI() 替换的字符进行解码。而 decodeURIComponent() 能够解码使用encodeURIComponent() 编码的字符,即它可以解码任何特殊字符的编码。

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

在 eval() 中创建的任何变量或函数都不会被提升,因为在解析代码的时候,,他们被包含在一个字符串中;它们只在 eval() 执行的时候创建。
严格模式下,在外部访问不到 eval() 中创建的任何变量或函数,为 eval 赋值也会导致错误。


能够解释代码字符串的能力非常强大,但也非常危险。因此在使用 eval() 时必须极为谨慎,特别要防止用户恶意进行代码注入。


  1. Global 对象的属性
属性 说明
undefined 特殊值
NaN 特殊值
Infinity 特殊值
Object 构造函数
Array 构造函数
Function 构造函数
Boolean 构造函数
String 构造函数
Number 构造函数
Date 构造函数
RegExp 构造函数
Error 构造函数
EvalError 构造函数
RangeError 构造函数
ReferenceError 构造函数
SyntaxError 构造函数
TypeError 构造函数
URIError 构造函数

ECMAScript 5 明确禁止给 undefined、NaN 和 Infinity 赋值,这样做即便在非严格模式下也会导致错误。

  1. window 对象

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

var global = function(){
    return this;
}();

在没有给函数明确指定 this 值的情况下,this 值等于 Global 对象。

7.2 Math 对象

ECMAScript 为保存数学公式和信息提供了一个公共位置,即 Math 对象。

  1. Math 对象的属性

Math 对象包含的属性大都是数学计算中可能会用到的一些特殊值。

属性 说明
Math.E 自然对数的底数,即常量 e 的值
Math.LN10 10 的自然对数
Math.LN2 2 的自然对数
Math.LN2E 以 2 为底 e 的对数
Math.LN10E 以 10 为底 e 的对数
Math.PI 圆周率的值
Math.SQRT1_2 1/2 的平方根
Math.SQRT2 2 的平方根
  1. min() 和 max() 方法

这两个方法用于确定一组数值中的最小值和最大值。
alert(Math.max(1, 3, 5)); //5
由于 JavaScript 数组没有 max() 方法,我们可以使用 Math.max() 方法确定数组中的最大值。

var values = [1, 3, 5, 7, 9];
var max = Math.max.apply(Math, values);

apply() 方法的第一个参数 Math 无关紧要,在本例中未使用它,换成 null 或者空字符串也行。

  1. 舍入方法
  • Math.ceil() 执行向上舍入
  • Math.floor() 执行向下舍入
  • Math.round() 执行标准舍入,即四舍五入
  1. random() 方法

Math.random() 方法返回大于等于 0 小于 1 的一个随机数。
如果你想选择一个 600 到 1000 之间(包括 600 和 1000)的整数:
var num = Math.floor(Math.random() * 401 + 600);

  1. 其他方法
&g
方法 说明
Math.abs(num) 返回 num 的绝对值
Math.exp(num) 返回 Math.E 的 num 次幂


评论


亲,登录后才可以留言!