JavaScript函数与闭包

一、定义函数

1、定义函数的方式有两种:函数声明与函数表达式。

2、函数声明

函数声明方式有一个特征是函数声明提升,也就是说在执行代码之前会先读取函数声明,这意味着可以把函数声明放在调用它的语句的后面,如下:

getColor();

function getColor() {
	return "red";
}
3、函数表达式

对于函数表达式,与其它表达式一样,在使用前必须先赋值,不会像函数声明一样得到提升,如下:

getColor();  // 错误,必须先赋值

var getColor = function() {
	return "red";
}
4、理解函数提升

如下代码所示,对于示例一,在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,但是各个浏览器修正错误的实现不一样,所以对于这种语法可能得到不一致的结果;但是使用示例二的方式就是正确的。

// 示例一
if (condition) {
	function getColor() {
		return "red";
	}
} else {
	function getColor() {
		return "blue";
	}
}

// 示例二
var getColor;
if (condition) {
	getColor = function() {
		return "red";
	};
} else {
	getColor = function() {
		return "blue";
	};
}
5、递归函数存在的问题

如下代码所示,代码中最后只有myFactorial变量指向了原始函数,执行myFactorial(5)时由于必须执行factorial(),而factorial已经不再是函数了,所以导致错误。

function factorial(num) {
	if (num <= 1) {
		return 1;
	} else {
		return num * factorial(num - 1);
	}
}

var myFactorial = factorial;
factorial = null;
myFactorial(5); // 出错
此时可以使用arguments.callee来解决这个问题,arguments.callee是一个指向正在执行的函数的指针,如下:

function factorial(num) {
	if (num <= 1) {
		return 1;
	} else {
		return num * arguments.callee(num - 1);
	}
}
但是在严格模式下,不能通过脚本访问arguments.callee,此时就可以使用命名函数表达式来解决问题,如下:代码中创建了一个名为f()的命名函数表达式,并赋值给factorial变量,所以即使执行"factorial = null",函数的名字f仍然有效。

var factorial = (function f(num) {
	if (num <= 1) {
		return 1;
	} else {
		return num * f(num - 1);
	}
});
二、闭包

1、闭包是指有权访问另一个函数作用域中的变量的函数。

2、创建闭包最常见的方式就是在一个函数内部创建另一个函数,如下:

function closure(name) {

	return function(obj) {
		return obj[name];
	};
}
其中的内部函数就访问了外部函数的参数name,即使该内部函数被返回了,而且是在其它地方被调用的,仍然可以访问参数name,之所以还能够访问name参数,是因为内部函数的作用域链中包含外部函数的作用域。

3、当某个函数第一次被调用时,会创建一个执行环境以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([[Scope]]),然后使用this、arguments和其它命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,依次类推直到作为作用域链终点的全局执行环境。而在函数执行过程中,读取或写入变量的值就需要在作用域链中查找变量,如下代码所示:

function compare(v1, v2) {
	if (v1 < v2) {
		return -1;
	} else if (v1 > v2) {
		return 1;
	} else {
		return 0;
	}
}

var result = compare(5, 10);
以上代码在全局作用域中调用了compare()函数,当第一次调用compare()时,会创建一个包含this、arguments、v1和v2的活动对象,全局执行环境的变量对象包含this、result和compare,且它在compare()执行环境的作用域链中处于第二位,如下图:

JavaScript函数与闭包_第1张图片

后台的每个执行环境都有一个表示变量的对象,即变量对象;全局环境的变量对象始终存在,而函数的局部环境的变量对象只在函数执行的过程中存在。在创建compare()函数时会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中;当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链,然后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端,所以这里的compare()函数的执行环境中,其作用域链包含两个变量对象:本地活动对象和全局变量对象。所以作用域链本质就是一个指向变量对象的指针列表,只包含引用但不实际包含变量对象。

无论何时在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量,一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况不同,在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中,所以以上的closure函数内部定义的匿名函数的作用域链中实际包含外部函数closure的活动对象,如下图所示是执行这些代码时外部函数与内部匿名函数的作用域链:

JavaScript函数与闭包_第2张图片

