闭包整理

来源:互联网 发布:地图填色软件 编辑:程序博客网 时间:2024/06/05 21:04

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的创建方式,就是在一个函数的内部创建另外一个函数。

createComparisonFunction (propertyName){return function (object1,object2) {var value1 = object1[propertyName];//*var value2 = object1[propertyName];//*if (value1 < value2) {return -1;}else if (value1 > value2) {return 1;}else {return 0;}}}

上面标记星号的二行代码是内部函数(一个匿名函数)中的代码,
这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被
返回了,而且是在其他的地方被调用了,但它仍然可以访问变量propertyName。
之所以还可以访问到这个变量,是因为内部函数的作用域链中包含了createComparisonFunction()的作用域。

当某个函数第一次被调用时,会创建一个执行环境(execution context)及相应的作用链,并把作用域链赋值给
一个特殊的内部属性(即[[Scope]])。然后使用this,arguments和其他命名参数的值来初始化函数的活动对象
(activation object)。但在作用域链中,外部函数的活动对始终处于第二位,外部函数的外部函数的活动对象
处于第三位,.....直到作用域链终点的全局执行环境。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

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()时,
会创建一个包含this,arguments,value1,value2的活动对象。
全局执行环境的变量也包含this,result,compare,在compre()执行环境的作用域链中则处于第二位。

图:compare()函数执行时的作用域链。


后台的每个执行环境都有一个表示变量的对象-变量对象。全局环境的变量对象始终存在,而象compare()
函数这样的局部环境变量对象,则只在函数执行的过程中存在。
在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的
[[Scope]]属性中。当调用compare函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]
属性中的对象构建起执行环境的作用域链。此后又有一个活动对象(在此作为变量对象使用)被创建并推入
执行环境作用域链的前端。对于这个例子中的compare函数的执行环境而言,起作用域链中包含二个变量对象;
本地活动对象和全局变量对象。显然,作用域链本质上就是一个指向变量对象的指针列表,它引用但不实际
包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,
当函数执行完毕后,局部活动对象就会销毁,内存中仅仅保存全局作用域(全局执行环境的变量对象)
但是,闭包的情况下就不同了。
在另一个函数内部定义的函数将包含函数的活动对象添加到它的作用域链中。
所以:在createComparisonFunction函数内部定义的匿名函数的作用域链中,实际将会包含外部函数
createComparisonFunction()的活动对象。图:


var compare = createComparisonFunction ("name");var result = compare ({name:"aa"},{name:"b"});

在匿名函数从createComparisonFunction()中被返回后,它的作用域就被
初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。
这样,匿名函数就可以访问在createComparisonFunction()中定义的所有的变量。
更为重要的是,createComparisonFunction()函数执行完毕后,其活动对象也不会销毁,
因为匿名对象的作用域仍然在引用这个活动对象。换句话说:
当createComparisonFunction()函数返回后,其执行环境的作用域会被销毁,但它的活动对象仍然
会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

//创建函数var compareNames = createComparisonFunction("name");//调用函数var compare = createComparisonFunction ("name");//解除对匿名函数的引用(以便释放内存)compareNames = null;

通过将compareNames设置为null,解除对函数的引用,就等于通知垃圾回收例程将其清除。
随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也可以安全的销毁了。
上图为调用compareNames()的过程产生的作用域链之间的关系。

闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包保存的是整个变量对象而不是某个特殊的变量。

function createFunctions () {var result = new Array();for (var i = 0;i < 10;i++) {result[i] = function () {return i;};}return result;}var array = createFunctions ();for (var i = 0;i < array.length;i++) { alert (array[i]());//所有的结果都是10}array = null;

因为每个函数的作用域中都保存着createFunctions()函数的活动对象,所以它们引用的
都是同一个变量i。
我们可以通过创建另外一个匿名函数强制闭包的行为符合预期,

function createFunctions () {var result = new Array ();for (var i = 0;i < 10;i++) {result[i] = function (num) {return function () {return num;};}(i);}return result;}

我们定义一个匿名的函数,并立即执行该匿名函数的结果赋值给数组。
这里的匿名函数有一个参数num,也就是最终的函数要返回的值。
在调用哪个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,
所以就会将变量i的当前复制给参数num。而在这个匿名函数的内部,又
创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回
各自不同的数值了。
关于this对象
this对象在运行的时候是基于函数执行的环境绑定的:
在全局环境中this等于window,而函数被作为某个对象的方法调用时,
this等于那个对象。不过,匿名函数的执行环境具有全局性,因此this对象通常指向window。
但有的时候由于编写闭包的方式不同,可能不会那么的明显。

var name = "The Window";var object = {name:"myObject",getNameFunc:function () {return function () {return this.name;};}};
alert (object.getNameFunc()());//"The Window"

前面曾经提到过,每个函数在被调用的时候,其活动对象都会自动取得两个特殊的
变量:this和arguments。内部函数在搜索着两个变量时,只会搜索其活动对象为止,
因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象,保存在
一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

var name = "The Window";var object = {name:"myObject",getNameFunc:function () {var that = this;return function () {return that.name;};}};

内存的泄漏问题
由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程。
具体来说:如果闭包的作用域保存着一个HTML元素,那么就意味着该元素无法被销毁。
eg:
function assignHandler () {var element = document.getElementById("demo");element.onclick = function () {alert (element.id);};}assignHandler();

上面的代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。
由于匿名函保存了一个对assignHandler()活动对象的引用,因此就会导致无法减少element的引用数。
只要匿名函数存在,element的引用数至少为1,因此它所占用的内存就永远不会被回收,不过。这个问题就可以
通过稍微改写一些代码来解决。

function assignHandler () {var element = document.getElementById("demo");var id = element.id;element.onclick = function () {alert (id);};element = null;}assignHandler();

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。
但是做到这一步,还是不可以解决内存泄露的问题。必须记住:闭包会引用包含函数的整个活动对象,而其中包含
着element。即使闭包不直接引用element,包含函数的活动对象也仍然会保存一个引用。因此,有必要把element
变为null。这样就能够接触对DOM对象的引用,顺利的减少引用数,确保正常的回收器占用的内存。
模仿块级作用域。
JavaScript是没有块级作用域的概念。

function outputNumbers (count) {for (var i = 0;i < count;i++) {//......}alert(i);//可以访问到 10}outputNumbers(10);

匿名函数可以用来模仿块级作用域避免这个问题。
用作块级作用据的匿名函数的语法如下所示:

(function () {//这里是块级作用域 })();

以上定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示他实际上 是一个函数表达式。 无论什么时候,只要临时需要一些变量,就可以使用私有作用域。
 function outputNumbers (count) { (function(){ for (var i = 0;i < count;i++) { alert (i); } })(); alert(i);//error }




update:


模仿块级作用域
JavaScript 没有块级作用域的概念。

function box(count) {for (var i=0; i<count; i++) {}alert(i); //i 不会因为离开了for 块就失效}box(2);function box(count) {for (var i=0; i<count; i++) {}var i; //就算重新声明,也不会前面的值alert(i);}box(2);
以上两个例子,说明JavaScript 没有块级语句的作用域,if () {} for () {}等没有作用域,
如果有,出了这个范围i 就应该被销毁了。就算重新声明同一个变量也不会改变它的值。
JavaScript 不会提醒你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声
明视而不见(如果初始化了,当然还会执行的)。使用模仿块级作用域可避免这个问题。
//模仿块级作用域(私有作用域)(function () {//这里是块级作用域})();//使用块级作用域(私有作用域)改写function box(count) {(function () {for (var i = 0; i<count; i++) {}})();alert(i); //报错,无法访问}box(2);
使用了块级作用域(私有作用域)后,匿名函数中定义的任何变量,都会在执行结束时被
销毁。这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的
变量和函数。一般来说,我们都应该尽可能少向全局作用域中添加变量和函数。在大型项目
中,多人开发的时候,过多的全局变量和函数很容易导致命名冲突,引起灾难性的后果。如
果采用块级作用域(私有作用域),每个开发者既可以使用自己的变量,又不必担心搞乱全局
作用域。
(function () {var box = [1,2,3,4];alert(box); //box 出来就不认识了})();
在全局作用域中使用块级作用域可以减少闭包占用的内存问题,因为没有指向匿名函数
的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
JavaScript 没有私有属性的概念;所有的对象属性都是公有的。不过,却有一个私有变
量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问
这些变量。
function box() {var age = 100; //私有变量,外部无法访问}
而通过函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而
利用这一点,可以创建用于访问私有变量的公有方法。
function Box() {var age = 100; //私有变量function run() { //私有函数return '运行中...';}this.get = function () { //对外公共的特权方法return age + run();};}var box = new Box();alert(box.get());
可以通过构造方法传参来访问私有变量。
function Person(value) {var user = value; //这句其实可以省略this.getUser = function () {return user;};this.setUser = function (value) {user = value;};}
但是对象的方法,在多次调用的时候,会多次创建。可以使用静态私有变量来避免这个
问题。
静态私有变量
通过块级作用域(私有作用域)中定义私有变量或函数,同样可以创建对外公共的特权方
法。
(function () {var age = 100;function run() {return '运行中...';}Box = function () {}; //构造方法Box.prototype.go = function () { //原型方法return age + run();};})();var box = new Box();alert(box.go());
上面的对象声明,采用的是Box = function () {} 而不是function Box() {} 因为如果用后
面这种,就变成私有函数了,无法在全局访问到了,所以使用了前面这种。
(function () {var user = '';Person = function (value) {user = value;};Person.prototype.getUser = function () {return user;};Person.prototype.setUser = function (value) {user = value;}})();
使用了prototype 导致方法共享了,而user 也就变成静态属性了。(所谓静态属性,即共
享于不同对象中的属性)。
模块模式
之前采用的都是构造函数的方式来创建私有变量和特权方法。那么对象字面量方式就采
用模块模式来创建。
var box = { //字面量对象,也是单例对象age : 100, //这是公有属性,将要改成私有run : function () { //这时公有函数,将要改成私有return '运行中...';};};
私有化变量和函数:
var box = function () {var age = 100;function run() {return '运行中...';}return { //直接返回对象go : function () {return age + run();}};}();
上面的直接返回对象的例子,也可以这么写:
var box = function () {var age = 100;function run() {return '运行中...';}var obj = { //创建字面量对象go : function () {return age + run();}};return obj; //返回这个对象}();
字面量的对象声明,其实在设计模式中可以看作是一种单例模式,所谓单例模式,就是
永远保持对象的一个实例。
增强的模块模式,这种模式适合返回自定义对象,也就是构造函数。
function Desk() {};var box = function () {var age = 100;function run() {return '运行中...';}var desk = new Desk(); //可以实例化特定的对象desk.go = function () {return age + run();};return desk;}();alert(box.go());

原创粉丝点击