JavaScript闭包原理与用法学习笔记
本文实例讲述了javascript闭包原理与用法。分享给大家供大家参考,具体如下:
闭包(closure)
闭包是一个函数和词法环境的组合,函数声明在这个词法环境中。
- 词法作用域:
看下面的一个例子:
function init() { var name = 'gaopian'; // name是局部变量 function displayname() { //displayname();是内部函数,一个闭包 alert(name); // 使用外部函数声明的变量 } displayname(); } init();
init()创建了一个局部变量name和一个函数displayname()。
函数displayname()是一个已经定义在init()内部的函数,并且只能在函数init()里面才能访问得到。
函数displayname()没有自己的局部变量,但由于内部函数可以访问外部函数变量,displayname()可以访问到声明在外部函数init()的变量name,如果局部变量还存在的话,displayname()也可以访问他们。
- 闭包
看下面一个例子
function makefunc() { debugger var name = 'gaopian'; function displayname() { alert(name); } return displayname; } var myfunc = makefunc(); myfunc();
运行这段代码和之前init()的方法的效果是一样。
经过debugger一遍之后发现:
二者不同之处是,displayname()在执行之前,这个内部方法是从外部方法返回来的。
首先,代码还是会正确运行,在一些编程语言当中,一个函数内的局部变量只存在于该函数的执行期间,随后会被销毁,一旦makefunc()函数执行完毕的话,变量名就不能够被获取,但是,由于代码仍然正常执行,这显然在js里是不会这样的。这是因为函数在js里是以闭包的形式出现的。
闭包是一个函数和词法作环境的组合,词法环境是函数被声明的那个作用域,这个执行环境包括了创建闭包时同一创建的任意变量,即创建的这个函数和这些变量处于同一个作用域当中。在这个例子当中,myfunc()是displayname()的函数实例,makefunc创建的时候,displayname随之也创建了。displayname的实例可以获得词法作用域的引用,在这个词法作用域当中,存在变量name,对于这一点,当myfunc调用的话,变量name,仍然可以被调用,因此,变量'gaopian'传递给了alert函数。
这里还有一个例子
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进行加法运算——add5,一个与10进行加法运算——add10。add5和add10都是闭包,他们共享相同的函数定义,但却存储着不同的词法环境,在add5的词法环境当中,x为5;在add10的词法环境当中,x变成了10。
- 闭包的实践
闭包是很有用的,因为他让我们把一些数据(词法环境)和一些能够获取这些数据的函数联系起来,这有点和面向对象编程类似,在面向对象编程当中,对象让我们可以把一些数据(对象的属性)和一个或多个方法联系起来。
因此,你能够像对象的方法一样随时使用闭包。实际上,大多数的前端js代码都是事件驱动性的:我们定义一些事件,当这个事件被用户所触发的时候(例如用户的点击事件和键盘事件),我们的事件通常会带上一个回调:即事件触发所执行的函数。举个栗子,假设我们希望在页面上添加一些按钮,这些按钮能够调整文字的大小,实现这个功能的方式是确定body的字体大小,然后再设置页面上其他元素(例如标题)的字体大小,我们使用em作为单位。
<style> body { font-family: helvetica, arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } </style>
我们设置的调节字体大小的按钮能够改变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的函数,我们可以把他们绑定在按钮上。
<button id="size-12">12</button> <button id="size-14">14</button> <button id="size-16">16</button>
document.getelementbyid('size-12').onclick = size12; document.getelementbyid('size-14').onclick = size14; document.getelementbyid('size-16').onclick = size16;
通过闭包来封装私有方法:类似java语言能够声明私有方法,意味着只能够在相同的类里面被调用,js无法做到这一点,但却可以通过闭包来封装私有方法。私有方法不限制代码:他们提供了管理命名空间的一种强有力方式。
下面代码阐述了怎样使用闭包来定义公有函数,公有函数能够访问私有方法和属性。
var counter = (function () { debugger; 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());// 0 counter.increment(); counter.increment(); console.log(counter.value());// 2 counter.decrement(); console.log(counter.value()); // 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,所以,每个闭包绑定了块级作用域,也就意味着不需要额外的闭包。
感兴趣的朋友可以使用在线html/css/javascript代码运行工具:http://tools.jb51.net/code/htmljsrun测试上述代码运行效果。