JavaScript高级程序设计第三版 第5章 引用类型(一)5.1-5.5


引用类型的值(对象)是引用类型的一个实例。
在 ECMAScript 中, 引用类型是一种数据结构: 数据 + 功能。它也常被称为类,但这种称呼并不妥当。尽管 ECMAScript从技术上讲是一门面向对象的语言,但它不具备 类和接口等基本结构。
引用类型(对象定义): 描述的是一类对象所具有的属性和方法。
虽然引用类型与类看起来相似,但它们并不是相同的概念。
对象是某个特定引用类型的实例。
构造函数本身就是一个函数, 为创建新对象而定义的。

    var person = new Object();

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

5.1 Object 类型

到目前为止,我们看到的大多数引用类型值都是 Object 类型的实例;Object 是ECMAScript 中使用最多的一个类型。Object 的实例功能不多,但非常适用于在应用程序中存储和传输数据。
创建 Object 实例的方式有两种。

  1. new
    var person = new Object();
    person.name = "Nicholas";
    person.age = 29;
  1. 使用对象字面量表示法
    对象字面量是对象定义的一种简写形式,便于创建多属性对象。:
    var person = {
    name : "Nicholas",
    age : 29
    };
  • 表达式上下文和语句上下文
    在这个例子中,左边的花括号({)表示对象字面量的开始,因为它出现在了表达式上下文(expression context)中。 ECMAScript 中的表达式上下文指的是能够返回一个值(表达式)。赋值操作符表示后面是一个值,所以左花括号在这里表示一个表达式的开始。同样的花括号,如果出现在一个语句上下文(statement context)中,例如跟在 if 语句条件的后面,则表示一个语句块的开始。

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

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

数值属性名会自动转换为字符串。
使用对象字面量语法时,如果只用花括号,则定义只包含默认属性和方法的对象

    var person = {}; //与 new Object()相同
    person.name = "Nicholas";
    person.age = 29;

在通过对象字面量定义对象时,实际上不会调用 Object 构造函数(Firefox 2 及更早版本会调用 Object 构造函数;但 Firefox 3 之后就不会了)。

  • 最好的做法
    是对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数。

一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。

方括号表示法:

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

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

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

属性名两种情况可以使用方括号表示法
1-包含会导致语法错误的字符
2-使用的是关键字或保留字

  • 例如"first name"
    person["first name"] = "Nicholas";

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

5.2 Array 类型

  • 类型灵活
    ECMAScript 数组的每一项可以保存任何类型的数据
  • 数组大小灵活
    ECMAScript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。
    创建数组的基本方式有两种。
  1. 使用 Array 构造函数
    var colors = new Array();
    var colors = new Array(20);
    var colors = new Array("red", "blue", "green");

当然,给构造函数传递一个值也可以创建数组。但这时候问题就复杂一点了,传递的是数值,则创建这么大的数值;传递的是其他类型的参数,则创建包含该值的只有一项的数组。

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

省略 new 操作符的结果相同:

    var colors = Array(3); // 创建一个包含 3 项的数组
    var names = Array("Greg"); // 创建一个包含 1 项,即字符串"Greg"的数组
  1. 使用数组字面量表示法。
    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    var names = []; // 创建一个空数组
    var values = [1,2,]; // 不要这样!这样会创建一个包含 2 或 3 项的数组
    var options = [,,,,,]; // 不要这样!这样会创建一个包含 5 或 6 项的数组
    var colors = ["red", "blue", "green"]; // 定义一个字符串数组
    alert(colors[0]); // 显示第一项
    colors[2] = "black"; // 修改第三项
    colors[3] = "brown"; // 新增第四项

数组的项数保存在其 length 属性中,这个属性始终会返回 0 或更大的值

    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    var names = []; // 创建一个空数组
    alert(colors.length); //3
    alert(names.length); //0

数组的 length 属性很有特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。

    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    colors.length = 2;
    alert(colors[2]); //undefined

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

    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    colors.length = 4;
    alert(colors[3]); //undefined

在此,虽然 colors 数组包含 3 个项,但把它的 length 属性设置成了 4。这个数组不存在位置 3,所以访问这个位置的值就得到了特殊值 undefined。
利用 length 属性也可以方便地在数组末尾添加新项

    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    colors[colors.length] = "black"; //(在位置 3)添加一种颜色
    colors[colors.length] = "brown"; //(在位置 4)再添加一种颜色
    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    colors[99] = "black"; // (在位置 99)添加一种颜色
    alert(colors.length); // 100

数组最多可以包含 4 294 967 295 个项,如果想添加的项数超过这个上限值,就会发生异常。而创建一个初始大小与这个上限值接近的数组,则可能会导致运行时间超长的脚本错误。

5.2.1 检测数组

    if (value instanceof Array){
    //对数组执行某些操作
    }
  • instanceof 操作符的问题
    instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array 构造函数。
    如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
  • Array.isArray()方法
    为了解决这个问题, ECMAScript 5 新增了 Array.isArray()方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。这个方法的用法如下。
    if (Array.isArray(value)){
    //对数组执行某些操作
    }

支持 Array.isArray()方法的浏览器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 10.5+和 Chrome。要在尚未实现这个方法中的浏览器中准确检测数组,请参考 22.1.1 节。

5.2.2 转换方法

如前所述,所有对象都具有 toLocaleString()、 toString()和 valueOf()方法。

  • 调用数组的 toString()方法
    调用数组的 toString()方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。
  • 调用 valueOf()
    而调用 valueOf()返回的还是数组。实际上,为了创建这个字符串会调用数组每一项的 toString()方法。

来看下面这个例子。

    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    alert(colors.toString()); // red,blue,green
    alert(colors.valueOf()); // red,blue,green
    alert(colors); // red,blue,green

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

  • toLocaleString()方法
    另外, toLocaleString()方法经常也会返回与 toString()和 valueOf()方法相同的值,但也不总是如此。当调用数组的 toLocaleString()方法时,它也会创建一个数组值的以逗号分隔的字符串。而与前两个方法唯一的不同之处在于,这一次为了取得每一项的值,调用的是每一项的 toLocaleString()方法,而不是 toString()方法。请看下面这个例子。
    var person1 = {
    toLocaleString : function () {
    return "Nikolaos";
    },
    toString : function() {
    return "Nicholas";
    }
    };
    var person2 = {
    toLocaleString : function () {
    return "Grigorios";
    },
    toString : function() {
    return "Greg";
    }
    };
    var people = [person1, person2];
    alert(people); //Nicholas,Greg
    alert(people.toString()); //Nicholas,Greg
    alert(people.toLocaleString()); //Nikolaos,Grigorios

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

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

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

在这里,我们使用 join()方法重现了 toString()方法的输出。在传递逗号的情况下,得到了以逗号分隔的数组值。而在最后一行代码中,我们传递了双竖线符号,结果就得到了字符串"red||green||blue"。如果不给 join()方法传入任何值,或者给它传入 undefined,则使用逗号作为分隔符。 IE7 及更早版本会错误的使用字符串"undefined"作为分隔符。如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、toLocaleString()、 toString()和 valueOf()方法返回的结果中以空字符串表示。

5.2.3 栈方法

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

  • push()和 pop()方法
    ECMAScript 为数组专门提供了 push()和 pop()方法,以便实现类似栈的行为。push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而pop()方法则从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。请看下面的例子:
    var colors = new Array(); // 创建一个数组
    var count = colors.push("red", "green"); // 推入两项
    alert(count); //2
    count = colors.push("black"); // 推入另一项
    alert(count); //3
    var item = colors.pop(); // 取得最后一项
    alert(item); //"black"
    alert(colors.length); //2

以上代码中的数组可以看成是栈(代码本身没有任何区别,而 push()和 pop()都是数组默认的方法)。首先,我们使用 push()将两个字符串推入数组的末尾,并将返回的结果保存在变量 count 中(值为 2)。然后,再推入一个值,而结果仍然保存在 count 中。因为此时数组中包含 3 项,所以 push()返回 3。在调用 pop()时,它会返回数组的最后一项,即字符串"black"。此后,数组中仅剩两项。
可以将栈方法与其他数组方法连用,像下面这个例子一样。

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

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

5.2.4 队列方法

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

    var colors = new Array(); //创建一个数组
    var count = colors.push("red", "green"); //推入两项
    alert(count); //2
    count = colors.push("black"); //推入另一项
    alert(count); //3
    var item = colors.shift(); //取得第一项
    alert(item); //"red"
    alert(colors.length); //2

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

  • shift()
    能够移除数组中的第一个项并返回该项,同时将数组长度减 1。
  • unshift()方法
    ECMAScript 还为数组提供了一个 unshift()方法。顾名思义,unshift()与 shift()的用途相反:它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用 unshift()和 pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项,如下面的例子所示。
    var colors = new Array(); //创建一个数组
    var count = colors.unshift("red", "green"); //推入两项
    alert(count); //2
    count = colors.unshift("black"); //推入另一项
    alert(count); //3
    var item = colors.pop(); //取得最后一项
    alert(item); //"green"
    alert(colors.length); //2

这个例子创建了一个数组并使用 unshift()方法先后推入了 3 个值。首先是"red"和"green",然后是"black",数组中各项的顺序为"black"、 “red”、 “green”。在调用 pop()方法时,移除并返回的是最后一项,即"green"。
IE7 及更早版本对 JavaScript 的实现中存在一个偏差,其 unshift()方法总是返回 undefined 而不是数组的新长度。 IE8 在非兼容模式下会返回正确的长度值。

5.2.5 重排序方法

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

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

这里数组的初始值及顺序是 1、 2、 3、 4、 5。而调用数组的 reverse()方法后,其值的顺序变成了5、 4、 3、 2、 1。这个方法的作用相当直观明了,但不够灵活,因此才有了 sort()方法。
在默认情况下, sort()方法按升序排列数组项——即最小的值位于最前面,最大的值排在最后面。为了实现排序, sort()方法会调用每个数组项的 toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值, sort()方法比较的也是字符串,如下所示。

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

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

  • 比较函数
    比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回 0,如果第一个参数应该位于第二个之后则返回一个正数。以下就是一个简单的比较函数:
    function compare(value1, value2) {
    if (value1 < value2) {
    return -1;
    } else if (value1 > value2) {
    return 1;
    } else {
    return 0;
    }
    }

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

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

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

    function compare(value1, value2) {
    if (value1 < value2) {
    return 1;
    } else if (value1 > value2) {
    return -1;
    } else {
    return 0;
    }
    }
    var values = [0, 1, 5, 10, 15];
    values.sort(compare);
    alert(values); // 15,10,5,1,0

在这个修改后的例子中,比较函数在第一个值应该位于第二个之后的情况下返回 1,而在第一个值应该在第二个之前的情况下返回-1。交换返回值的意思是让更大的值排位更靠前,也就是对数组按照降序排序。当然,如果只想反转数组原来的顺序,使用 reverse()方法要更快一些。
reverse()和 sort()方法的返回值是经过排序之后的数组。
对于数值类型或者其 valueOf()方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。这个函数只要用第二个值减第一个值即可。

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

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

5.2.6 操作方法

ECMAScript 为操作已经包含在数组中的项提供了很多方法。其中, concat()方法可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给 concat()方法传递参数的情况下,它只是复制当前数组并返回副本。如果传递给 concat()方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中。如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。下面来看一个例子。

    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

以上代码开始定义了一个包含 3 个值的数组 colors。然后,基于 colors 调用了 concat()方法,并传入字符串"yellow"和一个包含"black"和"brown"的数组。最终,结果数组 colors2 中包含了"red"、 “green”、 “blue”、 “yellow”、 “black"和"brown”。至于原来的数组 colors,其值仍然保持不变。

  • slice()方法
    下一个方法是 slice(),它能够基于当前数组中的一或多个项创建一个新数组。 slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下, slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。注意, slice()方法不会影响原始数组。请看下面的例子。
    var colors = ["red", "green", "blue", "yellow", "purple"];
    var colors2 = colors.slice(1);
    var colors3 = colors.slice(1,4);
    alert(colors2); //green,blue,yellow,purple
    alert(colors3); //green,blue,yellow

如果 slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。例如,在一个包含 5 项的数组上调用 slice(-2,-1)与调用 slice(3,4)得到的结果相同。如果结束位置小于起始位置,则返回空数组。

  • splice()方法
    下面我们来介绍 splice()方法,这个方法恐怕要算是最强大的数组方法了,它有很多种用法。
    splice()的主要用途是向数组的中部插入项,但使用这种方法的方式则有如下 3 种。
功能做法例如
删除可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。splice(0,2)会删除数组中的前两项。
插入可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、 0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。splice(2,0,“red”,“green”)会从当前数组的位置 2 开始插入字符串"red"和"green"。
替换可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。splice (2,1,“red”,“green”)会删除当前数组位置 2 的项,然后再从位置 2 开始插入字符串"red"和"green"。

splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何
项,则返回一个空数组)。|
下面的代码展示了上述 3 种使用 splice()方法的方式。

    var colors = ["red", "green", "blue"];
    var removed = colors.splice(0,1); // 删除第一项
    alert(colors); // green,blue
    alert(removed); // red,返回的数组中只包含一项
    removed = colors.splice(1, 0, "yellow", "orange"); // 从位置 1 开始插入两项
    alert(colors); // green,yellow,orange,blue
    alert(removed); // 返回的是一个空数组
    removed = colors.splice(1, 1, "red", "purple"); // 插入两项,删除一项
    alert(colors); // green,red,purple,orange,blue
    alert(removed); // yellow,返回的数组中只包含一项

