从原型定义的函数访问私有成员变量

199

有没有办法让“private”变量(在构造函数中定义)对使用原型定义的方法可用?

TestClass = function(){
    var privateField = "hello";
    this.nonProtoHello = function(){alert(privateField)};
};
TestClass.prototype.prototypeHello = function(){alert(privateField)};

这个有效:

t.nonProtoHello()

但这个不行:

t.prototypeHello()

我习惯在构造函数中定义我的方法,但出于几个原因而放弃了这种做法。


可能是如何创建可访问原型函数的私有变量?的重复问题。 - educampver
14
@ecampver,除了这个问题是 2 年提出的…… - Pacerier
25个回答

208

不行,无法做到这一点。这本质上就是反向作用域。

在构造函数内定义的方法可以访问私有变量,因为所有函数都可以访问它们所定义的作用域。

在原型上定义的方法并不在构造函数作用域内定义,因此不能访问构造函数的局部变量。

您仍然可以拥有私有变量,但如果您希望在原型上定义的方法能够访问它们,那么应该在this对象上定义getter和setter,这样原型方法(以及其他所有内容)就可以访问它们了。例如:

function Person(name, secret) {
    // public
    this.name = name;

    // private
    var secret = secret;

    // public methods have access to private members
    this.setSecret = function(s) {
        secret = s;
    }

    this.getSecret = function() {
        return secret;
    }
}

// Must use getters/setters 
Person.prototype.spillSecret = function() { alert(this.getSecret()); };

