闭包(Closure)
闭包是一个函数和词法环境的组合, 函数声明在这个词法环境中
词法作用域
看下面一个例子
- function init() {
- var name = 'Mozilla'; // name 是局部变量
- function displayName() { // displayName()是内部函数, 一个闭包
- alert(name); // 使用外部函数声明的变量
- }
- displayName();
- }
- init();
init()创建了一个局部变量 name 和一个函数 displayName()函数 displayName()是一个已经定义在 init()内部的函数, 并且只能在函数 init()里面才能访问得到函数 displayName()没有自己的局部变量, 但由于内部函数可以访问外部函数变量, displayName()可以访问到声明在外部函数 init()的变量 name, 如果局部变量还存在的话, displayName()也可以访问他们
闭包
看下面一个例子
- function makeFunc() {
- var name = 'Mozilla';
- function displayName() {
- alert(name);
- }
- return displayName;
- }
- var myFunc = makeFunc();
- myFunc();
运行这段代码你会发现和之前 init()的方法是一样的效果, 但不同之处是, displayName()在执行之前, 这个内部方法是从外部方法返回来的 首先, 代码还是会正确运行, 在一些编程语言当中, 一个函数内的局部变量只存在于该函数的执行期间, 随后会被销毁, 一旦 makeFunc()函数执行完毕的话, 变量名就不能够被获取, 但是, 由于代码仍然正常执行, 这显然在 JS 里是不会这样的这是因为函数在 JS 里是以闭包的形式出现的, 闭包是一个函数和词法作环境的组合, 词法环境是函数被声明的那个作用域, 这个执行环境包括了创建闭包时同一创建的任意变量, 即创建的这个函数和这些变量处于同一个作用域当中在这个例子当中, myFunc()是 displayName()的函数实例, makeFunc 创建的时候, displayName 随之也创建了 displayName 的实例可以获得词法作用域的引用, 在这个词法作用域当中, 存在变量 name, 对于这一点, 当 myFunc 调用的话, 变量 name, 仍然可以被调用, 因此, 变量'Mozilla'传递给了 alert 函数
这里还有一个例子 - 一个 makeAdder 函数
- 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 进行加法运算, 一个与 10 进行加法运算 add5 和 add10 都是闭包, 他们共享相同的函数定义, 但却存储着不同的词法环境, 在 add5 的词法环境当中, x 为 5; 在 add10 的词法环境当中, x 变成了 10
闭包的实践
闭包是很有用的, 因为他让你把一些数据 (词法环境) 和一些能够获取这些数据的函数联系起来, 这有点和面向对象编程类似, 在面向对象编程当中, 对象让我们可以把一些数据 (对象的属性) 和一个或多个方法联系起来 因此, 你能够像对象的方法一样随时使用闭包实际上, 大多数的前端 JS 代码都是事件驱动性的 - 我们定义一些事件, 当这个事件被用户所触发的时候 (例如用户的点击事件和键盘事件), 我们的事件通常会带上一个回调: 即事件触发所执行的函数例如, 假设我们希望在页面上添加一些按钮, 这些按钮能够调整文字的大小, 实现这个功能的方式是确定 body 的字体大小, 然后再设置页面上其他元素(例如标题) 的字体大小, 我们使用 em 作为单位
- body {
- font-family: Helvetica, Arial, sans-serif;
- font-size: 12px;
- }
- h1 {
- font-size: 1.5em;
- }
- h2 {
- font-size: 1.2em;
- }
我们设置的调节字体大小的按钮能够改变 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 的函数, 我们可以把他们绑定在按钮上
- document.getElementById('size-12').onclick = size12;
- document.getElementById('size-14').onclick = size14;
- document.getElementById('size-16').onclick = size16;
- <a href="#" id="size-12">12</a>
- <a href="#" id="size-14">14</a>
- <a href="#" id="size-16">16</a>
通过闭包来封装私有方法
类似 JAVA 语言能够声明私有方法, 意味着只能够在相同的类里面被调用, JS 无法做到这一点, 但却可以通过闭包来封装私有方法私有方法不限制代码: 他们提供了管理命名空间的一种强有力方式 下面代码阐述了怎样使用闭包来定义公有函数, 公有函数能够访问私有方法和属性
- 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
在先前的例子当中, 每个闭包具有他们自己的词法环境, 在这个例子中, 我们创建了一个单独的词法环境, 这个词法环境被 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, 所以, 每个闭包绑定了块级作用域, 也就意味着不需要额外的闭包
性能考虑
如果闭包在实际案例中是不被允许的, 在一个函数中就不一定再创建一个函数, 因为这会影响脚本的性能, 例如处理的速度和内存的消耗例如, 当创建一个对象, 对象的方法应该跟对象的原型联系起来而不是在对象的构造器里定义, 这是因为无论什么时候构造器被调用, 方法都会被重新分配
下面一个例子
- 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;
- }
- };
然而, 我们不建议重新定义原型, 下面的例子中, 给原型分别定义方法而不是重新定义整个原型, 这样会改变 constructor 的指向
- 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;
- };
在前面两个例子中, 继承原型可以被所有对象所共享并且在每个对象创建的同时都不必定义方法
参考
MDN closure
来源: https://juejin.im/post/5aa90e5ef265da239f0713a1