0%

JavaScript学习笔记(5)

《JavaScript高级程序设计》第五章,引用类型。


ECMAScript中,引用类型(也称对象定义)是一种数据结构,用于将数据和功能组织在一起。它常被称为类,但并不妥当,事实上虽然ECMAScript是面向对象语言,却不具备传统面向对象语言支持的类和接口等基本结构。ECMAScript提供了很多原生引用类型,如Object等。

Object类型

创建

创建Object实例有两种方式:

  • Object构造函数
1
2
3
var person = new Object();
person.name = "Nicholas";
person.age = 29;
  • 对象字面量表示法
1
2
3
4
var person = {
name: "Nicholas",
age: 29
};

在使用对象字面量语法时,属性名可以是字符串。下面例子中对象有三个属性:name、age、5,不过此处数值属性名会自动转为字符串。

1
2
3
4
5
6
// 属性名为字符串
var person = {
"name": "Nicholas",
"age": 29,
5: true
};

在使用对象字面量语法时,如果留空其花括号,则可以定义只包含默认属性和方法的对象。

1
2
3
4
// 花括号为空
var person = {}; // 与new Object()相同
person.name = "Nicholas";
person.age = 29;

使用建议

当需要向函数传入大量可选参数时,适合使用字面量语法来传递参数。一般来讲命名参数虽然容易处理,但在有多个可选参数的情况下不灵活。因此建议对必须值使用命名参数,对可选参数使用对象字面量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function displayInfo(args) {
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({
name: "Greg"
});

访问属性

两种访问属性方法:点表示法、方括号语法。

1
2
3
4
5
6
7
8
9
alert(person.name);
alert(person["name"]);

// 方括号语法优点:可通过变量访问属性
var propertyName = "name";
alert(person[propertyName]);

// 方括号语法优点:当属性名中有导致语法错误的字符、关键字等时
alert(person["first name"]);

Array类型

与其他语言不同之处在于ECMAScript的数组每一项数据类型可以不一样,也可以动态调整数组长度。

创建

创建数组有两种方式:

  • Array构造函数
1
2
3
4
5
6
7
8
9
10
// 不传参数
var colors = new Array();

// 传参数
var colors = new Array(20); // 为length
var colors = new Array("red", "blue", "green"); // 为项

// 可省略new操作符
var colors = Array(20);
var colors = Array("white");
  • 数组字面量表示法
1
2
3
4
5
// 包含数组项的方括号表示
var colors = ["red", "blue", "green"]; // 可
var colors = []; // 可
var colors = ["red", "blue",]; // 不可,项数为2或3
var colors = [,,,,,]; // 不可,项数为5或6

若设置值时索引大于数组的length,则在数组会自动增加到该索引值加1的长度。length属性不是只读的,可以通过修改该属性来移除末尾项或添加新项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var colors = ["red", "blue", "green"];
colors[4] = "yellow"; // 数组现在有5个元素

// 移除末尾项
var colors = ["red", "blue", "green"];
colors.length = 2;
alert(colors[2]); // undefined

// 添加新项
var colors = ["red", "blue", "green"];
colors.length = 4;
alert(colors[3]); // undefined

// 使用length方便地append
var colors = ["red", "blue", "green"];
colors[colors.length] = "black"; // 位置3添加元素
colors[colors.length] = "brown"; // 位置4添加元素

检测数组

我们需要用些方法确定某个对象是否为数组。对于一个网页或一个全局作用域,使用instanceof操作符即可得到结果。

1
2
3
if(value instanceof Array) {
...
}

instanceof操作符存在的问题在于,它假定单一的全局执行环境。当网页包含多个框架,就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。为此ECMAScript5新增了Array.isArray()方法,它能最终确定某值是否为数组而不管其所处的全局执行环境。

1
2
3
if(Array.isArray(value)) {
...
}

转换方法

第三章曾说Object的每个实例都有一些属性和方法,包括toLocaleString()toString()valueOf()

  • 数组的toLocaleString()返回由每个项的字符串形式拼接,以逗号分隔的字符串。对每个项,其字符串形式为调用每一项的toLocaleString()得到的值。

  • 数组的toString()返回由每个项的字符串形式拼接,以逗号分隔的字符串。对每个项,其字符串形式为调用每一项的toString()得到的值。

  • 数组的valueOf()返回数组本身。

1
2
3
4
var colors = ["red", "blue", "green"];
colors.toLocaleString(); // "red,blue,green"
colors.toString(); // "red,blue,green"
colors.valueOf(); // ["red", "blue", "green"]

不过要注意的是,如果将内容传递给alert(),由于它接收字符串参数,因此它会在后台调用toString()方法,因此会得到直接调用toString()方法相同的结果(对于以上例子来说,若均传入alert()函数中,则均得到”red,blue,green”)。

可以在对象中自定义上述方法:

1
2
3
4
5
6
7
8
var person = {
toLocaleString: function() {
return "Hello from toLocaleString";
},
toString: function() {
return "Hello from toString";
}
}

可以使用join()指定构建字符串中使用的分隔符:

1
2
3
4
var colors = ["red", "blue", "green"];
alert(colors.join()); // "red,blue,green"
alert(colors.join(undefined));// "red,blue,green"
alert(colors.join("|")); // "red|blue|green"

如果数组某项为null或undefined,该值在join()toLocaleString()toString()valueOf()时返回的都是空字符串。

栈方法

ECMAScript为数组提供了push()pop()方法,以便类似栈行为。push()接收任意数量参数,将其添加到数组末尾,修改length,返回数组长度;pop()移除数组末尾项,修改length,返回移除项。

1
2
3
var colors = new Array();
var count = colors.push("red", "green");
var item = colors.pop(); // "green"

队列方法

ECMAScript为数组提供了shift()方法。shift()移除数组第一项,修改length,返回移除项。同时使用push()shift()可以模拟队列。

1
2
3
var colors = new Array();
var count = colors.push("red", "green");
var item = colors.shift(); // "red"

除此之外,ECMAScript还为数组提供了unshift()方法。unshift()接收任意数量参数,将其添加到数组前端,修改length,返回数组长度。同时使用unshift()pop()可以以相反的方向模拟队列。

1
2
3
4
5
var colors = new Array();
var count = colors.unshift("red", "green");
var count = colors.unshift("balck");
var item = colors.pop(); // "green"
// 此例unshift后数组按顺序为:"black"、"red"、"green"

重排序方法

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

sort()调用数组每项的toString()方法后再比较得到的字符串,在默认情况下升序排列。

1
2
3
var values = [0, 1, 5, 10, 15];
values.sort();
alert(values); // 0, 1, 10, 15, 5
  • sort加参数

sort()可以接收一个比较函数作为参数,以指定排序方式。比较函数接收两个参数,若第一个参数应该位于第二个参数之前则返回一个负数,反之亦然,若参数相等则返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
function compare1(value1, value2) {
return value1 - value2;
}

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

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

reverse()sort()方法的返回值均为排序后的数组。

操作方法

  • concat

concat()方法创建当前数组的一个副本,再将接收到的参数添加到这个副本末尾,返回新构建的数组。原数组不变。

1
2
3
4
5
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()接收一个参数时,返回从该参数指定位置到数组末尾的所有项;接收两个参数时,返回两个参数指定位置间不包括结束位置的所有项。原数组不变。
若参数中有负数则将参数均加上数组length,如length为5的数组上调用slice(-2, -1)slice(3, 4)一致。若结束位置小于起始位置,则返回空数组。

1
2
3
4
5
6
var colors = ["red", "green", "blue", "yellow", "purple"];
var colors2 = colors.slice(1);
var colors2 = colors.slice(1, 4);

alert(colors2); // green, blue, yellow, pruple
alert(colors3); // green, blue, yellow
  • splice

splice(<起始位置>,<要删除的项数>[,<要插入的项>]),返回从原始数组中删除的项。splice可用于删除、插入、替换,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
var colors = ["red", "green", "blue"];
var removed = colors.splice(0, 1); // 从0处删除1项
alert(colors); // green,blue
alert(removed); // red

removed = colors.splice(1, 0, "pink", "grey"); // 从1处添加两项
alert(colors); // green,pink,grey,blue
alert(removed); // 返回空数组

removed = colors.splice(1, 1, "red"); // 从1处删除1项添加一项
alert(colors); // green,red,grey,blue
alert(removed); // pink

位置方法

ECMAScript为数组提供了indexOf()lastIndexOf(),接收查找的项和可选的起点位置索引,分别从前往后和从后往前查找,查找成功返回下标,否则返回-1。在比较时使用全等操作符。

1
2
3
4
5
6
7
8
9
10
11
12
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

alert(numbers.indexOf(4)); // 3
alert(numbers.lastIndexOf(4)); // 5
alert(numbers.indexOf(4, 4)); // 5
alert(numbers.lastIndexOf(4, 4)); // 3

var person = {name: "Nicholas"};
var people = [{name: "Nicholas"}];
var morePeople = [person];
alert(people.indexOf(person)); // -1
alert(morePeople.indexOf(person)); // 0

迭代方法

五个迭代方法均接收两个参数:一个函数、可选的运行该函数的作用域对象。
该函数接收三个参数:该项的值、该项的下标、数组对象。

  • every():若所有都满足则返回true
  • filter():返回结果数组
  • forEach():无返回值,本质与for循环一样
  • map():返回结果数组
  • some():若存在一项满足则返回true

缩小方法

reduce()reduceRight(),二者分别从前往后和从后往前迭代数组所有项,构建一个最终返回值。方法接收两个参数:一个函数、可选的作为缩小基础的初始值。该函数接收四个参数:前一个值、当前值、项的索引、数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。

1
2
3
4
5
6
7
8
9
10
11
12
var values = [1, 2, 3, 4, 5];
var sum = values.reduce(function(prev, cur, index, array) {
return prev + cur;
});
alert(sum); // 15
// 第一次执行:prev为1,cur为2;第二次执行:prev为1+2,cur为3

var sum = values.reduceRight(function(prev, cur, index, array) {
return prev + cur;
});
alert(sum); // 15
// 第一次执行:prev为5,cur为4;...

Date类型

Date类型使用自UTC(Coordinated Universal Time,国际协调时间)1970年1月1日零时至今的毫秒数保存日期。

创建

创建日期对象使用Date()构造函数即可。
不传递参数构造出的对象表示当前的日期和时间。

1
var now = new Date();

若想创建特定日期和时间的对象,必须传入毫秒数。为了方便,ECMAScript提供了两个方法:Date.parse()Date.UTC()
Date.parse()接收一个表示日期的字符串参数,然后尝试根据该字符串返回相应日期的毫秒数,若该字符串无法得到解析则会返回NaN。该方法对日期格式没有严格定义,但通常接受以下日期格式:

  • 月/日/年
    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
1
2
3
4
var someDate = new Date(Date.parse("May 25,2004"));

// 可省略,它会在后台调用Date.parse()
var someDate = new Date("May 25,2004");

Date.UTC()有多个参数:年、月(一月为0,二月为1…)、日(1-31)、小时(0-23)、分组、秒、毫秒,只有前两个参数为必须。未提供天数默认为1,未提供其余参数默认为0。

1
2
3
4
5
6
7
8
9
// GMT时间2000.1.1 00:00:00
var y2k = new Date(Date.UTC(2000, 0));

// GMT时间2005.5.5下午5:55:55
var allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));