5.2.7 位置方法

ECMAScript 5 为数组实例添加了两个位置方法: indexOf()和 lastIndexOf()。

  • indexOf()和 lastIndexOf()
    这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。其中, indexOf()方法从数组的开头(位置 0)开始向后查找, lastIndexOf()方法则从数组的末尾开始向前查找。这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。在比较第一个参数与数组中的每一项时,会使用全等操作符;也就是说,要求查找的项必须严格相等(就像使用===一样)。
    以下是几个例子。
    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

使用 indexOf()和 lastIndexOf()方法查找特定项在数组中的位置非常简单,支持它们的浏览器包括 IE9+、 Firefox 2+、 Safari 3+、 Opera 9.5+和 Chrome。

5.2.8 迭代方法

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

  • every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
  • filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
  • forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
  • map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
  • some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。

以上方法都不会修改数组中的包含的值。

  • 最相似的是 every()和 some()
    在这些方法中,最相似的是 every()和 some(),它们都用于查询数组中的项是否满足某个条件。对 every()来说,传入的函数必须对每一项都返回 true,这个方法才返回 true;否则,它就返回false。而 some()方法则是只要传入的函数对数组中的某一项返回 true,就会返回 true。请看以下例子。
    var numbers = [1,2,3,4,5,4,3,2,1];
    var everyResult = numbers.every(function(item, index, array){
    return (item > 2);
    });
    alert(everyResult); //false
    var someResult = numbers.some(function(item, index, array){
    return (item > 2);
    });
    alert(someResult); //true

