浅谈JavaScript的闭包

说是浅谈,真是浅谈。平时接触闭包(Closure)也不止一次了,但一直没有去深究。其实,『犀牛书』里专门有一节讲闭包。谁让它太厚了,翻到后面都是匆匆而过。『犀牛书』:怪我喽?

JavaScript函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的,这样的作用域称为词法作用域(lexical scoping)。为了实现词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为『闭包』。

这里面又出现了一些概念,所以得先从变量的作用域说起。

变量作用域

变量的作用域有两种:全局变量和局部变量。Javascript语言的特殊之处,在于函数内部可以直接读取全局变量。

var blog = "biebu.xin";
function f1() {
  alert(blog);
}
f1(); // biebu.xin

另一方面,在函数外部无法读取函数内的局部变量。

function f1() {
  var blog = "biebu.xin";
}
alert(blog); // error

注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,声明的就是一个全局变量了。

function f1() {
  blog = "biebu.xin";
}
f1();
alert(blog); // biebu.xin

从外部读取局部变量

正常情况下,是无法从外部读取局部变量的。但是,有变通的方法,可以在函数的内部,再定义一个函数。

function f1() {
  var blog = "biebu.xin";
  function f2() {
    alert(blog); // biebu.xin
  }
}

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的作用域链(Scope Chain)结构,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了。

function f1() {
  var blog = "biebu.xin";
  function f2() {
    alert(blog);
  }
}
var result = f1();
result(); // biebu.xin

闭包理解

如开头所述,闭包,本质上是将函数内部和函数外部连接起来的一座桥梁。在Javascript中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成『定义在一个函数内部的函数』。

闭包用途

闭包最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

function f1() {
var blog = "biebu.xin";
blogAdd = function() {
blog += "别不信";
}
function f2() {
alert(blog);
}
return f2;
}
var result = f1();
result(); // biebu.xin
blogAdd();
result(); // biebu.xin别不信

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是biebu.xin,第二次的值是biebu.xin别不信。这证明了,函数f1中的局部变量blog一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

另一个值得注意的地方,就是blogAdd=function() {blog += "别不信"}这一行,首先在blogAdd前面没有使用var关键字,因此blogAdd是一个全局变量,而不是局部变量。其次,blogAdd本身也是一个闭包,所以可以在函数外部对函数内部的局部变量进行操作。

最后,闭包虽好,可不能滥用哦。