你的位置:首页 > Java教程

[Java教程]闭包和作用域链(《JavaScript 高级程序设计》读书笔记)


  当某个函数被调用时,会创建一个执行环境及相应的作用域链。

  执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

  每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。但是有其他办法来延长作用域链。有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。当执行流进入下列两种语句时,作用域链就会得到加长:

  • try-catch 语句的 catch 块
  • with 语句

  这两个语句都会在作用域链的前端添加一个变量对象。对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

在 IE8 及之前版本的 JavaScript 实现中,存在一个与标准不一致的地方,即在catch 语句中捕获的错误对象会被添加到执行环境的变量对象,而不是 catch 语句的变量对象中。换句话说,即使是在 catch 块的外部也可以访问到错误对象。 IE9 修复了这个问题。

 

  闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

  无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况有所不同,因为在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中,参考下面的代码:

function createComparisonFunction(propertyName) {  return function(object1, object2){    var value1 = object1[propertyName];    var value2 = object2[propertyName];        if (value1 < value2){      return -1;    } else if (value1 > value2){      return 1;    } else {      return 0;    }  };}

当下列代码执行时,包含函数与内部匿名函数的作用域链如图所示:

var compare = createComparisonFunction("name");var result = compare({ name: "Nicholas" }, { name: "Greg" });

 

  当createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。直到匿名函数被销毁后, createComparisonFunction()的活动对象才会被销毁:

compare = null; //解除对匿名函数的引用(以便释放内存)

  由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以在绝对必要时再考虑使用闭包。

  

  作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量:

function createFunctions(){  var result = new Array();  for (var i=0; i < 10; i++){    result[i] = function(){      return i;    };  }  return result;}

  这个函数会返回一个函数数组,且每个函数都返回 10。

 

  在闭包中使用 this 对象也可能会导致一些问题。我们知道, this 对象是在运行时基于函数的执行环境绑定的,而匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window(在通过 call()或 apply()改变函数执行环境的情况下, this 就会指向其他对象),看下面的例子:

var name = "The Window";var object = {  name : "My Object",  getNameFunc : function(){    return function(){      return this.name;    };  }};alert(object.getNameFunc()()); //"The Window"(在非严格模式下)

  把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示:

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

this 和 arguments 也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。