// 可省略,不过省略后日期为基于本地时区而非GMT的
// 本地时间2005.5.5下午5:55:55
var allFives = new Date(2005, 4, 5, 17, 55, 55);

继承的方法

同样,Date类型也重写了toLocaleString()toString()valueOf()。前两者返回日期时间等的字符串表示,具体格式因浏览器而异。valueOf()则不返回字符串,而是返回日期的毫秒表示,也因此可以使用比较操作符来比较日期。

格式化方法

将日期格式化为字符串,同样因浏览器而异。

  • toDateString()
  • toTimeString()
  • toLocaleDateString()
  • toLocaleTimeString()
  • toUTCString()

日期/时间组件方法

剩下的和Date类型相关的方法,都是直接缺德日期值中特定部分的方法。其中UTC日期指在没有时区偏差的情况下的日期值,即将日期转为GMT时间后。举例:getTime()setTime()getUTCFullYear()等。

RegExp类型

创建

  • 字面量

按照以下格式创建正则表达式:

1
var expression = / pattern / flags;

pattern为任何正则表达式,可包含字符类、限定类、分组、向前查找以及反向引用。flags为一个或多个标明正则表达式行为的值,有以下几个取值:

  • g:global,模式将被用于所有字符串,而非发现第一个匹配项就停止
  • i:case-insensitive,模式忽略大小写
  • m:multiple,模式在到达行尾时还会查找下一行是否存在模式匹配项
