《JavaScript高级程序设计》学习笔记之Object、Array、Date、Function等引用类型介绍

引用类型

引用类型的值(对象)是引用类型的一个实例,在ECMAScript中,引用类型是一种数据结构。对象是某个特定引用类型的实例,新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。如下面的一行代码:

var person = new Object();

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

Object类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象可以通过执行 new 操作符后跟要创建 的对象类型的名称来创建。而创建 Object 类型的实例并为其添加属性和(或)方法,就可以创建自定 义对象,如下所示:

var o = new Object();

这个语法与 Java中创建对象的语法相似;但在 ECMAScript中,如果不给构造函数传递参数,则可 以省略后面的那一对圆括号。也就是说,在像前面这个示例一样不传递参数的情况下,完全可以省略那 对圆括号(但这不是推荐的做法):

var o = new Object; // 有效,但不推荐省略圆括号

仅仅创建Object的实例并没有什么用处,但关键是要理解一个重要的思想:即在ECMAScript中,(就像Java中的 java.lang.Object 对象一样)Object 类型是所有它的实例的基础。换句话说, Object 类型所具有的任何属性和方法也同样存在于更具体的对象中。 Object 的每个实例都具有下列属性和方法:

1、constructor:保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是Object()。

2、hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例 的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定(例 如:o.hasOwnProperty("name"))。

3、isPrototypeOf(object):用于检查传入的对象是否是传入对象的原型

4、propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用 for-in 语句 来枚举。与 hasOwnProperty()方法一样,作为参数的属性名必须以字符 串形式指定。

5、 toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。

6、 toString():返回对象的字符串表示。

7、 valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值 相同。

到目前为止,我们看到的大多数引用类型值都是Object类型的实例;而且,Object也是ECMAScript中使用最多的一个类型。

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

var person = new Obeject();
person.name = "Wangwei";
person.age = 20;

另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。下面的例子就使用了对象字面量的方法定义了上面的例子:

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

在使用对象字面量表示法时,属性名也可以使用字符串,如下所示:

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

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

var person = {};
person.name = "Wangwei";
person.age = 20;

一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。不过,在JavaScript中也可以使用方括号表示法来访问对象的属性,在使用方括号时,应该将要访问的属性以字符串的形式放在方括号中,如下面的例子所示:

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

方括号的主要优点在于可以通过变量来访问属性,例如:

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

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

Array类型

除了Object之外,Array类型恐怕是ECMAScript中最常用的类型了。与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据,也就是说,可以用数组的第一个位置来保存字符串,用第二个位置来保存数值,用第三个位置来保存对象,以此类推。并且,其数组大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。

创建数组的基本方式有两种。第一种是使用Array构造函数,如下代码所示:

var color = new Array();

如果预先知道数组要保存的项目数量,也可以给构造函数传递该数量,得到的数组就会具有那么多的位置(其中每一项的初始值都是undefined)。例如,下面的代码将创建包含20个项的数组:

var colors = new Array(20);

也可以向Array构造函数传递数组中应该包含的项,如下:

var colors = new Array("red", "blue", "green");

在使用Array构造函数时,也可以省略new操作符。

创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间用逗号隔开,如下所示:

var colors = ["red", "blue", "green"]; //创建一个包含3个字符串的数组
var names = []; //创建一个空数组
var values = [1, 2, ]; //不要这样!会创建一个包含2或3项的数组
var option = [, , , , ,]; //不要这样!会创建一个包含5或5项的数组

与对象一样,使用数组字面量表示法 时,也不会调用Array构造函数。

在读取和设置数组的值时,要使用方括号并提供相应值的基于0的数字索引,如下所示:

var colors = ["red", "blue", "green"];
alert(colors[0]); //显示第一项
colors[2] = "black"; //修改第三项
colors[3] = "brown"; //新增第四项

数组栈方法

ECMAScript数组也提供了一种让数组的行为类似于其他数据结构的方法。具体来说,数组可以表现得就像栈一样,栈是一种可以限制插入和删除项的数据结构,栈是一种LIFO(last-in-first-out,后进先出)的数据结构。而栈中的插入(叫做推入)和移除(叫做弹出),只发生在一个位置---栈顶。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

可以将栈方法与其他数组方法连用,如下列所示:

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

数组队列方法

栈数据结构的访问规则是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 
"red"
alert(colors.length); //2 

ECMAScript还为数组提供了一个unshift()方法。顾名思义,unshift()与shift()的用途相反:它能在数组前端添加任意个项并返回新数组长度。因此,同时使用unshift()和pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项,如下面的例子所示:

var colors = new Array(); //创建一个数组
var count = colors.unshft("red", "green"); //推入两项
alert(count); //2
count = colors.unshift("black"); //推入另一项
var item = colors.pop(); //移除最后一项
alert(item); //"green"
alert(colors.length); //2 
"green"
alert(colors.length); //2 

重排序方法

 

数组中已经存在两个可以直接用来重排序的方法:reverse()和sort()。reverse()方法会反转数组项的顺序,如下列所示:

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

在默认情况下,sort()方法按升序排列数组项----即最小的值位于最前面,最大的值排在最后面。为了实现排序,sort()方法会调用每个数组项的toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串,如下所示:

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

可见,这种排序方式并不实用。为此,sort()方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值的前面。

比较函数接收两个参数,如果第一个参数应该位于第二个参数之前则返回一个负数(即不交换位置),如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数(即两数交换位置),如下列:

function compare(value1, value2) {
  if(value1 < value2) {
    return -1;   //不交换位置
  }else if(value1 > value2) {
    return 1;    //交换位置
  }else {
    return 0;
  }
}

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

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

当然,也可以通过比较函数产生降序的效果,只要交换比较函数返回的值即可:

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

对于数值类型后者其valueof()方法返回数值类型的对象类型,可以使用一个更简单的比较函数:

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

操作方法

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

下一个方法是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); //ggreen, blue, yellow

如果slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。

下面我们来介绍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()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。如下代码所示:

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

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日午夜起至该日期止经过的毫秒数)。为了简化这一计算过程,ECMAScript提供了两个方法:Date.parse()和Date.UTC().

其中,Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。将地区设置为美国的浏览器通常都接受下列日期格式:

“月/日/年”,如5/15/2018;

“英文月名 日,年”,如January 12,2018;

“英文星期几 英文月名 日 年 时:分:秒 时区”,如Tue May 25 2018 14:12:59 GMT-0700。

例如,要为2018年5月15日创建一个日期对象,可以使用下面的代码:

var someDate = new Date(Date.parse("May 15, 2018"));

实际上,如果直接将表示日期的字符串传递给Date构造函数,也会在后天台调用Date.parse()。换句话说,下面的代码与前面的例子是相等的:

var someDate = new Date("May 15, 2018");

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

//GMT
//本地时间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);

如模仿Date.parse()一样,Date构造函数也会模仿Date.UTC(),但有一点不同的是,其日期和时间都基于本地时区而非GMT来创建的。不过,Date构造函数接收的参数仍然与Date.UTC()相同,据此,可以将前面的例子重写如下:

 

var y2k = new Date(Date.UTC(2000, 1));
//GMT时间2005年5月5日下午5:55:55
var allFives = new Date(Date.UTC(2005,4, 5, 17, 55, 55));

日期格式化方法

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

toDateString()---以特定于实现的格式显示星期几、月、日和年;

toTimeString()---以特定于实现的格式显示时、分、秒和时区;

toLocaleDateString()---以特定于地区的格式显示星期几、月、日和年;

toLocaleTimeString()---以特定于实现的格式显示时、分、秒;

toUTCString()---以特定于实现的格式显示完整的UTC日期。

与toLocaleString()和toString()一样,以上这些字符串格式方法的输出也是因浏览器而异的,因此没有哪一个方法能够用来在用户界面显示一致的日期信息。

Function类型

说起来 ECMAScript中什么有意思,我想那莫过于函数了——而有意思的根源,则在于函数实际 上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函 数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函 数声明语法定义的,如下面的例子所示。

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

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

