闭包是一个函数和词法环境的组合,函数声明在这个词法环境中
看下面一个例子css
function init() { var name = 'Mozilla'; // name是局部变量 function displayName() { // displayName()是内部函数,一个闭包 alert(name); // 使用外部函数声明的变量 } displayName(); } init();
init()
建立了一个局部变量name
和一个函数displayName()
。函数displayName()是一个已经定义在init()内部的函数,而且只能在函数init()里面才能访问获得。函数displayName()没有本身的局部变量,但因为内部函数能够访问外部函数变量,displayName()能够访问到声明在外部函数init()的变量name,若是局部变量还存在的话,displayName()也能够访问他们。html
看下面一个例子前端
function makeFunc() { var name = 'Mozilla'; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
运行这段代码你会发现和以前init()的方法是同样的效果,但不一样之处是,displayName()在执行以前,这个内部方法是从外部方法返回来的。
首先,代码仍是会正确运行,在一些编程语言当中,一个函数内的局部变量只存在于该函数的执行期间,随后会被销毁,一旦makeFunc()函数执行完毕的话,变量名就不可以被获取,可是,因为代码仍然正常执行,这显然在JS里是不会这样的。这是由于函数在JS里是以闭包的形式出现的,闭包是一个函数和词法做环境的组合,词法环境是函数被声明的那个做用域,这个执行环境包括了建立闭包时同一建立的任意变量,即建立的这个函数和这些变量处于同一个做用域当中。在这个例子当中,myFunc()是displayName()的函数实例,makeFunc建立的时候,displayName随之也建立了。displayName的实例能够得到词法做用域的引用,在这个词法做用域当中,存在变量name,对于这一点,当myFunc调用的话,变量name,仍然能够被调用,所以,变量'Mozilla'传递给了alert函数。编程
这里还有一个例子 - 一个makeAdder函数数组
function makeAdder (x) { return function(y) { return x + y; } } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
在这个例子当中,咱们定义了一个函数makeAdder(x),传递一个参数x,而且返回一个函数,这个返回函数接收一个参数y,并返回x和y的和。
实际上,makeAdder是一个工厂模式 - 它建立了一个函数,这个函数能够计算特定值的和。在上面这个例子当中,咱们使用工厂模式来建立新的函数 - 一个与5进行加法运算,一个与10进行加法运算。add5和add10都是闭包,他们共享相同的函数定义,但却存储着不一样的词法环境,在add5的词法环境当中,x为5;在add10的词法环境当中,x变成了10。闭包
闭包是颇有用的,由于他让你把一些数据(词法环境)和一些可以获取这些数据的函数联系起来,这有点和面向对象编程相似,在面向对象编程当中,对象让咱们能够把一些数据(对象的属性)和一个或多个方法联系起来
所以,你可以像对象的方法同样随时使用闭包。实际上,大多数的前端JS代码都是事件驱动性的 - 咱们定义一些事件,当这个事件被用户所触发的时候(例如用户的点击事件和键盘事件),咱们的事件一般会带上一个回调:即事件触发所执行的函数。例如,假设咱们但愿在页面上添加一些按钮,这些按钮可以调整文字的大小,实现这个功能的方式是肯定body的字体大小,而后再设置页面上其余元素(例如标题)的字体大小,咱们使用em做为单位。app
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
咱们设置的调节字体大小的按钮可以改变body的font-size,而且这个调节可以经过相对字体单位,反应到其余元素上,编程语言
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语言可以声明私有方法,意味着只可以在相同的类里面被调用,JS没法作到这一点,但却能够经过闭包来封装私有方法。私有方法不限制代码:他们提供了管理命名空间的一种强有力方式。
下面代码阐述了怎样使用闭包来定义公有函数,公有函数可以访问私有方法和属性。性能
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
在先前的例子当中,每一个闭包具备他们本身的词法环境,在这个例子中,咱们建立了一个单独的词法环境,这个词法环境被3个函数所共享,这三个函数是counter.increment, counter.decrement和counter.value
共享的词法环境是由匿名函数建立的,必定义就能够被执行,词法环境包含两项:变量privateCounter和函数changeBy,这些私有方法和属性不可以被外面访问到,然而,他们可以被返回的公共函数访问到。这三个公有函数就是闭包,共享相同的环境,JS的词法做用域的好处就是他们能够互相访问变量privateCounter和changeBy函数
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 */
两个计数器counter1和counter2分别是互相独立的,每一个闭包具备不一样版本的privateCounter,每次计数器被调用,词法环境会改变变量的值,可是一个闭包里变量值的改变并不影响另外一个闭包里的变量。
下面一个例子
<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 数组定义了三个有用的hint,每一个分别与输入框的id相对应,每一个方法与onfocus事件绑定起来。当你运行这段代码的时候,不会像预期的那样工做,无论你聚焦在哪一个输入框,始终显示你的age信息。
缘由在于,分配给onfocus事件的函数是闭包,他们由函数定义构成,从setupHelp函数的函数做用域获取。三个闭包由循环所建立,每一个闭包具备同一个词法环境,环境中包含一个变量item.help,当onfocus的回调执行时,item.help的值也随之肯定,循环已经执行完毕,item对象已经指向了helpText列表的最后一项。解决这个问题的方法是使用更多的闭包,具体点就是提早使用一个封装好的函数:
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 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++) { (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // Immediate event listener attachment with the current value of item (preserved until iteration). } } setupHelp();
若是你不想使用闭包,你可使用ES6的let关键字
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++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
这个例子使用let代替var,因此,每一个闭包绑定了块级做用域,也就意味着不须要额外的闭包
若是闭包在实际案例中是不被容许的,在一个函数中就不必定再建立一个函数,由于这会影响脚本的性能,例如处理的速度和内存的消耗。例如,当建立一个对象,对象的方法应该跟对象的原型联系起来而不是在对象的构造器里定义,这是由于不管何时构造器被调用,方法都会被从新分配
下面一个例子
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; } };
然而,咱们不建议从新定义原型,下面的例子中,给原型分别定义方法而不是从新定义整个原型,这样会改变constructor的指向。
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; };
在前面两个例子中,继承原型能够被全部对象所共享而且在每一个对象建立的同时都没必要定义方法。