1
2
3
var pattern1 = /at/g;         // 匹配字符串中所有"at"实例
var pattern2 = /[bc]at/i; // 匹配第一个"bat"或"cat",不分大小写
var pattern3 = /.at/gi // 匹配所有"at"结尾的三个字符组合,不分大小写

正则表达式中元字符要转义,转义符为\,元字符包括:( [ { \ ^ $ | ) ? * + . ] }

1
2
var pattern2 = /\[bc\]at/i;     // 匹配第一个"[bc]at",不分大小写
var pattern3 = /\.at/gi // 匹配所有".at",不分大小写
  • RegExp构造函数
1
2
3
// 二者等价
var pattern = /[bc]at/i;
var pattern = new RegExp("[bc]at", "i");

传递给其构造函数的两个参数均为字符串,所以在某些情况下要对字符进行双重转义。

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

1
2
3
4
5
6
7
8
9
10
11
var re = null, i;

for(i = 0; i < 10; i++) {
re = /cat/g;
re.test("catestrophe");
}

for(i = 0; i < 10; i++) {
re = new RegExp("cat", "g");
re.test("catestrophe");
}

第一个例子即使在循环体中指定,实际上只为/cat/创建了一个RegExp实例。由于实例属性不会重置,所以循环中再次调用test()会失败。因为第一次调用test()找到了”cat”;第二次调用从上次匹配末尾(即索引为3)的字符开始,直到测试到末尾,就找不到它了;第三次又从头开始。第二个例子每次循环均创建一个新的RegExp实例,因此每次调用test()都会返回true。
ECMAScript5则明确规定,使用正则表达式字面量必须像直接调用RegExp构造函数一样,每次创建新的RegExp实例。

实例属性

RegExp的每个实例都有以下属性:

  • global:布尔值,表示是否设置标志g
  • ignoreCase:布尔值,表示是否设置标志i
  • lastIndex:表示下一个搜索的字符位置
  • multiline:布尔值,表示是否设置标志g
  • source:正则表达式的字符串表示,按字面量形式的字符串返回

实例方法

  • exec

RegExp对象的主要方法是exec(),该方法接受要应用模式的字符串参数,若能够匹配则返回包含第一个匹配项信息的数组,否则返回null。返回的数组还包含两个额外属性:index用于表示匹配项在字符串中的位置,input用于表示应用正则表达式的字符串。

1
2
3
4
5
6
7
8
9
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标签情况下,在同一字符串上多次调用exec()会始终返回第一个匹配项信息;在设置g标签情况下,每次调用exec()都会在字符串中继续查找新项。

  • test

test()接收一个字符串参数,当模式与其匹配时返回true,否则返回false。

1
2
3
4
5
6
var text = "000-00-0000";
var pattern = /\d{3}-\d{2}-\d{4}/;

if(pattern.test(text)) {
alert("The pattern was matched.");
}
  • 其他

RegExp实例继承的toLocaleString()toString()方法均返回正则表达式的字面量,valueOf()方法返回正则表达式本身。

构造函数属性

