理解JS闭包

  闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它。因此,本文直接从代码角度来理解闭包。

1、什么是闭包?

  先看一段代码

function A(){
    function B(){
       console.log("Hello Closure!");
    }
    return B;
}
var C = A();
C();//Hello Closure!

  这是最简单的闭包。有了初步认识后,我们简单分析一下它和普通函数有什么不同,上面代码翻译成自然语言如下:
  (1)定义普通函数 A
  (2)在 A 中定义普通函数B
  (3)在 A 中返回 B
  (4)执行 A, 并把 A 的返回结果赋值给变量 C
  (5)执行 C
  把这5步操作总结成一句话就是:函数A的内部函数B被函数A外的一个变量C引用。

  把这句话再加工一下就变成了闭包的定义:当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。因此,当你执行上述5步操作时,就已经定义了一个闭包!

  另一段代码1

function a(){
  var n = 0;
  function inc() {
    n++;
    console.log(n);
  }
  inc(); 
  inc(); 
}
a(); //控制台输出1,再输出2

  变式代码2

function a(){
  var n = 0;
  this.inc = function () {
    n++; 
    console.log(n);
  };
}
var c = new a();
c.inc();  //控制台输出1
c.inc();  //控制台输出2

  这就是闭包!有权访问另一个函数作用域内变量的函数都是闭包。这里 inc 函数访问了构造函数 a 里面的变量 n,所以形成了一个闭包。

  再来看另一段代码3:

function counter(){
  var n = 0;
  function inc(){
    n++; 
    console.log(n);
  }
  return inc;//这里将函数对象返回
}
var c = counter();
c();  //控制台输出1
c();  //控制台输出2

  看看是怎么执行的:var c = couter(),这一句 couter()返回的是函数 inc,那这句等同于 var c = inc;至于c(),这一句等同于 inc(); 注意,函数名只是一个标识(指向函数的指针),而()才是执行函数。后面三句翻译过来就是: var c = inc; inc(); inc();这跟上一段代码有区别吗? 没有。

  为啥要这样写?我们知道,js的每个函数都是一个个小黑屋,它可以获取外界信息,但是外界却无法直接看到里面的内容。将变量 n 放进小黑屋里,除了 inc 函数之外,没有其他办法能接触到变量 n,而且在函数 counter 外定义同名的变量 n 也是互不影响的,这就是所谓的增强“封装性”。而之所以要用 return 返回函数标识 inc,是因为在 counter函数外部无法直接调用 inc 函数,所以 return inc 与外部联系起来,代码 2 中的 this 也是将 inc 与外部联系起来而已。

2、闭包的用途

  在了解闭包的作用之前,我们先了解一下 Javascript 中的GC机制: 在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象一直会保存在内存中。在上面给出的第一段代码示例中,B 定义在 A 中,因此 B 依赖于 A ,而外部变量 C 又引用了 B , 所以A间接的被 C 引用。也就是说,A 不会被 GC 回收,会一直保存在内存中。为了证明我们的推理,对第一个代码示例稍作改进:

function A(){
    var count = 0;
    function B(){
       count ++;
       console.log(count);
    }
    return B;
}
var C = A();
C();// 1
C();// 2
C();// 3

  count 是函数A 中的一个变量,它的值在函数B 中被改变,函数B 每执行一次,count 的值就在原来的基础上累加 1 。因此,函数A中的 count 变量会一直保存在内存中。当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会“污染”全局的变量时,就可以用闭包来定义这个模块。

3、编写闭包的常见陷阱

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
    console.log(funcs[i]());
}

  乍一看,以为输出 0~9 ,万万没想到输出10个10? 这里的陷阱就是:函数带()才是执行函数! 单纯的一句 var f = function() { alert(‘Hi’); }; 是不会弹窗的,后面接一句 f(); 才会执行函数内部的代码。上面代码翻译一下就是:

var result = new Array();
var i;
result[0] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
result[1] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
...
result[9] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
i = 10;
funcs = result;
result = null;

console.log(i); // funcs[0]()就是执行 return i 语句,就是返回10
console.log(i); // funcs[1]()就是执行 return i 语句,就是返回10
...
console.log(i); // funcs[9]()就是执行 return i 语句,就是返回10

  为什么只垃圾回收了result,但却不回收i呢? 因为 i还在被 function引用着啊。好比一个餐厅,盘子总是有限的,所以服务员会去巡台回收空盘子,但还装着菜的盘子他怎么敢收? 当然,你自己手动倒掉了盘子里面的菜(=null),那盘子就会被收走了,这就是所谓的内存回收机制。

  至于i的值怎么还能保留,其实从文章开头一路读下来,这应该没有什么可以纠结的地方。盘子里面的菜,吃了一块不就应该少一块吗?

  闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量,在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用private修饰一个成员变量,在没有class机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。这是优点也是缺点,不必要的闭包只会徒增内存消耗!另外使用闭包也要注意变量的值是否符合你的要求,因为他就像一个静态私有变量一样。闭包通常会跟很多东西混搭起来,接触多了才能加深理解。

  上面的闭包陷阱示例代码运行结果与预期不同的原因在于:返回的函数引用了循环变量i,但函数并非立刻执行,等到10个函数都返回时,它们所引用的变量i已经变成了10,因此最终结果为10

  返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变,具体将代码改进如下:

//改进代码1
function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = (function(n){
            return function(){
                return n;
            }
        })(i);
    }
    return result;
}

var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
    console.log(funcs[i]());
}

  注意这里用了一个“创建一个匿名函数并立刻执行”的语法:。

(function (x) {
    return x;
})(3); // 3

  理论上讲,创建一个匿名函数并立刻执行可以这么写:

function (x) { return x; } (3);

  但是由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义括起来:

(function (x) { return x * x }) (3);

  从控制台中可以看到返回的funcs数组中存放的是10个函数,后续的funcs[i]();语句才开始执行函数。

  执行下面这段改进代码2也能得到0-9的输出,但是与上面的改进代码1有本质不同:

//改进代码2
function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = (function(n){
            return i;
        })();
    }
    return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
    console.log(funcs[i]);
}

  在改进代码2 中,返回的funcs数组中存放的是10个Number,后续的funcs[i];语句只是访问数组中的Number操作。


参考资料:
1、http://www.cnblogs.com/qieguo/p/5457040.html
2、http://www.jb51.net/article/83524.htm

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值