闭包
什么是闭包
首先来看下 MDN(Mozilla Developer Network) 官网对于闭包这一概念的定义:
提示
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
读起来不太好理解,实际上翻译成白话文就是:在一个作用域中可以访问另一个函数内部的局部变量的函数。
下面是闭包的一个基本使用:
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
可以发现在 displayName
这个作用域下访问了另外一个函数 makeFunc
下的局部变量 name
闭包的实现,实际上是利用了 JavaScript
中作用域链的概念,简单理解就是:在 JavaScript
中,如果在某个作用域下访问某个变量的时候,如果不存在,就一直向外层寻找,直到在全局作用域下找到对应的变量为止,这里就形成了所谓的作用域链。
闭包的特性
- 闭包可以访问到父级函数的变量
- 访问到父级函数的变量不会销毁
现在来看下闭包的相关应用,首先来看下下面这段代码:
var age = 18;
function person() {
age++;
console.log(age);
}
person(); // 19
person(); // 20
person(); // 21
可以看到这里调用了 3 次函数,age
的值也从 18 增长到了 21,但是这么写会导致全局变量被污染,所以将 age
的定义移动到 person
函数内部,代码如下:
function person() {
var age = 18;
age++;
console.log(age);
}
person(); // 19
person(); // 19
person(); // 19
但是这又导致了另一个问题,变为局部变量的 age
不会自增了,所以那么就可以利用闭包的这个特性将每次调用时的 age
保存起来这样就可以实现变量的自增了,代码如下:
function person() {
var age = 18;
return function () {
age++;
console.log(age);
};
}
let getPersonAge = person();
getPersonAge(); // 19
getPersonAge(); // 20
getPersonAge(); // 21
每当调用 getPersonAge()
函数的时候,首先要获取 age
变量,因为 JavaScript
中存在作用域链的关系,所以会从 person
函数下得到对应的 age
,因为闭包存在着闭包可以访问到父级函数的变量,且该变量不会销毁的特性所以上次的变量会被保留下来,所以可以做到自增的实现。
应用场景
- 改变字体大小
我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body
元素的 font-size
,然后通过相对的 em
单位设置页面中其他元素(例如 header
)的字号:
function makeSizer(size) {
return function () {
document.body.style.fontSize = size + "px";
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;
<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>
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
- 循环注册事件
比如就可以利用闭包的特性做循环点击事件,比如下面的给输入框添加 onblur 事件:
需求:点击输入框,上面的提示栏显示对应的内容
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 func = function (i) {
// document.getElementById(helpText[i].id).onfocus = function () {
// showHelp(helpText[i].help);
// }
// };
// func(i);
(function (i) {
document.getElementById(helpText[i].id).onfocus = function () {
showHelp(helpText[i].help);
};
})(i);
}
}
setupHelp();
<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>
- 循环中的定时器
var lis = document.querySelector(".test").querySelectorAll("li");
for (var i = 0; i < lis.length; i++) {
// var fc = function (i) {
// setTimeout(function () {
// console.log(lis[i].innerHTML);
// }, 3000);
// };
// fc(i);
(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML);
}, 3000);
})(i);
}
利用立即执行函数所形成的闭包来保存当前循环中的 i
的值,进而解决异步任务所带来的 i
最后为 4(循环结束后 i 的值)的问题
- 模拟私有方法
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
存在其他变量中以便可以形成多个计数器
var counterInstance1 = Counter();
var counterInstance2 = Counter();
// c1 计数器1
console.log(counterInstance1.getValue()); // 0
counterInstance1.increment();
counterInstance1.increment();
counterInstance1.increment();
console.log(counterInstance1.getValue()); // 3
counterInstance1.decrement();
console.log(counterInstance1.getValue()); // 2
// c2 计数器2
console.log(counterInstance2.getValue()); // 0
counterInstance2.increment();
counterInstance2.increment();
console.log(counterInstance2.getValue()); // 2
counterInstance2.decrement();
counterInstance2.decrement();
counterInstance2.decrement();
console.log(counterInstance2.getValue()); // -1
性能考量
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
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() {
return this.name;
},
getMessage() {
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;
};