Javascript 闭包全面解析

阅读更多

        今天有人问了下有关javascript的闭包的问题,自己也没有看相关的文档,只是模糊的回答了下。回答完之后感觉那样对自己不好,一定要弄清javascript的闭包。正好在火狐开发者社区看到一篇有关闭包的文章https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures 

一、什么是闭包?

     闭包是指函数有自主独立的变量,也就是说定义闭包中的函数可以记忆它创建时候的“环境”。

二、 语法的作用域

看看下面的函数

 

 

 

在函数init()里面定义一个变量名称为name的临时变量,然后定义一个名称为displayName 的内部函数。此内部函数仅仅只有在本init方法内可用,其他都没有办法调用,你看displayName函数内并没有自己的局部变量,然而它可以访问到外包函数的变量-可以使用父函数使用的变量。

我们更改上面的代码

 

 这是作用域的一个例子,在javascript 中,变量的作用域是嵌套函数可以访问外部的变量。

 

 

三、闭包

我们先看下面的一个例子

 

        运行代码与上面的结果一样,这段代码看起来比较别扭,但是运行正常,我们都知道一个函数的作用域仅仅在运行期可用,当init方法运行完之后我们会认为name变量已经失效不可用了,但是虽然运行没有问题,实际上是对应name变量不是那样不可用已经失效了。

 

   因为这个init已经变成闭包了,是闭包的一个特殊对象,它有两部分构成:函数以及创建函数的环境,环境由闭包创建时候在作用域的任何局部变量组成,-上面我们可以说 init 是个闭包,由displayName函数和闭包创建时候的name="dongtian"组成。

下面看一个加法器的一个闭包的写法:

 

 

 

   这个例子我们定义了subAdd()函数,带有一个参数x,并且返回一个新的函数,返回的函数中带有一个参数y并返回 x和y的和。从本质上讲 subAdd()函数是个工厂函数,创建它并且要指定值并且包含一个求和的函数。在上面的例子中我们通过此函数工厂创建了两个函数一个将参数2求和一个将参数5求和,从这我们可以看出此两个函数都是闭包,它们共享函数的定义并且他们处在不同的环境中。

运行结果是 弹出 4 和 8

 

四、为何使用闭包

理论就是这些了 — 可是闭包确实有用吗?让我们看看闭包的实践意义。闭包允许将函数与其所操作的某些数据(环境)关连起来。这显然类似于面向对象编程。在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

       因而,一般说来,可以使用只有一个方法的对象的地方,都可以使用闭包。

       在 Web 中,您可能想这样做的情形非常普遍。大部分我们所写的 Web JavaScript 代码都是事件驱动的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常添加为回调:响应事件而执行的函数。

         以下是一个实际的示例:假设我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如页眉)的字号:

 

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

 

 

       我们的交互式的文本尺寸按钮可以修改 body 元素的 font-size 属性,而由于我们使用相对的单位,页面中的其它元素也会相应地调整。

 

以下是 JavaScript:

 

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

 

 

      size12size14 和 size16 为将 body 文本相应地调整为 12,14,16 像素的函数。我们可以将它们分别添加到按钮上(这里是链接)。如下所示:

 

 

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

 

12
14
16

 

 

五、用闭包模拟私有方法

 

      诸如 Java 在内的一些语言支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

对此,JavaScript 并不提供原生的支持,但是可以使用闭包模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,且其可以访问私有函数和变量。这个方式也称为 

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

         这里有很多细节。在以往的示例中,每个闭包都有它自己的环境;而这次我们只创建了一个环境,为三个函数所共享:Counter.increment,Counter.decrement  Counter.value

 

         该共享环境创建于一个匿名函数体内,该函数一经定义立刻执行。环境中包含两个私有项:名为privateCounter 的变量和名为 changeBy 的函数。 这两项都无法在匿名函数外部直接访问。必须通过匿名包装器返回的三个公共函数访问。

 

        这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法范围的作用域,它们都可以访问privateCounter 变量和 changeBy 函数。

 

         您应该注意到了,我们定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给Counter 变量。也可以将这个函数保存到另一个变量中,以便创建多个计数器。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

      请注意两个计数器是如何维护它们各自的独立性的。每次调用 makeCounter() 函数期间,其环境是不同的。每次调用中, privateCounter 中含有不同的实例。

       这种形式的闭包提供了许多通常由面向对象编程U所享有的益处,尤其是数据隐藏和封装。

 

六、在循环中创建闭包---- 一种常见的错误

    在 JavaScript 1.7 引入 let 关键字 之前,闭包的一个常见的问题发生于在循环中创建闭包。参考下面的示例:

Helpful notes will appear here

E-mail:

Name:

Age:

 

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

 

      数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus 事件处理函数,以便显示帮助信息。

运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。

          该问题的原因在于赋给 onfocus 是闭包setupHelp中的匿名函数而不是闭包对象;在闭包(setupHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus 的回调被执行时,循环早已经完成,且此时 item 变量(由所有三个闭包所共享)已经指向了 helpText 列表中的最后一项。

    解决这个问题的一种方案是使onfocus指向一个新的闭包对象。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

           这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的环境。在这些环境中,help 指向 helpText 数组中对应的字符串。

 

七、性能

       如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。

       考虑以下虽然不切实际但却说明问题的示例:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

     上面的代码并未利用到闭包的益处,因此,应该修改为如下常规形式:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

    或者改成

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

 

   在前面的两个示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时定义方法。

八、总结

记住闭包的作用一般有两种,所有局部变量永驻内存和外包无法访问到局部变量。

 

你可能感兴趣的:(javascript,javascript闭包,Closures)