再谈对闭包的理解

闭包只是为了实现词法作用域而用到的一种数据结构而已

先从阮一峰09年写的一篇关于闭包的文章开始(原文地址)文中说”可以把闭包理解为就是能够读取其他函数内部变量的函数,因为js中,只有函数内部的子函数才能读取局部变量,因此也可以把闭包定义在一个函数内部的函数。所以闭包本质就是将函数内部和函数外部连接起来的一座桥梁”。

毕竟不是专业学习js的,也不是程序语言方面的专家,在这里就不去计较了,下文会给出更完整的闭包说明。(PS:个人还是比较欣赏阮一峰写的文章的,能将一些概念讲得通俗易懂)

最后还留下了一个思考题

  • 示例01:

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          return function(){
            return this.name;
          };
        }
      };
      alert(object.getNameFunc()());
  • 示例02:

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          var that = this;
          return function(){
            return that.name;
          };
        }
      };
      alert(object.getNameFunc()());

看完这道题,就希望大家将this和闭包分开,不要给自己找麻烦?

在开始说闭包之前,需要理解好以下的概念:

  • 词法作用域(静态作用域)
  • 函数上下文

之前简单的提过,词法作用域简单的理解就是函数的上下文是在声明是确定的,而不是在调用时确定的。这里不想对这两个名词作过多的解释,我们知道js/ruby/python等主流的语言都是词法作用域就好,因为与之相对的动态作用域有许多的问题,所以现在的语言基本都是词法作用域的。

函数上下文就简单的理解为函数执行的环境好了,在这个环境中保存了函数执行所需的变量。

JavaScript权威指南第六版关于闭包的说明

JavaScript采用词法作用域,也就是说函数的执行依赖于变量的作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为”闭包”。

当定义一个函数时,它实际上保存了一个作用域链。当调用这个函数时,它创建
一个新的对象来存储它的局部变量,并将这个对象添加到保存的作用域链上。

(闭包可以简单的理解为函数用来存储它的局部变量的对象,这个对象我们来说是不可见的,是js解释器实现的过程中才会用到的。每一个函数都会有这样的一个对象,作用域链则是这些对象之间的关系。)

“闭包”这个术语的来源:指函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量”包裹”了起来。

看文字可能会比较绕,下面是书中的一个例子:

var scope = "global scope"; /*全局变量*/
function checkscope(){
var scope = "local scope"; /*局部变量*/
function f(){return scope;}
return f();
}
checkscope(); /*=>"local scope"*/
var scope = "global scope"; /*全局变量*/
function checkscope(){
var scope = "local scope"; /*局部变量*/
function f(){return scope;}
return f;
}
checkscope()(); /*=>"local scope"*/

checkscope最后的返回值都是一样的,即”local scope”。

上面两个例子的不同之处就是函数f执行的地方不同,一个在checkscope这个作用域里调用的(每一个函数都是一个作用域),一个在全局作用域里调用的。回忆之前说的,函数调用时的上下文或者说作用域是在声明时确定的,所以与调用的位置无关,即f函数的调用位置虽然不同,调用的环境虽然不同,但最终的结果都是一样的。

说到这里,闭包就说得差不多了,回过头来看一下常规的对于闭包的理解:

通过返回函数的形式取得函数的局部变量。

这种说法本身没有错,但它只是闭包的一种表现形式,

比如将上例进行下更改:

var scope = "global scope"; /*全局变量*/
function checkscope(fn){
var scope = "local scope"; /*局部变量*/
function f(){return scope;}
fn(f);
}
checkscope(function(func){
var scope = "func scope";
return func();
}); /*=>"local scope"*/

改成类似回调的执行方式,结果还是一样的,注意结果并不是func scope,但是并没有返回f函数这一说,难道这就不是闭包了吗?(当然这里有点扣字眼)

复杂点的闭包题

真不知道按照前面那种简单的理解该怎么来理解这道题,但是如果理解了闭包的实质,那么就会发现其实很简单。

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); /*undefined,?,?,?*/
var b = fun(0).fun(1).fun(2).fun(3); /*undefined,?,?,?*/
var c = fun(0).fun(1); c.fun(2); c.fun(3); /*undefined,?,?,?*/
/*
* 问:三行a,b,c的输出分别是什么?
* 提示:形参也属于局部变量,包含在作用域中
*/

这里就不给出答案了,自行运行即可知道结果。

说说this

想起最开始时的那个思考题了吗?与闭包就没什么关系(注:任何一个函数其实都用到了闭包,但我们暂且考虑两层以及两层以上的嵌套情况,未嵌套情况下因为使用的都是全局作用域,结果应该是很直观的)。this一般用来代表函数的调用对象,它和上下文对象并不是同一个,上下文对象对我们来说是不可见的,除了全局作用域。

  • 示例01:

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          return function(){
            return this.name;
          };
        }
      };
      alert(object.getNameFunc()()); /*The Window*/
  • 示例02:

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          var that = this;
          return function(){
            return that.name;
          };
        }
      };
    var that = {name: 'xiaofu'}; /*干扰项*/
      alert(object.getNameFunc()()); /*My Object*/

我们再来看一下题目,示例01输出的是”The Window”,示例02输出的是”My Object”。

说明:
示例01最终执行的是 function(){return this.name},因为没有显示指明调用对象,所以其this执行全局对象。
示例02先调用object.getNameFunc(),因为显示的指定了调用对象,所以内部的this是object(注:这里说的是this指向的问题,还没有说闭包),接着执行function (){return that.name},这个函数在getNameFunc这个函数作用域中声明的,所以它调用的时候使用的是这个作用域,即得到var that = this;的这个that;而不是外面的that。

作用域链不等同于原型链

真不知道这两个不相关的东西怎么会被等同起来看待,以后谁告诉我它们是同一个东西,我就想问了,ruby、python这种没有原型概念的语言难道就没有作用域链了吗?

更有甚者将变量声明提升和闭包混在一起,也是醉了。

如果无法理解,但记住下面的一点使用技巧,那么遇到闭包和this时也能够轻松的应对

  • 闭包:函数执行时变量的获取从声明的作用域处去获取(注意链式关系,当前没有就往父级找,知道全局作用域)
  • this:显示指定调用者则this就指向谁,如未指定则为全局对象
Prev Next