以上代码调用了 every()和 some(),传入的函数只要给定项大于 2就会返回 true。对于 every(),它返回的是 false,因为只有部分数组项符合条件。对于 some(),结果就是 true,因为至少有一项是大于 2 的。

  • filter()函数
    下面再看一看 filter()函数,它利用指定的函数确定是否在返回的数组中包含某一项。

例如,要返回一个所有数值都大于 2 的数组,可以使用以下代码。

    var numbers = [1,2,3,4,5,4,3,2,1];
    var filterResult = numbers.filter(function(item, index, array){
    return (item > 2);
    });
    alert(filterResult); //[3,4,5,4,3]

这里,通过调用 filter()方法创建并返回了包含 3、 4、 5、 4、 3 的数组,因为传入的函数对它们每一项都返回 true。这个方法对查询符合某些条件的所有数组项非常有用。

  • map()
    map()也返回一个数组,而这个数组的每一项都是在原始数组中的对应项上运行传入函数的结果。
    例如,可以给数组中的每一项乘以 2,然后返回这些乘积组成的数组,如下所示。
    var numbers = [1,2,3,4,5,4,3,2,1];
    var mapResult = numbers.map(function(item, index, array){
    return item * 2;
    });
    alert(mapResult); //[2,4,6,8,10,8,6,4,2]

以上代码返回的数组中包含给每个数乘以 2 之后的结果。这个方法适合创建包含的项与另一个数组一一对应的数组。

  • forEach()
    最后一个方法是 forEach(),它只是对数组中的每一项运行传入的函数。这个方法没有返回值,本质上与使用 for 循环迭代数组一样。来看一个例子。
    var numbers = [1,2,3,4,5,4,3,2,1];
    numbers.forEach(function(item, index, array){
    //执行某些操作
    });

这些数组方法通过执行不同的操作,可以大大方便处理数组的任务。支持这些迭代方法的浏览器有IE9+、 Firefox 2+、 Safari 3+、 Opera 9.5+和 Chrome。

5.2.9 归并方法

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

使用 reduce()方法可以执行求数组中所有值之和的操作,比如:

    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 是 3(1 加 2 的结果), cur 是 3(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果。reduceRight()的作用类似,只不过方向相反而已。来看下面这个例子。

    var values = [1,2,3,4,5];
    var sum = values.reduceRight(function(prev, cur, index, array){
    return prev + cur;
    });
    alert(sum); //15

在这个例子中,第一次执行回调函数, prev 是 5, cur 是 4。当然,最终结果相同,因为执行的都是简单相加的操作。
使用 reduce()还是 reduceRight(),主要取决于要从哪头开始遍历数组。除此之外,它们完全相同。
支持这两个归并函数的浏览器有 IE9+、 Firefox 3+、 Safari 4+、 Opera 10.5 和 Chrome。

5.3 Date 类型

ECMAScript 中的 Date 类型是在早期 Java 中的 java.util.Date 类基础上构建的。为此, Date类型使用自UTC(Coordinated Universal Time,国际协调时间) 1970 年 1 月 1 日午夜(零时)开始经过的毫秒数来保存日期。在使用这种数据存储格式的条件下, Date 类型保存的日期能够精确到 1970 年 1月 1 日之前或之后的 285 616 年。
要创建一个日期对象,使用 new 操作符和 Date 构造函数即可,如下所示。

    var now = new Date();

在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从 UTC 时间 1970 年 1 月 1 日午夜起至该日期止经过的毫秒数)。

  • Date.parse()和 Date.UTC()
    为了简化这一计算过程, ECMAScript 提供了两个方法: Date.parse()和 Date.UTC()。
  • Date.parse()
    其中, Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。 ECMA-262 没有定义 Date.parse()应该支持哪种日期格式,因此这个方法的行为因实现而异,而且通常是因地区而异。将地区设置为美国的浏览器通常都接受下列日期格式:
  • “月/日/年”,如 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)。只有兼容ECMAScript 5的实现支持这种格式。
    例如,要为 2004年 5月 25日创建一个日期对象,可以使用下面的代码:
    var someDate = new Date(Date.parse("May 25, 2004"));

如果传入 Date.parse()方法的字符串不能表示日期,那么它会返回 NaN。实际上,如果直接将表示日期的字符串传递给 Date 构造函数,也会在后台调用 Date.parse()。换句话说,下面的代码与前面的例子是等价的:

    var someDate = new Date("May 25, 2004");

