FED实验室 - 专注WEB端开发和用户体验

深入javascript(二):闭包

JAVASCRIPT 煦涵 2376℃ 0评论

闭包通常被视作JavaScript的高级特性,但是,理解闭包对于掌握这门语言至关重要。

一、闭包的定义

1.闭包:
最简单的描述,即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数及其他内部函数。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
 2.闭包形成:
当其中的一个内部函数在其外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。
3.在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

二、闭包的应用

1.函数外部读取函数的局部变量,如下例:

function A (){
	var str = "Benjamin";
	function B(){
		return str;
	}
	return B;
}
var b = A();

此时b就是一个闭包,由B函数和闭包创建时存在的"Benjamin"字符串形成。

console.log(b());//Benjamin
console.log(A()());//Benjamin

2.让这些变量的值始终保持在内存中。
例一:

function A (){
    var i = 0;
    function B (){
        return i += 1;
    }
    return B;
}
console.log(A()());//1
console.log(A()());//1
console.log(A()());//1

例二:(形成闭包)

function A (){
	var i = 0;
	function B (){
		return i += 1;
	}
	return B;
}
var b = A();

console.log(b());//1
console.log(b());//2
console.log(b());//3

例三:

function adder (x){
	return function(){
		return x + y;
	}
}
var adder5 = adder(5);
var adder10 = adder(10);

console.log(adder5(10));//15
console.log(adder10(10));//30

从本质上讲,adder 是一个函数工厂 — 创建将指定的值和它的参数求和的函数,在上例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5求和,另一个和 10 求和。
adder5和adder10都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在 adder5 的环境中,x 为 5。而在 adder10 中,x 则为 10。

3.为函数引用设置延时

闭包的一个常见用法是在执行函数之前为要执行的函数提供参数。例如:将函数作为 setTimout 函数第一个参数传参的问题;
we all know ,如果第一个参数不需要传参,我们直接传入函数名即可。需要传参的情况下就要考虑使用闭包了。代码如下:

function A (a,b){
	console.log(a+b);	
	setTimeout(function(){
		A(10,20)
	},1000);
}
A(10,20);

4.闭包模拟私有方法(计数器)

var counter = function (){
	//私有属性和方法
	var 
	i       = 0,
	_change = function(step){
		i += step;
	}
	//公有方法
	return 	{
		increment : function(){
			_change(1);
		},
		decrement : function(){
			_change(-1);
		},
		getValue  : function(){
			return i;
		}
	};
};
var counter1 = counter();
var counter2 = counter();
counter1.increment();
counter1.increment();
console.log(counter1.getValue());//2
counter1.decrement();
console.log(counter1.getValue());//1
counter2.increment();
console.log(counter2.getValue());//1

请注意两个计数器是如何维护它们各自的独立性的。每次调用 counter() 函数期间,其环境是不同的。每次调用中,i中含有不同的实例。
这种形式的闭包提供了许多通常由面向对象编程所享有的益处,尤其是数据隐藏和封装。

5.循环中创建闭包(常见错误)

function bindFocus (){
	var item = [{
		"type":"email",
		"message":"Please input your E-mail"
	},{
		"type":"age",
		"message":"Please input your age"
	},{
		"type":"address",
		"message":"Please input your address"
	}];

	for(var i = 0, ilen = item.length; i < ilen ; i++){
		var 
		itemi   = item[i],
		typeobj = document.getElementById(itemi.type);
		typeobj.onfocus = function(){
			this.value = itemi.message;
		}
		typeobj.onblur = function(){
			this.value = "";
		}
	}
}
bindFocus();

//运行上面代码后我们发现,无论鼠标移动到哪个input上面,其文本值显示的都是"Please input your address"。
该问题的原因在于赋给 onfocus 的函数是闭包;它们由函数定义和记录自 bindFocus 函数作用域的环境构成。一共创建了三个闭包,但是它们都共享同一个环境。在 onfocus 的回调被执行时,循环早已经完成,且此时 itemi 变量(由所有三个闭包所共享)已经指向了 item 列表中的最后一项。

如果解决此问题呢?
使用更多的闭包,代码如下:

function bindFocus (){
	var item = [{
		"type":"email",
		"message":"Please input your E-mail"
	},{
		"type":"age",
		"message":"Please input your age"
	},{
		"type":"address",
		"message":"Please input your address"
	}];
	function B(itemi){
		return function (){
			this.value = itemi.message;
		}
	}
	for(var i = 0, ilen = item.length; i < ilen ; i++){
		var 
		itemi   = item[i],
		typeobj = document.getElementById(itemi.type);
		typeobj.onfocus = B(itemi);
		typeobj.onblur = function (){
			this.value = "";
		};
	}
}
bindFocus();