18
“scoping in reverse”是C++的一个特性,使用“friend”关键字。基本上,任何函数都应该将其原型定义为其友元。可悲的是,这个概念只适用于C++而不是JS :( - TWiStErRob
1
我想将这篇帖子添加到我的收藏夹列表的顶部并保持在那里。 - Daniel Viglione
2
我看不出这样做的意义-你只是添加了一个没有任何作用的抽象层。你最好将 secret 作为 this 的属性。JavaScript 并不支持原型私有变量,因为原型与调用位置上下文绑定,而不是“创建位置”上下文。 - nicodemus13
1
为什么不直接使用 person.getSecret() 呢? - Fahmi
1
为什么这个问题有这么多赞?这并没有将变量设为私有。如上所述,使用person.getSecret()可以让你从任何地方访问该私有变量。 - alexr101
显示剩余2条评论

66

更新:使用ES6,有更好的方法:

简而言之,您可以使用新的Symbol来创建私有字段。
这里有一个非常好的描述:https://curiosity-driven.org/private-properties-in-javascript

示例:

var Person = (function() {
    // Only Person can access nameSymbol
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());

对于所有支持ES5的现代浏览器:

你可以仅使用闭包

构建对象的最简单方法是完全避免原型继承。只需在闭包中定义私有变量和公共函数,所有公共方法将具有对变量的私有访问。

或者你可以仅使用原型

在JavaScript中,原型继承主要是一种优化。它允许多个实例共享原型方法,而不是每个实例都有自己的方法。
缺点是每次调用原型函数时,this 是唯一不同的。
因此,任何私有字段必须通过this访问,这意味着它们将是公共的。因此,我们只需为_private字段使用命名约定。

不要费力将闭包与原型混合使用

我认为你不应该将闭包变量与原型方法混合使用。你应该使用其中之一。

当你使用闭包访问私有变量时,原型方法无法访问该变量。因此,你必须将闭包暴露到this上,这意味着你以某种方式公开了它。这种方法几乎没有什么可获得的好处。

我该选择哪个?

对于非常简单的对象,只需使用带有闭包的普通对象。

如果你需要原型继承--用于继承、性能等,请坚持使用“_private”命名约定,不要费心使用闭包。

我不明白为什么JS开发人员这么努力地使字段真正私有。


5
遗憾的是,如果你想利用原型继承,_private 命名约定仍然是最好的解决方案。 - crush
1
ES6将会有一个新的概念,即“Symbol”,这是创建私有字段的绝佳方式。这里有一个很好的解释:https://curiosity-driven.org/private-properties-in-javascript - Scott Rippey
1
不,你可以将Symbol保存在一个包含整个类的闭包中。这样,所有原型方法都可以使用该符号,但它永远不会暴露在类外部。 - Scott Rippey
2
你提供的文章中说:“*符号类似于私有名称,但与私有名称不同的是,它们并不提供真正的隐私。”。实际上,如果你拥有该实例,你可以使用Object.getOwnPropertySymbols获取其符号。因此,这只是一种通过模糊化来实现的隐私保护。 - Oriol
2
@Oriol 是的,隐私是通过重度模糊实现的。仍然可以通过符号进行迭代,并通过 toString 推断符号的用途。这与 Java 或 C# 没有区别... 私有成员仍然可以通过反射访问,但通常会被强烈模糊化。这一切都加强了我的最终观点,“我不明白为什么 JS 开发人员要如此努力地使字段真正私有化。” - Scott Rippey
显示剩余7条评论

31

阅读此文时,我觉得这听起来是一个艰巨的挑战,所以我决定想出一种方法。我想出了一种疯狂的方法,但它完全有效。

首先,我尝试在立即函数中定义类,这样你就可以访问该函数的一些私有属性。这个方法可以使您获取一些私有数据,但是,如果您尝试设置私有数据,您很快就会发现所有对象都将共享相同的值。

var SharedPrivateClass = (function() { // use immediate function
    // our private data
    var private = "Default";

    // create the constructor
    function SharedPrivateClass() {}

    // add to the prototype
    SharedPrivateClass.prototype.getPrivate = function() {
        // It has access to private vars from the immediate function!
        return private;
    };

    SharedPrivateClass.prototype.setPrivate = function(value) {
        private = value;
    };

    return SharedPrivateClass;
})();

var a = new SharedPrivateClass();
console.log("a:", a.getPrivate()); // "a: Default"

var b = new SharedPrivateClass();
console.log("b:", b.getPrivate()); // "b: Default"

a.setPrivate("foo"); // a Sets private to "foo"
console.log("a:", a.getPrivate()); // "a: foo"
console.log("b:", b.getPrivate()); // oh no, b.getPrivate() is "foo"!

console.log(a.hasOwnProperty("getPrivate")); // false. belongs to the prototype
console.log(a.private); // undefined

// getPrivate() is only created once and instanceof still works
console.log(a.getPrivate === b.getPrivate);
console.log(a instanceof SharedPrivateClass);
console.log(b instanceof SharedPrivateClass);

在很多情况下,这种方式是足够的,例如当你想要拥有像事件名称这样的常量值并在实例之间共享时。但本质上它们就像私有的静态变量。

如果你绝对需要从原型中定义的方法中访问私有命名空间中的变量,可以尝试使用这种模式。

var PrivateNamespaceClass = (function() { // immediate function
    var instance = 0, // counts the number of instances
        defaultName = "Default Name",  
        p = []; // an array of private objects

    // create the constructor
    function PrivateNamespaceClass() {
        // Increment the instance count and save it to the instance. 
        // This will become your key to your private space.
        this.i = instance++; 
        
        // Create a new object in the private space.
        p[this.i] = {};
        // Define properties or methods in the private space.
        p[this.i].name = defaultName;
        
        console.log("New instance " + this.i);        
    }

    PrivateNamespaceClass.prototype.getPrivateName = function() {
        // It has access to the private space and it's children!
        return p[this.i].name;
    };
    PrivateNamespaceClass.prototype.setPrivateName = function(value) {
        // Because you use the instance number assigned to the object (this.i)
        // as a key, the values set will not change in other instances.
        p[this.i].name = value;
        return "Set " + p[this.i].name;
    };

    return PrivateNamespaceClass;
})();

var a = new PrivateNamespaceClass();
console.log(a.getPrivateName()); // Default Name

var b = new PrivateNamespaceClass();
console.log(b.getPrivateName()); // Default Name

console.log(a.setPrivateName("A"));
console.log(b.setPrivateName("B"));
console.log(a.getPrivateName()); // A
console.log(b.getPrivateName()); // B

// private objects are not accessible outside the PrivateNamespaceClass function
console.log(a.p);

// the prototype functions are not re-created for each instance
// and instanceof still works
console.log(a.getPrivateName === b.getPrivateName);
console.log(a instanceof PrivateNamespaceClass);
console.log(b instanceof PrivateNamespaceClass);

我希望任何看到这种方法有错误的人都能提供建议。


4
我猜一个潜在的担忧是,任何一种情况下都可以通过使用不同的实例ID访问其他实例的私有变量。这并不一定是坏事... - Mims H. Wright
15
每次构造函数调用时,您都会重新定义原型函数。 - Lu4
10
@Lu4,我不确定那是真的。构造函数是在闭包内返回的;原型函数只在第一次定义,在那个立即调用的函数表达式中。除了上面提到的隐私问题之外,在我看来这看起来很好(乍一看)。 - guypursey
3
我想提一下,所有实例中都已添加了 i。因此它不是完全“透明”的,i仍然可能会被篡改。 - Scott Rippey
1
有人对@Lu4的评论进行了深入测试吗?我怀疑他是正确的。在每个PrivateNamespaceClass的新实例上,您都会创建一个新的闭包实例,这将创建内部函数和原型的新实例。 - crush
显示剩余15条评论

19

请参考Doug Crockford的页面。您需要通过能够访问私有变量作用域的方法来间接地实现它。

另一个例子:

Incrementer = function(init) {
  var counter = init || 0;  // "counter" is a private variable
  this._increment = function() { return counter++; }
  this._set = function(x) { counter = x; }
}
Incrementer.prototype.increment = function() { return this._increment(); }
Incrementer.prototype.set = function(x) { return this._set(x); }

使用案例:

js>i = new Incrementer(100);
[object Object]
js>i.increment()
100
js>i.increment()
101
js>i.increment()
102
js>i.increment()
103
js>i.set(-44)
js>i.increment()
-44
js>i.increment()
-43
js>i.increment()
-42

47
这个例子似乎是一个糟糕的实践。使用原型方法的目的是避免为每个实例创建新的方法。但你却仍然在这样做,在为每个方法创建另一个方法。 - Kir
2
@ArmedMonkey 这个概念看起来很不错,但同意这个例子很糟糕,因为所示的原型函数是微不足道的。如果原型函数是更长的函数,需要对“私有”变量进行简单的获取/设置访问,那么这将是有意义的。 - pancake
9
为什么要通过 set 暴露 _set? 为什么不一开始就将其命名为 set - Scott Rippey

15

我建议将“在构造函数中拥有原型分配”的做法描述为JavaScript反模式可能是一个不错的主意。仔细想想,这太冒险了。

实际上,在创建第二个对象(即b)时,您正在重新定义使用该原型的所有对象的原型函数。这将有效地重置示例a的值。如果您想要共享变量并且恰好提前创建了所有对象实例,则这样做可以起作用,但这感觉太冒险。

最近,我在编写的一些JavaScript中发现了一个错误,它源于这种反模式。 它试图为正在创建的特定对象设置拖放处理程序,但却为所有实例执行了此操作。 不好。

Doug Crockford的解决方案是最好的。


10

@Kai

那样做是行不通的。如果你这样做,

var t2 = new TestClass();

那么t2.prototypeHello将访问t的私有部分。

@AnglesCrimes

示例代码可以正常工作,但实际上它创建了一个所有实例共享的“静态”私有成员。这可能不是morgancodes寻找的解决方案。

到目前为止,我还没有找到一种简单而干净的方法来做到这一点,而不需要引入一个私有哈希和额外的清理函数。可以在一定程度上模拟私有成员函数:

(function() {
    function Foo() { ... }
    Foo.prototype.bar = function() {
       privateFoo.call(this, blah);
    };
    function privateFoo(blah) { 
        // scoped to the instance by passing this to call 
    }

    window.Foo = Foo;
}());

我清楚地理解了你的观点,但是你能否解释一下你的代码片段试图做什么? - Vishwanath
privateFoo 是完全私有的,因此在获取 new Foo() 时是不可见的。这里只有 bar() 是公共方法,它可以访问 privateFoo。你可以使用相同的机制来处理简单变量和对象,但是你需要始终记住,这些“私有变量”实际上是静态的,并且将被所有创建的对象共享。 - Philzen

7
是的,这是可能的。PPF设计模式可以解决这个问题。
PPF代表私有原型函数。基本的PPF解决了以下问题:
  1. 原型函数可以访问私有实例数据。
  2. 原型函数可以被设置为私有。
对于第一个问题,只需要:
  1. 将您想要从原型函数中访问的所有私有实例变量放在单独的数据容器中,
  2. 将对数据容器的引用作为参数传递给所有原型函数。
就是这么简单。例如:
// Helper class to store private data.
function Data() {};

// Object constructor
function Point(x, y)
{
  // container for private vars: all private vars go here
  // we want x, y be changeable via methods only
  var data = new Data;
  data.x = x;
  data.y = y;

  ...
}

// Prototype functions now have access to private instance data
Point.prototype.getX = function(data)
{
  return data.x;
}

Point.prototype.getY = function(data)
{
  return data.y;
}

...

在此阅读完整故事:

PPF设计模式


4
仅提供链接的答案通常在SO上不被看好。请给出一个例子。 - Corey Adler
这篇文章里面有例子,请自行查看。 - Edward
5
如果某一天该网站关闭了,那么该怎么办呢?那么别人怎么看到这个例子呢?这个政策的制定是为了保留任何有价值的链接内容,并且不必依赖于不在我们控制之下的网站。 - Corey Adler
3
@Edward,你的链接很有趣!然而,我认为使用原型函数访问私有数据的主要原因是防止每个对象使用相同的公共函数浪费内存。你描述的方法并没有解决这个问题,因为对于公共使用,原型函数需要包装在常规公共函数中。我猜这种模式在节省内存方面可能很有用,如果你有很多组合在一个公共函数中的ppf。你还用它们做其他事情吗? - Dining Philosopher
@DiningPhilosofer,感谢您欣赏我的文章。是的,您说得对,我们仍然使用实例函数。但是,想法是尽可能轻量化它们,只需重新调用它们的PPF副本即可完成所有繁重的工作。最终,所有实例都会调用相同的PPF(当然是通过包装器),因此可以期望某种内存节省。问题是有多少。我预计会有大量节省。 - Edward

5
您可以通过使用访问器验证实现此目的:
(function(key, global) {
  // Creates a private data accessor function.
  function _(pData) {
    return function(aKey) {
      return aKey === key && pData;
    };
  }

  // Private data accessor verifier.  Verifies by making sure that the string
  // version of the function looks normal and that the toString function hasn't
  // been modified.  NOTE:  Verification can be duped if the rogue code replaces
  // Function.prototype.toString before this closure executes.
  function $(me) {
    if(me._ + '' == _asString && me._.toString === _toString) {
      return me._(key);
    }
  }
  var _asString = _({}) + '', _toString = _.toString;

  // Creates a Person class.
  var PersonPrototype = (global.Person = function(firstName, lastName) {
    this._ = _({
      firstName : firstName,
      lastName : lastName
    });
  }).prototype;
  PersonPrototype.getName = function() {
    var pData = $(this);
    return pData.firstName + ' ' + pData.lastName;
  };
  PersonPrototype.setFirstName = function(firstName) {
    var pData = $(this);
    pData.firstName = firstName;
    return this;
  };
  PersonPrototype.setLastName = function(lastName) {
    var pData = $(this);
    pData.lastName = lastName;
    return this;
  };
})({}, this);

var chris = new Person('Chris', 'West');
alert(chris.setFirstName('Christopher').setLastName('Webber').getName());

这个例子来自于我的文章“原型函数和私有数据”,并在那里有更详细的解释。


1
这个答案太“聪明”了,不够实用,但我喜欢使用IFFE绑定变量作为秘密握手的答案。这个实现使用了太多的闭包,不够实用;定义原型方法的目的是为了防止在每个对象上的每个方法中构造新的函数对象。 - Grae Kindel
这种方法使用秘密密钥来识别哪些原型方法是可信的,哪些不可信。然而,实例验证密钥,因此密钥必须发送到实例。但是,不受信任的代码可能会在虚假实例上调用受信任的方法,从而窃取密钥。有了那个密钥,可以创建新的方法,真实实例会认为这些方法是可信的。因此,这只是通过模糊性来保护隐私。 - Oriol

5
在当前的JavaScript中,我相当确定只有一种方法可以从原型函数访问私有状态,而不向this添加任何公共内容。 答案是使用“弱映射”模式。
总之:Person类有一个单一的弱映射,其中键是Person的实例,值是用于私有存储的普通对象。
这是一个完全功能的示例:(在http://jsfiddle.net/ScottRippey/BLNVr/上播放)
var Person = (function() {
    var _ = weakMap();
    // Now, _(this) returns an object, used for private storage.
    var Person = function(first, last) {
        // Assign private storage:
        _(this).firstName = first;
        _(this).lastName = last;
    }
    Person.prototype = {
        fullName: function() {
            // Retrieve private storage:
            return _(this).firstName + _(this).lastName;
        },
        firstName: function() {
            return _(this).firstName;
        },
        destroy: function() {
            // Free up the private storage:
            _(this, true);
        }
    };
    return Person;
})();

function weakMap() {
    var instances=[], values=[];
    return function(instance, destroy) {
        var index = instances.indexOf(instance);
        if (destroy) {
            // Delete the private state:
            instances.splice(index, 1);
            return values.splice(index, 1)[0];
        } else if (index === -1) {
            // Create the private state:
            instances.push(instance);
            values.push({});
            return values[values.length - 1];
        } else {
            // Return the private state:
            return values[index];
        }
    };
}

就像我说的那样,这确实是实现所有三个部分的唯一方法。

然而,有两个注意点。首先,这会影响性能 - 每次访问私有数据都是一个 O(n) 操作,其中 n 是实例的数量。所以如果你有大量的实例,就不要这样做。 其次,在完成一个实例后,必须调用 destroy 方法;否则,实例和数据将无法进行垃圾回收,导致内存泄漏。

这也正是为什么我的原始回答是:"你不应该这样做"


如果在Person实例超出范围之前不明确销毁它,那么weakmap会保留对它的引用,这样就会导致内存泄漏吗?我想出了一种protected模式,因为其他Person实例可以访问该变量,而继承自Person的实例也可以访问。只是试着摆弄了一下,所以不确定是否存在任何缺点,除了额外的处理(看起来不像访问私有成员变量那么多)。https://dev59.com/inzaa4cB1Zd3GeqPRIne#21800194 返回私有/受保护对象很麻烦,因为调用代码可能会改变您的私有/受保护对象。 - HMR
2
@HMR 是的,你必须显式地销毁私有数据。我会在我的答案中添加这个警告。 - Scott Rippey

3

试一下吧!

    function Potatoe(size) {
    var _image = new Image();
    _image.src = 'potatoe_'+size+'.png';
    function getImage() {
        if (getImage.caller == null || getImage.caller.owner != Potatoe.prototype)
            throw new Error('This is a private property.');
        return _image;
    }
    Object.defineProperty(this,'image',{
        configurable: false,
        enumerable: false,
        get : getImage          
    });
    Object.defineProperty(this,'size',{
        writable: false,
        configurable: false,
        enumerable: true,
        value : size            
    });
}
Potatoe.prototype.draw = function(ctx,x,y) {
    //ctx.drawImage(this.image,x,y);
    console.log(this.image);
}
Potatoe.prototype.draw.owner = Potatoe.prototype;

var pot = new Potatoe(32);
console.log('Potatoe size: '+pot.size);
try {
    console.log('Potatoe image: '+pot.image);
} catch(e) {
    console.log('Oops: '+e);
}
pot.draw();

1
这取决于caller,它是一个在严格模式下不允许的实现相关扩展。 - Oriol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接