这行代码将会得到与前面相同的日期对象。
日期对象及其在不同浏览器中的实现有许多奇怪的行为。其中有一种倾向是将超出范围的值替换成当前的值,以便生成输出。例如,在解析"January 32, 2007"时,有的浏览器会将其解释为"February 1, 2007"。而 Opera 则倾向于插入当前月份的当前日期,返回"January 当前日期, 2007"。也就是说,如果在 2007 年 9 月
21 日运行前面的代码,将会得到"January 21, 2007"(都是 21 日)。

  • Date.UTC()方法
    Date.UTC()方法同样也返回表示日期的毫秒数,但它与 Date.parse()在构建值时使用不同的信息。 Date.UTC()的参数分别是年份、基于 0的月份(一月是 0,二月是 1,以此类推)、月中的哪一天(1 到 31)、小时数(0 到 23)、分钟、秒以及毫秒数。在这些参数中,只有前两个参数(年和月)是必需的。如果没有提供月中的天数,则假设天数为 1;如果省略其他参数,则统统假设为 0。以下是两个使用 Date.UTC()方法的例子:
    // GMT 时间 2000 年 1 月 1 日午夜零时
    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));

如同模仿 Date.parse()一样, Date 构造函数也会模仿 Date.UTC(),但有一点明显不同:日期和时间都基于本地时区而非 GMT来创建。不过, Date 构造函数接收的参数仍然与 Date.UTC()相同。因此,如果第一个参数是数值, Date 构造函数就会假设该值是日期中的年份,而第二个参数是月份,以此类推。据此,可以将前面的例子重写如下。

    // 本地时间 2000 年 1 月 1 日午夜零时
    var y2k = new Date(2000, 0);
    // 本地时间 2005 年 5 月 5 日下午 5:55:55
    var allFives = new Date(2005, 4, 5, 17, 55, 55);

以上代码创建了与前面例子中相同的两个日期对象,只不过这次的日期都是基于系统设置的本地时区创建的。

  • Data.now()方法
    ECMAScript 5 添加了 Data.now()方法,返回表示调用这个方法时的日期和时间的毫秒数。这个方法简化了使用 Data 对象分析代码的工作。例如:
    //取得开始时间
    var start = Date.now();
    //调用函数
    doSomething();
    //取得停止时间
    var stop = Date.now(),
    result = stop – start;

支持 Data.now()方法的浏览器包括 IE9+、 Firefox 3+、 Safari 3+、 Opera 10.5 和 Chrome。在不支持它的浏览器中,使用+操作符把 Data 对象转换成字符串,也可以达到同样的目的。

    //取得开始时间
    var start = +new Date();
    //调用函数
    doSomething();
    //取得停止时间
    var stop = +new Date(),
    result = stop - start;

5.3.1 继承的方法

与其他引用类型一样, Date 类型也重写了 toLocaleString()、toString()和 valueOf()方法;但这些方法返回的值与其他类型中的方法不同。 Date 类型的 toLocaleString()方法会按照与浏览器设置的地区相适应的格式返回日期和时间。这大致意味着时间格式中会包含 AM 或 PM,但不会包含时区信息(当然,具体的格式会因浏览器而异)。而 toString()方法则通常返回带有时区信息的日期和时间,其中时间一般以军用时间(即小时的范围是 0 到 23)表示。

  • 在不同浏览器中调用toLocaleString()和 toString()方法
    下面给出了在不同浏览器中调用toLocaleString()和 toString()方法,输出 PST(Pacific Standard Time,太平洋标准时间)时间 2007年 2 月 1 日午夜零时的结果。
浏览器toLocaleString()toString()
Internet Explorer 8Thursday, February 01, 2007 12:00:00 AMThu Feb 1 00:00:00 PST 2007
Firefox 3.5Thursday, February 01, 2007 12:00:00 AMThu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
Safari 4Thursday, February 01, 2007 00:00:00Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
Chrome 4Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)Thu Feb 01 2007 00:00:00 GMT-0800 (Pacific Standard Time)
Opera 102/1/2007 12:00:00 AMThu, 01 Feb 2007 00:00:00 GMT-0800

显然,这两个方法在不同的浏览器中返回的日期和时间格式可谓大相径庭。事实 上,toLocaleString()和 toString()的这一差别仅在调试代码时比较有用,而在显示日期和时间时没有什么价值。

  • Date 类型的 valueOf()方法
    至于 Date 类型的 valueOf()方法,则根本不返回字符串,而是返回日期的毫秒表示。因此,可以
    方便使用比较操作符(小于或大于)来比较日期值。请看下面的例子。
    var date1 = new Date(2007, 0, 1); //"January 1, 2007"
    var date2 = new Date(2007, 1, 1); //"February 1, 2007"
    alert(date1 < date2); //true
    alert(date1 > date2); //false

从逻辑上讲, 2007 年 1 月 1 日要早于 2007 年 2 月 1 日,此时如果我们说前者小于后者比较符合常理。而表示 2007 年 1 月 1 日的毫秒值小于表示 2007 年 2 月 1 日的毫秒值,因此在首先使用小于操作符比较日期时,返回的结果是 true。这样,就为我们比较日期提供了极大方便。

5.3.2 日期格式化方法

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

  • toDateString()——以特定于实现的格式显示星期几、月、日和年;
  • toTimeString()——以特定于实现的格式显示时、分、秒和时区;
  • toLocaleDateString()——以特定于地区的格式显示星期几、月、日和年;
  • toLocaleTimeString()——以特定于实现的格式显示时、分、秒;
  • toUTCString()——以特定于实现的格式完整的 UTC 日期。

与 toLocaleString()和 toString()方法一样,以上这些字符串格式方法的输出也是因浏览器而异的,因此没有哪一个方法能够用来在用户界面中显示一致的日期信息。除了前面介绍的方法之外,还有一个名叫toGMTString()的方法,这是一个与toUTCString()等价的方法,其存在目的在于确保向后兼容。不过, ECMAScript 推荐现在编写的代码一律使用 toUTCString()方法。

5.3.3 日期/时间组件方法

到目前为止,剩下还未介绍的 Date 类型的方法(如下表所示),都是直接取得和设置日期值中特定部分的方法了。需要注意的是, UTC 日期指的是在没有时区偏差的情况下(将日期转换为 GMT 时间)的日期值。

