【问题标题】:javascript can't access private propertiesjavascript无法访问私有属性
【发布时间】:2014-11-21 14:24:33
【问题描述】:

我有以下代码,不明白为什么我重新声明get方法时无法访问私有属性。

(function(w,d,a,undefined){
    var cfg = {
        currency: 'GBP',
        exponent: 2
    };
    var get = function () {
        return cfg;
    };
    a.init = function (settings) {
        for (var k in settings) {
            cfg[k] = settings[k];
        }
    };
    a.set = function (args) {
        get = args.get || get;
        //eval(args) //works but why??
    };
    a.get = function () {
        return get();
    };
})(window,document,window.fxc = {});

fxc.init({currency: 'EUR'});

// prints, Object { currency="EUR", exponent=2}
console.log(fxc.get());

fxc.set({get: function(msg){
    // cannot access private properties
    return cfg;
}});

// prints, undefined
console.log(fxc.get());

我一直在尝试找到正确的方法来执行此操作,但我似乎无法找到正确的组合。我确实有eval(),但这肯定不是正确的方法吗?希望得到任何帮助。

【问题讨论】:

  • 您在私人get 末尾忘记了return
  • 为什么在get 之上命名var get 和属性get 也是javascript 中getter 的默认getter 表示法?这只需要发生错误:)
  • @Winchestro 是真的,在生产中不会是这样,但我这样做是为了看看 javascript 是如何思考的。通过命名相同,我可以看到js有区别。
  • 有一件事是,您的第一个console.log(fxc.get()); 不会打印Object { currency="EUR", exponent=2},而是init
  • 另外,如果你不介意,你能摆脱 HTML 吗?很多人更喜欢干净的 js 代码。

标签: javascript


【解决方案1】:

没错。部分原因是 javascript 没有私有属性。你所做的不是宣布私有财产。您正在使用一种使用闭包来模拟私有属性的设计模式。

闭包超出范围。范围是指变量的生命周期,而对象属性是指变量的绑定。

所以在讨论闭包之前,让我们先简短讨论一下范围。

堆栈:

作用域与堆栈框架相关(在计算机科学中,它被称为“激活记录”,但大多数熟悉 C 或汇编语言的开发人员都更清楚地知道它是堆栈框架)。范围对于堆栈框架就像类对于对象一样。我的意思是当一个对象是一个类的一个实例时,一个栈帧就是一个作用域的实例。

让我们以一种虚构的语言为例。在这种语言中,就像在 javascript 中一样,函数定义了范围。让我们看一个示例代码:

var global_var

function b {
    var bb
}

function a {
    var aa
    b();
}

当我们阅读上面的代码时,我们说变量aa在函数a的范围内,而变量bb在函数b的范围内。请注意,我们不称这个东西为私有变量。因为私有变量的对立面是公共变量,它们都引用绑定到对象的属性。相反,我们调用 aabb 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
};

【讨论】:

  • 实际上,在阅读了 plalx 的回答后,这是一个相当不错的解决方法。
  • 请注意,当我说在大多数编程语言中您无法访问其他人的堆栈帧时,我实际上是在说我知道一些您可以访问其他人的堆栈帧的编程语言(好吧,至少是你的调用者的堆栈帧)。
  • 刚刚发现了这个漂亮的答案,正在检查/纠正我的旧答案。一个很好的解释。遗憾的是这样的宝石在 stackoverflow 上丢失了。
  • @Winchestro:说实话。这不完全是解释的适当问题。但是解释对于回答这个问题是必要的。这可能属于“闭包如何工作”问题之一,但这些问题有很多和数百个答案,所以即使在那里也会丢失。
  • 你的答案被广泛引用here - 判断你自己是否这个级别的复制和粘贴是可以的,或者你是否想加入一个模组:-)
【解决方案2】:

嗯,你刚才说了,不能访问私有属性。 IIFE 中定义的变量不能从外部定义的函数中访问。

如果您不想公开cfg 变量,也许您可​​以这样做:

(function(w, d, a, undefined) {
    var cfg = {
        currency: 'GBP',
        exponent: 2
    };
    var get = function() {
        return cfg; //must return cfg
    };
    a.init = function(settings) {
        for (var k in settings) {
            cfg[k] = settings[k];
        }
    };
    a.set = function(args) {
        get = args.get(get) || get;
    };
    a.get = function() {
        return get();
    };
})(window, document, window.fxc = window.fxc || {});

fxc.set({
    get: function(initialGet) {
        return function(msg) {
            var cfg = initialGet();
            console.log('custom get');
            return cfg;
        };
    }
});


console.log(fxc.get());
//custom get
//{currency: "GBP", exponent: 2}

【讨论】:

  • 美丽的FP!我没有考虑保存旧的访问器函数来提供对闭包的受限访问:)
  • 我不太确定这里发生了什么。我看不到(initialGet)何时仅分配了 fxc.get()
  • @Christian 不是直接传递get 覆盖函数,而是传递一个负责创建它的工厂函数。然后,当您使用fxc.set(...) 注册get 的工厂函数时,内部get 函数作为initialGet 参数传递给工厂。这允许工厂创建的新 get 函数保留 initialGet 并使用它来查询组件的内部状态。
【解决方案3】:

Javascript 没有私有属性。如果我正确理解您的问题,从您的第二个脚本标签,您想要访问在您的第一个脚本标签中声明的“cfg”对象。

为此,您需要了解闭包的工作原理。在这种情况下,您将“cfg”声明为匿名函数中的变量。这意味着您在该范围内声明的其他函数将可以访问“cfg”对象,但是,它不会在任何其他上下文中工作。

要解决此问题,请将 var cfg = 替换为 a.cfg =

【讨论】:

  • 这会起作用,但会使 cfg 进入公共访问权限
  • 为什么要“私有”?称它为 _cfg,人们就会知道他们不应该乱用它。
猜你喜欢
  • 1970-01-01
  • 2019-11-20
  • 2016-02-15
  • 1970-01-01
  • 2018-09-13
  • 1970-01-01
  • 1970-01-01
  • 2021-11-06
  • 1970-01-01
相关资源
最近更新 更多