从面向对象看JavaScript(二)

函数也是对象

  原文再续,书接上一回。话说讲到JavaScript对象里拥有属性与函数,上一讲介绍了属性,这一讲重点讲一讲函数。JavaScript的函数与Java的方法还是有很多不一样的地方,最主要的一点是,JavaScript的函数也是对象。它也可以拥有自己的属性与方法,它可以作为值像一般属性一样传递,成为其他函数的参数或者返回值。

函数的定义方式

函数声明

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

  相当于创建了一个函数对象,名字为sum的引用作为指针指向这个函数对象。与后面函数表达式的定义方式表示的意义完全等同。

函数表达式

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

  这种方式定义有一个缺点。如果用函数声明定义函数,那么JavaScript解释器会进行函数声明提升,即在代码开始执行前,把所有函数声明提到所有代码的前面,因此,即便函数调用在函数声明之前,也能正确执行,不会出错。但采用函数表达式定义函数,则没有提升的效果。

构造函数方式

  在JavaScript里,函数是一种原生对象类型,Function类型。可以用上一讲我们介绍的构造函数方式创建对象。

1
var sum = new Function("num1","num2","return num1 + num2");

  最后一个参数是函数体,前面的全部作为函数的传入参数。不推荐用这种方式定义函数,因为会导致两次解析代码,影响性能。第一次解析常规的JavaScript代码,第二次解析字符串。

函数传递与函数调用

  如果在函数名后面加上(),则代表调用这个函数。如果没有这个括号,则表明将函数作为对象值传递,可以作为其他函数的参数值传入其他函数,也可以作为其他函数的返回值返回函数。

  当函数作为值时,与其他对象属性没有任何区别,可以赋值给其他引用,也可以将函数引用设为null,提示内存的垃圾回收系统将其清理。

两种函数对象

  定义的函数对象,与被调用后的函数对象,在JavaScript里是两个概念,是两个不同的对象。我们一般说的函数对象,是指用上面方法定义的函数。但是,当我们调用函数时,还会另外产生一个对象,通常称为变量对象,当其正被调用时,也称为活动对象。下面,我将分两大部分,分别介绍这两种对象。

定义好的函数对象

  这种函数对象,就是一般我们能访问的函数对象,拥有与其他普通对象一样的特性,可以有自己的属性与函数,并且能够直接访问这些属性和函数。

常用的属性
arguments

  类数组对象,每个函数都有这个对象,函数被定义后,初始值为null。当函数被调用时,传入函数的参数全部推入这个arguments对象里,可以按数组的索引方式访问所有传入参数。当函数调用结束后,arguments又被设为null。

  可以用arguments.length得到传入参数的个数。

  在定义函数时,我们往往会指定一些参数的名字,我们称为命名参数。命名参数只是arguments相应位置的参数的别名。因此,命名参数的个数与实际调用时可以传递的参数数目完全无关,只是方便引用传入参数而已。

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

  仍以sum函数为例。num1,num2是两个命名参数,但实际调用时,我们可以传入任意数量的参数,也可以不传入参数。我们可以用arguments[0]代替num1,用arguments[1]代替num2,也可以用arguments[3],argument[4]等引用更多传入的参数。

  正是由于这个特性,JavaScript不存在函数重载,因为命名参数根本不能起到标识函数的作用。如果试图进行重载,那么只有最后一个函数才生效,其余都被覆盖。这是因为函数也是对象,相当于将同一个函数引用先后指向多个对象,显然只有最后一次赋值才是最终的值。

  arguments.callee指向拥有该arguments对象的函数。如果是递归调用函数的话,可以用arguments.callee代替函数名,达到函数的执行与函数名解耦合的效果。但在严格模式下,访问arguments.callee会出错。

length

  length属性表示函数的命名参数的个数。

prototype

  原型属性,每一个函数都有的一个属性,指向其原型对象。

caller

  保存着调用当前函数的函数的引用。如果在全局环境中调用函数,则为null。caller初始值为null,调用后也为null,只有在调用时才有值。