方 法说 明
getTime()返回表示日期的毫秒数;与valueOf()方法返回的值相同
setTime(毫秒)以毫秒数设置日期,会改变整个日期
getFullYear()取得4位数的年份(如2007而非仅07)
getUTCFullYear()返回UTC日期的4位数年份
setFullYear(年)设置日期的年份。传入的年份值必须是4位数字(如2007而非仅07)
setUTCFullYear(年)设置UTC日期的年份。传入的年份值必须是4位数字(如2007而非仅07)
getMonth()返回日期中的月份,其中0表示一月, 11表示十二月
getUTCMonth()返回UTC日期中的月份,其中0表示一月, 11表示十二月
setMonth(月)设置日期的月份。传入的月份值必须大于0,超过11则增加年份
setUTCMonth(月)设置UTC日期的月份。传入的月份值必须大于0,超过11则增加年份
getDate()返回日期月份中的天数(1到31)
getUTCDate()返回UTC日期月份中的天数(1到31)
setDate(日)设置日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份
setUTCDate(日)设置UTC日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份
getDay()返回日期中星期的星期几(其中0表示星期日, 6表示星期六)
getUTCDay()返回UTC日期中星期的星期几(其中0表示星期日, 6表示星期六)
getHours()返回日期中的小时数(0到23)
getUTCHours()返回UTC日期中的小时数(0到23)
setHours(时)设置日期中的小时数。传入的值超过了23则增加月份中的天数
setUTCHours(时)设置UTC日期中的小时数。传入的值超过了23则增加月份中的天数
getMinutes()返回日期中的分钟数(0到59)
getUTCMinutes()返回UTC日期中的分钟数(0到59)
setMinutes(分)设置日期中的分钟数。传入的值超过59则增加小时数
setUTCMinutes(分)设置UTC日期中的分钟数。传入的值超过59则增加小时数
getSeconds()返回日期中的秒数(0到59)
getUTCSeconds()返回UTC日期中的秒数(0到59)
setSeconds(秒)设置日期中的秒数。传入的值超过了59会增加分钟数
setUTCSeconds(秒)设置UTC日期中的秒数。传入的值超过了59会增加分钟数
getMilliseconds()返回日期中的毫秒数
getUTCMilliseconds()返回UTC日期中的毫秒数
setMilliseconds(毫秒)设置日期中的毫秒数
setUTCMilliseconds(毫秒)设置UTC日期中的毫秒数
getTimezoneOffset()返回本地时间与UTC时间相差的分钟数。例如,美国东部标准时间返回300。在某地进入夏令时的情况下,这个值会有所变化

5.4 RegExp 类型

ECMAScript 通过 RegExp 类型来支持正则表达式。使用下面类似 Perl 的语法,就可以创建一个正
则表达式。

    var expression = / pattern / flags ;
  • 模式(pattern)部分
    其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。

  • 正则表达式的匹配模式支持下列 3 个标志。
    g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
    i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
    m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

  • 以字面量形式来定义的正则表达式
    因此,一个正则表达式就是一个模式与上述 3 个标志的组合体。不同组合产生不同结果,如下面的例子所示。

    /*
    * 匹配字符串中所有"at"的实例
    */
    var pattern1 = /at/g;
    /*
    * 匹配第一个"bat"或"cat",不区分大小写
    */
    var pattern2 = /[bc]at/i;
    /*
    * 匹配所有以"at"结尾的 3 个字符的组合,不区分大小写
    */
    var pattern3 = /.at/gi;

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:

    ( [ { \ ^ $ | ) ? * + .]}

这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。下面给出几个例子。

    /*
    * 匹配第一个"bat"或"cat",不区分大小写
    */
    var pattern1 = /[bc]at/i;
    /*
    * 匹配第一个" [bc]at",不区分大小写
    */
    var pattern2 = /\[bc\]at/i;
    /*
    * 匹配所有以"at"结尾的 3 个字符的组合,不区分大小写
    */
    var pattern3 = /.at/gi;
    /*
    * 匹配所有".at",不区分大小写
    */
    var pattern4 = /\.at/gi;

前面举的这些例子都是以字面量形式来定义的正则表达式。

  • 使用RegExp 构造函数创建正则表达式
    另一种创建正则表达式的方式是使用RegExp 构造函数,它接收两个参数:一个是要匹配的字符串模式,另一个是可选的标志字符串。可以使用字面量定义的任何表达式,都可以使用构造函数来定义,如下面的例子所示。
    /*
    * 匹配第一个"bat"或"cat",不区分大小写
    */
    var pattern1 = /[bc]at/i;
    /*
    * 与 pattern1 相同,只不过是使用构造函数创建的
    */
    var pattern2 = new RegExp("[bc]at", "i");

要注意的是,传递给 RegExp 构造函数的两个参数都是字符串(不能把正则表达式字面量传递给 RegExp 构造函数)。由于 RegExp 构造函数的模式参数是字符串,所以在某些情况下要对字符进行双重转义。所有元字符都必须双重转义,那些已经转义过的字符也是如此,例如\n(字符\在字符串中通常被转义为\,而在正则表达式字符串中就会变成\\)。下表给出了一些模式,左边是这些模式的字面量形式,右边是使用 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”

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

    var re = null,
    i;
    for (i=0; i < 10; i++){
    re = /cat/g;
    re.test("catastrophe");//在循环中再次调用 test()方法会失败
    }
    for (i=0; i < 10; i++){
    re = new RegExp("cat", "g");
    re.test("catastrophe");//每次调用 test()都会返回 true
    }

在第一个循环中,即使是循环体中指定的,但实际上只为/cat/创建了一个 RegExp 实例。由于实例属性(下一节介绍实例属性)不会重置,所以在循环中再次调用 test()方法会失败。这是因为第一次调用 test()找了"cat",但第二次调用是从索引为 3 的字符(上一次匹配的末尾)开始的,所以就找不到它了。由于会测试到字符串末尾,所以下一次再调用 test()就又从开头开始了。
第二个循环使用 RegExp 构造函数在每次循环中创建正则表达式。因为每次迭代都会创建一个新的
RegExp 实例,所以每次调用 test()都会返回 true。

ECMAScript 5 明确规定,使用正则表达式字面量必须像直接调用 RegExp 构造函数一样,每次都创建新的 RegExp 实例。 IE9+、 Firefox 4+和 Chrome 都据此做出了修改。

5.4.1 RegExp实例属性

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

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

通过这些属性可以获知一个正则表达式的各方面信息,但却没有多大用处,因为这些信息全都包含
在模式声明中。例如:

    var pattern1 = /\[bc\]at/i;
    alert(pattern1.global); //false
    alert(pattern1.ignoreCase); //true
    alert(pattern1.multiline); //false
    alert(pattern1.lastIndex); //0
    alert(pattern1.source); //"\[bc\]at"
    var pattern2 = new RegExp("\\[bc\\]at", "i");
    alert(pattern2.global); //false
    alert(pattern2.ignoreCase); //true
    alert(pattern2.multiline); //false
    alert(pattern2.lastIndex); //0
    alert(pattern2.source); //"\[bc\]at"

