头像网站模板baidu百度首页官网
本文由Tim Severien和Michaela Lehr进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
要说有很多关于闭包的文章被轻描淡写了。 大多数人将解释闭包的定义,通常将其归结为一个简单的句子:闭包是一种记住其创建环境的函数。 但它是如何记得? 为什么闭包在局部变量超出范围后很长时间仍可以使用局部变量? 为了消除围绕闭合的魔幻面纱,我将假装JavaScript 没有闭合并且不能嵌套函数,然后我们将从头开始重新实现闭合。 通过这样做,我们将发现真正的闭包以及它们在后台的工作方式。
对于本练习,我还需要假装JavaScript具有它实际上没有的功能。 我需要假装一个普通对象可以被当作函数来调用。 您可能已经用其他语言查看过此功能。 Python使您可以定义__call__
方法,而PHP具有特殊的__invoke
方法,当对象被调用时就好像是函数一样执行这些方法。 如果我们假装JavaScript也具有此功能,则可能是这样:
// An otherwise ordinary object with a "__call__" method
let o = {n: 42,__call__() {return this.n;}
};// Call object as if it were a function
o(); // 42
这里我们有一个普通的对象,我们假装可以调用它,就像它是一个函数一样,当我们这样做时,将执行特殊的__call__
方法,就像编写o.__call__()
。
这样,我们现在来看一个简单的闭包示例。
function f() {// This variable is local to "f"// Normally it would be destroyed when we leave "f"'s scopelet n = 42;// An inner function that references "n"function g() {return n;}return g;
}// Get the "g" function created by "f"
let g = f();// The variable "n" should be destroyed by now, right?
// After all, "f" is done executing and we've left its scope
// So how can "g" still reference a freed variable?
g(); // 42
在这里,我们有一个带有局部变量的外部函数f
和一个引用f
的局部变量的内部函数g
。 然后,我们返回内部函数g
,并从外部f
的范围执行该函数。 但是,如果f
执行完毕,那么g
仍如何使用超出范围的变量?
这是魔术:闭包不仅是一个函数。 这是一个具有构造函数和私有数据的对象 ,我们可以将其当作函数来调用。 如果JavaScript没有闭包,而我们必须自己实现闭包,那么情况就是这样。
class G {// An instance of "G" will be constructed with a value "n",// and it stores that value in its private dataconstructor(n) {this._n = n;}// When we call an instance of "G", it returns the value from its private data__call__() {return this._n;}
}function f() {let n = 42;// This is the closure// Our inner function isn't really a function// It's a callable object, and we pass "n" to its constructorlet g = new G(n);return g;
}// Get the "g" callable object created by "f"
let g = f();// It's okay if the original variable "n" from "f"'s scope is destroyed now
// The callable object "g" is actually referencing its own private data
g(); // 42
在这里,我们用类G
的实例替换了内部函数g
,并通过将f
的局部变量传递给G
的构造函数来捕获了该局部变量,该构造函数随后将该值存储在新实例的私有数据中。 女士们,先生们,那是一个封闭。 真的就是这么简单。 闭包是一个可调用的对象,它私有存储从实例化该实例的环境中通过构造函数传递的值。
进一步发展
精明的读者会注意到有些行为我们还没有解决。 让我们看另一个闭包示例。
function f() {let n = 42;// An inner function that references "n"function get() {return n;}// Another inner function that also references "n"function next() {n++;}return {get, next};
}let o = f();o.get(); // 42
o.next();
o.get(); // 43
在此示例中,我们有两个都引用相同变量n
闭包。 一个函数对该变量的操作会影响另一个函数的值。 但是,如果JavaScript没有闭包,而我们必须自己实现闭包,那么我们将不会获得相同的行为。
class Get {constructor(n) {this._n = n;}__call__() {return this._n;}
}class Next {constructor(n) {this._n = n;}__call__() {this._n++;}
}function f() {let n = 42;// These are the closures// They're callable objects that privately store the values// passed through their constructorslet get = new Get(n);let next = new Next(n);return {get, next};
}let o = f();o.get(); // 42
o.next();
o.get(); // 42
像以前一样,我们用类Get
和Next
实例替换了内部函数get
和next
,它们通过将f
的局部变量传递给构造函数并将该值存储在每个实例的私有数据中来捕获。 但是请注意,一个可调用对象对n
的操作不会影响另一个可调用对象的值。 发生这种情况是因为他们没有捕获对n
的引用 ; 他们捕获了n
值的副本 。
为了解释为什么JavaScript的闭包会引用相同的n
,我们需要解释变量本身。 在传统意义上,JavaScript的局部变量并不是真正的局部变量。 相反,它们是动态分配和引用计数的对象(称为“ LexicalEnvironment”对象)的属性,JavaScript的闭包捕获对整个环境的引用,而不是对任何特定变量的引用。
让我们更改可调用对象的实现,以捕获词法环境,而不是专门捕获n
。
class Get {constructor(lexicalEnvironment) {this._lexicalEnvironment = lexicalEnvironment;}__call__() {return this._lexicalEnvironment.n;}
}class Next {constructor(lexicalEnvironment) {this._lexicalEnvironment = lexicalEnvironment;}__call__() {this._lexicalEnvironment.n++;}
}function f() {let lexicalEnvironment = {n: 42};// These callable objects capture a reference to the lexical environment,// so they will share a reference to the same "n"let get = new Get(lexicalEnvironment);let next = new Next(lexicalEnvironment);return {get, next};
}let o = f();// Now our callable objects exhibit the same behavior as JavaScript's functions
o.get(); // 42
o.next();
o.get(); // 43
在这里,我们更换了局部变量n
与lexicalEnvironment
具有属性对象n
。 闭包(类Get
和Next
的可调用实例)捕获对词法环境对象的引用,而不是n
的值。 并且由于它们现在共享对相同n
的引用,因此一个可调用对象对n
的操作会影响另一个可调用对象的值。
结论
闭包是我们可以调用的对象,就好像它们是函数一样。 实际上,JavaScript中的每个函数都是一个可调用对象,也称为“函数对象”或“函数器”,即使它是最外部的全局词法环境,也可以用词法环境对象实例化并私下存储。 在JavaScript中,函数不会创建闭包。 功能是关闭。
这篇文章有没有帮助您了解闭包? 我很高兴在下面的评论中听到您的想法或问题。
From: https://www.sitepoint.com/quick-tip-master-closures-by-reimplementing-them-from-scratch/