理解Javascript中的闭包概念

思索数月,仍无法想出可将JavaScript闭包通俗易懂解释出的例子。

在去年的一篇解释JavaScript原型链的文章里,我试图借助遗传与进化🧬这一生物学的概念,收效甚好,所以用已知的概念学习未知的概念是有帮助的,可以称为类比学习法。

一年后的突发奇想,受16年我的毕业设计影响与启发,打算用一敏感话题类比JavaScript闭包——不同国家(即不同作用域内)在法律条文上(活动上下文)的差异性与人口流动对这种差异性的改变

想要理解闭包,需要先理解作用域和作用域链。

一、作用域

大到国家,小至个人,皆可看作一个又一个作用域。

作用域有大小之分,却无贵贱之分;小与大是相对的;大作用域容纳小作用域;作用域可包含也可对峙。

  1. 一般情况下,小作用域可访问包含该作用域的大作用域中的信息;换句话说,小作用域会受到大作用域的影响。
  2. 一般情况下,大作用域不可访问其中小作用域内的信息;换句话说,大作用域不会受到小作用域的影响。

二、作用域链

结合上图,我们看这样的一段代码:

function funA(){
  var funA_age = 28
  return function funB(){
    var funB_age = 20
    return function funC(){
      var funC_age = 18
      return function funD(){
        var funD_age = 12
      }
    }
  }
}

//将函数funA的属性输出
console.dir(funA)

执行上面代码,看看控制台输出了什么。

在Scopes内有1个元素,展开他发现,他就是所有全局变量。所以我们才可以在funA函数内直接访问window中的各种方法和属性,比如window.setInterval()等。

接着,我将输出改一下。

function funA(){
  var funA_age = 28
  return function funB(){
    var funB_age = 20
    return function funC(){
      var funC_age = 18
      return function funD(){
        var funD_age = 12
      }
    }
  }
}

//将funA()的返回值(也就是函数funB)的属性输出
console.dir(funA())

同样能在控制台看到函数funB内可以访问window中的所有方法和属性。以此类推,结果都一样,不管函数嵌套多深,好像window这个全局变量的方法和属性在哪里都能访问到,这难道就是作用域链起到的神奇作用么。好戏还在后头。

function funA(){
  var funA_age = 28
  return function funB(){
    var funB_age = 20
	//在函数funB中访问funA中定义的funA_age,能获取到么?
	console.log(funA_age)
    return function funC(){
      var funC_age = 18
      return function funD(){
        var funD_age = 12
      }
    }
  }
}

//将funA()的返回值(也就是函数funB)的属性输出
console.dir(funA())

而且你有沒有发现,js很聪明,因为在第一次没有console.dir()来输出funA_age而只是输出函数funA的时候,我们并没有在控制台的[[Scopes]]中找到被定义的funA_age变量,所以说,作用域链上每个函数作用域内用到了其父级什么变量就存什么变量,没有用到的就不存。

比如说,我们改写函数funD。

function funA(){
  var funA_age = 28
  return function funB(){
    var funB_age = 20
    return function funC(){
      var funC_age = 18
      return function funD(){
        var funD_age = 12
        console.log(funA_age)
        console.log(funB_age)
        console.log(funC_age)
        console.log(funD_age)
      }
    }
  }
}

//执行funD(),将其属性输出
console.dir(funA()()())

这下明白了,funD中需要输出非funD作用域中的funA_age、funB_age、funC_age变量,所以我们在[[Scopes]]中发现了funA作用域中的funA_age;funB作用域中的funB_age;funC作用域中的funC_age… …

这就是作用域链!

三、闭包

闭包一个用处就是在内存中永久存储数据。

执行Ajax请求很耗时间,如果直接将Ajax写入for循环中,你会的到最后一次循环的异步请求 结果。这里我用延时模拟一下这种糟糕的情况。

for(var i=0; i<3; i++){
  setTimeout(function(){
    console.log(i)
  },1000)
}

最后输出了3次3,并不是我们希望的0,1,2,难道我写了一个假循环!

for(var i=0; i<3; i++){
  print(i)
}
function print(num){
  console.log(num)
}

也许你会问,print怎么算闭包,闭包不是应该签到在函数里的么,白眼.jpg。请把全局作用域也考虑在内,全局作用域内的函数都是闭包,所以print函数也是啊。

也许你还会问,参数也可以被保存进内存?白眼.jpg。是的!如果没有传进来实参,那么在该函数作用域内访问才参数会返回undefined。如果你觉得不舒服,可以将传入的参数重新赋值给你自己定义的变量,后续就用这个变量,当然我就经常喜欢重新赋值一下。

function A(){
  var _age = 0
  return function(){
    console.log(_age++)
  }
}
var a = A()
a()
a()
a()
a()

猜猜,控制台输出什么,4个0么?

当然不是啦,每次执行一次函数a,都相当于在当前私有变量_age的基础上加1。注意我喜欢私有变量前边加上一个下划线(JS里变量首字母可以是字母、美刀符号和下划线),不然容易和全局变量同名引起副作用。

我再用另一种写法写出相同的功能。这种写法就不存在同名冲突的问题。

function Person(){
  this.age = 0
}
Person.prototype.add = function(){
  console.log(this.age++)
}
var csh = new Person()
csh.add()
csh.add()
csh.add()
csh.add()

这就是我理解的作用域、作用域链和闭包,希望你也理解了,甚至比我更好的理解了。

技术的目的是为了解决实际问题,不是炫耀,就像闭包,他不是内置函数,只是一个概念,以前不认识它,只要写过函数就一定在用它。多像老子口中的哪位智者呐,不见行踪又无处不在,好像无用实则有大用。

请使用Github账号登录留言