我们注意到,尽管第一个模式使用的是字面量,第二个模式使用了 RegExp 构造函数,但它们的source 属性是相同的。可见, source 属性保存的是规范形式的字符串,即字面量形式所用的字符串

5.4.2 RegExp实例方法

exec()

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"

这个例子中的模式包含两个捕获组。最内部的捕获组匹配"and baby",而包含它的捕获组匹配"and dad"或者"and dad and baby"。当把字符串传入 exec()方法中之后,发现了一个匹配项。因为整个字符串本身与模式匹配,所以返回的数组 matchs 的 index 属性值为 0。数组中的第一项是匹配的整个字符串,第二项包含与第一个捕获组匹配的内容,第三项包含与第二个捕获组匹配的内容。对于 exec()方法而言,即使在模式中设置了全局标志(g),它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用exec()将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用 exec()则都会在字符串中继续查找新匹配项,如下面的例子所示。

    var text = "cat, bat, sat, fat";
    var pattern1 = /.at/;
    var matches = pattern1.exec(text);
    alert(matches.index); //0
    alert(matches[0]); //cat
    alert(pattern1.lastIndex); //0
    matches = pattern1.exec(text);
    alert(matches.index); //0
    alert(matches[0]); //cat
    alert(pattern1.lastIndex); //0
    var pattern2 = /.at/g;
    var matches = pattern2.exec(text);
    alert(matches.index); //0
    alert(matches[0]); //cat
    alert(pattern2.lastIndex); //3
    matches = pattern2.exec(text);
    alert(matches.index); //5
    alert(matches[0]); //bat
    alert(pattern2.lastIndex); //8

这个例子中的第一个模式 pattern1 不是全局模式,因此每次调用 exec()返回的都是第一个匹配项(“cat”)。而第二个模式 pattern2 是全局模式,因此每次调用 exec()都会返回字符串中的下一个匹配项,直至搜索到字符串末尾为止。此外,还应该注意模式的 lastIndex 属性的变化情况。在全局匹配模式下, lastIndex 的值在每次调用 exec()后都会增加,而在非全局模式下则始终保持不变。

test()

IE 的 JavaScript 实现在 lastIndex 属性上存在偏差,即使在非全局模式下,lastIndex 属性每次也会变化。正则表达式的第二个方法是 test(),它接受一个字符串参数。在模式与该参数匹配的情况下返回true;否则,返回 false。在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,使用这个方法非常方便。因此, test()方法经常被用在 if 语句中,如下面的例子所示。

    var text = "000-00-0000";
    var pattern = /\d{3}-\d{2}-\d{4}/;
    if (pattern.test(text)){
    alert("The pattern was matched.");
    }

在这个例子中,我们使用正则表达式来测试了一个数字序列。如果输入的文本与模式匹配,则显示一条消息。这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效,至于它为什么无效就无关紧要了。

valueOf()方法, toLocaleString()和 toString()方法

RegExp 实例继承的 toLocaleString()和 toString()方法都会返回正则表达式的字面量,与创建正则表达式的方式无关。例如:

    var pattern = new RegExp("\\[bc\\]at", "gi");
    alert(pattern.toString()); // /\[bc\]at/gi
    alert(pattern.toLocaleString()); // /\[bc\]at/gi

即使上例中的模式是通过调用 RegExp 构造函数创建的,但 toLocaleString()和 toString()方法仍然会像它是以字面量形式创建的一样显示其字符串表示。
正则表达式的 valueOf()方法返回正则表达式本身。

5.4.3 RegExp构造函数属性

RegExp 构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名(Opera 是例外,它不支持短属性名)。下表列出了 RegExp 构造函数的属性。

长属性名短属性名说 明
input$_最近一次要匹配的字符串。 Opera未实现此属性
lastMatch$&最近一次的匹配项。 Opera未实现此属性
lastParen$+最近一次匹配的捕获组。 Opera未实现此属性
leftContext$`input字符串中lastMatch之前的文本
multiline$*布尔值,表示是否所有表达式都使用多行模式。 IE和Opera未实现此属性
rightContext$’Input字符串中lastMatch之后的文本

使用这些属性可以从 exec()或 test()执行的操作中提取出更具体的信息。请看下面的例子。

    var text = "this has been a short summer";
    var pattern = /(.)hort/g;
    /*
    * 注意: Opera 不支持 input、 lastMatch、 lastParen 和 multiline 属性
    * Internet Explorer 不支持 multiline 属性
    */
    if (pattern.test(text)){
    alert(RegExp.input); // this has been a short summer
    alert(RegExp.leftContext); // this has been a
    alert(RegExp.rightContext); // summer
    alert(RegExp.lastMatch); // short
    alert(RegExp.lastParen); // s
    alert(RegExp.multiline); // false
    }

以上代码创建了一个模式,匹配任何一个字符后跟 hort,而且把第一个字符放在了一个捕获组中。

RegExp 构造函数的各个属性返回了下列值:

  • input 属性返回了原始字符串;
  • leftContext 属性返回了单词 short 之前的字符串,而 rightContext 属性则返回了 short之后的字符串;
  • lastMatch 属性返回最近一次与整个正则表达式匹配的字符串,即 short;
  • lastParen 属性返回最近一次匹配的捕获组,即例子中的 s。
    如前所述,例子使用的长属性名都可以用相应的短属性名来代替。只不过,由于这些短属性名大都
    不是有效的 ECMAScript 标识符,因此必须通过方括号语法来访问它们,如下所示。
    var text = "this has been a short summer";
    var pattern = /(.)hort/g;
    /*
    * 注意: Opera 不支持 input、 lastMatch、 lastParen 和 multiline 属性
    * Internet Explorer 不支持 multiline 属性
    */
    if (pattern.test(text)){
    alert(RegExp.$_); // this has been a short summer
    alert(RegExp["$`"]); // this has been a
    alert(RegExp["$'"]); // summer
    alert(RegExp["$&"]); // short
    alert(RegExp["$+"]); // s
    alert(RegExp["$*"]); // false
    }

除了上面介绍的几个属性之外,还有多达 9 个用于存储捕获组的构造函数属性。访问这些属性的语法是RegExp.$1、 RegExp.$2…RegExp.$9,分别用于存储第一、第二……第九个匹配的捕获组。在调用 exec()或 test()方法时,这些属性会被自动填充。然后,我们就可以像下面这样来使用它们。

    var text = "this has been a short summer";
    var pattern = /(..)or(.)/g;
    if (pattern.test(text)){
    alert(RegExp.$1); //sh
    alert(RegExp.$2); //t
    }