RegExp构造函数包含一些属性(在其他语言中称为静态属性),它适用于作用域中所有实例,且基于所执行的最近一次正则表达式操作而变化。这些属性分别有长属性名和短属性名(Opera不支持短属性名)。以下举例格式为”长属性名:短属性名”:

  • input:$_
  • leftContext:$`
  • rightContext:$’
  • lastMatch:$&
  • lastParen:$+
  • multiline:$*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var text = "this has been a short summer";
vat pattern = /(.)hort/g;

if(pattern.test(text)) {
// 要匹配的字符串
alert(RegExp.input); // this has been a short summer
alert(RegExp.$_);

// lastMatch前的文本
alert(RegExp.leftContext); // this has been a
alert(RegExp["$`"]);

// lastMatch后的文本
alert(RegExp.rightContext); // summer
alert(RegExp["$'"]);

// 最近一次与整个正则表达式匹配的字符串
alert(RegExp.lastMatch); // short
alert(RegExp["$&"]);

// 最近一次匹配的捕获组
alert(RegExp.lastParen); // s
alert(RegExp["$+"]);

// 是否使用多行模式
alert(RegExp.multiline); // false
alert(RegExp["$*"]);
}

除了以上属性,还有九个用于存储捕获组的构造函数属性,为RegExp.$1RegExp.$1RegExp.$9。在调用exec()test()方法时,这些属性会被自动填充。

1
2
3
4
5
6
7
var text = "this has been a short summer";
var pattern = /(..)or(.)/g;

if(pattern.test(text)) {
alert(RegExp.$1); // sh
alert(RegExp.$2); // t
}

模式的局限性

尽管ECMAScript中正则表达式功能还算比较完备,但仍缺少一些高级正则表达式特性。详情:regular expression

Fuction类型

函数实际上是对象,每个函数都是Fuction类型的实例,都具有与其他引用类型一样具有的属性和方法。由于函数是对象,因此函数名实际是一个指向函数对象的指针。

创建

  • 函数声明和函数表达式
1
2
3
4
5
6
7
8
9
// 函数声明
function sum(num1, num2) {
return num1 + num2;
}

// 函数表达式
var sum = function(num1, num2) {
return sum1 + sum2;
};
  • Function构造函数
1
var sum = new Function("num1", "num2", "return num1 + num2");

Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成函数体,前面的参数则为新函数的参数。但不建议如此方式的定义,因为这种语法会导致解析两次代码:第一次解析常规ECMAScript代码,第二次解析传入构造函数中的字符串。
由于函数名为指向函数对象的指针,因此函数名与包含对象指针的其他变量没什么不同,也就是说,一个函数可以有多个名字。

1
2
3
4
5
6
7
8
9
function sum(num1, num2) {
return num1 + num2;
}

var anotherSum = sum;
alert(anotherSum(10, 10)); // 20

sum = null; // 解绑sum
alert(anotherSum(10, 10)); // 20

没有重载

第三章曾说ECMAScript中没有函数重载的概念,当时的解释是”ECMAScript函数没有签名”,现在再看可以发现,原因在于函数名为指针,当创建相同函数名的第二个函数时,实际上是覆盖了引用第一个函数的变量(即第一个函数的函数名)。

函数声明与函数表达式

解析器在向执行环境中加载数据时,对函数声明来说,它会率先读取函数声明,并使其在执行任何代码之前可用;对函数表达式来说,只有到解析器执行到它所在位置它才会被解析执行。

1
2
3
4
5
// 可以运行
alert(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
1
2
3
4
5
// 不可运行,会导致unexpected identifier错误
alert(sum(10, 10));
var sum = function(num1, num2) {
return num1 + num2;
};

也可同时使用函数声明和函数表达式,如var sum = function sum(){},但在Safari中会导致错误。

作为值的函数

  • 将函数作为参数传递
1
2
3
4
5
6
7
8
9
10
function callFunc(func, arg) {
return func(arg);
}

function add10(num) {
return num + 10;
}

var result = callFunc(add10, 20);
alert(result); // 30
  • 将函数作为结果返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createCompFunc(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2) {
return -1;
} else if(value1 > value2) {
return 1;
} else {
return 0;
}
}
}

var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
data.sort(createCompFunc("name")); // 按name属性排序
data.sort(createCompFunc("age")); // 按age属性排序

函数的内部属性

函数内部有三个特殊对象:arguments、this、caller。

  • arguments

arguments在第三章讲过,它是一个类数组对象,可以用来保存函数参数,它本身也有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。可以通过例子来体会它的一个作用:消除递归函数与其函数名的耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}

var trueFactorial = factorial;
factorial = function() {
return 0;
};

alert(trueFactorial(5)); // 120
alert(factorial(5)); // 0

在重写后的factorial()函数的函数体内,没有再引用函数名factorial。这样,无论函数使用的是什么名字,都可以保证正常完成递归调用。如果不使用arguments.callee(num - 1)而是使用factorial(num - 1),则均会返回0。

  • this

this引用的是函数据以执行的环境对象。

1
2
3
4
5
6
7
8
9
10
11
window.color = "red";
var o = {color: "blue"};

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

sayColor(); // "red"

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

sayColor()是在全局环境下定义的,它引用了this对象,但在调用前this的值是不确定的,因此this可能会在代码执行过程中引用不同的对象。全局作用域调用时,this引用全局对象window,this.color为window.color;对象o调用时,this引用对象o,this.color为o.color。
有一点要牢记:函数的名字仅为一个包含指针的变量。因此此处即使在不同的环境中执行,全局的sayColor()函数和o.sayColor()指向的仍然时同一个函数。

  • caller

