四、JavaScript 引用类型
引用类型的值(对象)是引用类型的一个实例。
在 ECMAscript 中,引用类型是一种数据结构,用于将数据和功能组织在一起。
引用类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。
对象是某个特定引用类型的实例。新对象是使用 new 操作符后跟一个构造函数来创建,构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。
let person = new Object();
这行代码创建了 Object 引用类型的一个新实例,然后把该实例保存在了变量 person 中。使用的构造函数是 Object,它只为新对象定义了默认的属性和方法。
1、Object 类型
到目前为止,我们看到的大多数引用类型值都是 Object 类型的实例。而且,Object 也是 ECMAscript 中使用最多的一个类型。
创建 Object 实例的方式有两种。
第一种是使用 new 操作符后跟 Object 构造函数:
let person = new Object();
person.name = 'Jack';
person.age = 21;
另一种是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。示例:
let person = {
name: 'Jack',
age: 21
};
对象字面量也是向函数传递大量可选参数的首选方式,例如:
function displayInfo(args) {
let output = '';
if (typeof args.name === 'string') {
output += `Name: ${args.name}\n`;
}
if (typeof args.age === 'number') {
output += `Age: ${args.age}\n`;
}
console.log(output);
}
displayInfo({
name: 'Jack',
age: 21
});
displayInfo({
name: 'Jack'
});
这种传递参数的模式最适合需要向函数传入可选参数的情形。一般来讲,命名参数虽然容易处理,但在有多个可选参数的情况下就会显示不够灵活。最好的做法是对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数。
一般来说,访问对象属性时使用点表示法。但在 JavaScript 中,也可以使用方括号表示法来访问对象的属性。使用方括号表示法时,将要访问的属性以字符串的形式放在方括号中。示例:
console.log(person['name']); // 'Jack'
console.log(person.name); // 'Jack'
方括号语法的主要有点是可以通过变量来访问属性,例如:
let propertyName = 'name';
console.log(person[porpertyName]); // 'Jack'
如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法。
建议:除非必须使用变量来访问属性,否则我们建议使用点表示法。
2、Array 类型
除 Object 之外, Array 类型恐怕是 ECMAscript 中最常用的类型了。
ECMAscript 中的数组与其他语言的不同:
- ECMAscript 数组的每一项可以保存任何类型的数据
- ECMAscript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。
创建数组的基本方式有两种。第一种使用 Array 构造函数,示例:
let colors = new Array();
如果预先知道数组要保存的项目数量,也可以给构造函数传递该数量,而该数量会自动变成 length 属性的值。例如:
// 创建 length 值为 20 的数组
let colors = new Array(20);
也可以向 Array 构造函数传递数组中应该包含的项。例如:
// 创建包含 3 个字符串值的数组
let colors = new Array('red', 'blue', 'green');
给构造函数传递一个值时,如果传递的是数值,则会按照该数值创建包含给定项数的数组;如果传递的是其他类型的参数,则会创建包含那个值的只有一项的数组。
使用 Array 构造函数时可以省略 new 操作符。
创建数组的第二种基本方法是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开,示例:
// 创建一个包含 3 个字符串的数组
let colors = ['red', 'blue', 'green'];
// 创建一个空数组
let name = [];
// 不要这样!会创建一个包含 2 或 3(IE8 及之前版本)项的数组
let values = [1, 2, ];
// 不要这样!会创建一个包含 5 或 6(IE8 及之前版本)项的数组
let options = [ , , , , , ];
如上述最后一行代码所示,在像这种省略值的情况下,每一项都将获得 undefined 值;这个结果与调用 Array 构造函数时传递项数在逻辑上是相同的。但是由于 IE(IE8 及之前版本)的实现与其它浏览器不一致,因此我们不建议使用这种语法。
在读取和设置数组的值是,要使用方括号并提供相应的基于 0 的数字索引。示例:
let colors = ['red', 'blue', 'green']; // 定义一个字符串数组
console.log(colors[0]); // 显示第一项
colors[2] = 'black'; // 修改第三项
colors[3] = 'brown'; // 新增第四项
方括号中的索引表示要访问的值。
数组的项数保存在其 length 属性中。
数组的 length 属性不是只读的,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。示例:
let colors = ['red', 'blue', 'green']; // 创建一个包含 3 个字符串的数组
colors.length = 2;
console.log(colors[2]); // undefined
如果将其 length 属性设置为大于数组项数的值,则新增的每一项都会取得 undefined 值,示例:
let colors = ['red', 'blue', 'green']; // 创建一个包含 3 个字符串的数组
colors.length = 4;
console.log(colors[3]); // undefined
利用 length 属性也可以方便地在数组末尾添加新项, 示例:
let colors = ['red', 'blue', 'green']; // 创建一个包含 3 个字符串的数组
colors[colors.length] = 'black'; // (在位置 3)添加一种颜色
colors[colors.length] = 'brown'; // (在位置 4)添加一种颜色
数组最多可以包含 4 294 967 295 个项。如果想添加的项数超过这个上限,就会发生异常。而创建一个初始大小与这个上限值接近的数组,咋可能会导致运行时间超长的脚本错误。
2.1、检测数组
使用 Array.isArray() 方法检测某个值是不是数组,示例:
if (Array.isArray(value)) {
// 对数组执行某些操作
}
支持 Array.isArray() 方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 10.5+ 和 chrome。
2.2、转换方法
调用数组的 toString() 方法会返回由数组中的每个值的字符串形式拼接而成的一个以逗号分隔的字符串,而调用 valueOf() 方法返回的还是数组。示例:
let colors = ['red', 'blue', 'green']; // 创建一个包含 3 个字符串的数组
console.log(colors.toString()); // red,blue,green
console.log(colors.valueOf()); // ['red', 'blue', 'green']
console.log(colors); // ['red', 'blue', 'green']
数组继承的 toLocaleString()、toString() 方法,在默认情况下都会以逗号分隔的字符串形式返回数组项。
使用 join() 方法,可以使用不同的分隔符来构建这个字符串。join() 方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。示例:
let colors = ['red', 'blue', 'green'];
console.log(colors.join(',')); // red,blue,green
console.log(colors.join('||')); // red||blue||green
如果不给 join() 方法传入任何值,或者传入 undefined,则默认使用逗号作为分隔符。IE7及更早版本会错误的使用字符串 'undefined' 作为分隔符。
如果数组中的某一项值是 null 或 undefined,那么该值在 join()、toLocaleString()、toString() 方法返回的结果中以空字符串表示。
2.3、栈方法
栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构。栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。
ECMAscript 为数组专门提供了 push() 和 pop() 方法,以便实现类似栈的行为。
push() 方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而 pop() 方法则从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。示例:
let colors = new Array(); // 创建一个数组
let count = colors.push('red', 'green'); // 推入两项
console.log(count); //2
count = colors.push('black'); // 推入另一项
console.log(count); //3
let item = colors.pop(); // 取得最后一项
console.log(item); // 'black'
console.log(colors.length); //2
2.4、队列方法
队列数据结构的访问规则是FIFO(First-In-First-Out,先进先出)。队列在列表末端添加项,从列表的前端移除项。
shift() 方法能够移除数组中的第一个项并返回该项,同时将数组长度减 1。结合使用 shift() 和 push() 方法,可以像使用队列一样使用数组。示例:
let colors = new Array(); // 创建一个数组
let count = colors.push('red', 'green'); // 推入两项
console.log(count); // 2
count = colors.push('black'); // 推入另一项
console.log(count); // 3
let item = colors.shift(); // 取得第一项
console.log(item); // 'red'
console.log(colors.length); // 2
unshift() 方法与 shift() 方法用途相反,它能在数组前端添加任意个项并返回新数组的长度。同时使用 unshift() 和 pop() 方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项。示例:
let colors = new Array(); // 创建一个数组
let count = colors.unshift('red', 'green'); // 推入两项
console.log(count); // 2
count = colors.unshift('black'); // 推入另一项
console.log(count); // 3
let item = colors.pop(); // 取得最后一项
console.log(item); // 'green'
console.log(colors.length); // 2
2.5、重排序方法
数组中存在两个可以直接用来重排序的方法: reverse() 和 sort() 方法。
reverse() 方法会反转数组项的顺序。示例:
let values = [1, 2, 3, 4, 5];
values.reverse();
console.log(values); // [5, 4, 3, 2, 1];
默认情况下,sort() 方法按升序排列数组项——即最小的值位于最前面,最大值排在最后面。为了实现排序,sort() 方法会调用每个数组项的 toString() 转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort() 方法比较的也是字符串,示例:
let values = [0, 1, 5, 10, 15];
values.sort();
console.log(values); // [0, 1, 10, 15, 5];
sort() 方法可以(仅可以)接收一个比较函数作为参数,以便我们制定数组的排序。接收函数的返回值判断数组的排序。
a 和 b 比较之后:
- 返回一个小于 0 的值, a 在 b 之前。
- 返回 0,a、b 的顺序不变。
- 返回一个大于 0 的值,a 在 b 之后。
对于数值类型或者其 valueOf() 方法会返回数值类型的对象类型,可以使用下面的比较函数,示例:
function compare(a, b) {
return a - b;
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
console.log(values); // [0, 1, 10, 15, 5];
对于其它对象类型,可以使用下列比较函数,指定返回值:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
reverse() 和 sort() 方法的返回值是经过排序之后的数组。
2.6、操作方法
concat() —— 可以基于当前数组中的所有项创建一个新数组。
具体来说,这个方法会先创建当前数组的一个副本然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给 concat() 方法传递参数的情况下,它只是复制当前数组并返回副本。如果传递给 concat() 方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中。如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。
let colors = ['red', 'green', 'blue'];
let colors2 = colors.concat('yellow', ['black', 'brown']);
console.log(colors); // ['red', 'green', 'blue']
console.log(colors2); // ['red', 'green', 'blue', 'yellow', 'black', 'brown']
slice() —— 它能够基于当前数组中的一或多个项创建一个新数组。
slice() 方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice() 方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。示例:
let colors = ['red', 'green', 'blue', 'yellow', 'black', 'brown'];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1,4);
console.log(colors2); // ['green', 'blue', 'yellow', 'black', 'brown']
console.log(colors3); // ['green', 'blue', 'yellow']
slice() 方法不会影响原始数组。
如果 slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。例如,在一个包含 5 项的数组上调用 slice(-2, -1)与调用 slice(3, 4) 得到的结果相同。如果结束位置小于起始位置,则返回空数组。
splice() —— 最强大的数组方法。
删除
功能:可以删除任意数量的项。
参数:2 个;要删除的第一项的位置,要删除的项数
示例:
let colors = ['red', 'green', 'blue', 'yellow'];
let colors2 = colors.splice(1, 2); // 从位置 1 开始删除 2 项
console.log(colors); // ['red', 'yellow']
console.log(colors2); // ['green', 'blue'], 返回的数组中包含 2 项
插入
功能:可以向指定位置插入任意数量的项。
参数:3;起始位置,0(要删除的项数),要插入的项(如果要插入多个项,可以再传入第四、第五,以至任意项。
示例:
let colors = ['red', 'green', 'blue', 'yellow'];
let colors2 = colors.splice(1, 0, 'black', 'orange'); // 从位置 1 开始插入 2 项
console.log(colors); // ['red', 'black', 'orange', 'green', 'blue', 'yellow']
console.log(colors2); // [], 返回一个空数组
替换
功能:可以向指定位置插入任意数量的项,且同时删除任意数量的项。
参数:3;起始位置,要删除的项数,要插入的任意数量的项
示例:
let colors = ['red', 'green', 'blue', 'yellow'];
let colors2 = colors.splice(1, 1, 'pink', 'purple'); // 从位置 1 开始删除 1 项,插入 2 项
console.log(colors); // ['red', 'pink', 'purple', 'blue', 'yellow']
console.log(colors2); // ['green'], 返回的数组中包含 1 项
splice() 方法始终会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。
2.7、位置方法
indexOf()
功能:返回要查找的项在数组中的位置,从数组开头向后查找。
参数:2;要查找的项,(可选)查找起点位置的索引。
lastIndexOf()
功能:返回要查找的项在数组中的位置,从数组末尾向前查找。
参数:2;要查找的项,(可选)查找起点位置的索引。
这两个方法都返回要查找的项在数组中的位置,或者在没有找到的情况下返回 -1。使用全等操作符(===)进行比较。
示例:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
console.log(numbers.indexOf(4)); // 3,从头开始向后的第一个 4
console.log(numbers.lastIndexOf(4)); // 5,从末尾开始向前的第一个 4
console.log(numbers.indexOf(4, 4)); // 5,从第 4 位开始向后的第一个 4
console.log(numbers.lastIndexOf(4, 4)); // 3,从第 4 位开始向前的第一个 4
let person = { name: "Nicholas" };
let people = [{ name: "Nicholas" }];
let morePeople = [person];
console.log(people.indexOf(person)); // -1
console.log(morePeople.indexOf(person)); // 0
2.8、迭代方法
数组的每个迭代方法都接收 2 参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值。
传入这些这些方法中的函数会接受 3 个参数:数组项的值、该项在数组中的位置、数组对象本身。
以下是 5 个迭代方法的作用:
- every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true ,则返回 true。
- some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。
- filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
- map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
- forEach():对数组中的每一项运行给定函数,这个方法没有返回值。
以上方法都不会修改数组中包含的值。
every() 和 some() 都用于查询数组中的项是否满足某个条件。示例:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every(function(item, index, array){
return (item > 2);
});
console.log(everyResult); // false
let someResult = numbers.some(function(item, index, array){
return (item > 2);
});
console.log(someResult); // true
filter() 函数:利用指定的函数确定是否在返回的数组中包含某一项。
// 要返回一个所有数值都大于 2 的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter(function(item, index, array){
return (item > 2);
});
console.log(filterResult); // [3, 4, 5, 4, 3]
map() 函数:返回一个数组,而这个数组的每一项都是在原始数组中的对应项运行传入函数的结果。cif
// 给数组中的每一项乘以 2
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map(function(item, index, array){
return item * 2;
});
console.log(mapResult); // [2, 4, 6, 8, 10, 8, 6, 4, 2]
forEach() 方法:对数组的每一项运行传入的函数,没有返回值。本质上与使用 for 循环迭代数组一样。
let numbers = [1,2,3,4,5,4,3,2,1];
numbers.forEach(function(item, index, array){
// 执行某些操作
});
2.9、归并方法
reduce() 和 reduceRight() 是归并数组的方法。两个方法都会迭代数组的所有项,然后构建一个最终返回的值。
reduce() 方法从数组的第一项开始,逐个遍历到最后。
reduceRight() 方法从数组的最后一项开始,向前遍历到第一项。
这两个方法都接收两个参数:在每一项上调用的函数,(可选)最为归并基础的初始值。
传给 reduce() 和 reduceRight() 的函数接收 4 个函数:前一个值、当前值、项的索引、数组对象。这个函数的返回的任何值都会作为第一个参数自动传给下一项。
第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数是数组的第二项。
// 求数组中所有值之和
let values = [1, 2, 3, 4, 5];
let sum = values.reduce(function(prev, cur, index, array){
return prev + cur;
});
console.log(sum); // 15
第一次执行回调函数, prev 是 1(数组的第一项), cur 是 2(数组的第二项);第二次,prev 是 3(1 + 2 的结果),cur 是 3(数组的第三项)。这个过程会持续到吧数组中的每一项都访问一遍,最后返回结果。
reduceRight() 的作用类似,只不过方向相反。
// 求数组中所有值之和
let values = [1, 2, 3, 4, 5];
let sum = values.reduceRight(function(prev, cur, index, array){
return prev + cur;
});
console.log(sum); // 15
上述例子中,第一次执行回调函数, prev 是 5(数组的最后一项), cur 是 4(数组的倒数第二项);
3、Date 类型
Date 类型使用自 UTC(Coordinated Universal Time, 国际协调时间)1970 年 1 月 1 日午夜(零时)开始经过的毫秒数来保存日期。在使用这种数据存储格式的条件下,Date 类型保存的日期能够精确到 1970 年 1 月 1 日之前或之后的 285 616 年。
创建一个日期对象,使用 new 操作符和 Date 构造函数,示例:
let now = new Date();
调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象,必须传入表示改日期的毫秒数(即从 UTC 时间 1970 年 1 月 1 日午夜起至该日期止经过的毫秒数)。为了简化这一计算过程,使用以下两个方法:Date.parse() 和 Date.UTC()。
Date.parse() 方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。日期格式通常为以下几种类型:
- '月/日/年',如 12/25/2017;
- '英文月名 日,年',如 December 25,2017;
- '英文星期几 英文月名 日 年 时:分:秒 时区',如 Monday December 25 2017 00:00:00 GMT-0700;
- ISO 8601 扩展格式 YYYY-MM-DDTHH:mm:ss.sssZ,如 2017-12-25T00:00:00。
// 2017 年 12 月 25 日创建一个日期对象
let someDate = new Date(Date.parse('May 25, 2004'));
// 等价于
let someDate = new Date('May 25, 2004');
如果传入 Date.parse() 方法的字符串不能表示日期,那么会返回 NaN。如上所述,直接将表示日期的字符串传递给 Date 构造函数,也会在后台调用 Date.parse()。
Date.UTC() 方法同样也返回表示日期的毫秒数。
Date.UTC() 的参数分别是:
- 年份(必需)
- 基于 0 的月份(一月是 0,二月是 1,以此类推)(必需)
- 月中的一天(1 到 31)
- 小时数(0 到 23)
- 分钟
- 秒
- 毫秒数
如果没有提供月中的天数,则假设天数为 1;如果省略其他参数,则统统假设为 0。示例:
// GMT 时间 2000 年 1 月 1 日午夜零时
let y2k = new Date(Date.UTC(2000, 0));
// GMT 时间 2017 年 12 月 25 日下午 5:55:55
let allFives = new Date(Date.UTC(2017, 11, 25, 17, 55, 55));
Date 构造函数会模仿 Date.UTC(),但有一点明显不同:日期和时间都基于本地时区而非 GMT 来创建。不过,Date 构造函数接收的参数仍然与 Date.UTC() 相同。因此,如果第一个参数是数值, Date() 构造函数就会假设该值是日期中的年份,而第二个参数是月份,以此类推,示例:
// 本地时间 2000 年 1 月 1 日午夜零时
let y2k = new Date(2000, 0);
// 本地时间 2017 年 12 月 25 日下午 5:55:55
let allFives = new Date(2017, 11, 25, 17, 55, 55);
Date.now() 方法:返回调用这个方法时的日期和时间的毫秒数。这个方法简化了使用 Date 对象分析代码的工作。例如:
// 取得开始时间
let start = Date.now();
// 调用函数
doSomething();
// 取得停止时间
let stop = Date.now();
let result = stop – start;
// 等价于
// 取得开始时间
let start = +new Date();
// 调用函数
doSomething();
// 取得停止时间
let stop = +new Date();
let result = stop - start;
使用 + 操作符把 Date 对象转换为字符串。
3.1、继承的方法
与其它引用类型一样, Date 类型也重写了 toLocaleString()、toString()、valueOf() 方法。
Date 类型的 toLocaleString() 方法会按照与浏览器设置的地区相适应的格式返回日期和时间。这大致意味着时间格式中会包含 AM 或 PM,但不会包含时区信息(当然,具体的格式会因浏览器而异)。
Date 类型的 toString() 方法则通常返回带有时区信息的日期和时间,其中时间一般以军用时间(即小时的范围是 0 到 23)表示。
下面给出了在不同浏览器中调用 toLocaleString() 和 toString() 方法,输出 PST(Pacific Standard Time,太平洋标准时间)时间 2007 年 2 月 1 日午夜零时的结果。
// Internet Explorer 8
toLocaleString() — Thursday, February 01, 2007 12:00:00 AM
toString() — Thu Feb 1 00:00:00 PST 2007
// Firefox 3.5
toLocaleString() — Thursday, February 01, 2007 12:00:00 AM
toString() — Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
// Safari 4
toLocaleString() — Thursday, February 01, 2007 00:00:00
toString() — Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
// Chrome 4
toLocaleString() — Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
toString() — Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
// Opera 10
toLocaleString() — 2/1/2007 12:00:00 AM
toString() — Thu, 01 Feb 2007 00:00:00 GMT-0800
显然,这两个方法在不同的浏览器中返回的日期和时间格式可谓大相径庭。事实上,toLocaleString() 和 toString() 的这一差别仅在调试代码时比较有用,而在显示日期和时间时没有什么价值。
Date 类型的 valueOf() 方法,不返回字符串,而是返回日期的毫秒表示。因此,可以方便使用比较操作符(小于或大于)来比较日期值。示例:
var date1 = new Date(2017, 0, 1); // 'January 1, 2017'
var date2 = new Date(2017, 1, 1); // 'February 1, 2017'
console.log(date1 < date2); // true
console.log(date1 > date2); // false
3.2、日期格式化方法
Date() 类型有一些专门用于将日期格式化为字符串的方法,
- toDateString() —— 以特定于实现的格式显示星期几、月、日、年;
- toTimeString() —— 以特定于实现的格式显示时、分、秒、时区;
- toLocaleDateString() —— 以特定于地区的格式显示星期几、月、日、年;
- toLocaleTimeString() —— 以特定于地区的格式显示时、分、秒;
- toUTCString() —— 以特定于实现的格式完整的 UTC 日期。
与 toLocaleString() 和 toString() 方法一样,以上这些字符串格式方法的输出也是因浏览器而异的,因此没有哪一个方法能够用来在用户界面中显示一致的日期信息。
3.3、日期/时间组件方法
以下是直接取得和设置日期值中特定部分的方法:
本地日期:
方法 | 说明 |
---|---|
getTime() | 返回毫秒数;与 valueOf() 方法返回值相同 |
getFullYear() | 返回4位数的年份 |
getMonth() | 返回月份,其中 0 表示一月,11 表示十二月 |
getDate() | 返回月份中的天数(1 到 31) |
getDay() | 返回星期的星期几(其中 0 表示星期日,6 表示星期六) |
getHours() | 返回小时数(0 到 23) |
getMinutes() | 返回分钟数(0 到 59) |
getSeconds() | 返回秒数(0 到 59) |
getMilliseconds() | 返回毫秒数 |
getTimezoneOffset() | 返回本地时间与 UTC 时间相差的分钟数 |
UTC 日期:
方法 | 说明 |
---|---|
getUTCFullYear() | 返回UTC日期的4位数年份 |
getUTCMonth() | 返回UTC日期中的月份,其中0表示一月,11表示十二月 |
getUTCDate() | 返回UTC日期月份中的天数(1到31) |
getUTCDay() | 返回UTC日期中星期的星期几(其中0表示星期日,6表示星期六) |
getUTCHours() | 返回UTC日期中的小时数(0到23) |
getUTCMinutes() | 返回UTC日期中的分钟数(0到59) |
getUTCSeconds() | 返回UTC日期中的秒数(0到59) |
getUTCMilliseconds() | 返回UTC日期中的毫秒数 |
设置本地日期:
方法 | 说明 |
---|---|
setTime(毫秒) | 以毫秒数设置日期,会改变整个日期 |
setFullYear(年) | 设置日期的年份。传入的年份值必须是 4 位数字 |
setMonth(月) | 设置日期的月份。传入的月份值必须大于 0,超过 11 则增加年份 |
setDate(日) | 设置日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份 |
setHours(时) | 设置日期中的小时数。传入的值超过了 23 则增加月份中的天数 |
setMinutes(分) | 设置日期中的分钟数。传入的值超过 59 则增加小时数 |
setSeconds(秒) | 设置日期中的秒数。传入的值超过了 59 会增加分钟数 |
setMilliseconds(毫秒) | 设置日期中的毫秒数 |
设置 UTC 日期:
方法 | 说明 |
---|---|
setUTCFullYear(年) | 设置UTC日期的年份。传入的年份值必须是 4 位数字 |
setUTCMonth(月) | 设置UTC日期的月份。传入的月份值必须大于 0,超过11则增加年份 |
setUTCDate(日) | 设置UTC日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份 |
setUTCHours(时) | 设置UTC日期中的小时数。传入的值超过了23则增加月份中的天数 |
setUTCMinutes(分) | 设置UTC日期中的分钟数。传入的值超过59则增加小时数 |
setUTCSeconds(秒) | 设置UTC日期中的秒数。传入的值超过了59会增加分钟数 |
setUTCMilliseconds(毫秒) | 设置UTC日期中的毫秒数 |
4、RegExp 类型
ECMAScript 通过 RegExp 类型来支持正则表达式。2 种创建正则表达式的方法:
- 使用字面量形式来定义正则表达式
- 使用 RegExp 构造函数
使用字面量形式来定义正则表达式
let expression = / pattern / flags ;
其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找、反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。
正则表达式的匹配模式支持下列 3 个标志:
- g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
- i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
- m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。
因此,一个正则表达式就是一个模式与上述 3 个标志的组合体。示例:
// 匹配字符串中所有 "at" 的实例
let pattern1 = /at/g;
// 匹配第一个 "bat" 或 "cat",不区分大小写
let pattern2 = /[bc]at/i;
// 匹配所有以 "at" 结尾的 3 个字符的组合,不区分大小写
let pattern3 = /.at/gi;
与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:
( [ { \ ^ $ | ) ? * + . ] }
这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。示例:
// 匹配第一个 "bat" 或 "cat",不区分大小写
let pattern1 = /[bc]at/i;
// 匹配第一个 "[bc]at" ,不区分大小写
let patter2 = /\[bc\]at/i;
上述例子中,pattern1 匹配第一个 "bat" 或 "cat",不区分大小写。而要想直接匹配 "[bc]at" 的话,就需要像定义 pattern2 一样,对其中的两个方括号进行转义。
// 匹配所有以 "at" 结尾的 3 个字符的组合,不区分大小写
let pattern3 = /.at/gi;
// 匹配所有".at",不区分大小写
let pattern4 = /\.at/gi;
上述例子中,对于 pattern3 来说,句点表示位于"at"之前的任意一个可以构成匹配项的字符。但如果想匹配".at",则必须对句点本身进行转义,如 pattern4 所示。
使用 RegExp 构造函数来创建正则表达式
RegExp 构造函数接收 2 个参数:要匹配的字符串模式、可选的标志字符串。
可以使用字面量定义的任何表达式,都可以使用构造函数来定义。示例:
// 匹配第一个 "bat" 或 "cat",不区分大小写
let pattern1 = /[bc]at/i;
// 与 pattern1 相同,只不过是使用构造函数创建的
let pattern2 = new RegExp("[bc]at", "i");
要注意的是,传递给 RegExp 构造函数的两个参数都是字符串(不能把正则表达式字面量传递给 RegExp 构造函数)。
由于 RegExp 构造函数的模式参数是字符串,所以在某些情况下要对字符进行双重转义。所有元字符都必须双重转义,那些已经转义过的字符也是如此。例如:字符\在字符串中通常被转义为\,而在正则表达式字符串中就会变成\\。
字面量模式 | 等价的字符串 |
---|---|
/\[bc\]at/ | "\\[bc\\]at" |
/\.at/ | "\\.at" |
/name\/age/ | "name\\/age" |
/\d.\d{1,2}/ | "\\d.\\d{1,2}" |
/\w\\hello\\123/ | "\w\\\\hello\\\\123" |
4.1、RegExp 实例属性
RegExp 的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。
- global:布尔值,表示是否设置了 g 标志。
- ignoreCase:布尔值,表示是否设置了 i 标志。
- lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从 0 算起。
- multiline:布尔值,表示是否设置了 m 标志。
- source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。
通过这些属性可以获知一个正则表达式的各方面信息,但却没有多大用处,因为这些信息全都包含在模式声明中。例如:
let pattern1 = /\[bc\]at/i;
console.log(pattern1.global); // false
console.log(pattern1.ignoreCase); // true
console.log(pattern1.multiline); // false
console.log(pattern1.lastIndex); // 0
console.log(pattern1.source); // "\[bc\]at"
let pattern2 = new RegExp("\\[bc\\]at", "i");
console.log(pattern2.global); // false
console.log(pattern2.ignoreCase); // true
console.log(pattern2.multiline); // false
console.log(pattern2.lastIndex); // 0
console.log(pattern2.source); // "\[bc\]at"
4.1、RegExp 实例方法
RegExp 对象的方法:
- exec(),该方法是专门为捕获组而设计的;
- test(),判断目标字符串与某个模式是否匹配;
- toLocaleString(),返回正则表达式的字面量;
- toString(),返回正则表达式的字面量;
- valueOf(),返回正则表达式本身。
exec()方法
exec() 接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回 null。
exec() 返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。其中,index 表示匹配项在字符串中的位置,而 input 表示应用正则表达式的字符串。
在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。示例:
let text = "mom and dad and baby";
let pattern = /mom( and dad( and baby)?)?/gi;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches.input); // "mom and dad and baby"
console.log(matches[0]); // "mom and dad and baby"
console.log(matches[1]); // " and dad and baby"
console.log(matches[2]); // " and baby"
对于 exec() 方法而言,即使在模式中设置了全局标志(g),它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用 exec() 将始终返回第一个匹配项的信息。而在设
置全局标志的情况下,每次调用 exec() 则都会在字符串中继续查找新匹配项,示例:
let text = "cat, bat, sat, fat";
let pattern1 = /.at/;
let matches = pattern1.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern1.lastIndex); // 0
matches = pattern1.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern1.lastIndex); // 0
let pattern2 = /.at/g;
let matches = pattern2.exec(text);
console.log(matches.index); // 0
console.log(matches[0]); // cat
console.log(pattern2.lastIndex); // 3
matches = pattern2.exec(text);
console.log(matches.index); // 5
console.log(matches[0]); // bat
console.log(pattern2.lastIndex); // 8
test()方法
test() 接受一个字符串参数。在模式与该参数匹配的情况下返回 true;否则,返回 false。
在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,使用这个方法非常方便。因此,test()方法经常被用在 if 语句中,示例:
let text = "000-00-0000";
let pattern = /\d{3}-\d{2}-\d{4}/;
if (pattern.test(text)){
console.log("The pattern was matched.");
}
在上述例子中,我们使用正则表达式来测试了一个数字序列。如果输入的文本与模式匹配,则显示一条消息。这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效,至于它为什么无效就无关紧要了。
toLocaleString() 和 toString() 方法
RegExp 实例继承的 toLocaleString()和 toString()方法都会返回正则表达式的字面量,与创建正则表达式的方式无关。例如:
let pattern = new RegExp("\\[bc\\]at", "gi");
console.log(pattern.toString()); // /\[bc\]at/gi
console.log(pattern.toLocaleString()); // /\[bc\]at/gi
valueOf() 方法
正则表达式的 valueOf()方法返回正则表达式本身。示例:
let pattern = /\[bc\]at/gi;
console.log(pattern.toString()); // /\[bc\]at/gi
console.log(typeof pattern.toString()); // string
console.log(pattern.valueOf()); // /\[bc\]at/gi
console.log(typeof pattern.valueOf()); // object
4.3、RegExp 构造函数属性
RegExp 构造函数包含一些属性。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名。
RegExp 构造函数的属性:
长属性名 | 短属性名 | 说明 |
---|---|---|
input | $_ | 最近一次要匹配的字符串 |
lastMatch | $& | 最近一次的匹配项 |
lastParen | $+ | 最近一次匹配的捕获组 |
leftContext | $` | input字符串中lastMatch之前的文本 |
multiline | $* | 布尔值,表示是否所有表达式都使用多行模式 |
rightContext | $' | Input字符串中lastMatch之后的文本 |
示例:
let text = "this has been a short summer";
let pattern = /(.)hort/g;
if (pattern.test(text)){
console.log(RegExp.input); // this has been a short summer
console.log(RegExp.leftContext); // this has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
console.log(RegExp.multiline); // false
}
以上代码创建了一个模式,匹配任何一个字符后跟 hort,而且把第一个字符放在了一个捕获组中。
RegExp 构造函数的各个属性返回了下列值:
- input 属性返回了原始字符串;
- leftContext 属性返回了单词 short 之前的字符串;
- rightContext 属性返回了 short 之后的字符串;
- lastMatch 属性返回最近一次与整个正则表达式匹配的字符串,即short;
- lastParen 属性返回最近一次匹配的捕获组,即例子中的 s。
例子使用的长属性名都可以用相应的短属性名来代替。只不过,由于这些短属性名大都不是有效的 ECMAScript 标识符,因此必须通过方括号语法来访问它们,示例:
let text = "this has been a short summer";
let pattern = /(.)hort/g;
if (pattern.test(text)){
console.log(RegExp.$_); // this has been a short summer
console.log(RegExp["$`"]); // this has been a
console.log(RegExp["$'"]); // summer
console.log(RegExp["$&"]); // short
console.log(RegExp["$+"]); // s
console.log(RegExp["$*"]); // false
}
除了上面介绍的几个属性之外,还有多达 9 个用于存储捕获组的构造函数属性。访问这些属性的语法是 RegExp.$1、RegExp.$2…RegExp.$9,分别用于存储第一、第二……第九个匹配的捕获组。在调用 exec() 或 test() 方法时,这些属性会被自动填充。示例:
let text = "this has been a short summer";
let pattern = /(..)or(.)/g;
if (pattern.test(text)){
console.log(RegExp.$1); // sh
console.log(RegExp.$2); // t
}
这里创建了一个包含两个捕获组的模式,并用该模式测试了一个字符串。即使 test() 方法只返回一个布尔值,但 RegExp 构造函数的属性 $1 和 $2 也会被匹配相应捕获组的字符串自动填充。
4.4、模式的局限性
ECMAScript 正则表达式不支持的特性:
- 匹配字符串开始和结尾的\A 和\Z 锚(但支持以插入符号(^)和美元符号($)来匹配字符串的开始和结尾)
- 向后查找(lookbehind)(但完全支持向前查找(lookahead))
- 并集和交集类
- 原子组(atomic grouping)
- Unicode 支持(单个字符除外,如\uFFFF)
- 命名的捕获组(但支持编号的捕获组)
- s(single,单行)和 x(free-spacing,无间隔)匹配模式
- 条件匹配
- 正则表达式注释
5、Function 类型
每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
定义函数:
- 使用函数声明语法定义;
- 定义变量并将其初始化为一个函数;
- 使用 Function 构造函数。
function sum (num1, num2) {
return num1 + num2;
}
let sum = function(num1, num2){
return num1 + num2;
};
let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
上述第二个例子,定义了变量 sum 并将其初始化为一个函数。function 关键字后面没有函数名,这是因为在使用函数表达式定义函数时,没有必要使用函数名——通过变量 sum 即可以引用函数。另外,还要注意函数末尾有一个分号,就像声明其他变量时一样。
上述第三个例子,从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字,示例:
function sum(num1, num2){
return num1 + num2;
}
console.log(sum(10,10)); // 20
let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20
sum = null;
console.log(anotherSum(10, 10)); // 20
以上代码首先定义了一个名为 sum()的函数,用于求两个值的和。然后,又声明了变量 anotherSum,并将其设置为与 sum 相等(将 sum 的值赋给 anotherSum)。此时,anotherSum 和 sum 就都指向了同一个函数,因此 anotherSum()也可以被调用并返回结果。即使将 sum 设置为 null,让它与函数“断绝关系”,但仍然可以正常调用anotherSum()。
注意,使用不带圆括号的函数名是访问函数指针,而非调用函数。
5.1、没有重载(深入理解)
将函数名想象为指针,也有助于理解为什么 ECMAScript 中没有函数重载的概念。
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
let result = addSomeNumber(100); // 300
// 等价于
let addSomeNumber = function (num){
return num + 100;
};
addSomeNumber = function (num) {
return num + 200;
};
let result = addSomeNumber(100); // 300
声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。通过观察重写之后的代码,很容易看清楚到底是怎么回事儿——在创建第二个函数时,实际上覆盖了引用第一个函数的变量 addSomeNumber。
5.2、函数声明与函数表达式
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。示例:
console.log(sum(10,10));
function sum(num1, num2){
return num1 + num2;
}
以上代码可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript 引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript 引擎也能把函数声明提升到顶部。
console.log(sum(10,10));
let sum = function(num1, num2){
return num1 + num2;
};
以上代码会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一行代码就会导致 “unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。
除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。
5.3、作为值的函数
因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
将函数作为参数传递给另一个函数
function callSomeFunction(someFunction, someArgument){
return someFunction(someArgument);
}
这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。
示例:
function add10(num){
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name){
return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas"
这里的 callSomeFunction() 函数是通用的,即无论第一个参数中传递进来的是什么函数,它都会返回执行第一个参数后的结果。
要访问函数的指针而不执行函数的话,必须去掉函数名后面的那对圆括号。因此上面例子中传递给 callSomeFunction() 的是 add10 和 getGreeting,而不是执行它们之后的结果。
将函数作为另一个函数的结果返回
可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组 sort() 方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数。示例:
function createComparisonFunction(propertyName) {
return function(object1, object2){
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
// 具体使用实例:
let data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name); // Zachary
5.4、函数内部属性
在函数内部,有两个特殊的对象:
- arguments
- this
arguments 是一个类数组对象,包含着传入函数中的所有参数。虽然 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) {
return 1;
} else {
return num * arguments.callee(num-1)
}
}
在这个重写后的 factorial()函数的函数体内,没有再引用函数名 factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。示例:
let trueFactorial = factorial;
factorial = function(){
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
在此,变量 trueFactorial 获得了 factorial 的值,实际上是在另一个位置上保存了一个函数的指针。然后,我们又将一个简单地返回 0 的函数赋值给 factorial 变量。如果像原来的 factorial() 那样不使用 arguments.callee,调用 trueFactorial() 就会返回 0。可是,在解除了函数体内的代码与函数名的耦合状态之后,trueFactorial() 仍然能够正常地计算阶乘;至于 factorial(),它现在只是一个返回 0 的函数。
注意:严格模式下,访问 arguments.callee 会导致错误。
this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是 window)。示例:
window.color = "red";
let o = { color: "blue" };
function sayColor(){
console.log(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"。
注意:函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的 sayColor() 函数与 o.sayColor() 指向的仍然是同一个函数。
caller 是函数对象的属性,这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为 null。示例:
function outer(){
inner();
}
function inner(){
console.log(inner.caller);
}
outer();
以上代码会打印 outer() 函数的源代码。因为 outer()调用了 inter(),所以 inner.caller 就指向 outer()。
为了实现更松散的耦合,也可以通过 arguments.callee.caller 来访问相同的信息。示例:
function outer(){
inner();
}
function inner(){
console.log(arguments.callee.caller);
}
outer();
arguments 也有一个 arguments.caller 属性,这个属性始终返回 undefined。定义这个属性是为了分清 arguments.caller 和函数的 caller 属性。以上变化都是为了加强这门语言的安全性,这样第三方代码就不能在相同的环境里窥视其他代码了。
5.5、函数属性和方法
ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。
length 属性表示函数希望接收的命名参数的个数。示例:
function sayName(name){
console.log(name);
}
function sum(num1, num2){
return num1 + num2;
}
function sayHi(){
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
对于 ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。换句话说,诸如 toString() 和 valueOf() 等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。
每个函数都包含两个非继承而来的方法:apply() 和 call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。
apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是 arguments 对象。示例:
function sum(num1, num2){
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); //20
console.log(callSum2(10, 10)); //20
在上面这个例子中,callSum1() 在执行 sum()函数时传入了 this 作为 this 值(因为是在全局作用域中调用的,所以传入的就是 window 对象)和 arguments 对象。而 callSum2 同样也调用了 sum() 函数,但它传入的则是 this 和一个参数数组。这两个函数都会正常执行并返回正确的结果。
call() 方法与 apply() 方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call() 方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来,示例:
function sum(num1, num2){
return num1 + num2;
}
function callSum(num1, num2){
return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
在使用 call() 方法的情况下,callSum() 必须明确地传入每一个参数。结果与使用 apply() 没有什么不同。
至于是使用 apply() 还是 call(),完全取决于你采取哪种给函数传递参数的方式最方便。如果你打算直接传入 arguments 对象,或者包含函数中先接收到的也是一个数组,那么使用 apply() 肯定更方便;否则,选择 call() 可能更合适。(在不给函数传递参数的情况下,使用哪个方法都无所谓。)
事实上,传递参数并非 apply() 和 call() 真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。示例:
window.color = "red";
let o = { color: "blue" };
function sayColor(){
console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
sayColor() 是作为全局函数定义的,当在全局作用域中调用它时,它确实会显示 "red"——因为对 this.color 的求值会转换成对 window.color 的求值。
而 sayColor.call(this) 和 sayColor.call(window),则是两种显式地在全局作用域中调用函数的方式,结果当然都会显示 "red"。但是,当运行 sayColor.call(o) 时,函数的执行环境就不一样了,因为此时函数体内的 this 对象指向了 o,于是结果显示的是 "blue"。
使用 call() 或 apply() 来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本中,我们是先将 sayColor() 函数放到了对象 o 中,然后再通过 o 来调用它的;而在这里重写的例子中,就不需要先前那个多余的步骤了。
bind() 方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值。示例:
window.color = "red";
let o = { color: "blue" };
function sayColor(){
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
在这里,sayColor() 调用 bind() 并传入对象 o,创建了 objectSayColor() 函数。objectSayColor() 函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到 "blue"。
每个函数继承的 toLocaleString() 和 toString() 方法始终都返回函数的代码。返回代码的格式则因浏览器而异 —— 有的返回的代码与源代码中的函数代码一样,而有的则返回函数代码的内部表示,即由解析器删除了注释并对某些代码作了改动后的代码。由于存在这些差异,我们无法根据这两个方法返回的结果来实现任何重要功能;不过,这些信息在调试代码时倒是很有用。另外一个继承的 valueOf() 方法同样也只返回函数代码。
6、基本包装类型
为了便于操作基本类型值,ECMAScript 还提供了 3 个特殊的引用类型:Boolean、Number 和 String。这些类型与本章介绍的其他引用类型相似,但同时也具有与各自的基本类型相应的特殊行为。实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。示例:
let s1 = "some text";
let s2 = s1.substring(2);
这个例子中的变量 s1 包含一个字符串,字符串当然是基本类型值。而下一行调用了 s1 的 substring()方法,并将返回的结果保存在了 s2 中。我们知道,基本类型值不是对象,因而从逻辑上讲它们不应该有方法。
其实,为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理。当第二行代码访问 s1 时,访问过程处于一种读取模式,也就是要从内存中读取这个字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理:
- 创建 String 类型的一个实例;
- 在实例上调用指定的方法;
- 销毁这个实例。
可以将以上三个步骤想象成是执行了下列 ECMAScript 代码。
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
经过此番处理,基本的字符串值就变得跟对象一样了。而且,上面这三个步骤也分别适用于 Boolean 和 Number 类型对应的布尔值和数字值。
引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。示例:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
在此,第二行代码试图为字符串 s1 添加一个 color 属性。但是,当第三行代码再次访问 s1 时,其 color 属性不见了。问题的原因就是第二行创建的 String 对象在执行第三行代码时已经被销毁了。第三行代码又创建自己的 String 对象,而该对象没有 color 属性。
当然,可以显式地调用 Boolean、Number 和 String 来创建基本包装类型的对象。不过,应该在绝对必要的情况下再这样做,因为这种做法很容易让人分不清自己是在处理基本类型还是引用类型的值。
对基本包装类型的实例调用 typeof 会返回 "object",而且所有基本包装类型的对象都会被转换为布尔值 true。
Object 构造函数也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。示例:
let obj = new Object("some text");
console.log(obj instanceof String); // true
把字符串传给 Object 构造函数,就会创建 String 的实例;而传入数值参数会得到 Number 的实]例,传入布尔值参数就会得到 Boolean 的实例。
要注意的是,使用 new 调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。示例:
let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数
console.log(typeof obj); // "object"
在这个例子中,变量 number 中保存的是基本类型的值 25,而变量 obj 中保存的是 Number 的实例。
尽管我们不建议显式地创建基本包装类型的对象,但它们操作基本类型值的能力还是相当重要的。而每个基本包装类型都提供了操作相应值的便捷方法。
6.1、Boolean 类型
Boolean 类型是与布尔值对应的引用类型。要创建 Boolean 对象,可以像下面这样调用 Boolean 构造函数并传入 true 或 false 值。示例:
let booleanObject = new Boolean(true);
Boolean 类型的实例
- 重写了 valueOf() 方法,返回基本类型值 true 或 false;
- 重写了 toString() 方法,返回字符串 "true" 和 "false";
- 重写了 toLocaleString() 方法,返回字符串 "true" 和 "false"。
let BooleanFalse= new Boolean(true);
console.log(BooleanFalse.valueOf()); // false
console.log(typeof BooleanFalse.valueOf()); // boolean
console.log(BooleanFalse.toString()); // false
console.log(typeof BooleanFalse.toString()); // string
console.log(BooleanFalse.toLocaleString()); // false
console.log(typeof BooleanFalse.toLocaleString()); // string
Boolean 对象在 ECMAScript 中的用处不大,因为它经常会造成人们的误解。其中最常见的问题就是在布尔表达式中使用 Boolean 对象,例如:
let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
let falseValue = false;
result = falseValue && true;
console.log(result); // false
在这个例子中,我们使用 false 值创建了一个 Boolean 对象。然后,将这个对象与基本类型值 true 构成了逻辑与表达式。在布尔运算中,false && true 等于 false。可是,示例中的这行代码是对 falseObject 而不是对它的值(false)进行求值。前面讨论过,布尔表达式中的所有对象都会被转换为 true,因此 falseObject 对象在布尔表达式中代表的是 true。结果,true && true 当然就等于 true 了。
基本类型与引用类型的布尔值还有两个区别:
- typeof 操作符对基本类型返回 "boolean",而对引用类型返回 "object"。
- 由于 Boolean 对象是 Boolean 类型的实例,所以使用 instanceof 操作符测试 Boolean 对象会返回 true,而测试基本类型的布尔值则返回 false。
示例:
let falseObject = new Boolean(false);
let falseValue = false;
console.log(typeof falseObject); // object
console.log(typeof falseValue); // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean); // false
建议:永远不要使用 Boolean 对象。
6.2、Number 类型
Number 是与数字值对应的引用类型。要创建 Number 对象,可以在调用 Number 构造函数时向其中传递相应的数值。示例:
let numberObject = new Number(10);
Number 类型的实例:
- 重写了 valueOf() 方法,返回对象表示的基本类型的数值;
- 重写了 toString() 方法,返回字符串形式的数值;
- 重写了 toLocaleString() 方法,返回字符串形式的数值。
let num = new Number(10);
console.log(num.valueOf()); // 10
console.log(typeof num.valueOf()); // number
console.log(num.toString()); // 10
console.log(typeof num.toString()); // string
console.log(num.toLocaleString()); // 10
console.log(typeof num.toLocaleString()); // string
可以为 toString()方法传递一个表示基数的参数,告诉它返回几进制
数值的字符串形式。示例:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
除了继承的方法之外,Number 类型还提供了一些用于将数值格式化为字符串的方法:
- toFixed():会按照指定的小数位返回数值的字符串表示。
- toExponential():返回以指数表示法(也称 e 表示法)表示的数值的字符串形式。
- toPrecision():返回表示某个数值的最合适的格式。
toFixed()
let num = 10;
console.log(num.toFixed(2)); // "10.00"
这里给 toFixed() 方法传入了数值 2,意思是显示几位小数。于是,这个方法返回了 "10.00",即以 0 填补了必要的小数位。如果数值本身包含的小数位比指定的还多,那么接近指定的最大小数位的值就会舍入。示例:
let num = 10.005;
console.log(num.toFixed(2)); // "10.01"
能够自动舍入的特性,使得 toFixed()方法很适合处理货币值。
toExponential()
toExponential() 接收一个参数,该参数指定输出结果中的小数位数。示例:
let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"
toPrecision()
对于一个数值来说,toPrecision() 方法可能会返回固定大小(fixed)格式,也可能返回指数(exponential)格式;具体规则是看哪种格式最合适。
toPrecision() 方法接收一个参数,即表示数值的所有数字的位数(不包括指数部分)。示例:
let num = 99;
console.log(num.toPrecision(1)); // "1e+2"
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
toPrecision() 会根据要处理的数值决定到底是调用 toFixed() 还是调用toExponential()。而这三个方法都可以通过向上或向下舍入,做到以最准确的形式来表示带有正确小数位的值。
在使用 typeof 操作符测试基本类型数值时,始终会返回 "number",而在测试 Number 对象时,则会返回 "object"。类似地,Number 对象是 Number 类型的实例,而基本类型的数值则不是。
let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false
6.3、String 类型
**String 类型是字符串对应的引用类型。示例:
let stringObject = new String("hello world");
String 类型的实例:
- 重写了 valueOf() 方法,返回对象所表示的基本字符串值;
- 重写了 toString() 方法,返回对象所表示的基本字符串值;
- 重写了 toLocaleString() 方法,返回对象所表示的基本字符串值。
let str = new String('hello world');
console.log(str.valueOf()); // "hello world"
console.log(typeof str.valueOf()); // string
console.log(str.toString()); // "hello world"
console.log(typeof str.toString()); // string
console.log(str.toLocaleString()); // "hello world"
console.log(typeof str.toLocaleString()); // string
String 类型的每个实例都有一个 length 属性,表示字符串中包含多个字符。示例:
let stringValue = "hello world";
console.log(stringValue.length); // "11"
注意:即使字符串中包含双字节字符(不是占一个字节的 ASCII 字符),每个字符也仍然算一个字符。
String 类型提供了很多方法,用于辅助完成对 ECMAScript 中字符串的解析和操作。
- 字符方法
- charAt() :返回给定位置的字符。
- charCodeAt():返回给定位置的字符的字符编码。
- stringValue():返回给定位置的字符。
- 字符串操作方法
- concat():将一或多个字符串拼接起来,返回拼接得到的新字符串。
- slice():返回被操作字符串的一个子字符串。
- substr():返回被操作字符串的一个子字符串。
- substring():返回被操作字符串的一个子字符串。
- 字符串位置方法
- indexOf():从前向后搜索,返回字符串中指定字符串的位置。
- lastIndexOf():从后向前搜索,返回字符串中指定字符串的位置。
- trim() 方法
- trim():会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。
- 字符串大小写转换方法
- toLowerCase():将字符串全部转换成小写。
- toLocaleLowerCase():针对特定地区,将字符串全部转换成小写。
- toUpperCase():将字符串全部转换成大写。
- toLocaleUpperCase():针对特定地区,将字符串全部转换成大写。
- 字符串的模式匹配方法
- match():返回匹配项信息的数组。
- search():返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回 -1。
- replace():进行子字符串的替换操作。
- split():基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。
- localeCompare() 方法
- localeCompare():用于比较两个字符串。
- fromCharCode() 方法
- fromCharCode():
- HTML 方法
1. 字符方法
charAt() 方法接收一个参数,即基于 0 的字符位置,以单字符字符串的形式返回指定位置的字符。示例:
let stringValue = "hello world";
console.log(stringValue.charAt(1)); // "e"
charCodeAt() 方法接收一个参数,即基于 0 的字符位置,返回给定位置的字符的字符编码。示例:
let stringValue = "hello world";
console.log(stringValue.charCodeAt(1)); // "101"
stringValue() 方法使用方括号加数字索引来访问字符串中的特定字符。示例:
let stringValue = "hello world";
console.log(stringValue[1]); // "e"
2. 字符串操作方法
concat() 方法用于将一或多个字符串拼接起来,返回拼接得到的新字符串。示例:
let stringValue = "hello ";
let result = stringValue.concat("world", "!");
console.log(result); // "hello world!"
console.log(stringValue); // "hello"
slice() 方法会返回被操作字符串的一个子字符串。接收参数:指定子字符串的开始位置,(可选)子字符串最后一个字符后面的位置(默认:到字符串末尾)。当参数是负值时,slice() 方法会将传入的负值与字符串的长度相加。
substr() 方法会返回被操作字符串的一个子字符串。接收参数:指定子字符串的开始位置,(可选)返回的字符个数(默认:到字符串末尾)。当参数是负值时,substr() 方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为 0。
substring() 方法会返回被操作字符串的一个子字符串。接收参数:指定子字
符串的开始位置,(可选)子字符串最后一个字符后面的位置(默认:到字符串末尾)。当参数是负值时,substring() 方法会把所有负值参数都转换为 0。
let stringValue = "hello world"; // length === 11
console.log(slice(3)); // "lo world"
console.log(substr(3)); // "lo world"
console.log(substring(3)); // "lo world"
console.log(slice(3, 7)); // "lo w"
console.log(substr(3, 7)); // "lo worl"
console.log(substring(3, 7)); // "lo w"
console.log(slice(-3)); // "rld" (8)
console.log(substr(-3)); // "rld" (8)
console.log(substring(-3)); // "hello world" (0)
console.log(slice(3, -4)); // "lo w" (3, 7)
console.log(substr(3, -4)); // ""(空字符串) (0, 0)
console.log(substring(3, -4)); // "hel" (3, 0)
3. 字符串位置方法
indexOf() 方法从字符串的开头向后搜索指定的子字符串,然后返回子字符串的位置(如果没有找到该子字符串,则返回-1)。接收参数:指定字符串,(可选)开始搜索的位置(indexOf() 会从该参数指定的位置向后搜索,忽略该位置之前的所有字符)。
lastIndexOf() 方法从字符串的末尾向前搜索指定的子字符串,然后返回子字符串的位置(如果没有找到该子字符串,则返回-1)。接收参数:指定字符串,(可选)开始搜索的位置(lastIndexOf() 会从指定的位置向前搜索,忽略该位置之后的所有字符)。
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
console.log(stringValue.lastIndexOf("o")); // 7
console.log(stringValue.indexOf("o", 6)); // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4
在使用第二个参数的情况下,可以通过循环调用 indexOf() 或 lastIndexOf() 来找到所有匹配的子字符串。示例:
let stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
let positions = new Array();
let pos = stringValue.indexOf("e");
while(pos > -1){
positions.push(pos);
pos = stringValue.indexOf("e", pos + 1);
}
console.log(positions); // [3, 24, 32, 35, 52]
这个例子通过不断增加 indexOf() 方法开始查找的位置,遍历了一个长字符串。在循环之外,首先找到了 "e" 在字符串中的初始位置;而进入循环后,则每次都给 indexOf() 传递上一次的位置加 1。这样,就确保了每次新搜索都从上一次找到的子字符串的后面开始。每次搜索返回的位置依次被保存在数组 positions 中,以便将来使用。
4. trim() 方法
trim() 方法会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。由于 trim()返回的是字符串的副本,所以原始字符串中的前置及后缀空格会保持不变。示例:
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
5. 字符串大小写转换方法
涉及字符串大小写转换的方法有 4 个:toLowerCase()、toLocaleLowerCase()、toUpperCase() 和 toLocaleUpperCase()。
toLocaleLowerCase() 和 toLocaleUpperCase() 方法是针对特定地区的实现。一般来说,在不知道自己的代码将在哪种语言环境中运行的情况下,还是使用针对地区的方法更稳妥一些。
let stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"
6. 字符串的模式匹配方法
match() 只接受一个参数,一个正则表达式 / 一个 RegExp 对象。返回一个数组。在字符串上调用这个方法,本质上与调用 RegExp 的 exec() 方法相同。示例:
let text = "cat, bat, sat, fat";
let pattern = /.at/;
//与 pattern.exec(text) 相同
let matches = text.match(pattern);
console.log(matches.index); // 0
console.log(matches[0]); // "cat"
console.log(pattern.lastIndex); // 0
上述示例中的 match() 方法返回一个数组;如果调用 RegExp 对象的 exec() 方法并传递本例中的字符串作为参数,那么也会得到与此相同的数组:数组的第一项是与整个模式匹配的字符串,之后的每一项(如果有)保存着与正则表达式中的捕获组匹配的字符串。
search() 只接受一个参数,由字符串或 RegExp 对象指定的一个正则表达式。返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回 -1。search() 方法始终是从前向后查找。示例:
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
replace() 可以进行子字符串的替换操作。接受 2 个参数:一个 RegExp 对象或者一个字符串(这个字符串不会被转换成正则表达式);一个字符串或者一个函数。
如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,唯一的办法就是提供一个正则表达式,而且要指定全局(g)标志。示例:
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
result = text.replace(/at/g, "ond");
console.log(result); // "cond, bond, sond, fond"
如果第二个参数是字符串,那么还可以使用一些特殊的字符序列,将正则表达式操作得到的值插入到结果字符串中。
字符序列 | 替换文本 |
---|---|
$$ | $ |
$& | 匹配整个模式的子字符串。与RegExp.lastMatch的值相同 |
$' | 匹配的子字符串之前的子字符串。与RegExp.leftContext的值相同 |
$` | 匹配的子字符串之后的子字符串。与RegExp.rightContext的值相同 |
$n | 匹配第n个捕获组的子字符串,其中n等于0~9。例如,$1是匹配第一个捕获组的子字符串,$2是匹配第二个捕获组的子字符串,以此类推。如果正则表达式中没有定义捕获组,则使用空字符串 |
$nn | 匹配第nn个捕获组的子字符串,其中nn等于01~99。例如,$01是匹配第一个捕获组的子字符串,$02是匹配第二个捕获组的子字符串,以此类推。如果正则表达式中没有定义捕获组,则使用空字符串 |
通过这些特殊的字符序列,可以使用最近一次匹配结果中的内容,示例:
let text = "cat, bat, sat, fat";
let result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)
上述例子中,每个以"at"结尾的单词都被替换了,替换结果是"word"后跟一对圆括号,而圆括号中是被
字符序列$1 所替换的单词。
如果第二个参数是函数。
在只有一个匹配项(即与模式匹配的字符串)的情况下,会向这个函数传递 3 个参数:模式的匹配项、模式匹配项在字符串中的位置、原始字符串。
在正则表达式中定义了多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,但最后两个参数仍然分别是模式的匹配项在字符串中的位置、原始字符串。
这个函数应该返回一个字符串,表示应该被替换的匹配项使用函数作为 replace() 方法的第二个参数可以实现更加精细的替换操作,示例:
function htmlEscape(text) {
return text.replace(/[<>"&]/g, function(match, pos, originalText){
switch(match){
case "<":
return "<";
case ">":
return ">";
case "&":
return "&";
case "\"":
return """;
}
});
}
console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>"));
// <p class="greeting">Hello world!</p>
上述例子中,我们为插入 HTML 代码定义了函数 htmlEscape(),这个函数能够转义 4 个字符:小于号、大于号、双引号、和号。。实现这种转义的最简单方式,就是使用正则表达式查找这几个字符,然后定义一个能够针对每个匹配的字符返回特定 HTML 实体的函数。
split() 方法基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。
分隔符可以是字符串,也可以是一个 RegExp 对象(这个方法不会将字符串看成正则表达式)。
split()方法可以接受可选的第二个参数,用于指定数组的大小,以便确保返回的数组不会超过既定大小。
示例:
let colorText = "red, blue, green, yellow";
let colors1 = colorText.split(","); // ["red", "blue", "green", "yellow"]
let colors2 = colorText.split(",", 2); // ["red", "blue"]
let colors3 = colorText.split(/[^\,]+/); // [" ", ",", ",", ",", " "]
/[^\,]+/:[^\,] 表示任何非,(逗号)的字符,+ 表示一个或者多个。
在最后一次调用 split() 返回的数组中,第一项和最后一项是两个空字符串。之所以会这样,是因为通过正则表达式指定的分隔符出现在了字符串的开头(即子字符串 "red")和末尾(即子字符串 "yellow")。
7. localeCompare() 方法
localeCompare() 方法用于比较两个字符串,并返回下列值中的一个:
- 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是 -1,具体的值要视实现而定);
- 如果字符串等于字符串参数,则返回 0;
- 如果字符串在字母表中应该排在字符串参数之后,则返回一个正数(大多数情况下是 1,具体的值同样要视实现而定)。
示例:
let stringValue = "yellow";
console.log(stringValue.localeCompare("brick")); // 1
console.log(stringValue.localeCompare("yellow")); // 0
console.log(stringValue.localeCompare("zoo")); // -1
强调:因为 localeCompare() 返回的数值取决于实现,所以最好是像下面例子所示的这样使用这个方法。
let stringValue = "yellow";
function determineOrder(value) {
let result = stringValue.localeCompare(value);
if (result < 0){
console.log("The string 'yellow' comes before the string '" + value + "'.");
} else if (result > 0) {
console.log("The string 'yellow' comes after the string '" + value + "'.");
} else {
console.log("The string 'yellow' is equal to the string '" + value + "'.");
}
}
determineOrder("brick");
// The string 'yellow' comes after the string 'brick'.
determineOrder("yellow");
// The string 'yellow' is equal to the string 'yellow'.
determineOrder("zoo");
// The string 'yellow' comes before the string 'zoo'.
8. fromCharCode() 方法
fromCharCode() 方法是String 构造函数本身的一个一个静态方法。接收一或多个字符编码,然后将它们转换成一个字符串。从本质上来看,这个方法与实例方法 charCodeAt() 执行的是相反的操作。示例:
console.log(String.fromCharCode(104, 101, 108, 108, 111));
// "hello"
9. HTML 方法
方 法 | 输出结果 |
---|---|
anchor(name) | <a name= "name">string</a> |
big() | <big>string</big> |
bold() | <b>string</b> |
fixed() | <tt>string</tt> |
fontcolor(color) | <font color="color">string</font> |
fontsize(size) | <font size="size">string</font> |
italics() | <i>string</i> |
link(url) | <a href="url">string</a> |
small() | <small>string</small> |
strike() | <strike>string</strike> |
sub() | <sub/>string</sub/> |
sup() | <sup/>string</sup/> |
注意:表格最后两个 sub() 、sup() 输出结果中字母后斜杠(/)不是书写错误,而是markdown 会自动解析,不能正确展示,故意为之。
使用方法:
let str="Hello world!"
console.log(str.big()); // <big>Hello world!</big>
console.log(str.anchor(name)); // <a name= "name">Hello world!</a>
7、单体内置对象
ECMA-262 对内置对象的定义是:“由 ECMAScript 实现提供的、不依赖于宿主环境的对象,这些对象在 ECMAScript 程序执行之前就已经存在了。” 意思就是说,开发人员不必显式地实例化内置对象,因为它们已经实例化了。
前面我们已经介绍了大多数内置对象,例如Object、Array 和 String。
ECMA-262 还定义了两个单体内置对象:Global 和 Math。
7.1、Global 对象
Global(全局)对象可以说是 ECMAScript 中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。
ECMAScript 中的 Global 对象在某种意义上是作为一个终极的“兜底儿对象”来定义的。换句话说,不属于任何其他对象的属性和方法,最终都是它的属性和方法。
事实上,没有全局变量或全局函数;所有在全局作用域中定义的属性和函数,都是 Global 对象的属性。例如:isNaN()、isFinite()、parseInt() 、parseFloat(),实际上全都是 Global对象的方法。除此之外,Global 对象还包含其他一些方法。
1. URI 编码方法
Global 对象的 encodeURI() 和 encodeURIComponent() 方法可以对 URI(Uniform Resource Identifiers,通用资源标识符)进行编码,以便发送给浏览器。
有效的 URI 中不能包含某些字符,例如空格。而这两个 URI 编码方法就可以对 URI 进行编码,它们用特殊的 UTF-8 编码替换所有无效的字符,从而让浏览器能够接受和理解。
encodeURI() 主要用于整个 URI
(例如,http://www.wrox.com/illegal value.html)。
encodeURIComponent() 主要用于对 URI 中的某一段
(例如前面 URI 中的 illegal value.html)进行编码。
区别:encodeURI() 不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;encodeURIComponent( ) 则会对它发现的任何非标准字符进行编码。示例:
let uri = "http://www.wrox.com/illegal value.html#start";
console.log(encodeURI(uri));
// "http://www.wrox.com/illegal%20value.html#start"
console.log(encodeURIComponent(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.html%23start"
使用 encodeURI() 编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了%20。而 encodeURIComponent() 方法则会使用对应的编码替换所有非字母数字字符。这也正是可以对整个 URI 使用 encodeURI(),而只能对附加在现有 URI 后面的字符串使用 encodeURIComponent() 的原因所在。
一般来说,我们使用 encodeURIComponent() 方法的时候要比使用
encodeURI() 更多,因为在实践中更常见的是对查询字符串参数而不是对基础 URI 进行编码。
与 encodeURI() 和 encodeURIComponent() 方法对应的两个方法分别是 decodeURI() 和 decodeURIComponent()。
decodeURI() 只能对使用 encodeURI() 替换的字符进行解码。
decodeURIComponent() 能够解码使用 encodeURIComponent() 编码的所有字符,即它可以解码任何特殊字符的编码。
示例:
let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.html%23start";
console.log(decodeURI(uri));
// http%3A%2F%2Fwww.wrox.com%2Fillegal value.html%23start
console.log(decodeURIComponent(uri));
// http://www.wrox.com/illegal value.html#start
2. eval() 方法
eval() 方法就像是一个完整的 ECMAScript 解析器,它只接受一个参数,即要执行的 ECMAScript(或 JavaScript)字符串。示例:
eval("console.log('hi')");
// 等价于
console.log('hi');
当解析器发现代码中调用 eval() 方法时,它会将传入的参数当作实际的 ECMAScript 语句来解析,然后把执行结果插入到原位置。
通过 eval() 执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过 eval() 执行的代码可以引用在包含环境中定义的变量,示例:
let msg = "hello world";
eval("console.log(msg)"); // "hello world"
变量 msg 是在 eval() 调用的环境之外定义的,但其中调用的 console.log() 仍然能够显示 "hello world"。这是因为上面第二行代码最终被替换成了一行真正的代码。
同样地,我们也可以在 eval() 调用中定义一个函数,然后再在该调用的外部代码中引用这个函数:
eval("function sayHi() { console.log('hi'); }");
sayHi(); // "hi"
显然,函数 sayHi() 是在 eval() 内部定义的。但由于对 eval() 的调用最终会被替换成定义函数的实际代码,因此可以在下一行调用 sayHi()。
对于变量也一样:
eval("let msg = 'hello world'; ");
console.log(msg); // "hello world"
在 eval() 中创建的任何变量或函数都不会被提升,因为在解析代码的时候,它们被包含在一个字符串中;它们只在 eval() 执行的时候创建。
严格模式下,在外部访问不到 eval() 中创建的任何变量或函数,因此前面两个例子都会导致错误。同样,在严格模式下,为 eval 赋值也会导致错误:
"use strict";
eval = "hi"; // causes error
能够解释代码字符串的能力非常强大,但也非常危险。因此在使用 eval() 时必须极为谨慎,特别是在用它执行用户输入数据的情况下。否则,可能会有恶意用户输入威胁你的站点或应用程序安全的代码(即所谓的代码注入)。
3. Global 对象的属性
Global 对象还包含一些属性,例如:特殊的值undefined、NaN、Infinity都是 Global 对象的属性。此外,所有原生引用类型的构造函数,像 Object 和Function,也都是 Global 对象的属性。
属 性 | 说 明 | 属 性 | 说 明 |
---|---|---|---|
undefined | 特殊值undefined | Date | 构造函数Date |
NaN | 特殊值NaN | RegExp | 构造函数RegExp |
Infinity | 特殊值Infinity | Error | 构造函数Error |
Object | 构造函数Object | EvalError | 构造函数EvalError |
Array | 构造函数Array | RangeError | 构造函数RangeError |
Function | 构造函数Function | ReferenceError | 构造函数ReferenceError |
Boolean | 构造函数Boolean | SyntaxError | 构造函数SyntaxError |
String | 构造函数String | TypeError | 构造函数TypeError |
Number | 构造函数Number | URIError | 构造函数URIError |
4. window 对象
ECMAScript 虽然没有指出如何直接访问 Global 对象,但 Web 浏览器都是将这个全局对象作为 window 对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了 window 对象的属性。示例:
let color = "red";
function sayColor(){
console.log(window.color);
}
window.sayColor(); // "red"
另一种取得 Global 对象的方法是使用以下代码:
let global = function(){
return this;
}();
以上代码创建了一个立即调用的函数表达式,返回 this 的值。如前所述,在没有给函数明确指定 this 值的情况下(无论是通过将函数添加为对象的方法,还是通过调用 call() 或 apply()),this 值等于 Global 对象。而像这样通过简单地返回 this 来取得 Global 对象,在任何执行环境下都是可行的。
7.2、Math 对象
ECMAScript 还为保存数学公式和信息提供了一个公共位置,即 Math 对象。与我们在 JavaScript 直接编写的计算功能相比,Math 对象提供的计算功能执行起来要快得多。Math 对象中还提供了辅助完成这些计算的属性和方法。
1. Math 对象的属性
Math 对象包含的属性大都是数学计算中可能会用到的一些特殊值。下表列出了这些属性。
属 性 | 说 明 |
---|---|
Math.E | 自然对数的底数,即常量 e 的值 |
Math.LN10 | 10 的自然对数 |
Math.LN2 | 2 的自然对数 |
Math.LOG2E | 以 2 为底e的对数 |
Math.LOG10E | 以 10 为底e的对数 |
Math.PI | π 的值 |
Math.SQRT1_2 | 1/2 的平方根(即 2 的平方根的倒数) |
Math.SQRT2 | 2 的平方根 |
2. min() 和 max() 方法
min() 和 max() 方法用于确定一组数值中的最小值和最大值。这两个方法都可以接收任意多个数值参数,示例:
let max = Math.max(3, 54, 32, 16);
console.log(max); // 54
let min = Math.min(3, 54, 32, 16);
console.log(min); // 3
这两个方法经常用于避免多余的循环和在 if 语句中确定一组数的最大值。
要找到数组中的最大或最小值,可以像下面这样使用 apply() 方法。示例:
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max.apply(Math, values);
console.log(max); // 8
3. 舍入方法
将小数值舍入为整数的方法:
- Math.ceil():执行向上舍入,即它总是将数值向上舍入为最接近的整数;
- Math.floor():执行向下舍入,即它总是将数值向下舍入为最接近的整数;
- Math.round():执行标准舍入,即它总是将数值四舍五入为最接近的整数(这也是我们在数学课上学到的舍入规则)。
示例:
console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26
console.log(Math.round(25.9)); // 26
console.log(Math.round(25.5)); // 26
console.log(Math.round(25.1)); // 25
console.log(Math.floor(25.9)); // 25
console.log(Math.floor(25.5)); // 25
console.log(Math.floor(25.1)); // 25
4. random() 方法
Math.random() 方法返回大于等于 0 小于 1 的一个随机数。
利用 Math.random() 从某个整数范围内随机选择一个值。
值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)
公式中用到了 Math.floor() 方法,这是因为 Math.random() 总返回一个小数值。而用这个小数值乘以一个整数,然后再加上一个整数,最终结果仍然还是一个小数。第一个可能的值规定随机整数的最小值。示例:
// 1 ~ 10 之间的随机整数
let num = Math.floor(Math.random() * 10 + 1);
// 2 ~ 10 之间的随机整数
let num = Math.floor(Math.random() *9 + 2);
多数情况下,都可以通过一个函数来计算可能值的总数和第一个可能的值,例如:
function selectFrom(lowerValue, upperValue) {
let choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
let num = selectFrom(2, 10);
console.log(num); // 介于 2 和 10 之间(包括 2 和 10)的一个数值
利用这个函数,可以方便地从数组中随机取出一项,例如:
let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
let color = colors[selectFrom(0, colors.length-1)];
console.log(color); // 可能是数组中包含的任何一个字符串
5. 其他方法
方 法 | 说 明 |
---|---|
Math.abs(num) | 返回 num 的绝对值 |
Math.exp(num) | 返回 Math.E 的 num 次幂 |
Math.log(num) | 返回 num 的自然对数 |
Math.pow(num, power) | 返回 num 的 power 次幂 |
Math.sqrt(num) | 返回 num 的平方根 |
Math.acos(x) | 返回 x 的反余弦值 |
Math.asin(x) | 返回 x 的反正弦值 |
Math.atan(x) | 返回 x 的反正切值 |
Math.atan2(y,x) | 返回 y/x 的反正切值 |
Math.cos(x) | 返回 x 的余弦值 |
Math.sin(x) | 返回 x 的正弦值 |
Math.tan(x) | 返回 x 的正切值 |
虽然 ECMA-262 规定了这些方法,但不同实现可能会对这些方法采用不同的算法。毕竟,计算某个值的正弦、余弦和正切的方式多种多样。也正因为如此,这些方法在不同的实现中可能会有不同的精度。
小结
对象在 JavaScript 中被称为引用类型的值,而且有一些内置的引用类型可以用来创建特定的对象,现简要总结如下:
- 引用类型与传统面向对象程序设计中的类相似,但实现不同;
- Object 是一个基础类型,其他所有类型都从 Object 继承了基本的行为;
- Array 类型是一组值的有序列表,同时还提供了操作和转换这些值的功能;
- Date 类型提供了有关日期和时间的信息,包括当前日期和时间以及相关的计算功能;
- RegExp 类型是 ECMAScript 支持正则表达式的一个接口,提供了最基本的和一些高级的正则表达式功能。
函数实际上是 Function 类型的实例,因此函数也是对象;而这一点正是 JavaScript 最有特色的地方。由于函数是对象,所以函数也拥有方法,可以用来增强其行为。
因为有了基本包装类型,所以 JavaScript 中的基本类型值可以被当作对象来访问。三种基本包装类型分别是:Boolean、Number 和 String。以下是它们共同的特征:
- 每个包装类型都映射到同名的基本类型;
- 在读取模式下访问基本类型值时,就会创建对应的基本包装类型的一个对象,从而方便了数据操作;
- 操作基本类型值的语句一经执行完毕,就会立即销毁新创建的包装对象。
在所有代码执行之前,作用域中就已经存在两个内置对象:Global 和 Math。在大多数 ECMAScript 实现中都不能直接访问 Global 对象;不过,Web 浏览器实现了承担该角色的 window 对象。全局变量和函数都是 Global 对象的属性。Math 对象提供了很多属性和方法,用于辅助完成复杂的数学计算任务。