这里创建了一个包含两个捕获组的模式,并用该模式测试了一个字符串。即使 test()方法只返回一个布尔值,但 RegExp 构造函数的属性$1 和$2 也会被匹配相应捕获组的字符串自动填充。

5.4.4 模式的局限性

尽管 ECMAScript 中的正则表达式功能还是比较完备的,但仍然缺少某些语言(特别是 Perl)所支持的高级正则表达式特性。下面列出了 ECMAScript 正则表达式不支持的特性(要了解更多相关信息,请访问 www.regular-expressions.info)。

ECMAScript 正则表达式不支持的特性

  • 匹配字符串开始和结尾的\A 和\Z 锚①
  • 向后查找(lookbehind) ②
  • 并集和交集类
  • 原子组(atomic grouping)
  • Unicode 支持(单个字符除外,如\uFFFF)
  • 命名的捕获组③
  • s(single,单行)和 x(free-spacing,无间隔)匹配模式
  • 条件匹配
  • 正则表达式注释

即使存在这些限制, ECMAScript 正则表达式仍然是非常强大的,能够帮我们完成绝大多数模式匹配任务。
① 但支持以插入符号(^)和美元符号($)来匹配字符串的开始和结尾。
② 但完全支持向前查找(lookahead)。
③ 但支持编号的捕获组。

5.5 Function 类型

函数实际上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。

函数对象
函数名一个指向函数对象的指针,不会与某个函数绑定

函数通常是使用函数声明语法定义的,如下面的例子所示。

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

这与下面使用函数表达式定义函数的方式几乎相差无几。

    var sum = function(num1, num2){// function 关键字后面没有函数名
    return num1 + num2;
    };//注意函数末尾有一个分号,就像声明其他变量时一样。

以上代码定义了变量 sum 并将其初始化为一个函数。 function 关键字后面没有函数名。这是因为在使用函数表达式定义函数时,没有必要使用函数名——通过变量 sum 即可以引用函数。

最后一种定义函数的方式是使用 Function 构造函数。
Function 构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。来看下面的例子:

    var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字,如下面的例子所示。

    function sum(num1, num2){
    return num1 + num2;
    }
    alert(sum(10,10)); //20
    var anotherSum = sum;
    alert(anotherSum(10,10)); //20
    sum = null;
    alert(anotherSum(10,10)); //20

以上代码首先定义了一个名为 sum()的函数,用于求两个值的和。然后,又声明了变量 anotherSum,并将其设置为与 sum 相等(将 sum 的值赋给 anotherSum)。注意,**使用不带圆括号的函数名是访问函数指针,而非调用函数。**此时, anotherSum 和 sum 就都指向了同一个函数,因此 anotherSum()也可以被调用并返回结果。即使将 sum 设置为 null,让它与函数“断绝关系”,但仍然可以正常调用anotherSum()。

5.5.1 没有重载(深入理解)

将函数名想象为指针,也有助于理解为什么 ECMAScript 中没有函数重载的概念。以下是曾在第 3章使用过的例子。

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

显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。以上代码实际上与下面的代码没有什么区别。

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

通过观察重写之后的代码,很容易看清楚到底是怎么回事儿——在创建第二个函数时,实际上覆盖了引用第一个函数的变量 addSomeNumber。

5.5.2 函数声明与函数表达式

  • 函数声明和函数表达式读取顺序
    解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

请看下面的例子。

    alert(sum(10,10));//解析器已经读取并将函数声明添加到执行环境中
    function sum(num1, num2){
    return num1 + num2;
    }

以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时, JavaScript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面, JavaScript 引擎也能把函数声明提升到顶部。
如果像下面例子所示的,把上面的函数声明改为等价的函数表达式,就会在执行期间导致错误。

    var sum = function(num1, num2){//会导致“unexpected identifier”(意外标识符)错误
    return num1 + num2;
    };

以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。

函数声明与函数表达式的语法其实是等价的,唯一区别:什么时候可以通过变量访问函数。

也可以同时使用函数声明和函数表达式,例如 var sum = function sum(){}。不过,这种语法在 Safari 中会导致错误。

5.5.3 作为值的函数

  • 为什么函数可以作为值来使用?
    因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用(传参数,结果返回)。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

来看一看下面的函数。

    function callSomeFunction(someFunction, someArgument){
    return someFunction(someArgument);
    }

这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。
然后,就可以像下面的例子一样传递函数了。

    function add10(num){
    return num + 10;
    }
    var result1 = callSomeFunction(add10, 10);
    alert(result1); //20
    function getGreeting(name){
    return "Hello, " + name;
    }
    var result2 = callSomeFunction(getGreeting, "Nicholas");
    alert(result2); //"Hello, Nicholas"

这里的 callSomeFunction()函数是通用的,即无论第一个参数中传递进来的是什么函数,它都会返回执行第一个参数后的结果。要访问函数的指针而不执行函数的话,必须去掉函数名后面的圆括号。因此上面例子中传递给 callSomeFunction()的是 add10 和 getGreeting,而不是执行它们之后的结果。
当然,可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。

  • 假设有一个对象数组,我们想要根据某个对象属性对数组进行排序
    例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组 sort()方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数,下面就是这个函数的定义。
    function createComparisonFunction(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;
    }
    };
    }

这个函数定义是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个 return 操作符。在内部函数接收到 propertyName 参数后,它会使用方括号表示法来取得给定属性的值。取得了想要的属性值之后,定义比较函数就非常简单了。上面这个函数可以像在下面例子中这样使用。

    var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
    data.sort(createComparisonFunction("name"));
    alert(data[0].name); //Nicholas
    data.sort(createComparisonFunction("age"));
    alert(data[0].name); //Zachary

在默认情况下, sort()方法会调用每个对象的 toString()方法以确定它们的次序;但得到的结果往往并不符合人类的思维习惯。因此,我们调用 createComparisonFunction(“name”)方法创建了一个比较函数,以便按照每个对象的 name 属性值进行排序。而结果排在前面的第一项是 name为"Nicholas", age 是 29 的对象。然后,我们又使用了 createComparisonFunction(“age”)返回的比较函数,这次是按照对象的 age 属性排序。得到的结果是 name 值为"Zachary", age 值是 28 的对象排在了第一位。

5.5.4 函数内部属性

在函数内部,有两个特殊的对象: arguments 和 this。其中, arguments 在第 3 章曾经介绍过,它是一个类数组对象,包含着传入函数中的所有参数。

  • callee属性
    虽然 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。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。例如:

    var trueFactorial = factorial;//变量 trueFactorial 获得了 factorial 的值,实际上是在另一个位置上保存了一个函数的指针。
    factorial = function(){
    return 0;
    };//将一个简单地返回 0 的函数赋值给 factorial 变量
    alert(trueFactorial(5)); //120  在解除了函数体内的代码与函数名的耦合状态之后, trueFactorial()仍然能够正常地计算阶乘。
    alert(factorial(5)); //0//至于 factorial(),它现在只是一个返回 0 的函数。

