javascript—闭包

本文详细解析了JavaScript中闭包的概念及其应用场景,并探讨了闭包可能导致的问题,如内存泄漏及性能影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们先来思考一个问题:怎么读取一个函数里的私有变量?

众所周知,javascript具有函数作用域,也即是说函数内部可以读取外部的变量,但外部是无法读取函数内部的私有变量的。因此如果我们在这个函数(暂且叫做outer)里再添加一个内部函数innerinner就能读取到outer的私有变量。如果我们再把inner当作返回值返回,不就相当于间接得到了outer的私有变量了嘛!而事实上,这就是闭包。

官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包的特点: 
  1.作为一个函数变量的一个引用,当函数返回时,其处于激活状态。 
  2.一个闭包就是当一个函数返回时,一个没有释放资源的栈区。

这样的解释有点抽象,从狭义上来讲其实可以理解为 “一个函数引用了另一个函数作用域里的变量”。

我们来看下面的例子:

  1. function closure(){
  2. var name = "enm";
  3. return {
  4. getStr:function(){
  5. return name;
  6. }
  7. }
  8. }
  9. var builder = new closure();
  10. builder.name;//undefined
  11. name;//undefined,这里是访问不了function的私有变量的
  12. console.log(builder.getStr()); //返回了enm

上面构造了一个闭包,这个闭包都维持着对外部作用域的引用,因此不管在哪调用总是能够访问函数中的变量。在一个函数内部定义的函数,会将外部函数的活跃对象添加到自己的作用域链中,因此上面实例中通过内部函数能够访问外部函数的属性,这也是javascript模拟私有变量的一种方式。


闭包经典问题

我们来看看下面经典的例子,相信任何一位初学者在接触闭包时都会遇到这个问题:


  1. function timeManage() {
  2. for (var i = 0; i < 5; i++) {
  3. setTimeout(function() {
  4. console.log(i);
  5. },1000)
  6. };
  7. }
  8.  

上面的程序并没有按照我们预期的输入1-5的数字,而是5次全部输出了5。我们先来看解决办法:

  1. function timeManage() {
  2. for (var i = 0; i < 5; i++) {
  3. (function(num) {
  4. setTimeout(function() {
  5. console.log(num);
  6. }, 1000);
  7. })(i);
  8. }
  9. }

或者在闭包匿名函数中再返回一个匿名函数赋值:

  1. function timeManage() {
  2. for (var i = 0; i < 10; i++) {
  3. setTimeout((function(e) {
  4. return function() {
  5. console.log(e);
  6. }
  7. })(i), 1000)
  8. }
  9. }
  10. //timeManager();输出1,2,3,4,5

我再尝试另一种方法,我把它叫做“消除延迟方法”:

  1. function timeManage() {
  2. function foo() {
  3. console.log(i);
  4. }
  5. for (var i = 0; i < 5; i++) {
  6. foo()
  7. };
  8. }
  9. timeManage();//输出1,2,3,4,5

继续看另一个例子

  1. function createClosure(){
  2. var result = [];
  3. for (var i = 0; i < 5; i++) {
  4. result[i] = function(){
  5. return i;
  6. }
  7. }
  8. return result;
  9. }


调用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。原因我们最后再说,因为现在我自己的理解跟网上其他人的理解有出入。我们先来看看怎么解决。


  1.  
  2. function createClosure() {
  3. var result = [];
  4. for (var i = 0; i < 5; i++) {
  5. result[i] = function(num) {
  6. return function() {
  7. console.log(num);
  8. }
  9. }(i);
  10. }
  11. return result;
  12. }
  13. //createClosure()[1]()输出1;createClosure()[2]()输出2

再使用我自己的“消除延迟”方法看看

  1. function createClosure(){
  2. var result = [];
  3. for (var i = 0; i < 5; i++) {
  4. result[i] = (function(){
  5. return i;
  6. })();
  7. }
  8. return result;
  9. }
  10. var w = new createClosure();
  11. w; //成功输出[0,1,2,3,4]

以上提供的解决办法,无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。而第三种方法消除延迟则是先定义函数,或者定义立即执行函数,然后在for循环中直接调用函数,这也是一个闭包,也依然保持对外部变量i的访问,但是不会出现我们说的问题

  1. function createClosure(){
  2. var result = [];
  3. for (var i = 0; i < 5; i++) {
  4. result[i] = function(){
  5. return i;
  6. };
  7. }
  8. return result;
  9. }
  10. var w = new createClosure();
  11. w; //输出 [object Array][function, function, function, function, function]
  12. w[0](); //5

这里就很明显了,我们把函数表达式直接赋值给result[i],此时函数并没有执行,看看我们控制台输出的结果就知道了,w只是一个包含了函数表达式的Array,而不是具体数值。而等到我们调用w[0]()的时候,函数才开始执行计算,此时再调用外部的变量i(循环已完毕,i == 5),所以w[0]()为5。到此,该问题就解决了。

事实上,这个例子的本意是用来说明闭包保持对外部函数变量的引用(reference),而不是复制值(copy)。因此当外部变量改变时,内部函数的引用也会跟着改变。但是,这个例子很容易让人迷糊,让初学者摸不着头脑,以为闭包非常的magic,以为是闭包的奇异功能导致了这个奇怪现象的发生。所以,有人解释出现这个问题的原因是由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这样说没有错,因为你执行完循环再调用函数,函数引用同一个i,那么结果肯定是最后修改的值5。但这样解释会让这个问题变得迷糊,特别是对初学者而言,他们会混乱,会觉得闭包很深奥很不可理解。因为其实本质上这个例子是通过setTimeout函数故意延迟了函数的执行,通过我第三种解决方法可以看出,假如每一次循环都能立即执行函数,那么是完全可以输出每一个i的正确值的。也就是说,其实这个问题的容易让人迷糊的原因是函数没有立即执行,而不能说是闭包产生的结果。


闭包中的this

闭包中的this 在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:

  1. var scope = "global";
  2. var object = {
  3. scope:"local",
  4. getScope:function(){
  5. return function(){
  6. return this.scope;
  7. }
  8. }
  9. }

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。 幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:

  1. var scope = "global";
  2. var object = {
  3. scope:"local",
  4. getScope:function(){
  5. var that = this;
  6. return function(){
  7. return that.scope;
  8. }
  9. }
  10. }

object.getScope()()返回值为local。

内存与性能 由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:

  1. function bindEvent(){
  2. var target = document.getElementById("elem");
  3. target.onclick = function(){
  4. console.log(target.name);
  5. }
  6. }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:

  1. function bindEvent(){
  2. var target = document.getElementById("elem");
  3. var name = target.name;
  4. target.onclick = function(){
  5. console.log(name);
  6. }
  7. target = null;
  8. }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值