在匿名内部函数从外部函数closure()中被返回后,它的作用域链被初始化为包含外部函数closure()的活动对象和全局变量对象,所以匿名内部函数就可以访问在closure()函数中定义的所有变量;而且当closure()函数执行完毕后,其活动对象也不会被销毁,因为匿名内部函数的作用域链仍然在引用该活动对象;也就是说当closure()函数返回后,其执行环境的作用域链会被销毁,但是它的活动对象仍然会留在内存中,直到匿名内部函数被销毁后,closure()函数的活动对象才会被销毁,如下:设置f = null就解除了对匿名内部函数的引用,等于通知垃圾收集例程将其清除,随着匿名函数的作用域链被销毁,除了全局作用域之外的其它作用域也都可以安全的销毁。

var f = closure("age");
var result = f({"age": 10});

// 解除对匿名函数的引用,以便释放内存
f = null;
4、由于闭包会携带包含它的函数的作用域,所以会比其他函数占用更多的内存。

5、闭包与变量

闭包只能取得包含函数(外部函数)中任何变量的最后一个值,如下:

function closure() {
	var result = new Array();
	
	for (var i = 0; i < 10; i++) {
		result[i] = function() {
			return i;
		};
	}
	
	return result;
}
这里的closure()函数返回一个函数数组,调用这个被返回的函数数组中的任何一个索引位置的函数都只会返回10,因为数组中的每个函数的作用域链中都保存着closure()函数的活动对象,所以它们访问到的都是同一个变量i,而变量i在closure函数返回之前已经变为10了。在这种情况下,可以通过创建另一个匿名函数强制让返回的数组中的每个函数都返回创建它自己时的变量i的值,如下:

function closure() {
	var result = new Array();
	
	for (var i = 0; i < 10; i++) {
		result[i] = function(num) {
			return function() {
				return num;
			}
		}(i);
	}
	
	return result;
}
这里并没有直接把闭包赋值给数组,而是定义了一个匿名函数并将立即执行该匿名函数的结果赋值给数组,而这个匿名函数接收一个参数num,这个参数num就是最终要返回的值,在调用每个匿名函数时,传入了当前变量i的值,由于函数参数是按值传递的,所以就会将当前变量i的值复制给参数num,而在这个匿名函数的内部又创建并返回了一个访问num的闭包,所以最终返回的result函数数组调用时,每个函数返回的值就不是一样的了。

6、关于this对象

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window;而当函数被作为某个对象的方法调用时,this等于那个对象。但是由于匿名函数的执行环境具有全局性,因此其this对象通常指向window(在通过call()或apply()改变函数执行环境的情况下,this就会指向其它对象),但有时由于编写闭包的方式不同,这点可能不是那么明显,如下:

var name = "zhangsan";

var obj = {
	name: "lisi",
	getName: function() {
		return function() {
			return this.name;
		}
	}
};

obj.getName()(); // 在非严格模式下结果为zhangsan
这里由于getName()函数返回一个匿名函数,所以obj.getName()()就会立即调用返回的那个匿名函数,最终结果在非严格模式下就是返回了字符串"zhangsan",即这里的this表示的是window对象,而不是其包含作用域(或外部作用域)的this对象,因为每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments,内部函数在搜索这两个变量时,只会搜索到自己的活动对象的这两个变量,所以是不可能直接访问到外部函数中的这两个变量的。如果非得访问外部函数的这两个变量,则可以把外部作用域中的这两个变量保存在一个闭包能够访问到的变量里,就可以让闭包访问该这两个变量了,如下:

var name = "zhangsan";

var obj = {
	name: "lisi",
	getName: function() {
		var thisObj = this;
		return function() {
			return thisObj.name;
		}
	}
};

obj.getName()(); // lisi
这里在返回匿名函数之前,把this对象赋值给了一个名叫thisObj的变量,而在定义了闭包后,闭包就可以访问这个变量了,而该变量引用的就是obj对象,即在函数返回之后,thisObj变量也引用着obj对象,所以最终结果返回了字符串"lisi"。

另外,在一些特殊情况下,this的值可能会意外的被改变,如下:

var name = "zhangsan";

var obj = {
	name: "lisi",
	getName: function() {
		return this.name;
	}
};

obj.getName(); // lisi
(obj.getName)(); // lisi
(obj.getName = obj.getName)(); // 在非严格模式下结果是zhangsan
obj.getName()方式是在obj对象上调用的getName()方法,所以this对象表示的是obj对象,所以结果是lisi。(obj.getName)()方式虽然在调用前加上了一个括号,好像只是在引用一个函数,但是this对象得到了维持,因为obj.getName和(obj.getName)的定义是一样的。最后一种调用方式,先执行赋值语句,然后再调用赋值后的结果,因为这个赋值表达式的值是函数本身,所以this对象不能得到维持,结果返回zhangsan。