如果像原来的 factorial()那样不使用arguments.callee,调用 trueFactorial()就会返回 0。

  • 特殊对象 this
    函数内部的另一个特殊对象是 this,其行为与 Java 和 C#中的 this 大致类似。换句话说, this引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是 window)。

来看下面的例子。

    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){//函数 sayColor()是在全局作用域中定义的
    alert(this.color);
    }
    sayColor(); //"red"
    o.sayColor = sayColor;
    o.sayColor(); //"blue"

上面这个函数 sayColor()引用了 this 对象。

  • 为什么 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
    ECMAScript 5 也规范化了另一个函数对象的属性: caller。除了 Opera 的早期版本不支持,其他浏览器都支持这个 ECMAScript 3 并没有定义的属性。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为 null。

例如:

    function outer(){
    inner();
    }
    function inner(){
    alert(inner.caller);//警告框中显示 outer()函数的源代码
    }
    outer();

以上代码会导致警告框中显示 outer()函数的源代码。因为 outer()调用了 inter(),所以inner.caller 就指向 outer()。为了实现更松散的耦合,也可以通过 arguments.callee.caller来访问相同的信息。

    function outer(){
    inner();
    }
    function inner(){
    alert(arguments.callee.caller);
    }
    outer();
  • 严格模式那些事儿
    IE、 Firefox、 Chrome 和 Safari 的所有版本以及 Opera 9.6 都支持 caller 属性。当函数在严格模式下运行时,访问 arguments.callee 会导致错误。 ECMAScript 5 还定义了arguments.caller 属性,但在严格模式下访问它也会导致错误,而在非严格模式下这个属性始终是undefined。定义这个属性是为了分清 arguments.caller 和函数的 caller 属性。以上变化都是为了加强这门语言的安全性,这样第三方代码就不能在相同的环境里窥视其他代码了。
    严格模式还有一个限制:不能为函数的 caller 属性赋值,否则会导致错误。

5.5.5 函数属性和方法

前面曾经提到过, ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性: length 和 prototype。

  • length 属性
    其中, length 属性表示函数希望接收的命名参数的个数,如下面的例子所示。
    function sayName(name){
    alert(name);
    }
    function sum(num1, num2){
    return num1 + num2;
    }
    function sayHi(){
    alert("hi");
    }
    alert(sayName.length); //1
    alert(sum.length); //2
    alert(sayHi.length); //0

以上代码定义了 3 个函数,但每个函数接收的命名参数个数不同。首先, sayName()函数定义了一个参数,因此其 length 属性的值为 1。类似地, sum()函数定义了两个参数,结果其 length 属性中保存的值为 2。而 sayHi()没有命名参数,所以其 length 值为 0。

  • prototype 属性
    在 ECMAScript 核心所定义的全部属性中,最耐人寻味的就要数 prototype 属性了。对于ECMAScript 中的引用类型而言, prototype 是保存它们所有实例方法的真正所在。换句话说,诸如toString()和 valueOf()等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时, prototype 属性的作用是极为重要的(第 6 章将详细介绍)。在 ECMAScript 5 中, prototype 属性是不可枚举的,因此使用 for-in 无法发现。

  • apply()和 call()方法
    每个函数都包含两个非继承而来的方法: 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]); // 传入数组
    }
    alert(callSum1(10,10)); //20
    alert(callSum2(10,10)); //20

在上面这个例子中, callSum1()在执行 sum()函数时传入了 this 作为 this 值(因为是在全局作用域中调用的,所以传入的就是 window 对象)和 arguments 对象。而 callSum2 同样也调用了sum()函数,但它传入的则是 this 和一个参数数组。这两个函数都会正常执行并返回正确的结果。

在严格模式下,未指定环境对象而调用函数,则 this 值不会转型为 window。除非明确把函数添加到某个对象或者调用 apply()或 call(),否则 this 值将是undefined。

call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call()方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下面的例子所示。

    function sum(num1, num2){
    return num1 + num2;
    }
    function callSum(num1, num2){
    return sum.call(this, num1, num2);
    }
    alert(callSum(10,10)); //20

在使用 call()方法的情况下, callSum()必须明确地传入每一个参数。结果与使用 apply()没有什么不同。至于是使用 apply()还是 call(),完全取决于你采取哪种给函数传递参数的方式最方便。
如果你打算直接传入 arguments 对象,或者包含函数中先接收到的也是一个数组,那么使用 apply()肯定更方便;否则,选择 call()可能更合适。(在不给函数传递参数的情况下,使用哪个方法都无所谓。)

事实上,传递参数并非 apply()和 call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。下面来看一个例子。

    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){// sayColor()也是作为全局函数定义
    alert(this.color);
    }
    sayColor(); //red 在全局作用域中调用它时,它确实会显示"red"——因为对 this.color 的求值会转换成对 window.color 的求值
    sayColor.call(this); //red 显式地在全局作用域中调用函数的方式
    sayColor.call(window); //red 显式地在全局作用域中调用函数的方式
    sayColor.call(o); //blue 函数的执行环境就不一样了,因为此时函数体内的 this 对象指向了 o,于是结果显示的是"blue"

使用 call()(或 apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本中,我们是先将 sayColor()函数放到了对象 o 中,然后再通过 o 来调用它的;而在这里重写的例子中,就不需要先前那个多余的步骤了。

  • bind()方法
    ECMAScript 5 还定义了一个方法: bind()。这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind()函数的值。例如:
    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){
    alert(this.color);
    }
    var objectSayColor = sayColor.bind(o);// sayColor()调用 bind()并传入对象 o,创建了 objectSayColor()函数
    objectSayColor(); //blue  objectSayColor()函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。

支持 bind()方法的浏览器有 IE9+、 Firefox 4+、 Safari 5.1+、 Opera 12+和 Chrome。

  • 每个函数继承的 toLocaleString()和 toString()方法始终都返回函数的代码
    每个函数继承的 toLocaleString()和 toString()方法始终都返回函数的代码。返回代码的格式则因浏览器而异——有的返回的代码与源代码中的函数代码一样,而有的则返回函数代码的内部表示,即由解析器删除了注释并对某些代码作了改动后的代码。由于存在这些差异,我们无法根据这两个方法返回的结果来实现任何重要功能;不过,这些信息在调试代码时倒是很有用。另外一个继承的valueOf()方法同样也只返回函数代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值