caller属性保存着调用当前函数的函数的引用,如果在全局作用域中调用当前函数,它的值为null。

1
2
3
4
5
6
7
8
9
10
11
function outer() {
inner();
}

function inner() {
alert(inner.caller);
// 或使用
alert(arguments.callee.caller);
}

outer();

以上代码会导致警告框显示outer()函数的源代码。outer()调用了inner(),因此inner.caller就指向outer()

* 易混淆
ECMAScript5还定义了arguments.caller属性,该属性始终为undefined。定义该属性是为了区分arguments.caller和函数的caller属性。

* 严格模式的限制
严格模式下不能访问arguments.callee、arguments.caller,不能为函数的caller属性赋值。

函数属性和方法

属性

函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length、prototype。

  • length

length属性表示函数希望接收的命名参数个数。

1
2
3
4
5
6
7
function func0() {...}
function func1(a) {...}
function func2(a, b) {...}

alert(func0.length); // 0
alert(func1.length); // 1
alert(func2.length); // 2
  • prototype

对于ECMAScript中的引用类型而言,prototype是保存它们所有实例方法的真正所在。也就是说,诸如toString()valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in无法实现。

方法

每个函数都包含两个非继承而来的方法:apply()call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。除此外ECMAScript5还定义了方法bind()

  • apply

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。第二个参数可以是Array实例也可以是arguments对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sum(num1, num2) {
return num1 + num2;
}

// 传入Array实例
function callSum1(num1, num2) {
return sum.apply(this, [num1, num2]);
}

// 传入arguments对象
function callSum2(num1, num2) {
return sum.apply(this, arguments);
}

alert(callSum1(10, 10)); // 20
alert(callSum2(10, 10)); // 20

callSum()在执行sum()函数时传入了this作为this值(因为是在全局作用域中调用的,所以传入的this时window对象)和参数数组。
* 在严格模式下,未指定环境对象而调用函数,则this值不会转型为window。除非明确把函数添加到某个对象或者调用apply()call(),否则this值将是undefined。

  • call

call()方法与apply()方法作用相同,区别在于:将函数的参数传递给apply()时,传递的是参数数组;将函数的参数传递给call()时,传递的是逐个列举的值。

1
2
3
4
5
6
7
8
9
function sum(num1, num2) {
return num1 + num2;
}

function callSum(num1, num2) {
return sum.call(this, num1, num2);
}

alert(callSum(10, 10)); // 20

* apply和call的另一用途

事实上传递参数并非apply()call()真正的用武之地,它们的强大之处在于能够扩充函数赖以运行的作用域。

1
2
3
4
5
6
7
8
9
10
11
window.color = "red";
var o = {color: "blue"};

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

sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue

第一句中,全局环境调用sayColor(),this为window,windows.color为red,故为red;第二三句中,显式地在全局作用域中调用函数,故也为red;第四句中,此时函数体内的this对象指向了o,o.color为blue,故为blue。

* 用apply和call扩充作用域的好处

好处在于对象不需要与方法有任何耦合关系。将上述例子与”函数的内部属性”中的this例子进行比较,之前我们先将sayColor()函数放入对象o中,再通过o来调用它;重写的例子中则无需如此麻烦。

  • bind

bind()方法创建一个函数的实例,其this值会被绑定到传给bind()函数的值。

1
2
3
4
5
6
7
8
9
window.color = "red";
var o = {color: "blue"};

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

var objectSayHello = sayColor.bind(o);
objectSayHello(); // blue

此处sayColor()调用bind()并传入对象o,创建了objectSayHello()函数,该含函数的this值等于o。

  • 其他

每个函数的toLocaleString()toString()方法始终返回函数的代码,格式因浏览器而异。有的返回的代码与源代码中的函数代码一致,有的返回函数代码的内部表示(即由解析器删除了注释并对某些代码作了改动)。valueOf()方法同样也只返回函数代码。

基本包装类型

为了便于操作基本类型值,ECMAScript提供了3中特殊的引用类型:Boolean、Number、String,称为基本包装类型。它们与其他引用类型相似,但同时也具有各自的基本类型相应的特殊行为。

怎么发现是基本包装类型的?

1
2
var s1 = "some text";
var s2 = s1.substring(2);

发现盲点:变量s1是基本类型值中的字符串,基本类型值不是对象,从逻辑上讲它们不应该有方法,但是这里它却有方法substring()。其实,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。如在以上的例子中,就进行了如下操作:

  • 创建String类型的一个实例
  • 在实例上调用指定的方法
  • 销毁这个实例

相当于:

1
2
3
var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;

以上三个步骤同样适用于Boolean和Number类型对应的布尔值和数字值。

基本包装类型与引用类型的区别

主要区别在于对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。自动创建的基本包装类型的对象,只存在于代码执行的瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。

1
2
3
var s1 = ="some text";
s1.color = "red";
alert(s1.color); // 会报错

第二行创建的String对象被添加了color属性,但是它在执行第三行代码时就被销毁了,第三行代码又创建了自己的String对象,而该对象没有color属性。

typeof和instanceof

对基本包装类型调用typeof返回”object”,且所有基本包装类型的对象都会被转换为布尔值true。Object构造函数会像工厂方法一样,根据传入值的类型返回相应的基本包装类型的实例。

