递归 arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。
1
2
3
4
5
6
7
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}
通过使用 argument.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。 但在严格模式下,不能通过脚本访问 arguments.callee, 访问这个属性会导致错误。不过,可以使用函数表达式来达成相同的结果。
1
2
3
4
5
6
7
var factorial = (function f(num)) {
if (num <= 1) {
return 1;
} else{
return num * f(num-1);
}
}
即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。
闭包 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}
内部函数的代码访问了外部函数中的变量 propertyName。 即使这个内部函数被返回了,而且是在其它地方被调用了,但它仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域中包含 createComparisonFunction() 的作用域。要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。 当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终位于第二位,外部函数的外部函数的活动对象始终位于第三位,……直至作为作用域终点的全局执行环境。 在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量。
1
2
3
4
5
6
7
8
9
10
11
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
以上代码先定义了 compare() 函数, 然后又在全局作用域中调用了它。当调用 compare() 时,会创建一个包含 arguments、value1、value2 的活动对象。全局执行环境(包含 result 和 compare) 在 compare() 执行环境的作用域链中则处于第二位。
后台的每个执行环境都有一个表示变量的对象---变量对象。全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数的执行的过程中存在。 无论什么时候再函数中访问一个变量时,就会从作用域链中搜索具有响应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。 在另一个 在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction() 函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction() 的活动对象。
1
2
var compare = createComparisonFunction("name" );
var result = compare({name: "Nicholas" }, {name: "Greg" });
在匿名函数从 createComparisonFunction() 中被返回后,它的作用域链被初始化为包含 createComparisonFunction() 函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction() 中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁返回后,createComparisonFunction() 的活动对象才会被销毁。
1
2
3
4
5
6
7
8
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas"}, { name, "Greg"});
//解除对匿名函数的引用(以便释放内存)
compareNames = null;
首先,创建的比较函数被保存在变量 compareNames 中。而通过将 compareNames 设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其它作用域(除了全局作用域)也都可以安全地销毁了。
闭包与变量 作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量。
1
2
3
4
5
6
7
8
9
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++) {
result[i] = function(){
return i;
};
}
return result;
}
此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10.但是,可以通过创建另一个匿名函数强制让闭包的行为符合预期。
1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions (){
var result = new Array();
for (var i=0; i < 10; i++) {
result[i] = function (num){
return function () {
return num;
};
}(i);
}
return result;
}
关于this this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向window。
1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window" ;
var object = {
name : "My Object" ,
getNameFunc : function (){
return function (){
return this.name;
}
}
}
alert(object.getNameFunc()()); //"The Window" (在非严格模式下)
每个函数在被调用时都会自动取得这两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
this 和 arguments 也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。
内存泄漏 如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素无法被销毁。
1
2
3
4
5
6
function assignHandler (){
var element = document.getElementById("someElement" );
element.onclick = function (){
alert(element.id);
}
}
这个闭包创建了一个循环引用。由于匿名函数保存了一个对 assignHandler() 的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改一下代码来解决。
1
2
3
4
5
6
7
8
9
10
function assignHandler (){
var element = document.getElementById("someElement" );
var id = element.id;
element.onclick = function (){
alert(id);
}
element = null;
}
通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。因为闭包会引用闭包包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存引用。因此,有必要把 element 变量设为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
模仿块级作用域 无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。
1
2
3
4
5
6
7
8
function outputNumbers(count) {
(function () {
for (var i=0; i < count; i++) {
alert(i);
}
})();
alert(i); //导致一个错误!
}
私有变量 有权访问私有变量和私有函数的公有方法称为特权方法 。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyObject () {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction (){
return false ;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
}
}
利用私有和特权成员,可以隐藏那些不应该直接被修改的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
this.getName = function (){
return name;
}
this.setName = function (value){
name = value;
}
}
var person = new Person("Nicholas" );
alert(person.getName()); //"Nicholas"
person.setName("Greg" );
alert(person.getName()); //"Greg"
在函数中定义特权方法也有缺点,那就是必须使用构造函数模式来达到这个目的。构造函数的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。
静态私有变量 通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function (){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction (){
return false ;
}
//构造函数
MyObject = function (){
};
//公有/特权方法
MyObject.prototype.publicMethod = function (){
privateVariable++;
return privateFunction();
}
})();
这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明Object时使用 var 关键字。初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域外被访问到。但在严格模式下给未经生命的变量赋值会导致错误。 这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function (){
var name = "" ;
Person = function (value){
name = value;
}
Person.prototype.getName = function (){
return name;
}
Person.prototype.setName = function (value){
name = value;
}
})();
var person1 = new Person("Nicholas" );
alert(person1.getName()); //"Nicholas"
person1.setName("Greg" );
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael" );
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用 setName() 会影响所有实例。而调用 seName() 或新建一个 Person 实例都会赋予 name 属性一个新值。 以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视具体需求而定。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。
模块模式 前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。所谓单例,值得就是只有一个实例的对象。按照惯例,Javascript是以对象字面量的方式来创建单例对象的。
1
2
3
4
5
6
var singleton = {
name : value,
method : function (){
//这里是方法的代码
}
}
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var singleton = function (){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction (){
return false ;
}
//特权/公有方法和属性
return {
publicProperty: true ,
publicMethod : function (){
privateVariable++;
return privateFunction();
}
}
}()
从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object") {
components.push(component);
}
}
}
}()
如果必须创建一个对象并以某些数据对其初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。