javascript中的闭包

原文链接地址:https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures?redirectlocale=en-US&redirectslug=Core_JavaScript_1.5_Guide%2FClosures

在酷壳网中有篇介绍闭包的文章写的特别好:http://coolshell.cn/articles/6731.html

在阅读本文的过程中遇到什么不懂的地方可以发表评论,希望能够和大家多多进行交流。

闭包(Closures)

闭包是javascript中的高级特性,理解闭包对掌握javascript这门语言是很必要的。

让我们来看看下面这个函数:

function init() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  displayName();
}
init(); 
在这个init()初始化函数定义了一个叫做name的局部变量,然后又定义了displayName()这个内部函数(在c、c++、java中是不允许在函数的内部再定义函数)displayName()这个函数只能只能在init()这个函数内部使用,displayName()这个函数在其内部是没有局部变量的,但它可以它可以访问定义在它外部的变量name。

这种机制真是恰到好处-试着运行下代码,看看会发生什么奇妙的事。这是个函数级作用域的例子:
在javascript中,变量的作用域是被定义在被源码包围着的范围(从它被定义的代码开始一直到大括号结束),被嵌套定义的函数(内部函数)可以访问在其外部定义的的变量。

接下来再让我们看看下面这个例子:

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

如果你运行过这段代码后你会发现它与上一段代码init()的表现结果是一样的:在alert警告窗口中会弹出“Mozilla”字符串。不同的而且有意思的是-displayName()这个内部函数在被执行前就被外部函数返回了。这段代码看起来还是不直观。正常的话,一个位于函数内部的局部变量只有在函数的执行期间才是存在的。一旦makeFunction()函数已经执行完了,我们会理所当然的认为name这个局部变量不再存在了,即不能被访问到了。由于代码仍能很好的工作,很显然不像我们上面分析的那样。

闭包就是用来解决这个谜题的。闭包是一个关联两部分的特殊对象:1,函数2,函数创建的环境。环境是由所有的局部变量组成的,这些局部变量是在闭包被创建时就存在的(局部变量是外部函数的变量,比如在上个函数中的 name)。在这种情形下,myFunc 就是一个闭包,具体的来说就是两部分组成:displayName 函数和在闭包创建时就存在的“Mozilla”字符串。

这是一个更有趣的例子:makeAdder 一个加法函数

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}
 
var add5 = makeAdder(5);
var add10 = makeAdder(10);
 
alert(add5(2));  // 7
alert(add10(2)); // 12

在这个例子中,我们定义了一个函数makeAdder(x),这个函数只有一个参数,返回值也是一个函数。被返回的这个内部函数也是一个只有一个参数的函数,它的返回值是外部函数的参数x和它自己的参数y相加的和即x + y。

一个不太恰当的说法,makeAdder 就是一个函数工厂-它可以产生各种各样的函数。这些函数可以把一个特定的值和传入这个函数的参数的值进行相加。在上面的例子中我们用了我们的函数工厂来产生了两个新的函数-一个是用5和传入它的参数来相加的函数,另一个是把10和传入它的参数来相加。

add5 和 add10 这两个对象都是闭包。他们共享了同一个函数体的定义,但是存储着不同的环境。在 add5的环境中x是5,而在add10中x却是10。

练习闭包

说句与理论题外的话-闭包真有用吗?让我们来看看闭包在实践中的应用,用一个实际应用中的闭包来说明一切吧:
闭包让你与环境中的数据发生关联,这些数据可以被函数操作。很明显这与面向对象编程是类似的,在面向对象编程中,允许我们用函数来对数据进行操作。所以,我们在javascript中想要用面向对象编程的思想来解决问题,而且这个对象只有一个函数时,我们可以考虑用闭包来解决问题。ps:闭包就是个返回的函数,一般返回一个函数,而不是返回好几个函数,所以说这个对象只有一个函数时,我们可以考虑用闭包来解决问题。

在web应用中用到闭包的情况尤其常见。在web应用中我们写的javascript脚本更多的是基于事件的-我们定义一些行为,然后把它与用户触发的事件绑定在一起。我们的代码通常与一个回调函数关联:一个只对事件作出响应的函数。