所有的回调不再共享同一个环境,B函数为每一个回调创建一个新的环境。在这些环境中,itemi 指向 item 数组中对应的字符串。

三、闭包的作用域链

1.函数对象的[[scope]]属性、ScopeChain(作用域链)
javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。
2.Execution Context(运行期上下文)、Activation Object(活动对象)

a.函数被创建时:
函数所在的全局作用域的全局对象被放置到函数的作用域链([[scope]]属性)中。此时作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this,window,document以及全局对象中的函数。这也就是我们可以在全局作用域下的函数中访问window(this),访问全局变量,访问函数自身的原因。当然还有函数作用域不是全局的情况。

b.函数开始执行时:
就会创建一个Execution Context的内部对象,该对象定义了函数运行时的作用域环境(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)。Execution Context对象有自己的作用域链,在Execution Context创建时初始化,会将函数创建时的作用域链对象[[scope]]中的全部内容按照在[[scope]]作用域链中的顺序复制到Execution Context的作用域链中。此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做Activation Object(活动对象),这个活动对象保存了函数中的所有命名参数,局部变量,arguments(参数集合),this指针等函数内部的数据情况,这个Activation Object是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因)。

c.函数在运行过程中:
每遇到一个变量,都会去Execution Context的作用域链中从上到下(0->1 => 活动对象->全局对象)依次搜索,如果在第一个作用域链(假如是Activation Object,因为with及try-catch的catch子句,可以在函数运行时临时改变函数运行期上下文的作用域链,此时一个新的对象被创建,并插入到作用域链的前端)中找到了,那么就返回这个变量,如果没有找到,那么继续向下查找,直到找到为止,这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量。所以标识符所处的位置越深,读取它的速度越慢。

d.通过一个实例来分析闭包的形成过程

function submitForm(id){

}
function A(){
	var id = "Benjamin";
	function B(){
		submitForm(id);
	}
	document.getElementById("submit").onclick = B;
}

因为闭包函数B是在函数A执行时被解析的,所以我们来看看函数A执行时的,闭包的解析。
1)A执行时,B解析的情况:
当A函数中执行到闭包时,javascript引擎发现了B的存在,像A函数解析一样,将B解析,为B函数对象创建[[scope]]属性,初始化作用域链(此时B函数对象的作用域链中有两个对象(A函数执行时的Activation Object和全局对象)。

此时B对象的作用域链和A函数的执行上下文作用域链是相同的,为什么呢?因为B是在A函数执行的过程中被发现并且解析的,而A函数执行时的作用域是Activation Object,那么结果就很明显了,B被解析的时候它的作用域正是A作用域链中的第一个作用域对象Activation Object,当然,由于作用域链的关系,全局对象作用域也被引入到B的作用域链中。

2)B执行时:
当闭包B执行时,一个运行期上下文被创建,它的作用域链与[[Scope]]属性中引用的两个相同的作用域链被同时初始化,然后一个新的活动对象为闭包自身创建。此时闭包作用域链从上到下一次为:0->闭包B的活动对象,1->引用的A活动对象,2->全局对象。
此时我们发现闭包使用id和submitForm,id在1上,sumitForm在2上。这也就是闭包性能的关注点。
因此提高性能:设置缓存,将数据保存在局部变量中。

e.注意:
1)对函数的每次运行而言,每个运行期上下文都是独一的,所以多次调用同一个函数会多次创建运行期上下文,函数执行完毕,运行期上下文就会被销毁。
2)同一个父环境创建的闭包是共用一个[[scope]]属性的。也就是说,某个闭包对其中[[scope]]的变量的修改会影响到其他闭包对其变量的读取。因为闭包作用域链中的Activation Object,引用了父函数的Activation Object .

四、闭包的缺点

闭包有一个非常严重的问题,那就是内存浪费问题,这个内存浪费不仅仅因为它常驻内存,更重要的是,对闭包的使用不当会造成无效内存的产生。

参考链接:
1.http://blog.csdn.net/pusongyang/article/details/8720654

2.JavaScriptClosures

3.https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures

下面是「FED实验室」的微信公众号二维码,欢迎扫描关注:

FED实验室

行文不易,如有帮助,欢迎打赏!

赞赏支持or喜欢 (1)or分享 (0)
捐赠共勉
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址