最近面试一家公司问到闭包,确实不知,现在恶补一下.
作用域链
在理解闭包以前.最好能先理解一下作用域链的含义,简单来说,作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止.当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined.
内存回收机制
一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了.对应的内存空间也就被回收了.下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用.但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题.如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收.
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。
也就是说,有了闭包,嵌套的函数结构才可以运作,这也是符合我们的预期的.然后,闭包还有一些特性,却往往让程序员觉得很难理解.
什么是闭包
闭包(closure),是javascript语言的一个难点,也是它的特色,很多高级的应该依靠闭包来实现的。
概念
一个拥有许多变量和绑定了这些变量的环境表达式,通常是一个函数。即Javascript允许使用内部函数,函数定义和表达式位于另一个函数体内。而且这些内部函数可以访问它所在的外部函数的所有变量,参数和声明的其它内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
}
};
alert(object.getNameFunc()()); //The Window
这个不是闭包
function outerFun() {
var a = 0;
function innerFun() {
a++;
alert(a);
}
}
innerFun();//.innerFun()的作用域在outerFun()内部,所在outerFun()外部调用它是错误的.
改成如下,也就是闭包
function outerFun() {
var a = 0;
function innerFun() {
a++;
alert(a);
}
return innerFun; //注意这里
}
var obj = outerFun();
obj(); //结果为1
obj(); //结果为2
var obj2 = outerFun();
obj2(); //结果为1
obj2(); //结果为2
当内部函数在它的作用域的外部被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被释放,因为闭包需要它们.
出于种种原因,我们有时候需要得到函数内的局部变量。但是,正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
function f1() {
var n = 999;
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),
子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了
var result = [];
function foo() {
var i = 0;
for (; i < 3; i = i + 1) {
result[i] = function () {
alert(i)
}
}
}
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3
这段代码中,程序员希望foo函数中的变量i被内部循环的函数使用,并且能分别获得他们的索引,而实际上,只能获得该变量最后保留的值,也就是说.闭包中所记录的自由变量,只是对这个变量的一个引用,而非变量的值,当这个变量被改变了,闭包里获取到的变量值,也会被改变.
解决的方法之一,是让内部函数在循环创建的时候立即执行,并且捕捉当前的索引值,然后记录在自己的一个本地变量里.然后利用返回函数的方法,重写内部函数,让下一次调用的时候,返回本地变量的值,改进后的代码:
var result = [];
function foo() {
var i = 0;
for (; i < 3; i = i + 1) {
result[i] = (function (j) {
return function () {
alert(j);
};
})(i);//匿名函数和返回函数
}
}
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2
用法
-
用法1
function Circle(r) { this.r = r; } Circle.PI = 3.14159; Circle.prototype.area = function () { return Circle.PI * this.r * this.r;//函数内部可以直接读取全局变量 } var c = new Circle(2.0); console.debug(c.area()); //12.56636
-
用法2
var Circle = function () {
var obj = new Object();
obj.PI = 3.14159;
obj.area = function (r) {
return this.PI * r * r;
}
return obj;
}
var c = new Circle();
console.debug(c.area(2)); //12.56636 -
用法3
var Circle = new Object();
Circle.PI = 3.14159;
Circle.area = function (r) {
return this.PI * r * r;
};
console.debug(Circle.area(2)); //12.56636 -
用法4
var Circle = {
"PI": 3.14159,
"area": function (r) {
return this.PI * r * r;
}
};console.debug(Circle.area(2)); //12.56636
-
用法5
var Circle = new Function("this.PI = 3.14159;this.area = function( r ) {return rrthis.PI;}");
console.debug(Circle.area(2)); //我本机并不能工作
闭包有什么用
它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function f1() {
var n = 999;
nAdd = function () {
n += 1
};
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个
匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
在函数作用域内 加var定义的变量是局部变量,不加var定义的就成了全局变量
在全局作用域下,使用var定义的变量不可以delete,没有var 定义的变量可以delete.也就说明隐含全局变量严格来说不是真正的变量,而是全局对象的属性,因为属性可以通过delete删除,而变量不可以。
使用var 定义变量还会提升变量声明,即使用var定义:
function test(){ console.log(a); var a = 'hello world'; } test() //undefined
不使用var定义:
function test(){ console.log(a); a = 'hello world'; } test() // 'a is not defined'
这就是使用var定义的变量的声明提前。
在ES5的'use strict' 模式下,如果变量没有使用var定义,就会报错
闭包的缺点是什么
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便
改变父函数内部变量的值。
学习总结:
参考:
- javascript深入理解js闭包 http://www.jb51.net/article/24101.htm
- 学习Javascript闭包(Closure)
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html - 全面理解Javascript闭包和闭包的几种写法及用途
http://www.2cto.com/kf/201410/342609.html - [简单说说我理解的js中的闭包]
http://www.cnblogs.com/xhk-yjp/archive/2012/08/29/xhk-yjp-jsbb.html - JS闭包的理解http://blog.sina.com.cn/s/blog_65c2ec5e0101degi.html
- JS的闭包和它的代价
http://bbs.blueidea.com/thread-2862562-1-1.html