var sum = function(num1, num2){     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()。

 没有重载(深入理解)

将函数名想象为指针,也有助于理解为什么 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。

函数声明与函数表达式

本节到目前为止,我们一直没有对函数声明和函数表达式加以区别。而实际上,解析器在向执行环 境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行 任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真 正被解释执行。请看下面的例子。
 

alert(sum(10,10));

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

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

alert(sum(10,10));

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


以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一 行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。 除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。
也可以同时使用函数声明和函数表达式,例如 var sum = function sum(){}。 不过,这种语法在 Safari中会导致错误。


作为值的函数

因为 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    


 这里,我们创建了一个包含两个对象的数组 data。其中,每个对象都包含一个 name 属性和一个 age 属性。在默认情况下,sort()方法会调用每个对象的toString()方法以确定它们的次序;但得到的结果往往并不符合人类的思维习惯。

因此,我们调用 createComparisonFunction("name")方法创建了一个比较函数,以便按照每个对象的 name 属性值进行排序。而结果排在前面的第一项是 name 为"Nicholas",age 是 29的对象。然后,我们又使用了 createComparisonFunction("age")返回 的比较函数,这次是按照对象的 age 属性排序。得到的结果是 name 值为"Zachary",age 值是 28的 对象排在了第一位。

函数内部属性

在函数内部,有两个特殊的对象: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。这样,无论引用 函数时使用的是什么名字,都可以保证正常完成递归调用。例如:
 

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

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


 在此,变量 trueFactorial 获得了 factorial 的值,实际上是在另一个位置上保存了一个函数 的指针。然后,我们又将一个简单地返回 0的函数赋值给 factorial 变量。如果像原来的 factorial() 那样不使用 arguments.callee,调用 trueFactorial()就会返回 0。可是,在解除了函数体内的代 码与函数名的耦合状态之后,trueFactorial()仍然能够正常地计算阶乘;至于 factorial(),它现 在只是一个返回 0的函数。 函数内部的另一个特殊对象是 this,其行为与 Java和 C#中的 this 大致类似。

换句话说,this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时, this 对象引用的就是 window)。来看下面的例子。

window.color = "red"; var o = { color: "blue" };
function sayColor(){     alert(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()指向的仍然是同一 个函数。


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

function outer(){     inner();  }
function inner(){     alert(inner.caller); }
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 属性赋值,否则会导致错误。
 
函数属性和方法

前面曾经提到过,ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。其中,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。

在 ECMAScript 核心所定义的全部属性中,耐人寻味的就要数 prototype 属性了。对于 ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。换句话说,诸如 toString()和 valueOf()等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访 问罢了。在创建自定义引用类型以及实现继承时,prototype 属性的作用是极为重要的。

在 ECMAScript 5中,prototype 属性是不可枚举的,因此使用 for-in 无法发现。

每个函数都包含两个非继承而来的方法: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(){     alert(this.color); }
sayColor();                //red

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

这个例子是在前面说明 this 对象的示例基础上修改而成的。这一次,sayColor()也是作为全局 函数定义的,而且当在全局作用域中调用它时,它确实会显示"red"——因为对 this.color 的求值会转换成对 window.color 的求值。而 sayColor.call(this)和 sayColor.call(window),则是两 种显式地在全局作用域中调用函数的方式,结果当然都会显示"red"。

但是,当运行 sayColor.call(o) 时,函数的执行环境就不一样了,因为此时函数体内的 this 对象指向了 o,于是结果显示的是"blue"。


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

ECMAScript 5还定义了一个方法:bind()。这个方法会创建一个函数的实例,其 this 值会被绑 定到传给 bind()函数的值。例如:
 

window.color = "red"; 
var o = { color: "blue" };
 
function sayColor(){     
    alert(this.color); 
 }  
var objectSayColor = sayColor.bind(o); 
objectSayColor();    //blue


在这里,sayColor()调用 bind()并传入对象 o,创建了 objectSayColor()函数。object- SayColor()函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。

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

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值