OP 要求对整体概念进行详细解释。这里的尝试是描述发生闭包所必需的核心元素。
我认为与来自 javascriptissexy 的示例的混淆部分在于,这些函数的名称并不能清楚地代表它们应该做什么,尤其是对于不熟悉 javascript 或一般新编码的人来说。
让我们从范围开始:
在 Javascript 中,每个函数都会创建自己的本地范围或内存空间。这称为词法作用域。此内存空间存储函数参数中的所有变量以及函数体中(大括号内)声明的变量和表达式。
从 javascriptissexy 的示例中可以看出,我们可以嵌套函数。由于每个函数都创建了自己的本地范围,我们需要了解这些范围如何相互关联和交互。范围可以具有三种不同类型的关系。
我鼓励您在浏览器开发控制台中测试所有这些代码 sn-ps:
子作用域可以访问其父母(以及祖父母、曾祖父母等)的作用域变量
function parent() {
var parentAsset = 'The Minivan'
function child() {
//child has access to parent asset
console.log(parentAsset);
}
// call child function
child();
}
parent(); // 'The Minivan'
父作用域无权访问其子作用域变量
function parent() {
function child() {
var childAsset = 'Mp3 Player'
}
//parent can't get childAsset
console.log(childAsset);
}
parent(); // ReferenceError childAsset not defined
同级作用域不能访问彼此的作用域变量
function childOne() {
var childOneAsset = 'Iphone'
}
function childTwo() {
console.log(childOneAsset);
}
childTwo(); // ReferenceError childOneAsset not defined
好的,回到OP提到的函数。让我们尝试用更好的名称重新制作这个函数。我正在向第一个示例函数添加一个变量以显示一个点。
在下面的示例中,当您调用 getFirstName('Michael') 时,会发生以下 4 件事:
- 在此函数中,变量
firstName 设置为
“迈克尔”
- var
nameIntro 将其设置为值“此名人
是“
- var
unusedString 设置为值“此字符串将被垃圾回收”
- 函数
introduceCelebrity被声明
-
返回函数introduceCelebrity
function getFirstName (firstName) {
var nameIntro = "This celebrity is ";
var unusedString = "This string will be garbage collected";
function introduceCelebrity (lastName) {
return nameIntro + firstName + " " + lastName;
}
return introduceCelebrity;
}
var mjName = getFirstName('Michael');
你可能已经知道了。
这里有一些有趣的事情需要注意:
-
getFirstName 函数对firstName 或nameIntro 除了设置它们的值之外没有任何作用。所以那里没有魔法。
- 子函数
introduceCelebrity 引用这两个变量。如前所述,它可以做到这一点,因为子作用域可以访问父作用域变量。这是关闭的第一个重要步骤。
- 然后返回
introduceCelebrity 函数(但不执行),大概是为了我们可以在以后调用它。这是关闭的第二步。
- 因为
introduceCelebrity 引用父范围变量,并且我们返回整个函数,所以即使在getFirstName 函数返回之后,javascript 运行时也会维护指向这些变量的指针。
- 由于存在该指针,垃圾收集器将不理会这些变量。如果这些指针不存在,垃圾收集器就会通过并清理这些内存地址,而这些值将无法访问。
-
unusedString 变量在子函数中没有被引用,因此它被垃圾回收并且不再可用。
那么让我们再看一遍代码:
function getFirstName (firstName) {
var nameIntro = "This celebrity is ";
function introduceCelebrity (theLastName) {
return nameIntro + firstName + " " + theLastName;
}
return introduceCelebrity;
}
var mjName = getFirstName('Michael');
当这段代码执行时,我们基本上是这样做的:
var mjName = function(theLastName) {
return nameIntro + firstName + " " + theLastName;
}
这有什么特别之处?关闭在哪里?
因为我们的getFirstName 函数已经被执行,我们可能会认为整个事情已经连同它的局部变量或资产一起消失了。 这是不正确的。
我们通过在子函数中引用父作用域变量并返回子函数来创建闭包。真的,上面代码的新范围实际上看起来更像这样:
var nameIntro = "This celebrity is ";
var firstName = "Michael"
var mjName = function(theLastName) {
return nameIntro + firstName + " " + theLastName;
}
了解我们现在如何使用nameIntro 和firstName? 那是因为我们创造了闭包。
所以现在我们调用mjName:
mjName('Jackson'); // 'This celebrity is Michael Jackson'
我们得到了预期的结果。
等等,最后一件事!
为了真正深入人心,让我们将我们的示例与稍作修改的示例进行比较。
注意原来的函数是嵌套的。 闭包只发生在嵌套函数中。
function getFirstName (firstName) {
var nameIntro = "This celebrity is ";
function introduceCelebrity (theLastName) {
return nameIntro + firstName + " " + theLastName;
}
return introduceCelebrity;
}
var mjName = getFirstName('Michael');
让我们尝试移除那个嵌套:
function getFirstName (firstName) {
var nameIntro = "This celebrity is ";
}
function introduceCelebrity (theLastName) {
return nameIntro + firstName + " " + theLastName;
}
var mjName = getFirstName('Michael');
introduceCelebrity('Jackson');
// ReferenceError: nameIntro is not defined
这行得通吗?
不,不会。因为兄弟作用域不能访问彼此的变量。
那么我们如何在没有闭包的情况下让它工作呢?
-
getFirstName 必须返回带有我们变量的对象或数组
- 我们必须将
getFirstName('Michael')设置为一个全局变量mjName
-
调用introduceCelebrity('Jackon'),传入我们mjName的值
function getFirstName (firstName) {
var nameIntro = "This celebrity is ";
return {
firstName: firstName,
nameIntro: nameIntro
}
}
var mjName = getFirstName('Michael'); // returns our object
function introduceCelebrity (theLastName, firstName, nameIntro) {
return nameIntro + firstName + " " + theLastName;
}
introduceCelebrity('Jackson', mjName.firstName, mjName.nameIntro);
// 'This celebrity is Michael Jackson'