js 闭包详解

什么是闭包

在讲解闭包之前,请先理解变量作用域。

变量无非就两种:全局变量和局部变量。

作用域定义了变量的区域。

作用域中的作用域链保证对执行环境有权访问的所有变量和函数进行有序访问。

作用域完全由写代码期间函数所声明的位置来定义。

而闭包是基于作用域书写代码时所产生的自然结果。

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
  var a = 2;
  function bar() {
    console.log( a );
  }
  return bar;
}
var fn = foo();
fn()

函数 bar() 能够访问foo的内部作用域,然后将bar()函数当作函数的返回值进行传递,在函数foo执行后,返回值赋值给了变量fn并调用fn(),实际上只是通过不同的标识符引用,调用了内部函数的bar();在这个例子中,bar()可以在自己定义的作用域以外被执行。

回顾作用域链的概念。当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象并被推入执行环境作用域链的前端。一般来讲,当函数执行完毕后,局部活动对象就会被销毁(垃圾回收机制),内存中仅保存全局作用域,但是闭包的情况又有所不同。在上述例子中bar函数从foo()中被返回后,bar函数的作用域链中包含了foo函数的活动对象,更为重要的是foo函数在执行完毕之后,其执行环境的作用域链会被销毁,但它的活动对象不会被销毁,因为bar函数的作用域链仍然在引用foo函数的活动对象。只有解除对bar函数的引用,foo函数的活动对象才会被销毁。

闭包的传递方式

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包

function foo() {
  var a = 2;
  function baz() {
    console.log( a ); // 2
  }
  con( baz );
}
function con(fn) {
  fn(); // 这是闭包
}

传递函数当然也可以是间接的

var fn;
function foo() {
  var a = 2;
  function baz() {
    console.log( a );
  }
  fn = baz; // 将 baz 赋值给全局变量
}
function bar() {
  fn(); // 这是闭包
}
foo();
bar();

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

现在我懂了

function setupBot(name, selector) {
  $( selector ).click( function activator() {
  console.log( "Activating: " + name );
});
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使

用了回调函数,实际上就是在使用闭包。

模块

还有其他的代码模式利用闭包的强大威力

function block(){
  var another = 'wyh';
  var sex = 'man';
  function doSomething1(){ return another };
  function doSomething2(){ return sex};

  return{
    getName:doSomething1,
    getSex:doSomething2   
 }
};
var until = block();
var nm = until.getName();

首先, block() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建

其次, block() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。

这个对象类型的返回值最终被赋值给外部的变量 until,然后就可以通过它来访问 API 中的属性方法,比如 until.getName() 。

闭包的副作用

1. 闭包只能取得包含函数中任何变量的最后一个值,即闭包所保存的是整个活动对象,而不是某个特殊的变量。

function create(){
  var result = [];
  for(var i=0;i<10;i++){
    result[i] = function(){
     return i;
    }
  }
  return result;
}
var arr = create();
arr[0]();    // 10 !!

该函数返回一个函数数组,似乎每个函数都应该返回自己的索引,但每个函数都返回10,因为每个函数的作用域链中都保存着create函数的活动对象,所以它们引用的都是同一个变量i;当函数create()执行完成之后 i 的值为10。

可以通过创建另一个匿名函数强制让闭包行为符合预期

function create(){
  var result = [];
  for(var i=0;i<10;i++){
    result[i] = function(num){
     return function(){
       return num;
     };
    }(i);
  }
  return result;
}
var arr = create();
arr[0](); 

定义一个匿名函数,并将立即执行匿名函数的结果赋值给数组。在调用每个匿名函数时,将变量 i 赋值给参数 num ,而这个匿名函数内部,又创建并返回一个访问 num 的闭包,因此就可以返回各自不同的值。

2. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。

3. 闭包可以在父函数外部改变父函数内部变量的值。所以,如果把父函数当做对象使用,把闭包当做它的API,把内部变量当做私有属性,一定要小心,不要随便改变父函数内部变量的值


小结

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书写代码的。

当函数可以记住并访问所在的词法作用域,使函数是在当前词法作用域之外执行,就产生了闭包。

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错。

猜你喜欢

转载自blog.csdn.net/i13738612458/article/details/80204866