JS闭包

来源:互联网 发布:毕业设计的要求及数据 编辑:程序博客网 时间:2024/06/07 06:32

最近几天一直在学习Js的面向对象的闭包部分,参考了闭包 - JavaScript | MDN和Javascript的匿名函数与自执行_泡在网上的日子-提供互联网前沿咨询,聚焦web开发与探索这两篇博文,以下做一个小小的总结, 这是鄙人自己的理解,如有错误,欢迎指出!

在讲闭包之前我们现在了解下匿名函数:

函数是JavaScript中最灵活的一种对象,这里只是讲解其匿名函数的用途。匿名函数:就是没有函数名的函数。

匿名函数的创建
第一种方式:就是上面所讲的定义square函数,这也是最常用的方式之一。
第二种方式:

(function(x, y){       alert(x + y);     })(2, 3);

这里创建了一个匿名函数(在第一个括号内),第二个括号用于调用该匿名函数,并传入参数。括号是表达式,是表达式就有返回值,所以可以在后面加一对括号让它们执行.

自执行的匿名函数
1. 什么是自执行的匿名函数?
它是指形如这样的函数: (function {// code})();
2. 疑问
为什么(function {// code})();可以被执行, 而function {// code}();却会报错?
3. 分析
(1). 首先, 要清楚两者的区别:
(function {// code})是表达式, function {// code}是函数声明.
(2). 其次, js”预编译”的特点:
js在”预编译”阶段, 会解释函数声明, 但却会忽略表式.
(3). 当js执行到function() {//code}();时, 由于function() {//code}在”预编译”阶段已经被解释过, js会跳过function(){//code}, 试图去执行();, 故会报错;
当js执行到(function {// code})();时, 由于(function {// code})是表达式, js会去对它求解得到返回值, 由于返回值是一 个函数, 故而遇到();时, 便会被执行.
另外, 函数转换为表达式的方法并不一定要靠分组操作符(),我们还可以用void操作符,~操作符,!操作符……
如:

!function(){     alert("另类的匿名函数自执行");   }();

闭包在闭包 - JavaScript | MDN上的解释是:

Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)

闭包是指这样的作用域,它包含有一个函数,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。通常我们通过闭包所对应的函数来获得对闭包的访问。

用通俗的话可以理解成:闭包说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕(这点涉及JavaScript作用域链)。

1. 下面来看一个闭包例子:

function makeFunc() {  var name = "Mozilla";  function displayName() {    alert(name);  }  return displayName;}var myFunc = makeFunc();myFunc();

displayName() 内部函数在执行前被从其外围函数中返回了。

这段代码看起来别扭却能正常运行。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行过后,我们会很合理的认为 name 变量将不再可用。然而,因为代码运行的没问题,所以很显然在 JavaScript 中并不是这样的。

这个谜题的答案是 myFunc 变成一个 闭包 了。 闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。在我们的例子中,myFunc 是一个闭包,由 displayName 函数和闭包创建时存在的 “Mozilla” 字符串形成。

2.用闭包模拟私有方法

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

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

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

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所享有的益处,尤其是数据隐藏和封装。

3.在循环中创建闭包:一个常见错误

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

<p id="help">Helpful notes will appear here</p><p>E-mail: <input type="text" id="email" name="email"></p><p>Name: <input type="text" id="name" name="name"></p><p>Age: <input type="text" id="age" name="age"></p>
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 数组中对应的字符串。

性能考量

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

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

原创粉丝点击