没错。部分原因是 javascript 没有私有属性。你所做的不是宣布私有财产。您正在使用一种使用闭包来模拟私有属性的设计模式。
闭包超出范围。范围是指变量的生命周期,而对象属性是指变量的绑定。
所以在讨论闭包之前,让我们先简短讨论一下范围。
堆栈:
作用域与堆栈框架相关(在计算机科学中,它被称为“激活记录”,但大多数熟悉 C 或汇编语言的开发人员都更清楚地知道它是堆栈框架)。范围对于堆栈框架就像类对于对象一样。我的意思是当一个对象是一个类的一个实例时,一个栈帧就是一个作用域的实例。
让我们以一种虚构的语言为例。在这种语言中,就像在 javascript 中一样,函数定义了范围。让我们看一个示例代码:
var global_var
function b {
var bb
}
function a {
var aa
b();
}
当我们阅读上面的代码时,我们说变量aa在函数a的范围内,而变量bb在函数b的范围内。请注意,我们不称这个东西为私有变量。因为私有变量的对立面是公共变量,它们都引用绑定到对象的属性。相反,我们调用 aa 和 bb local variables。与局部变量相反的是全局变量(不是公共变量)。
现在,让我们看看当我们调用a 时会发生什么:
a() 被调用,创建一个新的堆栈帧。在堆栈上为局部变量分配空间:
The stack:
┌────────┐
│ var aa │ <── a's stack frame
╞════════╡
┆ ┆ <── caller's stack frame
a() 调用b(),创建一个新的栈帧。在堆栈上为局部变量分配空间:
The stack:
┌────────┐
│ var bb │ <── b's stack frame
╞════════╡
│ var aa │
╞════════╡
┆ ┆
在大多数编程语言中,包括 javascript,函数只能访问自己的堆栈帧。因此a() 不能访问b() 中的局部变量,全局范围内的任何其他函数或代码也不能访问a() 中的变量。唯一的例外是全局范围内的变量。从实现的角度来看,这是通过在不属于堆栈的内存区域中分配全局变量来实现的。这通常称为堆。因此,要完成图片,此时的内存如下所示:
The stack: The heap:
┌────────┐ ┌────────────┐
│ var bb │ │ global_var │
╞════════╡ │ │
│ var aa │ └────────────┘
╞════════╡
┆ ┆
(附带说明,您还可以使用 malloc() 或 new 在函数内部的堆上分配变量)
现在b() 完成并返回,它的堆栈帧从堆栈中移除:
The stack: The heap:
┌────────┐ ┌────────────┐
│ var aa │ │ global_var │
╞════════╡ │ │
┆ ┆ └────────────┘
当a() 完成时,它的堆栈帧也会发生同样的情况。这就是局部变量自动分配和释放的方式——通过从堆栈中推入和弹出对象。
闭包:
闭包是更高级的堆栈帧。但是,一旦函数返回,普通的堆栈帧就会被删除,而带有闭包的语言只会将堆栈帧(或它包含的对象)从堆栈中取消链接,同时在需要时保持对堆栈帧的引用。
现在让我们看一个带有闭包的语言的示例代码:
function b {
var bb
return function {
var cc
}
}
function a {
var aa
return b()
}
现在让我们看看如果我们这样做会发生什么:
var c = a()
第一个函数a() 被调用,然后又调用b()。创建堆栈帧并将其推入堆栈:
The stack:
┌────────┐
│ var bb │
╞════════╡
│ var aa │
╞════════╡
│ var c │
┆ ┆
函数b() 返回,所以它的栈帧从栈中弹出。但是,函数b() 返回一个匿名函数,该函数在闭包中捕获bb。所以我们弹出堆栈帧但不要从内存中删除它(直到所有对它的引用都被完全垃圾收集):
The stack: somewhere in RAM:
┌────────┐ ┌╶╶╶╶╶╶╶╶╶┐
│ var aa │ ┆ var bb ┆
╞════════╡ └╶╶╶╶╶╶╶╶╶┘
│ var c │
┆ ┆
a() 现在将函数返回给c。所以调用b() 的堆栈帧链接到变量c。请注意,链接的是堆栈帧,而不是范围。这有点像如果你从一个类创建对象,它是分配给变量的对象,而不是类:
The stack: somewhere in RAM:
┌────────┐ ┌╶╶╶╶╶╶╶╶╶┐
│ var c╶╶├╶╶╶╶╶╶╶╶╶╶╶┆ var bb ┆
╞════════╡ └╶╶╶╶╶╶╶╶╶┘
┆ ┆
还要注意,由于我们实际上并没有调用函数c(),所以变量cc 还没有分配到内存中的任何位置。在我们调用 c() 之前,它目前只是一个作用域,而不是堆栈帧。
现在当我们调用c() 时会发生什么? c() 的堆栈帧正常创建。但是这次有区别:
The stack:
┌────────┬──────────┐
│ var cc var bb │ <──── attached closure
╞════════╤──────────┘
│ var c │
┆ ┆
b() 的堆栈帧附加到c() 的堆栈帧。因此,从函数c() 的角度来看,它的堆栈还包含调用函数b() 时创建的所有变量(再次注意,不是函数 b() 中的变量,而是函数 b() 时创建的变量调用 - 换句话说,不是 b() 的范围,而是调用 b() 时创建的堆栈帧。这意味着只有一个可能的函数 b(),但对 b() 的多次调用会创建许多堆栈帧)。
但是局部和全局变量的规则仍然适用。 b() 中的所有变量都成为 c() 的局部变量,仅此而已。调用c() 的函数无权访问它们。
这意味着当你像这样在调用者的范围内重新定义c时:
var c = function {/* new function */}
发生这种情况:
somewhere in RAM:
┌╶╶╶╶╶╶╶╶╶┐
┆ var bb ┆
└╶╶╶╶╶╶╶╶╶┘
The stack:
┌────────┐ ┌╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶┐
│ var c╶╶├╶╶╶╶╶╶╶╶╶╶╶┆ /* new function */ ┆
╞════════╡ └╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶┘
┆ ┆
如您所见,由于c 所属的范围无权访问它,因此无法从对b() 的调用中重新获得对堆栈帧的访问权。
解决方法:
解决方法是使用对象绑定来存储您的cfg 对象,因为这是一个绑定(javascript 将其称为上下文)问题而不是范围问题。
不幸的是,javascript 没有私有变量。因此只能将其绑定为公共变量。解决此问题的解决方法是使用 Perl 约定告诉其他程序员不要接触该对象,除非他们正在修改实现本身。该约定是以下划线开头的变量名:
// WARNING: Private!
a._cfg = {
currency: 'GBP',
exponent: 2
};