下面是一个实践例子:假如我们想要在页面上添加一些可以适应文本大小的按钮。一种方法是通过用像素点来定义body元素的字体大小,然后用相对单元em来设置页面上其他元素(比如h1,h2)的大小。ps:对html不熟,估计翻译得不准确,可以参考原文。

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}
 
h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

我们定义的用来改变交互文本大小的按钮可以改变body元素字体大小的属性,多亏了各种各样的相对单元我们才可以方便的改变页面上其他元素的大小。

下面是javascript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}
 
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

现在size12,size14,size16是三个函数,他们分别可以把页面文本的大小设置成12,14,16像素。现在我们可以把他们与按钮绑定在一起,如下:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a> 

用闭包来模拟面向对象编程中的私有函数

好多语言中都可以定义私有函数,比如在java中就可以定义私有方法(java中一般把函数叫做方法,c++中叫函数),私有函数就是只能被同一个类的对象调用的函数。

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;
    }
  }   
})();
 
alert(Counter.value()); /* Alerts 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* Alerts 2 */
Counter.decrement();
alert(Counter.value()); /* Alerts 1 */
接下来我们还有很多要做的。在前面的例子中,每个闭包都有它自己的环境,下面我们就创建一个被三个函数共享的一个环境:
这三个函数分别是:Counter.increment,Counter.decrement and Counter.value.
共享的环境其实就是匿名的函数体,匿名函数从被定义后就立即执行。环境包含2个私有项:一个变量是privateCounter另一个是函数changeBy。他们两者都不可以在匿名函数的外部被直接访问到。他们必须通过被返回的匿名包裹块中的三个函数来访问。这三个函数是共享着同一个环境的闭包。
多亏了javascript的语法定义,他们才可以访问privateCounter变量和changeBy 函数。

你会发现我们定义了一个匿名的函数,它创建了一个计数器,我们立即调用它并且把返回的结果赋给makeCounter。我们可以用一个单独的变量来保存这个函数,我们可以用它来创建几个计数器。

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();
alert(Counter1.value()); /* Alerts 0 */
Counter1.increment();
Counter1.increment();
alert(Counter1.value()); /* Alerts 2 */
Counter1.decrement();
alert(Counter1.value()); /* Alerts 1 */
alert(Counter2.value()); /* Alerts 0 */
注意这两个计数器是怎样保持各自相互独立的。它的环境在每次调用makeCounter()函数时都是不同的。闭包的变量privateCounter在每次都包含一个不同的实例。

通过这种方式使用闭包可以提供好多面向对象编程的好处,尤其是数据隐藏和封装。

在循环体中创建闭包是一个常见的错误

前面在javascript1.7中介绍了left keyword,在循环内部创建闭包是一个常见的问题。来思考一下下面的这个例子: 

<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这个数组定义了三个有用的提示,在document中每个提示都关联了一个ID和文本输入域。for循环通过helpText数组获取了焦点事件,然后与该焦点事件相关联的方法就会显示对相应文本域的帮助提示信息。
如果你试着运行这些代码,你会发现代码并不像你想象的那样来执行。不管哪个域获得焦点,关于你的年龄的那个帮助提示信息都会弹出的。
发生这样的原因是匿名函数赋给 document.getElementById(item.id).onfocus 的是一些闭包。这些闭包是由定义的匿名函数和从setupHelp函数中捕获的环境变量(这儿的环境变量其实就是“showHelp(item.help);”中的item.help)组成的。三个闭包被创建了,但是每个闭包都共享了同样的一个环境。当onfocus回调函数执行的时候,其实for循环已经结束了此时循环变量 i 已经等于3了,被三个闭包共享的item(item = helpText[3])这个变量其实已经是helpText这个数组中的最后一项了。

在这种情形下的一种解决方法是使用更多的闭包:尤其,可以使用先前描述的函数工厂:

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;
};
在前面的两个例子中,prototype被所有的对象和函数继承而不需要在对象和函数定义显式创建。看 Details of the object model 来获取更多详细情况。

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