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

  • 原文地址:MDN
  • 译文地址:shixinzhang 的博客

读完本文你将了解到:

  • 词法作用域
  • 闭包
  • 闭包实战场景之回调
  • 用闭包模拟私有方法
  • 常见的错误在循环中创建闭包
  • 注意性能

词法作用域

考虑如下代码:

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

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();

运行上面的代码会和第一个例子有同样的结果。不同的是 - 同时很有趣的是- 内部函数 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

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

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

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

add5add10 都是闭包。他们使用相同的函数定义,但词法环境不同。在 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;
}

我们修改字体尺寸的按钮可以修改 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>

JavaScript 代码:

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 现在可以分别调整 body 的字体到 12, 14, 16 像素。我们接下来可以把它们绑定到按钮上:

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

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

用闭包模拟私有方法

一些编程语言,比如 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

之前的例子中,每个闭包都有其独自的词法环境。但是这个例子中,三个方法 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 */

现在两个计数器 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>
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();

上述代码中,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();

这样运行结果就正确了。不像前面的例子,三个回调共享一个词法环境,上面的代码中,使用 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();

如果你不想使用更多的闭包,也可以使用 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();

上面的代码使用 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;};
}

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

function MyObject(name, message) {this.name = name.toString();this.message = message.toString();
}
MyObject.prototype = {getName: function() {return this.name;},getMessage: function() {return this.message;}
};

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

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

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;
};

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

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);

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

JavaScript 的闭包用于什么场景相关推荐

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

    1 function fn(){2 var a = 0;3 return function (){4 return ++a;5 } 6 } 如上所示,上面第一个return返回的就是一个闭包,那么本质 ...

  2. 深入理解javascript的闭包

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

  3. Javascript的闭包及其使用技巧实例

    Javascript的闭包及其使用技巧实例 一.闭包的基本概念 闭包(Closure)是一个引用了自由变量的函数,记录了该函数在定义时的scope chain.又称词法闭包(Lexical Closu ...

  4. [转]在Javascript中闭包(Closure)

    转载自: http://baike.baidu.com/view/648413.htm 一.什么是闭包? "官方"的解释是:所谓"闭包",指的是一个拥有许多变量 ...

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

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

  6. 在Javascript中闭包(Closure)

    一.什么是闭包? "官方"的解释是:所谓"闭包",指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 相 ...

  7. mysql闭包的概念_彻底搞懂JavaScript的闭包、防抖跟节流

    最近出去面试了一下,收获颇多!!! 以前的我,追求实际,比较追求实用价值,然而最近面试,传说中的面试造火箭,工作拧螺丝,竟然被我遇到了.虽然很多知识点在实际工作中并不经常用到,但人家就是靠这个来筛选人 ...

  8. JavaScript中闭包实现的私有属性的getter()和setter()方法

    注意: 以下的输出都在浏览器的控制台中 <!DOCTYPE html> <html> <head><meta charset="utf-8" ...

  9. python闭包的应用场景_简单谈谈Python中的闭包

    Python中的闭包 前几天又有人留言,关于其中一个闭包和re.sub的使用不太清楚.我在脚本之家搜索了下,发现没有写过闭包相关的东西,所以决定总结一下,完善Python的内容. 1. 闭包的概念 首 ...

最新文章

  1. pcb怎么画边框_关于PCB焊盘,你了解多少?
  2. python 网络聊天客户端
  3. 【转】完美解决Asp.Net的MasterPage中添加JavaScript路径问题
  4. Windows Server Backup 备份活动目录
  5. 关于Angular使用http发送请求后的响应处理
  6. 机器人电焊电流电压怎么调_【华光】HG1000型电焊机现场校准仪
  7. TreeCtrl 查找功能的最简单实现
  8. 生信老司机以中心法则为主线讲解组学技术的应用和生信分析心得—限时免费
  9. 聚类算法的缺点_常用聚类算法
  10. thinkpad bios联想logo_最强12吋ThinkPad,X201终极改造:8代酷睿+双内存+NVMe
  11. 员工转正述职答辩问什么问题_展风采 创未来 | 记德信地产杭州公司新员工转正述职答辩...
  12. c52单片机控制l298n步进电机角度_【设计图文】单片机实现的步进电机控制系统(开题报告+论文+文献综述+外文翻译+DWG图纸)...
  13. 读jQuery源码释疑笔记3
  14. 上层应用开发是否真的没有底层开发有前途?
  15. C语言——函数的综合运用。自定义函数,gotoxy清屏函数与HideCursor隐藏光标,防闪屏,共同制作打飞机游戏。
  16. xy苹果助手未受信任_经过苹果企业签名的应用该如何安装
  17. 一种绘制有向图的方法<TSE93> - 1. 引言
  18. 微信自定义分享功能二次封装
  19. 使用photoshop 修复旧照片
  20. 打开和写入文件( fopen和fopen_s

热门文章

  1. c++控制台五子棋(简单)
  2. jpg转换成pdf格式?怎么把图片jpg转换成pdf格式?
  3. 今天终于可以静下心来搞学习了
  4. 软件开发 签合同问题 需要注意什么
  5. [CS231n Assignment 2 #04 ] 卷积神经网络(Convolutional Networks )
  6. PCAN-Explorer使用教程
  7. Adobe Magento 2 最新认证证书考前准备,考试过程和注意事项
  8. 云e办前端学习笔记(十一)员工基本资料管理
  9. 企业购买CRM时需要注意哪些要素
  10. 翻译D6(附AC码 POJ 05:Grocery Problem)