到底该怎么去理解闭包?

今天看到了一段关于闭包的代码:

代码片段A:

!function(){
    var num=1;
    var exp={};
    function add(num){
        return num++;
    } 
    exp.getAddNum=function(){
        return add(num);
    }
    window.a=exp;
}()

console.log(a.getAddNum()); // 1
console.log(a.getAddNum()); // 1

代码片段B:

!function(){
    var num=1;
    var exp={};
    function add(){
        return num++;
    }
    exp.getAddNum=function(){
        return add();
    }
    window.a=exp;
}()

console.log(a.getAddNum());  // 1
console.log(a.getAddNum());  // 2

谁能解释下这2段代码的区别吗?考验大家基本功的时候到啦~~~~

阅读 7.1k
14 个回答

第一个里面是你传递进去的,他会使用当前作用域接收到的这个形参的值,它并没有去改变外层num的值,因此你每次用它来传递,值都是1。
而第二个的'add'方法中并没有num变量,他会通过作用域链找到外层的num,那么你这样调用时每次都是操作的外层变量的值,而这个值在你return之后是会累加的。

关于上下文和作用域链你可以看看这篇文章
图解Javascript上下文与作用域

与闭包密切相关的两个概念是作用域链和词法作用域。简单解释下:

作用域链:JavaScript不存在大括号级的作用域,但有函数作用域,在函数内声明的变量在函数外不可见,而在代码块内声明的变量在代码块外是可见的。同时,一个作用域可以访问其内部变量,也可以访问其父级作用域的变量,比如函数内可以访问全局变量。

词法作用域:在声明一个函数的时候就会创建该函数的作用域环境,当其被调用的时候,它可以访问其内部作用域的变量,以及其父级作用域的变量。即便变量是在其之后声明的,一样可以访问。因为记录的是作用域范围,而不是作用域内的具体变量名。

举个例子

var a,b;
var c = function(){
   a = function(){
      console.log(b);
   };
   var d = function(){
   };
};
c();
var b = 1;
a();//1
typeof d;//undefined

全局函数a的声明是在全局作用域,b的赋值虽然在其后,但仍然可以访问。而局部函数d的声明则是在函数c的局部作用域里面,因此在全局作用域不能访问。

这时候闭包的作用就出来了!

来看一个最简单的代码

var e = (function(){
   var f = 1;
   return function(){
      console.log(f);
   }
})();
e();//1
typeof f;//undefined

变量f虽然是在闭包的局部作用域里,但由于e引用到了返回的匿名函数,而这个匿名函数处在变量f所在的作用域,可以访问之,因此在全局作用域里仍然可以得到f的值。

所以,闭包能够有效地减少对全局变量的依赖,并且保护局部变量(不能直接访问局部变量),同时延续局部变量寿命(在上面例子中,变量e保留了对返回的匿名函数的引用,因而其作用域没有在自调函数运行结束后被回收,变量f也就延续了寿命)。

好哒,最后我们来看一看,闭包如何面向对象进行设计。

闭包可以模仿其他面向对象语言的private和public。来举个PHP的例子吧:

<?php
class myClass(){
   private $name;
   function __construct($name){
      $this->name = $name;
   }
   function setName($name){
      $this->name = $name;
   }
   function getName(){
      return $this->name;
   }
}
$myName = new myName('defaultName');
$myName->setName('HaoyCn');
echo $myName->getName();
?>

那如果是在JavaScript里面我们怎么写呢?

第一,可以直接创建对象

var myName = {
   name: 'defaultName',
   setName: function(name){
      this.name = name;
   },
   getName: function(){
      return this.name;
   }
};
myName.setName('HaoyCn');
console.log(myName.getName());

第二,使用构造器

var MyName = function(){
   this.name = 'defaultName';
};
MyName.prototype.setName = function(name){
   this.name = name;
};
MyName.prototype.getName = function(){
   return this.name;
};
var myName = new MyName('defaultName');
myName.setName('HaoyCn');
console.log(myName.getName());

以上是使用JavaScript来设计对象,但name作为对象的属性,在JavaScript里面是完全暴露的,我们并不想这样。那怎么办?

var MyName = function(defaultName){
   var name = defaultName;
   return {
      setName: function(newName){
         name = newName;
      },
      getName: function(){
         return name;
      }
   };
};
var myName = MyName('defaultName');
myName.setName('HaoyCn');
console.log(myName.getName());

这样,name变量就完成了对private的模拟,同时返回了具有Getter和Setter作用的对象,可以操作name变量。

这么跟你讲,一个养猪场里有一个火腿肠加工厂,猪送进去进行加工最后运出火腿肠,这个加工过程不会对猪场的其他猪产生影响,这个加工厂就叫做 闭! 包!

片段A里add(num)里操作的是自己的参数……每次调用的时候都是add(1),当然返回也是1.
片段B里add()里操作的才是闭包里那个变量num

你把A里的函数改成

function add(param){
    return param++
}

就好理解了

这都是不合理的变量命名造成的混淆……不在于对闭包的理解

红皮书上有一章讲函数的参数是值传递去看下会有帮助

第一个例子中的add方法将num作为形参传递进方法,在该方法的内部实际是该变量的一个副本,因此自增时是不会改变外部num的值的,所以每次调用的结果都为1。 第二个例子中没有将num传递进add方法,那么每次调用add时会从上级的作用域中去寻找该变量。因此add方法内num自增时也就会对外部的num值修改了。

第一个例子中的add方法有混淆视听的作用,add中的参数就是一个形参,还定义成num,迷惑人。

新手上路,请多包涵
function add(num){
    return num++;
} 
exp.getAddNum=function(){
    return add(num);
}

每次调用exp.getAddNum(),便调用了add(num)方法,此时传进add()方法的num是一个参数,所以在add中的num++操作只针对add()函数内部的num参数,外面的num是不受影响的

我只想说,这个例子太啰嗦了,要是想对比,下面这样就足够了

var num=1;
function x(){
  return num++;
}

上面的代码对应例子A

var num = 1;
function x(num){
  return num++;
}

上面的代码对应例子B

运行代码还是

console.log(x());
console.log(x());

在第一段代码中,首先var num = 1为值类型,在调用add(num)时中的实参num复制了var num = 1的值,然后将值传递给定义函数时的命名参数num,在函数调用运行时,这个命名参数num是作为此函数的局部变量运行。
在第二段代码中在调用add()时,此时的var num = 1是作为嵌套函数add外部作用域的变量被使用,形成了闭包,因此结果被累加。
可以看看我的这篇文章,执行环境与作用域链以及函数执行

这是理解闭包的前提

关于函数参数传递可以看看这篇文章js中值的访问与参数传递的问题

新手上路,请多包涵

变量定义时形成对应的ao对象(活动对象),例子2 函数add形成一个对象ao = { num:1,function... } 这里的num值来自于var定义的值,但是和上文sum没有任何关系 是新建的ao对象中的元素,所以他的++不影响上文,而例子1中参数来自于上文var 值则一直取得是var定义的num 来形成ao,即每一次新形成的ao里的num刚加了1 就被新形成的ao中的num给替换了。

新手上路,请多包涵

问题的关键是:函数调用传参时,基本类型是按值传递的,也就是说 num 会被复制一份,实际上 return num++ 的 num 并不是 var num = 1 的 num,而是它的一个副本。

闭包最直观的理解,函数里面调用函数。改变执行环境,这个执行环境只有里面的函数才能访问到,外面的想访问他必须通过这个外部函数作为桥梁。就是变量没被销毁而是存下来,更直观点变量作用域是由声明的时候决定多,与执行过程无关。

推荐问题
宣传栏