抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

一:什么是闭包

闭包是一个函数和其词法环境的组合

换个意思来说,闭包可以让开发者可以从函数内部访问到外部函数的作用域

在JS中,闭包会随着函数的创建而被同时创建

词法环境

主要分两个对象

用于管理变量函数和作用域的关系

  • 环境记录:存储变量和函数声明的地方
  • 对外部环境的引用:指向包含当前词法环境的外部词法环境的引用。这使得内部环境可以引用外部环境的变量。这种引用关系形成了作用域链

词法环境例子

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

执行init()函数后,可以成功突出name变量的值,displayName函数的词法作用域可以引用外部Init函数词法作用域中的name变量

image-20230908192823064

闭包例子:

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

var myFunc = makeFunc();
myFunc();

  • 执行makeFunc函数,创建makeFunc函数,在之前displayName函数之前将该函数返回,myFunc变量是displayName函数的实例
  • makeFunc函数创建的局部变量name,在displayName函数中被引用
  • myFunc维持了displayName函数的词法环境的一个引用,使得makeFunc函数在执行结束后,变量name仍然可用

二:使用场景

  • 创建私有变量
  • 延长变量的生命周期

如果在执行上下文中创建的函数是一个闭包,并且在闭包中引用了外部词法环境的变量,那么该词法环境不会在执行上下文销毁后立即被销毁。相反,词法环境会被保留,直到不再有任何引用指向闭包或相关的词法环境。这是因为闭包需要持续访问外部变量,所以相关的词法环境不能被销毁。

事件处理

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

使用闭包创建和分配事件处理函数

  • makeSizer函数创建闭包,makeSizer返回一个匿名函数,并接收一个参数size,匿名函数捕获了外部函数的size变量,使得即使makeSizer函数执行结束后,也使得内部函数可以访问外部size变量
  • 事件处理函数内部执行,当用户点击链接时,与链接相关的事件处理函数(闭包)被触发。这些函数内部访问了它们创建时捕获的字体大小

相比将事件处理函数直接赋值给onclick属性的区别

  • 在作用域和数据封装方面,事件处理函数被封装到一个自包含的作用域中,它可以通过捕获外部函数的size变量,而避免将该变量引入到全局作用域中,避免全局变量污染。直接设置事件处理函数,是在全局作用域去设置事件处理函数,可能会导致全局命名函数的命名冲突,和污染全局变量
  • 动态性和复用性:直接设置事件处理函数想要设置不同的字体大小需要设置三个不同的事件处理函数,而闭包实现,可以通过传递不同的size参数,然后由事件处理函数捕获外部size值进行操作赋值

使用闭包模拟私有方法

JS可以通过闭包来模拟私有方法,即该私有属性只能被该类上的其余方法调用,而无法被外界直接使用

这种方式也称为模块化

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
28
29
30
31
32
function Counter ()
{
// 私有变量
var count = 0;

// 私有方法
function increment ()
{
count++;
}

// 公共方法,可以访问私有方法和私有变量
return {
getCount: function ()
{
return count;
},
incrementAndReturnCount: function ()
{
increment();
return count;
}
}
}

var myCounter = new Counter();

console.log(myCounter.getCount()); // 输出: 0
console.log(myCounter.incrementAndReturnCount()); // 输出: 1
console.log(myCounter.getCount()); // 输出: 1
console.log(myCounter.count); //输出: undefined

  • 通过闭包模拟私有方法
    • 在Counter构造函数内部创建count私有变量和increment私有方法
    • 通过在Counter函数中返回两个公共方法getCount和incrementAndReturnCount方法,通过闭包,getCount的词法环境可以引入外部Counter的词法环境的变量
    • 因此外部函数只能通过公共方法间接访问私有方法和修改私有属性

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

在循环中创建闭包

该案例想要实现,当三个不同的文本框获得光标时候,触发事件显示对应三个不同的文本内容

1
2
3
4
5
<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
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();

在该代码中出现错误,三个文本框显示的都是同一个内容

image-20230908221700386

发现错误

在setupHelp函数中,想要循环给三个文本框添加三个不同的事件,用var关键字声明item变量,由于var声明的变量具体变量提升特性,导致item变量的作用域是函数作用域,这会使得最终三个文本框的事件处理函数中的item指向的是同一个item变量,由于最终item变量值指向helpText的最后一个内容,所以三个文本框显示的是同一个内容

使用匿名闭包处理

1
2
3
4
5
6
7
8
9
10
11
12
for (var i = 0; i < helpText.length; i++) {
(function ()
{
var item = helpText[i];
// console.log('item = ' + item.help);
document.getElementById(item.id).onfocus = function ()
{
console.log('x');
showHelp(item.help);
};
})()
}
  • 通过匿名闭包函数,将每次迭代的item变量通过匿名闭包函数包裹起来,这个闭包函数会立即执行,因此每次迭代都会产生独立的词法环境用于存储item的副本,避免了共享的问题
  • 文本框的 onfocus 事件处理函数被设置为一个闭包函数。这个闭包函数可以访问自己的局部作用域,因此它能够正确地引用其所关联的 item 变量,显示正确的帮助文本。

onfous事件接收一个闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function makeHelpCallback (help)
{
return function ()
{
console.log(help);
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);
}
}
  • makeHelpCallback 函数接受一个参数 help,然后返回一个新的函数,这个新函数就是回调函数。这个回调函数的作用是在文本框获得焦点时显示相应的帮助文本。
  • 当在循环中调用 makeHelpCallback(item.help) 时,它会根据传递给它的 help 参数的值,创建一个独立的回调函数。这个回调函数“记住”了它创建时的 help 参数的值。
  • 因为每次循环迭代都会调用 makeHelpCallback,所以每次迭代都会创建一个新的回调函数,而且这些回调函数之间是相互独立的,它们没有共享相同的内部状态。
  • 当某个文本框获得焦点时,对应的独立回调函数会执行。这个回调函数使用自己“记住”的 help 参数的值,来显示正确的帮助文本。由于每个回调函数都有自己独立的 help 参数,所以它们能够正确地显示不同的帮助文本,而不会混淆或共享数据。

使用let声明item变量

1
2
3
4
5
6
7
8
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function ()
{
console.log('x');
showHelp(item.help);
};;
}

let作用域是块级作用域,每一次迭代都会声明不同的item变量,因此每个 onfocus 事件处理函数都捕获了自己迭代中的 item

评论