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数组中对应的字符串,使用匿名闭包来重写的例子如下:

  1. 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测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

以上是 JavaScript闭包原理与用法学习笔记 的全部内容, 来源链接: utcz.com/z/339113.html

回到顶部