JavaScript高级程序回顾(三)——吓唬人的闭包

碎碎念

  初学前端时,觉得闭包是个好难好难理解的概念啊。再加上本身闭包的重要性,以及各类看不懂的关于闭包的文章,更是打击了我的自信心。后来看了 JS 高程三中的相关章节,豁然开朗。下面,我尽量以最通俗的方式给大家讲一下神秘的闭包。

执行环境和作用域

  要理解闭包,必须先对执行环境作用域有一个概念。
  (代码的)执行环境,顾名思义,是指当前的代码处在怎样的环境中执行,类似于人类周边的环境有水、空气等等,代码的环境里有变量、函数等。(代码的)执行环境是代码能够真实感触到的变量和函数,它定义了代码有权访问的其他数据,环境中定义的所有变量和函数都保存在一个变量对象中。
  全局执行环境是最外围的一个执行环境。当我们在浏览器控制台时,总能访问到 window 对象;当我们在 node 终端时,总能访问到 global 对象。其实这两个对象都可以被认为是全局执行环境,因为所有的全局变量和函数都是作为它的属性和方法创建的。全局执行环境会一直持续到应用程序退出,例如关闭网页或者关闭 node 控制台。
  当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。通过这句话,我觉得可以得到很多的信息。首先,作用域链,是将变量对象链起来的,变量对象用来存储执行环境中的变量和函数(这里我觉得也可以认为作用域链将执行环境链起来了);再者,它保证了有序访问,那什么是有序访问呢?当然是先访问自己的变量对象,自己没有了就去上层变量对象找,直至将回溯到链条顶端,也就是全局执行环境。为了帮助理解,可以看下面的代码和图:

1
2
3
4
5
6
function A() {
var a = 1;
function B() {
var b = 2;
}
}

  可以看图中我的标注。红色的箭头链条代表函数 B 内的作用域链,该链所经过之处的所有变量和函数,就是函数 B 内部可以访问到的,而该链的方向就代表的是上面提到的有序访问的访问方向。黄色的矩形代表一个一个的执行环境,将其认为作变量对象我觉得也可以。

闭包

  在我还不清楚究竟什么是闭包的时候,别人问我闭包是什么,我就说“匿名函数”,可想而知,网络上有多少的流言蜚语是这样冤枉闭包的。闭包是指有权访问另一个函数作用域中的变量的函数,之所以很多人混用匿名函数和闭包,是因为匿名函数往往是造成闭包的原因之一。

可怕的闭包

无法进行的内存回收
  闭包是不是真的能够造成内存泄漏,我拿不准。但是它总是阻碍内存的释放。下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createComparisonFunction(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = objects[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}

  这个函数的返回值是一个匿名函数,在该匿名函数中用到了外层函数的 propertyName 变量。想象一下,该匿名函数被返回后,在其他地方被调用了,它仍能够访问 propertyName 变量,这是因为它的作用域链中包含了 createComparisonFunction 函数的活动对象。本来 createComparisonFunction 执行完毕后,它的活动对象就应该销毁,却因为这里的引用而迟迟留在内存中。为了释放这部分内存,应该解除对匿名函数的引用。如下代码:

1
2
3
var compareNames = createComparisonFunction('name');
compareNames({ name: 'Julia' }, { name: 'Zhuyali' });
compareNames = null;//解除引用

可爱的闭包

  • 模仿块级作用域

  在 JS 这门神奇的语言里,是没有块级作用域的概念的。运行这段代码得到的结果是前端

1
2
3
4
5
var str1 = 'web';
if(true) {
var str1 = '前端';
};
console.log(str1);

  不过匿名函数可以用来模仿块级作用域。如下代码为例,得到的结果是web

1
2
3
4
5
var str1 = 'web';
(function() {
var str1 = '前端';
})();
console.log(str1);

  可以说,用闭包模拟块级作用域,实际上就是用函数作用域来替代块级作用域。

  • 私有变量

  严格来讲,JS 没有私有成员的概念,所有对象属性都是公有的。不过,倒是有一个私有变量的概念(其实翻来倒去还是因为作用域)。我们可以利用闭包的作用域链,来创建用于访问私有变量的公有方法。如下示例代码:

1
2
3
4
5
6
7
8
9
10
function MyObject() {
var privateVariable = 10;
function privateFunction() {
return false;
}
this.publicMethod = function() {
privateVariable++;
return privateFunction();
}
}

  分析上面的代码,在创建了 MyObject 的实例后,除了使用 publicMethod() 这一个途径外,没有任何办法可以直接访问私有变量 privateVariable 和私有方法 privateFunction()。

好像不吓人

  讲道理,闭包的概念并不难理解,而且用途多而广泛,如果大开脑洞,也许还有更加神奇的效果哦~

参考资料

  1. 从一个小题目谈谈 JS 函数闭包
  2. JavaScript系列—-作用域链和闭包