A.1 创建内部函数

能够跻身支持内部函数声明的编程语言行列,对JavaScript来说应该算是一种幸运。许多传统的编程语言(例如C),都会把全部函数集中在顶级作用域中。而支持内部函数的语言,则允许开发者在必要的地方集合小型实用函数,以避免污染命名空间

所谓内部函数,就是定义在另一个函数中的函数。

代码清单A-1

  1. function outerFn() {
  2. function innerFn() {
  3. }
  4. }

innerFn()就是一个被包含在outerFn()作用域中的内部函数。这意味着,在outerFn()内部调用innerFn()是有效的,而在outerFn()外部调用innerFn()则是无效的。下列代码会导致一个JavaScript错误。

代码清单A-2

  1. function outerFn() {
  2. console.log('Outer function');
  3. function innerFn() {
  4. console.log('Inner Function');
  5. }
  6. }
  7. console.log('innerFn():');
  8. innerFn();

不过,通过在outerFn()内部调用innerFn(),则可以成功地运行:

代码清单A-3

  1. function outerFn() {
  2. console.log('Outer function');
  3. function innerFn() {
  4. console.log('Inner function');
  5. }
  6. innerFn();
  7. }
  8. console.log('outerFn():');
  9. outerFn();

结果会产生如下输出:

  1. outerFn():
  2. Outer function
  3. Inner function

这种技术特别适合于小型、单用途的函数。例如,递归但却带有非递归API包装的算法通常最适合通过内部函数来表达。

A.1.1 在任何地方调用内部函数

函数引用参与进来之后,问题就变得复杂了。有些语言,比如Pascal,只允许通过内部函数实现代码隐藏,而且这些函数因此也会永远被埋没在它们的父函数中。然而,JavaScript则允许开发人员像传递任何类型的数据一样传递函数。也就是说,JavaScript中的内部函数能够逃脱定义它们的外部函数。

逃脱的方式有很多种。例如,可以将内部函数指定给一个全局变量

代码清单A-4

  1. var globalVar;
  2.  
  3. function outerFn() {
  4. console.log('Outer function');
  5. function innerFn() {
  6. console.log('Inner function');
  7. }
  8. globalVar = innerFn;
  9. }
  10. console.log('outerFn():');
  11. outerFn();
  12. console.log('globalVar():');
  13. globalVar();

在函数定义之后调用outerFn()会修改全局变量globalVar,此时它引用的是innerFn()。这意味着,后面调用globalVar()的操作就如同调用innerFn()一样,也会执行输出消息的语句:

  1. outerFn():
  2. Outer function
  3. globalVar():
  4. Inner function

注意,此时在outerFn()外部直接调用innerFn()仍然会导致错误!这是因为虽然内部函数通过把引用保存在全局变量中实现了逃脱,但这个函数的名字仍然被截留在outerFn()的作用域中。

另外,也可以通过在父函数中返回值来“营救出”内部函数的引用:

代码清单A-5

  1. function outerFn() {
  2. console.log('Outer function');
  3. function innerFn() {
  4. console.log('Inner function');
  5. }
  6. return innerFn;
  7. }
  8. console.log('var fnRef = outerFn():');
  9. var fnRef = outerFn();
  10. console.log('fnRef():');
  11. fnRef();

这里,并没有在 outerFn() 内部修改全局变量,而是从 outerFn() 中返回了一个对innerFn()的引用。通过调用outerFn()能够取得这个引用,而且,这个引用可以保存在变量中,也可以自己调用自己,从而触发消息输出:

  1. var fnRef = outerFn():
  2. Outer function
  3. fnRef():
  4. Inner function

这种即使在离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用这些内部函数的可能,JavaScript就需要保留被引用的函数。而且,JavaScript 运行时需要跟踪引用这个内部函数的所有变量,直至最后一个变量废弃,JavaScript的垃圾收集器才能出面释放相应的内存空间。

A.1.2 理解变量作用域

内部函数当然也可以拥有自己的变量,只不过这些变量都被限制在内部函数的作用域中:

代码清单A-6

  1. function outerFn() {
  2. function innerFn() {
  3. var innerVar = 0;
  4. innerVar++;
  5. console.log('innerVar = ' + innerVar);
  6. }
  7. return innerFn;
  8. }
  9. var fnRef = outerFn();
  10. fnRef();
  11. fnRef();
  12. var fnRef2 = outerFn();
  13. fnRef2();
  14. fnRef2();

每当通过引用或其他方式调用这个内部函数时,都会创建一个新的innerVar变量,然后递增,最后显示:

  1. innerVar = 1
  2. innerVar = 1
  3. innerVar = 1
  4. innerVar = 1

内部函数可以像其他函数一样引用全局变量:

代码清单A-7

  1. var globalVar = 0;
  2. function outerFn() {
  3. function innerFn() {
  4. globalVar++;
  5. console.log('globalVar = ' + globalVar);
  6. }
  7. return innerFn;
  8. }
  9. var fnRef = outerFn();
  10. fnRef();
  11. fnRef();
  12. var fnRef2 = outerFn();
  13. fnRef2();
  14. fnRef2();

现在,每次调用内部函数都会持续地递增这个全局变量的值:

  1. globalVar = 1
  2. globalVar = 2
  3. globalVar = 3
  4. globalVar = 4

但是,如果这个变量是父函数的局部变量又会怎样呢?因为内部函数会继承父函数的作用域,所以内部函数也可以引用这个变量:

代码清单A-8

  1. function outerFn() {
  2. var outerVar = 0;
  3. function innerFn() {
  4. outerVar++;
  5. console.log('outerVar = ' + outerVar);
  6. }
  7. return innerFn;
  8. }
  9. var fnRef = outerFn();
  10. fnRef();
  11. fnRef();
  12. var fnRef2 = outerFn();
  13. fnRef2();
  14. fnRef2();

这一次,对内部函数的调用会产生有意思的行为:

  1. outerVar = 1
  2. outerVar = 2
  3. outerVar = 1
  4. outerVar = 2

我们看到了前面两种情况合成的效果。通过每个引用调用innerFn()都会独立地递增outerVar。也就是说,第二次调用outerFn()没有继续沿用outerVar的值,而是在第二次函数调用的作用域中创建并绑定了一个新的outerVar实例。结果,就造成了在上面的代码中调用两次fnRef()之后,再调用fnRef2()会输出1。这两个计数器完全是无关的。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。在这种情况下,我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放,因为闭包仍然需要使用它们。