闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建
案例一:
function init() {
var name = "Mozilla";
function displayName() {
alert(name);
}
displayName();
}
init();
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
这两段代码的区别在于闭包的使用方式和生命周期。
在第一段代码中:
init()
函数内部定义了一个局部变量name
,并且在函数内部声明了另一个函数displayName()
。- 在
init()
函数中,直接调用了displayName()
函数,导致displayName()
函数只执行了一次,并且在init()
函数执行完毕后就被释放了。
而在第二段代码中:
makeFunc()
函数内部定义了一个局部变量name
,并且在函数内部声明了另一个函数displayName()
。makeFunc()
函数返回了displayName()
函数的引用,并赋值给了变量myFunc
。- 在调用
myFunc()
时,实际上是调用了displayName()
函数。 - 在
displayName()
函数内部,它引用了外部函数makeFunc()
中的变量name
。 - 因为
makeFunc()
返回的是displayName()
函数本身,所以displayName()
函数被保存下来,并且可以在后续的调用中继续访问和操作外部环境中的变量name
。这符合闭包的定义,因此displayName()
函数是一个闭包。
因此,第二段代码中的函数可以被保存并多次调用。
案例二:
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();
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();
第一段代码:
运行这段代码后,你会发现它没有达到想要的效果。无论焦点在哪个 input
上,显示的都是关于年龄的信息。
onfocus
的是闭包。这些闭包是由他们的函数定义和在 setupHelp
作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量 item。这是因为变量 item
使用 var
进行声明,由于变量提升,所以具有函数作用域。当 onfocus
的回调执行时,item.help
的值被决定。由于循环在事件触发之前早已执行完毕,变量对象 item
(被三个闭包所共享)已经指向了 helpText
的最后一项。
第二段代码:
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback
函数为每一个回调创建一个新的词法环境。在这些环境中,help
指向 helpText
数组中对应的字符串。
案例三
function peoper1(){
let name = 'xx'
function print(){
console.log(name);
}
print()
}
function print(name){
console.log(name);
}
function peoper2(){
let name = 'xx'
print(name)
}
在内存占用和性能方面,peoper1()
和 peoper2()
的差异主要取决于函数调用的方式和对内存的使用。
-
对于
peoper1()
:peoper1()
内部定义了一个函数print()
,这个函数是一个闭包,可以访问外部函数中的局部变量name
。- 在执行
peoper1()
时,会额外占用一些内存来存储闭包函数print()
。 - 由于闭包函数需要访问外部变量,可能会在一定程度上影响性能,因为涉及到作用域链的查找和变量访问。
-
对于
peoper2()
:peoper2()
直接调用全局范围内定义的print()
函数,将局部变量name
作为参数传递给该函数。- 在执行
peoper2()
时,不需要额外存储闭包函数,因此内存占用可能相对较少。 - 由于直接传递参数给函数,可能在一定程度上提高性能,避免了闭包函数的作用域链查找过程。
总体而言,在内存占用方面,peoper2()
可能略优于 peoper1()
,因为它没有存储闭包函数。在性能方面,由于 peoper2()
直接传递参数给函数,可能稍微优于 peoper1()
,但这种差异通常是微不足道的,除非代码运行在高频率或大规模数据处理的情况下。在实际应用中,通常不会因为这种微小的差异而选择一个函数设计而取舍。