1
2
var obj = new Object("some text");
alert(obj instance of String);// true

把字符串传给Object构造函数会返回String实例;传入数值会返回Number实例;传入布尔值会返回Boolean实例。

基本包装类型的构造函数和转型函数

要注意区分。

1
2
3
4
5
6
7
var value = "25";

var number = Number(value); // 转型函数
alert(typeof number); // "number"

var obj = new Number(value); // 构造函数
alert(typeof obj); // "object"

Boolean类型

可使用构造函数传入true或false来创建Boolean对象。Boolean类型的实例重写了valueOf()方法,返回true或false;重写了toString()方法,返回”true”或”false”。基本类型和基本包装类型容易混淆,包括在真值上的混淆和使用类型判别符的混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var falseObject = new Boolean(false);
var falseValue = false;

// 真值上
alert(falseObject && true); // true
alert(falseValue && true); // false

// tyepeof
alert(typeof falseObject); // "object"
alert(typeof falseValue); // "boolean"

// instanceof
alert(falseObject instanceof Boolean); // true
alert(falseValue instanceof Boolean); // false

由于经常会造成人们的误解,因此不建议使用Boolean对象。

Number类型

可使用构造函数传入数值来创建Number对象。Number类型重写了valueOf()方法,返回数值;重写了toString()toLocaleString()方法,返回字符串。其中第三章中写过,可以为toString()方法传递一个表示基数的参数,来返回指定进制的该数值字符串形式。
除了继承的方法外,Number类型还提供了一些用于将数值格式化为字符串的方法。

  • toFixed

toFixed()方法按照指定的小数位返回数值的字符串表示,参数为指定输出结果中的小数位数。toFixed()可表示带有0到20个小数位的数值。

1
2
3
4
5
var num = 10;
alert(num.toFixed(2)); // "10:00"

var num = 10.005;
alert(num.toFixed(2)); // "10.01",四舍五入
  • toExponential

toExponential()方法返回以指数表示法表示的数值的字符串形式,参数为指定输出结果中的小数位数。

1
2
var num = 10;
alert(num.toExponential(1)); // "1.0e+1"
  • toPrecision

对一个数值来说,toPrecision()方法可能会返回固定大小(fixed)格式,也可能返回指数(exponential)格式,具体规则是看哪种格式最合适。此方法接收的参数为表示数值的所有数字的位数(不包括指数部分)。toPrecision()可表示1到21位小数。

1
2
3
4
var num = 99;
alert(num.toPrecision(1)); // "1e+2"
alert(num.toPrecision(2)); // "99"
alert(num.toPrecision(3)); // "99.0"

和Boolean对象类似,Number对象也以后台方式为数值提供了重要的功能。但同样存在易于造成误解的问题,因此也不建议直接实例化Number类型。

1
2
3
4
5
6
7
8
9
10
var numberObject = new Number(10);
var numberValue = 10;

// tyepeof
alert(typeof numberObject); // "object"
alert(typeof numberValue); // "number"

// instanceof
alert(numberObject instanceof Number); // true
alert(numberValue instanceof Number); // false

String类型

可使用构造函数传入数值来创建String对象。String类型继承的valueOf()toString()toLocaleString()方法,都返回字符串。每个String类型的实例都有length属性,表示字符串中的字符个数。String类型提供了许多方法,用于辅助完成对ECMAScript中字符串的解析。

字符方法

charAt()charCodeAt()用于访问字符串中特定字符,二者接收一个表示下标的参数,分别返回字符串该位置的字符和字符编码。ECMAScript5还提供了方括号加数字索引访问字符串的方法。

1
2
3
4
var stringValue = "hello world";
alert(stringValue.charAt(1)); // "e"
alert(stringValue.charCodeAt(1)); // "101","e"的字符编码
alert(stringValue[1]); // "e"

字符串操作方法

  • concat

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

1
2
3
4
5
6
7
var stringValue = "hello ";
var result1 = stringValue.concat("world");
var result2 = stringValue.concat("world", "!");

alert(stringValue); // "hello "
alert(result1); // "hello world"
alert(result2); // "hello world!"
  • slice

slice()接收一个或两个参数,第一个参数指定子字符串起始位置,第二个参数指定子字符串最后一个字符后面的位置。若参数中有负数则将负数参数加上字符串length。

1
2
3
4
5
6
7
8
9
var stringValue = "hello world";
alert(stringValue.slice(3)); // "lo world"
alert(stringValue.slice(3, 7)); // "lo w"

// 转为slice(8)
alert(stringValue.slice(-3)); // "rld"

// 转为slice(3, 7)
alert(stringValue.slice(3, -4)); // "lo w"
  • substring

substring()接收一个或两个参数,第一个参数指定子字符串起始位置,第二个参数指定子字符串最后一个字符后面的位置。若参数中有负数则将负数参数转换为0。

1
2
3
4
5
6
7
8
9
var stringValue = "hello world";
alert(stringValue.substring(3)); // "lo world"
alert(stringValue.substring(3, 7)); // "lo w"

// 转为substring(0)
alert(stringValue.substring(-3)); // "hello world"

// 转为substring(3, 0),将小的作为开始位置
alert(stringValue.substring(3, -4)); // "hel"
  • substr

substr()接收一个或两个参数,第一个参数指定子字符串起始位置,第二个参数指定子字符串的字符个数。若第一个参数为负数,则将其加上字符串length;若第二个参数为负数,则将其转换为0。