call和apply

  则是函数对象的两个方法,当调用这两个方法时,函数会被调用,并且把this设置为指定的值,这个指定的值是在这两个方法的传入参数里指定的。这两个方法的具体用法就不详细说明了,非常简单,有兴趣可以另外查阅资料。this也是一个函数属性,但是是变量对象里的属性,不是定义好的函数属性。这我放在后面再介绍。

其他方法

  不同函数可能会自己定义一些方法,例如上一讲提到的Object.defineProperty,Object.getOwnPropertyDescriptor等,都是在Object构造函数上定义的函数。

变量对象

  当执行调用函数指令之后,内存里会产生一个变量对象,相当于从定义好的函数对象中激活出来一个新的特殊的函数对象。需要说明的是,变量对象并不都是函数对象,例如全局环境的window对象等,后面会再稍微提及一下。但变量对象的确主要是函数对象。要认识变量对象,先得从作用域链说起。

作用域链

  JavaScript只有全局作用域和函数作用域,没有块作用域。每个作用域都由一个变量对象表示。作用域链就相当于函数的调用栈。下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColor(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColor();
}
changeColor();

  每个定义好的函数都有一个[[scope]]的内部属性,用来保存该函数调用前的作用域链。作用域链保存的就是变量对象。每个变量对象都维护一个作用域。

  changeColor()调用前,它本身的变量对象还没有创建,没有被加入它的作用域链中,[[scope]]里只包含全局变量对象window。

scope

  当调用changeColor后,调用swapColor前,会为changeColor创建一个执行环境,并复制changeColor的[[scope]]属性,赋值给该执行环境的作用域链,并在作用域链的前端插入由changeColor激活的一个变量对象。同时定义好的swapColor函数对象的[[scope]]内部属性,也会创建一个新的作用域链,包含全局作用域以及changeColor函数作用域。

scope

  我们看到,在changeColor函数里定义的所有变量和函数都作为变量对象的属性和函数保存。changeColor的执行环境里可以直接访问changeColor变量对象的所有属性和方法,而不用以“changeColor变量对象.属性”或“changeColor变量对象.函数”的方式访问。实际上,除了window对象以外,JavaScript也不允许我们访问变量对象,只有解析器能够在后台访问它。由于changeColor的作用域中也包含全局变量对象,因此可以直接访问全局对象的所有属性和方法,而不用显式地用window.属性或window.函数来访问。

  changeColor的变量对象也包含arguments对象,由于changeColor是window对象的函数,因此this指向window对象。这里再回顾一下上面提到的call和apply方法,调用定义好的函数的这两个方法,可以在创建变量对象时让this指向特定的对象,而不是默认地指向该函数所属的对象。如此,可以让方法与对象完全解耦合。

  当swapColor函数调用之后,它也会从swapColor的内部属性[[scope]]中复制作用域链,并且创建一个变量对象,插入到作用域链的前端。swapColor函数内可以直接访问其本身的变量对象,changeColor变量对象以及全局变量对象的所有属性与方法。

scope

  需要说明一点,swapColor的this指向的是window对象,它本身是changeColor变量对象的函数,但除window以外的变量对象是不能被访问的,它又不属于其他普通对象,因此仍被视为由window调用。

闭包

  一般函数调用结束后,它的执行环境和作用域链都会被销毁,它的变量对象也会被销毁。但闭包是一个例外。

  在JavaScript中,闭包是指有权访问另一个函数作用域中的变量的函数。通俗来说,就是在函数里面定义的函数。当闭包作为值被函数返回到再上一级的作用域时,由于它的[[scope]]仍然引用着上一级函数的变量对象,因此这个变量对象不会被销毁。下面修改一下上面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColor(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColor();
return swapColor;
}
var swap = changeColor();

  本来,当changeColor执行结束以后,它的变量对象会被销毁。但由于swapColor的[[scope]]指向的作用域链引用了changeColor的变量对象,而swapColor还被window对象的swap属性引用了,因此不会被销毁,而只销毁changeColor本身的作用域链以及执行环境。

总结

  这一讲主要介绍了函数对象,并重点分析了函数变量对象,以及引申出的作用域链以及闭包等知识点。下一讲将继续总结闭包的知识,总结一下怎么利用闭包与函数作用域来实现对象属性与方法的私有化,从而达到像java或c++的private权限控制的效果。欲知后事如何,请看下回分解。