7、内存泄漏

在IE9之前,对JavaScript对象和COM对象使用的是不同的垃圾收集例程,所以闭包在IE9之前的版本中会导致一些问题,就是如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。如下:

function closure() {
	var element = document.getElementById("id");
	
	element.onclick = function() {
		alert(element.id);
	};
}
其中创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用,由于匿名函数保存了一个对closure函数活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少为1,因此它所占用的内存就永远不会被回收。要解决这个问题,修改如下:

function closure() {
	var element = document.getElementById("id");
	var id = element.id;
	
	element.onclick = function() {
		alert(id);
	};
	
	element = null;
}
通过把element.id保存在了一个变量中,且在闭包中引用该变量,消除了循环引用。但是由于闭包会引用包含函数的整个活动对象,而这里的这个活动对象中还包含这个element元素对象,所以即使闭包中不直接引用element元素对象,包含函数的活动对象中还引用了该对象,所以,需要把element的引用设置为null,这样才能解除对DOM对象的引用,将其引用数变为0,使得得到正常的回收。

三、模仿块级作用域

1、由于在JavaScript中是没有块级作用域的,所以在块语句中定义的变量实际上是在包含函数中(函数活动对象)而非块语句中创建的。如下代码所示,即使错误的重新声明了变量i,也不会改变了它的值,因为JavaScript不会通知是否多次声明了同一个变量,只会对后续的声明视而不见,但是如果后续的声明中还包含对变量的初始化,则会执行后续声明中的变量初始化。

function block(count) {
	for (var i = 0; i < count; i++) {
		alert(i);
	}
	
	var i;   // 重新声明变量i
	alert(i);
}
2、匿名函数可以用来模仿块级作用域(私有作用域),写法如下:

(function() {
	// 在这里定义块级作用域的变量
})();
这里定义了一个匿名函数并立即执行该函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,紧随其后的一对圆括号则会立即执行该函数。要注意的是包围匿名函数声明的那一对圆括号是不能缺少的,因为JavaScript将function关键字当作一个函数声明的开始,而函数声明后面是不能跟圆括号的,而函数表达式的后面是可以跟圆括号的。将函数声明用一对圆括号包围起来就可以将函数声明转换成函数表达式。如下代码所示是使用匿名函数来模仿块级作用域的例子,这里的for循环是在匿名函数内部创建的,所以此时的变量i是在这个匿名函数内部定义的,所以在该匿名函数中创建的任何变量都会在该匿名函数执行完毕后被销毁,所以匿名函数执行完毕就销毁了变量i,所以接下来访问变量i就会引发错误,因为在block函数中并没有定义变量i。而在匿名函数内部可以访问block函数的参数count,是因为闭包的原因。

function block(count) {
	(function() {
		// 在这里定义块级作用域的变量
		for (var i = 0; i < count; i++) {
			alert(i);
		}
	})();
	
	alert(i);   // 错误
}
3、这种模仿块级作用域的方式常用在全局作用域中被用在函数外部,能够限制向全局作用域中添加过多的变量和参数,因为当匿名函数执行完毕后这些变量和参数都被销毁了,如下所示,当页面加载就会执行这个匿名函数,执行完毕后变量color就会被销毁;而如果是将变量color在全局作用域中定义,则只有当页面被关闭时该变量才会随着全局作用域被销毁而销毁;而且这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要匿名函数执行完毕就可以立即销毁其作用域链了。除此之外,还可以避免多人协作开发中过多的全局变量和函数导致的命名冲突问题。
<html>
	<body>
		<script type="text/javascript">
			(function() {
				var color = "red";
				
				if ("red" === color) {
					alert("OK");
				}
			})();
		</script>
	</body>
</html>
四、私有变量

1、严格来说JavaScript中没有私有成员的概念,所有对象属性都是公有的;但是有一个私有变量的概念,任何函数中定义的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量,私有变量包括函数的参数、局部变量和函数内部定义的其它函数。在函数内部创建闭包,则闭包可以通过自己的作用域链访问这些私有变量,所以可以通过这种方式就可以创建用以访问私有变量的公有方法。把有权访问私有变量和私有函数的公有方法称为特权方法。

2、有两种方式可以在对象上创建特权方法:

  • 方式一:在构造函数中定义特权方法,如下:
