本文翻译自 MDN ( Mozilla Developer Network ):


读完本文你将了解到:

词法作用域

考虑如下代码:

function init() {
var name = 'Mozilla'; // name 是 init 函数创建的局部变量
function displayName() { // displayName() 是函数内部方法,一个闭包
alert(name); // 它使用了父函数声明的变量
}
displayName();
}
init();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

init() 函数创建了本地变量 name 和函数 displayName() 。

displayName() 是定义在 init() 内部的内部函数,因此只能在 init() 函数内被访问。 displayName() 没有内部变量,但是由于内部函数可以访问外部函数的变量, displayName() 可以访问 init() 中的变量 name

运行上述代码,我们可以看到 name 的值成功地被打印出来。

这是“词法作用域”(其描述了 JS 解析器如何处理嵌套函数中的变量)的一个例子。

词法作用域是指一个变量在源码中声明的位置作为它的作用域。同时嵌套的函数可以访问到其外层作用域中声明的变量。

闭包

现在看一下下面的代码:

function makeFunc() {
var name = 'Mozilla';
function displayName() {
alert(name);
}
return displayName;
} var myFunc = makeFunc();
myFunc();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行上面的代码会和第一个例子有同样的结果。不同的是 - 同时很有趣的是- 内部函数 displayName() 在执行前先被外部函数作为返回值返回了。

乍一看,这个代码虽然能执行却并不直观。在一些编程语言中,函数内的局部变量只在函数执行期间存活。一旦 makeFunc() 函数执行完毕,你可能觉得 name 变量就不能存在了。然而,从代码的运行结果来看,JavaScript 跟我们前面说到的“一些编程语言”关于变量明显有不同之处。

上面代码的“不同之处”就在于,makeFunc() 返回了一个闭包。

闭包由函数和它的词法环境组成。这个环境指的是函数创建时,它可以访问的所有变量。在上面的例子中,myFunc 引用了一个闭包,这个闭包由 displayName() 函数和闭包创建时存在的 “Mozilla” 字符串组成。由于 displayName() 持有了 name 的引用,myFunc 持有了 displayName() 的引用,因此 myFunc 调用时,name 还是处于可以访问的状态。

下面是一个更有趣的例子:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面的例子中,makeAdder() 接受一个参数 x,然后返回一个函数,它的参数是 y,返回值是 x+y。

本质上,makeAdder() 是一个函数工厂 — 为它传入一个参数就可以创建一个参数与其他值求和的函数。

上面的例子中我们使用函数工厂创建了两个函数,一个将会给参数加 5,另一个加 10。

add5 和 add10 都是闭包。他们使用相同的函数定义,但词法环境不同。在 add5 中,x 是 5;add10 中 x 是 10。

闭包实战场景之回调

闭包有用之处在于它可以将一些数据和操作它的函数关联起来。这和面向对象编程明显相似。在面对象编程中,我们可以将某些数据(对象的属性)与一个或者多个方法相关联。

因此,当你想只用一个方法操作一个对象时,可以使用闭包。

在 web 编程时,你使用闭包的场景可能会很多。大部分前端 JavaScript 代码都是“事件驱动”的:我们定义行为,然后把它关联到某个用户事件上(点击或者按键)。我们的代码通常会作为一个回调(事件触发时调用的函数)绑定到事件上。

比如说,我们想要为一个页面添加几个用于调整字体大小的按钮。一种方法是以像素为单位指定 body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如页眉)的字号。

这里是 CSS 代码:

body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
} h1 {
font-size: 1.5em;
} h2 {
font-size: 1.2em;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我们修改字体尺寸的按钮可以修改 body 元素的 font-size 属性,而由于我们使用相对单位,页面中的其它元素也会相应地调整。

HTML 代码:

    <p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2> <a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

JavaScript 代码:

function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
} var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

size12, size14, 和 size16 现在可以分别调整 body 的字体到 12, 14, 16 像素。我们接下来可以把它们绑定到按钮上:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

现在分别点击几个按钮,整个页面的字体都会调整。

用闭包模拟私有方法

一些编程语言,比如 Java,可以创建私有方法(只能被同一个类中的其他方法调用的方法)。

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;
}
};
})(); console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

之前的例子中,每个闭包都有其独自的词法环境。但是这个例子中,三个方法 counter.value()counter.increment() 和 counter.decrement() 共享一个词法环境。

这个共享的环境创建于一个匿名函数体内,该函数一经定义就立刻执行。环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。 它俩都无法在匿名函数外部直接访问。必须通过匿名包装器返回的对象的三个公共函数访问。

多亏了 JavaScript 的词法作用域,这三个函数可以访问 privateCounter 和 changeBy(),使得它们三个闭包共享一个环境。

你可能注意到,上述代码中我们在匿名函数中创建了 privateCounter,然后立即执行了这个函数,为 privateCounter 赋了值,然后将结果返回给 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();
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 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

现在两个计数器 counter1, counter2 持有不同的词法环境,它俩有各自的 privateCounter 与值。调用其中一个计数器,不会影响另一个的值。

这样使用闭包可以提供很多面向对象编程里的好处,比如数据隐藏和封装。

常见的错误:在循环中创建闭包

在 ECMAScrpit 2015 以前,还没有 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>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
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'}
]; for (var i = 0; i < helpText.length; i++) {
var item = helpText[i]; //var 声明的变量,它的作用域在函数体内,而不是块内
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
} setupHelp();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代码中,helpText 是三个 id 与提示信息关联对象的数组。在循环中,我们遍历了 helpText 数组,为数组中的 id 对应的组件添加了聚焦事件的响应。

