在Javascript没有块级作用域,一般是为了给某个函数申明一些只有该函数才能使用的局部变量,会使用到闭包。那什么是闭包?
一、什么是闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数。也就是说,当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。
二、闭包的代价
每个函数的执行,都会创建一个与该函数相关的函数执行环境,或者说函数执行上下文,这个执行上下文中有一个属性scope chain(作用域链指针),这个指针指向一个作用域链结构,作用域链的指针又指向各个作用域对于的活动对象。正常情况,一个函数在调用开始执行时创建这个而函数执行上下文及相应的作用域链,在函数执行结束后释放函数执行上下文及相应作用域链所占的空间。
但是由于闭包函数可以访问外层函数的变量,所以外层函数在执行结束后,其作用域活动对象并不会被释放(而外层函数执行结束后,执行环境和对应的作用域链就会被销毁),而是被闭包函数的作用域链所占用,直到闭包函数被销毁后,外层函数的作用域活动对象才会被销毁,这也就是闭包的代价:内存的占用。
例子:
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; } }; } var compare = createComparisonFunction("name"); var result = compare({name:"Nicholas"},{name:"Greg"}); console.log(result);
结果:
下图为上例过程中产生的作用域链之间的关系。
在16行,createComparisonFunction()方法执行完后,作用域链变化为:
由图可知,在createComparisonFunction执行完后,其执行环境和作用域链都会被销毁,但是由于匿名函数的作用域链仍然在引用这个活动对象,所以createComparisonFunction()的活动对象仍然存在内存中。直到匿名函数被摧毁,createComparisonFunction()的活动对象才会被摧毁。
三、闭包的副作用
先来一个例子:
function createFunction(){ var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function(){ result i; }; } return result; } console.log(createFunction()[0]());
结果:
这段代码本意是希望每个函数都返回自己的索引值,即位置0的函数返回0,以此类推。但结果所示,每个函数都返回10,这是什么原因呢?
如上图所示,闭包函数在每次调用时都会产生自己的作用域活动对象,也能访问到外层函数的变量i,但是i只是外层函数作用域的活动对象的一个属性,在每次for的时候,它已经改变了,所以闭包函数只能访问i的最终值。
改进:
function createFunction(){ var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function(num){ return function(){ return num; } }(i); } return result; } console.log(createFunction()[0]());
结果:
闭包副作用避免的原因在于匿名函数有一个参数num,调用匿名函数时传入了变量i,变量i的值就会被复制给参数num,匿名函数内部又创建了访问num的闭包,这样就可以返回各自不同的数值。如下图所示:
四、经典的闭包面试题
function fun(n,o){ console.log(o); return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3); var b = fun(0).fun(1).fun(2).fun(3); var c = fun(0).fun(1); c.fun(2); c.fun(3);
结果是什么呢?一个个看下:
- 首先是:var a = fun(0),此时输出o,由于只传入了一个参数,所以参数2 o为undefined。
- a被赋值为一个对象,一个拥有fun函数的对象。
- a.fun(1),调用a对象的fun()函数,有一点比较重要的是,在对象中的fun函数中又调用了一个fun函数,那么最里边的fun函数到底是对象的fun函数呢还是全局的fun函数呢?(暂且称对象的fun函数为obj_fun)
- 众所周知,创建函数有几种方式,几种最常见的是声明函数、创建匿名函数表达式以及创建具名函数表达式,而题中对象里用的是匿名函数表达式,所以就是将一个匿名函数的引用赋值给了obj_fun,所以obj_fun方法在var a = fun(0)执行完后,只存在于a对象中,而a对象存在于全局变量对象中,而fun函数直接存在于全局变量对象中。所以如果想在obj_fun中调用obj_fun对象,可以this.obj_fun()就可以调用了。
- 可以进行测试下:
-
var obj = { fun:function(i){ if(i==1) return 1; else{ return fun(i-1)*i; } } }; console.log(obj.fun(4));
这个例子非常简单,就是调用对象中的fun函数,计算4的阶乘,可是结果是:。如果改为return this.fun(i-1)*i就对啦~
- 所以a.fun(1)、a.fun(2)、a.fun(3)都是调用最外边的fun函数,所以输出0。
- var b = fun(0).fun(1).fun(2).fun(3);
- fun(0)为返回的具有fun函数的对象1,先输出undefined。
- fun(0).fun(1)调用这个对象1的fun函数,fun(0).fun(1)为外部fun(1,0)函数返回的对象2,输出0。
- fun(0).fun(1).fun(2)调用对象2的fun函数,fun(0).fun(1).fun(2)为外部fun(2,1)函数返回的对象3,输出1。
- fun(0).fun(1).fun(2).fun(3)调用对象3的fun函数,fun(0).fun(1).fun(2).fun(3)为外部fun(3,2)函数返回的对象4,输出2。
- var c = fun(0).fun(1);
- fun(0)为返回的具有fun函数的对象1,先输出undefined。
- fun(0).fun(1)为调用这个对象1的fun函数,c为外部fun(1,0)函数返回的对象2,输出0。
- c.fun(2)调用c对象的fun函数,c.fun(2)为外部fun(2,1)函数返回的对象3,输出1。
- c.fun(3)调用c对象的fun函数,c.fun(3)为外部fun(3,1)函数返回的对象4,输出1。
结果: