JavaScript(六)闭包(未完待续......)

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

作用域链

  • 当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性( [[Scope]] ),然后,使用 this、arguments 和其他命名参数的值来初始化函数的活动对象。
function compare(value1,value2){
  if(value1 < value2){
    return -1
  } else if(value1 > value2){
    return 1
  } else {
    return 0
  }
}
var result = compare(5,10);
  • 上述代码先定义了 compare( ) 函数,然后又在全局作用域中调用了它。
  • 第一次调用时,会创建一个包含 this、arguments、value1、value2 的活动对象。
  • 全局执行环境的变量对象在 compare( ) 执行环境的作用域链中则处于第二位。

  • 后台的每个执行环境都有一个表示变量的对象——变量对象全局环境的变量对象始终存在,而像 compare( ) 函数这样的局部环境的变量对象,只在函数执行过程中存在
  • 创建 compare( ) 函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部 [[Scope]] 属性中。
  • 调用 compare( ) 函数时,会为函数创建一个执行环境,然后通过赋值函数的 [[Scope]] 属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。
  • 显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但实际不包含变量对象

1. 闭包与变量

闭包

  • 在一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。

  • 当包含函数执行完毕时,其执行环境的作用域链会被销毁,但它的活动对象仍然留在内存中,直到内部函数被销毁。

  • 由于闭包会携带包含它的函数的作用域,因此会占用更多的内存。

闭包存在的问题

function createFun(){
  var res = [];
  for(var i = 0; i < 10; i++){
    result[i] = function(){
      return i;
    }
  }
  return res;
}
// 每个函数都返回了 10,因为每个函数的作用域链中都保存着 createFun() 函数的活动对象,所以它们引用的是同一个变量 i。
  • 解决办法
function createFun(){
  var res = [];
  for(var i = 0; i < 10; i++){
    res[i] = function(num){
      return function(){
        return num;
      };
    }(i);
  }
  return res;
}

2. 关于 this 对象

  • this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 指向 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。
  • 但是,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。(当通过 call( ) 或 apply() 改变函数执行环境的情况下,this 就会指向其他对象。)
var name = 'window';
var object = {
  name: 'object',
  getNameFunc: function(){
    return function(){
      return this.name;
    }
  }
}
console.log(object.getNameFunc(){}); // 'window'(在非严格模式下)
  • 把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = 'window';
var object = {
  name: 'object',
  getNameFunc: function(){
    var that = this;
    return function(){
      return that.name;
    }
  }
}
console.log(getNameFunc(){}); // 'object'

3. 内存泄漏

  • 由于 IE9 之前的版本对 JScript 对象和 COM 对象使用不同的垃圾收集例程,因此闭包在这里使用时,如果闭包的作用域链中保存着一个 HTML 元素,那么就意味着该元素将无法被销毁。
function assignHandler(){
  var element = document.getElementById('someElement');
  element.onclick = function(){
    console.log(element.id);
  }
}
  • 以上代码由于匿名函数保存了一个对 assignHandler( ) 的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。解决方式如下:
function assignHandler(){
  var element = document.getElementById('someElement');
  var id = element.id;
  
  element.onclick = function(){
    console.log(id)
  }
  element = null; 
}
  • 必须要记住:闭包会引用包含函数的整个活动对象。把 element 变量设置为 null,就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

4. 模仿块级作用域

(function(){
  // 这里是块级作用域
})()

function fn(count){
  (function(){
    for(var i = 0; i < count; i++){
      console.log(i);
    }
  })();
  console.log(i); // error
}
  • 这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。
  • 这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

5. 私有变量

6. 小结

  • 挡在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下:
    • 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
    • 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
    • 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
  • 使用闭包可以模仿块级作用域。要点如下:
    • 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
    • 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。