function MyObject() {
	
	// 定义私有变量和私有函数
	var privateVar = 100;
	
	function privateFunc() {
		return true;
	}
	
	// 特权方法
	this.publicMethod = function() {
		privateVar++;
		return privateFunc();
	};
}
能够在构造函数中定义特权方法是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。这里的私有变量privateVar和私有函数privateFunc就只能通过特权方法publicMethod来访问,在创建MyObject的实例后,除了使用该途径外,没有任何办法可以直接访问这些私有变量和函数。这种方式的缺点就是必须使用构造函数模式来达到目的,而构造函数模式的缺点就是针对每个实例都会创建同一组新方法。
  • 方式二:使用静态私有变量来实现特权方法,通过在私有作用域中定义私有变量或函数来创建特权方法,如下:
(function() {
	// 定义私有变量和函数
	var privateVar = 10;
	
	function privateFunc() {
		return false;
	}
	
	// 构造函数
	MyObject = function() {};
	
	// 公有/特权方法
	MyObject.prototype.publicMethod = function() {
		privateVar++;
		privateFunc();
	};
	
})();
该模式创建了一个私有作用域,并在其中定义了私有变量和私有函数,且封装了一个构造函数以及相应的方法。注意的是在定义构造函数时并没有使用函数声明,而是使用了函数表达式的方式,因为在这里使用函数声明只能创建局部函数,而且也没有使用关键字var来定义MyObject,这就使得MyObject成为一个全局的变量 因为初始化未经声明的变量会使得该变量成为全局变量(在严格模式下,给未经声明的变量赋值会导致错误),所以这里的MyObject能够在私有作用域之外被访问到。
该模式与在构造函数中定义特权方法的区别在于私有变量和函数是由实例共享的,由于特权方法是在原型上定义的,所以所有实例都使用同一个函数,而该特权方法作为一个闭包,总是保存着对包含作用域的引用,如下:
(function() {
	var name = "";
	
	Person = function(value) {
		name = value;
	};
	
	Person.prototype.getName = function() {
		return name;
	};
	
	Person.prototype.setName = function(value) {
		name = value;
	};
	
})();

var p1 = new Person("zhangsan");
p1.getName(); // zhangsan
p1.setName("lisi");
p1.getName(); // lisi

var p2 = new Person("wangwu");
p2.getName(); // wangwu
p1.getName(); // wangwu
这里的私有变量name就成为一个静态的、由所有实例共享的的属性。使用这种方式创建的特权方法会因为使用原型而增进代码复用,但是每个实例都没有自己的私有变量。
3、使用闭包和私有变量的明显不足之处在于多查找作用域链中的一个层次就会在一定程序上影响查找速度。
4、模块模式
以上的两个模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法的。JavaScript是以对象字面量的方式来创建单例对象的。模块模式通过为单例添加私有变量和特权方法使得其得到增强,如下:
var singleton = function() {
	//  私有变量和私有函数
	var privateVar = 10;
	
	function privateFunc() {
		return false;
	}
	
	// 特权/公有方法和属性
	return {
		publicProperty: true,
		publicMethod: function() {
			privateVar++;
			return privateFunc();
		}
	};
}();
该模块模式使用了一个返回对象的匿名函数,在这个匿名函数内部定义了私有变量和私有函数,然后将一个对象字面量作为函数的返回值返回,由于返回的这个对象是在匿名函数内部定义的,所以它的公有方法有权访问私有变量和私有函数。从本质上说这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时很有用,如下:
var application = function() {
	// 私有变量
	var components = new Array();
	// 初始化
	components.push(new Object());
	
	return {
		getComponentCount: function() {
			return components.length;
		},
		registerComponent: function(component) {
			if (typeof component == "object") {
				components.push(component);
			}
		}
	};
}();
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以使用模块模式,以这种模式创建的每个单例都是Object的实例。
5、增强的模块模式
在返回对象之前加入对其增强的代码,这种模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况,如下:
var singleton = function() {
	//  私有变量和私有函数
	var privateVar = 10;
	
	function privateFunc() {
		return false;
	}
	
	// 创建对象,这里假设CustomObject类型已定义
	var obj = new CustomObject();
	
	// 添加特权/公有方法和属性
	obj.publicProperty = true;
	obj.publicMethod = function() {
		privateVar++;
		return privateFunc();
	};
	
	// 返回该对象
	return obj;
}();
在这个单例中不同之处在于命名变量obj的创建过程,因为它必须是CustomObject的实例,所以最后得到的singleton单例对象也是CustomObject的实例。


参考书籍:《JavaScript高级程序设计》(第三版)

你可能感兴趣的:(JavaScript,函数,闭包)