如果你运行上面的代码,就会发现,不论你选择哪个输入框,最终显示的提示信息都是 “Your age”。

原因就是:我们赋值给 onfocus 事件的是三个闭包。这三个闭包由函数和 setUpHelp() 函数内的环境组成。

循环中创建了三个闭包,但是它们都使用了相同的词法环境 item,item 有一个值会变的变量 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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这样运行结果就正确了。不像前面的例子,三个回调共享一个词法环境,上面的代码中,使用 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);
}
})(); // 立即调用绑定函数,使用正确的值绑定到事件上;而不是使用循环结束的值
}
} setupHelp();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如果你不想使用更多的闭包,也可以使用 ES2015 中介绍的块级作用域 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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上面的代码使用 let 而不是 var 修饰了变量 item,因此每个闭包绑定的是当前块内的变量。不需要额外的闭包。

注意性能

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

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

举个例子:

function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
}; this.getMessage = function() {
return this.message;
};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面的代码没有利用闭包的优点,我们可以把它改写成这样:

function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然而一般来说,不建议重定义原型。

下面的代码将属性添加到已有的原型上:

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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

但是,我们还可以将上面的代码简写成这样:

function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
(function() {
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}).call(MyObject.prototype);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在前面的三个示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时重新定义方法。

JavaScript 的闭包用于什么场景的更多相关文章

  1. JavaScript的闭包是什么意思以及作用和应用场景

    JavaScript闭包 1.什么是闭包 百度百科对于闭包的解释是:闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中 ...

  2. javascript之闭包理解以及应用场景

    半个月没写博文了,最近一直在弄小程序,感觉也没啥好写的. 之前读了js权威指南,也写了篇博文,但是实话实说当初看闭包确实还是一头雾水.现在时隔一个多月(当然这一段时间还是一直有在看闭包的相关知识)理解 ...

  3. 对JavaScript中闭包的理解

    在前端开发中闭包是一个很重要的知识点,是面试中一定会被问到的内容.之前我对闭包的理解主要是"通过闭包可以在函数外部能访问到函数内部的变量",对闭包运用的也很少,甚至自己写过闭包自己 ...

  4. 在JavaScript中闭包的作用和简单的用法

    在JavaScript中闭包的作用和简单的用法 一.闭包的简介 作用域链:在js中只有函数有作用域的概念,由于函数内能访问函数外部的数据,而函数外部不能访问函数内部的数据,由上述形成一种作用域访问的链 ...

  5. JavaScript中闭包的使用和各种继承介绍

    一.什么是闭包?     (1)闭包的概念:a.闭包就是函数嵌套时,让局部变量变成自由变量的环境,是一种让局部变量进化的方式.                 b.定义在一个函数内部的函数.      ...

  6. 转:对JavaScript中闭包的理解

    关于 const     let      var  总结: 建议使用 let  ,而不使用var,如果要声明常量,则用const. ES6(ES2015)出现之前,JavaScript中声明变量只有 ...

  7. 深入理解JavaScript的闭包特性如何给循环中的对象添加事件

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  8. 深入理解javascript的闭包

    闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...

  9. 如何给循环中的对象添加事件--深入理解JavaScript的闭包特性

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

随机推荐

  1. js如何弹出新窗口

    js如何弹出新窗口 时间:2012-4-22 弹出新窗口也是在网页设计中会经常用到的,其用法也很简单,是通过调用javascript的内置函数windows.open来产生的.  window.ope ...

  2. 愚蠢的LCAAAAA~~~~(>_<)~~~~

    很愤怒!特别愤怒!超级愤怒!!! 我LCA居然错了!!而且是那种特别愚蠢的错误 我把代码都交错了!!! silasila 话不多说,代码上特别详细了 #include<bits/stdc++.h ...

  3. bzoj 2324 ZJOI 营救皮卡丘 费用流

    题的大概意思就是给定一个无向图,边有权值,现在你有k个人在0点,要求走到n点,且满足 1:人们可以分头行动,可以停在某一点不走了 2:当你走到x时,前x-1个点必须全部走过(不同的人走过也行,即分两路 ...

  4. 【Shell 编程基础第一部分】第一个Shell脚本HelloShell及一些简单的Shell基础书写与概念;

    http://blog.csdn.net/xiaominghimi/article/details/7603000 本站文章均为李华明Himi原创,转载务必在明显处注明:转载自[黑米GameDev街区 ...

  5. linux 链表实例应用程序【转】

    转自:http://blog.csdn.net/echo_qiang/article/details/6233057 /**************************************** ...

  6. a标签里文本居中

    text-align:center; height: 30px; line-height:30px;

  7. [设计模式-行为型]访问者模式(Vistor)

    一句话 表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作. 概括

  8. java callable future futuretask

    Runnbale封装一个异步运行的任务,可以把它想象成一个没有任何参数和返回值的异步方法.Callable和Runnable相似,但是它有返回值.Callable接口是参数化的类型,只有一个方法cal ...

  9. sourceforge的FTP镜像

    https://www.mirrorservice.org/sites/ftp.sourceforge.net/

  10. [thinkPHP] buildSql可以查看tp CURD操作对应的SQL

    $goods = M('Goods')->where($map)->buildSql(); echo $goods;