1
2
3
4
5
6
7
8
9
var stringValue = "hello world";
alert(stringValue.substr(3)); // "lo world"
alert(stringValue.substr(3, 7)); // "lo worl"

// 转为substr(8)
alert(stringValue.substr(-3)); // "rld"

// 转为substr(3, 0)
alert(stringValue.substr(3, -4)); // ""

slice()substring()substr()三个方法均返回子字符串,不影响原字符串。若没有传递第二个参数,则将字符串的长度作为结束位置。

字符串位置方法

indexOf()lastIndexOf()用于在字符串中查找子字符串,若找到则返回子字符串的位置,否则返回-1。二者分别从前往后和从后往前进行查找。方法可接受一个或两个参数,第一个参数指定查找的子字符串,第二个参数指定开始搜索的位置。

1
2
3
4
5
6
var stringValue = "hello world";
alert(stringValue.indexOf("o")); // 4
alert(stringValue.lastIndexOf("o")); // 7

alert(stringValue.indexOf("o", 6)); // 7
alert(stringValue.lastIndexOf("o", 6)); // 4

字符串修剪方法

trim()方法创建字符串的副本,删除前置和后缀所有空格,返回结果。还有trimLeft()trimRight()方法用于删除字符串开头或末尾空格。

字符串大小写转换方法

共有四个方法:toLowerCase()toUpperCase()toLocaleLowerCase()toLocaleUpperCase()。locale的方法是针对特定地区的实现,有些地区中locale方法和通用方法得到的结果相同,而有些语言(如土耳其语)会为Unicode大小写转换应用特殊的规则,这时就必须使用locale方法来保证实现正确的转换。

字符串的模式匹配方法

  • match

在字符串上调用match()本质与调用RegExp的exec()方法相同。该方法只接收一个参数,参数为正则表达式或RegExp对象。

1
2
3
4
5
6
7
8
var text = "cat, bat, sat, fat";
var pattern = /.at/;

// 与pattern.exec(text)相同
var matches = text.match(pattern);
alert(matches.index); // 0
alert(matches[0]); // "cat"
alert(pattern.lastIndex); // 0
  • search

search()方法只接收一个参数,参数为正则表达式或RegExp对象,若查找成功返回第一个匹配项的索引,否则返回-1。search()方法始终从字符串开头查找。

1
2
3
var text = "cat, bat, sat, fat";
var pos = text.search(/at/)
alert(pos); // 1
  • replace

为了简化子字符串替换的操作,ECMAScript提供了replace()方法。该方法接收两个参数,第一个参数为正则表达式或字符串(这个字符串不会被转换为正则表达式),第二个参数为字符串或者函数。
若第一个参数为字符串,就会只替换第一个子字符串;若想替换所有子字符串,就必须提供一个带g标签的正则表达式。

1
2
3
4
5
6
var text = "cat, bat, sat, fat";
var result1 = text.replace("at", "ond");
alert(result1); // "cond, bat, sat, fat"

var result2 = text.replace(/at/g, "ond");
alert(result2); // "cond, bond, sond, fond"

