JavaScript 的闭包用于什么场景
本文翻译自 MDN ( Mozilla Developer Network ):
- 原文地址:MDN
- 译文地址:shixinzhang 的博客
读完本文你将了解到:
词法作用域
考虑如下代码:
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 的闭包用于什么场景的更多相关文章
- JavaScript的闭包是什么意思以及作用和应用场景
JavaScript闭包 1.什么是闭包 百度百科对于闭包的解释是:闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中 ...
- javascript之闭包理解以及应用场景
半个月没写博文了,最近一直在弄小程序,感觉也没啥好写的. 之前读了js权威指南,也写了篇博文,但是实话实说当初看闭包确实还是一头雾水.现在时隔一个多月(当然这一段时间还是一直有在看闭包的相关知识)理解 ...
- 对JavaScript中闭包的理解
在前端开发中闭包是一个很重要的知识点,是面试中一定会被问到的内容.之前我对闭包的理解主要是"通过闭包可以在函数外部能访问到函数内部的变量",对闭包运用的也很少,甚至自己写过闭包自己 ...
- 在JavaScript中闭包的作用和简单的用法
在JavaScript中闭包的作用和简单的用法 一.闭包的简介 作用域链:在js中只有函数有作用域的概念,由于函数内能访问函数外部的数据,而函数外部不能访问函数内部的数据,由上述形成一种作用域访问的链 ...
- JavaScript中闭包的使用和各种继承介绍
一.什么是闭包? (1)闭包的概念:a.闭包就是函数嵌套时,让局部变量变成自由变量的环境,是一种让局部变量进化的方式. b.定义在一个函数内部的函数. ...
- 转:对JavaScript中闭包的理解
关于 const let var 总结: 建议使用 let ,而不使用var,如果要声明常量,则用const. ES6(ES2015)出现之前,JavaScript中声明变量只有 ...
- 深入理解JavaScript的闭包特性如何给循环中的对象添加事件
初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...
- 深入理解javascript的闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...
- 如何给循环中的对象添加事件--深入理解JavaScript的闭包特性
初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...
随机推荐
- ORACLE中根据生日得到年龄
create or replace function F_GETAGE(dateofbirth date) return varchar2 is begin ) then ); else ) then ...
- Spring - IoC(12): 属性占位符
使用属性占位符可以将 Spring 配置文件中的部分元数据放在属性文件中设置,这样可以将相似的配置(如 JDBC 的参数配置)放在特定的属性文件中,如果只需要修改这部分配置,则无需修改 Spring ...
- [bzoj3238][Ahoi2013]差异——后缀自动机
Brief Description Algorithm Design 下面给出后缀自动机的一个性质: 两个子串的最长公共后缀,位于这两个串对应的状态在parent树上的lca状态上.并且最长公共后缀的 ...
- bzoj 1026 DP,数位统计
2013-11-20 08:11 原题传送门http://www.lydsy.com/JudgeOnline/problem.php?id=1026 首先我们用w[i,j]表示最高位是第i位,且是j的 ...
- MySQL 查询语句练习2
创建表 /* Navicat MySQL Data Transfer Source Server : localhost_3306 Source Server Version : 50719 Sour ...
- [Leetcode Week2]Sort Colors
Sort Colors题解 原创文章,拒绝转载 题目来源:https://leetcode.com/problems/sort-colors/description/ Description Give ...
- [转载]基于Redis的Bloomfilter去重(附Python代码)
前言: “去重”是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大.去重需要考虑两个点:去重的数据量.去重速度.为了保持较快的去重速度,一般选择在内存中进行去重. 数据量不大时 ...
- 【python】抄写大神的百度贴吧代码
原文链接:http://cuiqingcai.com/993.html 划重点: 1.提取帖子内容时,对图片,贴吧自动增加的超链接,制表符,换行符要做删除或替换处理 2.decode是把bytes转换 ...
- KVM(七)使用 libvirt 做 QEMU/KVM 快照和 Nova 实例的快照
本文将梳理 QEMU/KVM 快照相关的知识,以及在 OpenStack Nova 中使用 libvirt 来对 QEMU/KVM 虚机做快照的过程. 1. QEMU/KVM 快照 1.1 概念 QE ...
- mysql 保留点
例子如下: 在ticket表中先删除trainID=868的数据,设置一个保留点,然后插入一行数据,发现在插入数据插错了,这个时候我们的保留点就可以排上用场了,即rollback到保留点,而不是直接r ...