若第二个参数是字符串,还可以使用一些特殊的字符序列,将正则表达式操作得到的值插入到结果字符中。
字符序列:

  • $$:$
  • $&:匹配整个模式的子串
  • $’:匹配的子串之前的字符串
  • $`:匹配的子串之后的字符串
  • $n:匹配第n个捕获组的子串,n为1-9
  • $nn:匹配第nn个捕获组的子串,nn为01-99
1
2
3
var text = "cat, bat, sat, fat";
var result = text.replace(/(.at)/g, "word($1)");
alert(result); // word(cat), word(bat), word(sat), word(fat)

若第二个参数是函数,该函数的参数为:模式的匹配项(可能有多个)、模式匹配项在字符串中的位置、原始字符串。该函数的返回值为一个字符串,表示在函数内部一系列处理之后返回的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function htmlEscape(text) {
return text.replace(/[<>"&]/g, function(match, pos, originalText) {
switch(match) {
case "<":
return "&lt;";
case ">":
return "&gt;"
case "&":
return "&amp;";
case "\"":
return "&quot;";
}
});
}

var html = "<p class=\"greeting\">Hello world!</p>"
alert(htmlEscape(html));
// &lt;p class=&quot;greeting&quot;&gt;Hello world!&lt;/p&gt;

这里的函数htmlEscape()能够转义四个字符,对这四个字符的匹配使用了正则表达式。

  • split

split()方法基于指定的分隔符将一个字符串分割成多个字符串,并将结果放在一个数组中返回。分隔符可以是RegExp对象或字符串(这个字符串不会被转换为正则表达式)。split()方法可以接受可选的第二个参数,用于指定数组的大小。

1
2
3
4
5
6
7
8
9
10
11
var colorText = "red, blue, green, yellow";

// ["red", " blue", " green", " yellow"]
var colors1 = colorText.split(",");

// ["red", " blue"]
var colors2 = colorText.split(",", 2);

// ["", ",", ",", ",", ""]
// 这个匹配的是一个或多个任何非逗号的字符
var colors3 = colorText.split(/[^\,]+/);

split()中正则表达式的支持因浏览器而异。

字符串比较方法

localeCompare()方法比较两个字符串,若在字母表中字符串应排在参数前面,则返回一个负数;若等于返回0;若应排后面返回一个正数。返回的正数或负数取决于实现,大多数情况下为1和-1。

1
2
3
4
var stringValue = "yellow";
alert(stringValue.localeCompare("brick")); // 1
alert(stringValue.localeCompare("yellow")); // 0
alert(stringValue.localeCompare("zoo")); // -1

字符编码转换函数

String构造函数本身还有一个静态方法fromCharCode(),该方法接收一个或多个字符编码,再将它们转换为一个字符串。本质上与charCodeAt()执行的是相反的操作。

1
alert(String.fromCharCode(104, 101, 108, 108, 111)); // "hello"

HTML方法

这些方法是为了使用JavaScript动态格式化HTML,不过不建议使用,因为它们创建的标记通常无法表达语义。

  • anchor(name):输出结果为<a name="name">string</a>
  • big():输出结果为<big>string</big>

单体内置对象

内置对象:由ECMAScript实现提供的、不依赖于宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了。也就是说开发者不必显示地实例化对象,因为它们已经实例化了。

Global对象

Global对象在某种意义上是作为一个”兜底对象”定义的,即不属于任何其他对象的属性和方法,最终都是它的属性和方法。事实上没有全局变量或全局函数;所有在全局作用域中定义的属性和函数,都是Global对象的属性。如之前提到的isNaN()isFinite()parseInt()parseFloat(),实际上都是Global对象的方法。除此之外,Global对象还包含一些其他方法。

URI编码方法

encodeURI()encodeURIComponent()方法可以对URI(Uniform Resource Identifiers,通用资源标识符)进行编码,以便发送给浏览器。因为有效的URI中不能包含某些字符(如空格),编码方法的存在就可以对URI进行编码,用特殊的UTF-8编码替换无效字符,从而使浏览器接受和理解。

1
2
3
4
5
6
7
var uri = "https://www.wrox.com/illeegal value.htm#start";

// "https://www.wrox.com/illeegal%20value.htm#start"
alert(encodeURI(uri));

// "https%3A%2F%2Fwww.wrox.com%2Filleegal%20value.htm%23start"
alert(encodeURIComponent(uri));

encodeURI()只替换空格,encodeURIComponent()替换所有非字母数字字符。因此一般对整个URI用前者,对URI的某一段(如例子URI中的illeegal value.htm)用后者。由于更常见的是对查询字符串参数而不是基础URI进行编码,因此后者用得更多。
相应的解码方法为decodeURI()decodeURIComponent(),前者只对encodeURI()替换的字符进行解码,后者只对decodeURIComponent()替换的字符进行解码。

1
2
3
4
5
6
7
var uri = "https%3A%2F%2Fwww.wrox.com%2Filleegal%20value.htm%23start";

// https%3A%2F%2Fwww.wrox.com%2Filleegal value.htm%23start
alert(decodeURI(uri));

// https://www.wrox.com/illeegal value.htm#start
alert(decodeURIComponent(uri));

eval方法

eval()方法像一个ECMAScript解析器,接受一个参数:待执行的ECMAScript字符串。在调用eval()方法时,传入的参数会被当做实际的ECMAScript语句来解析,然后把执行结果插入到原位置。

1
2
3
4
eval("alert('Hello')");

// 相当于
alert("Hello");

通过eval()执行的代码被认为是包含该次调用的执行环境的一部分,因为eval行代码最终会被替换为其内部的真正要执行的代码。因此可以进行如下的使用:

1
2
3
4
5
6
7
8
var msg = "Hello";
eval("alert(msg)");

eval("function sayHello() { alert('Hello'); }");
sayHello();

eval("var msg = 'Hello';");
alert(msg);

严格模式下,在外部访问不到eval()中创建的任何变量或函数(也就是后两个例子会出错),也不能为eval赋值。
能够解释代码字符串的能力非常强大,但也非常危险。因此在使用eval()时要谨慎,防止恶意用户通过它来威胁站点或应用程序的安全(即代码注入)。

Global对象的属性

undefined、NaN、Infinity、Object、Array、Function、Boolean、String、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError,这些是Global对象的所有属性。前三个是特殊值,后面的均为原生引用类型的构造函数。

window对象

ECMAScript没有指出如何直接访问Global对象,但Web浏览器都是将全局对象作为window对象的一部分加以实现的。因此在全局作用域中声明的所有变量和函数都成了window对象的属性。
可通过以下代码取得Global对象:

1
2
3
var global = function() {
return this;
}();

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

Math对象

Math对象的属性

为计算会用到的特殊值:Math.EMath.LN10Math.PI

最值方法

max()min(),可接收任意多的数值参数。

1
var max = Math.max(1, 2, 3, 4, 5, 6, 7, 8);

若想找到数组中的最大值可以使用如下技巧:

1
2
var values = [1, 2, 3, 4, 5, 6, 7, 8];
var max = Math.max.apply(Math, values);

技巧在于把Math对象作为apply()的第一个参数,从而正确地设置this值。

舍入方法

ceil()floor()round(),分别向上舍入、向下舍入和四舍五入。

random方法

Math.random()方法返回介于0和1之间的随机数,不包括0和1。
若要从某个整数范围内随机选择一个值,可以使用如下公式:

1
2
3
4
值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值);

// 选择2到10的随机值
var num = Math.floor(Math.random() * 9 + 2);

其他方法

即完成各种运算的方法:Math.abs(num)Math.exp(num)