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

mor*_*des 184 javascript private-members

有没有办法制作"私有"变量(在构造函数中定义的变量),可用于原型定义的方法?

TestClass = function(){
    var privateField = "hello";
    this.nonProtoHello = function(){alert(privateField)};
};
TestClass.prototype.prototypeHello = function(){alert(privateField)};
Run Code Online (Sandbox Code Playgroud)

这有效:

t.nonProtoHello()
Run Code Online (Sandbox Code Playgroud)

但这不是:

t.prototypeHello()
Run Code Online (Sandbox Code Playgroud)

我习惯在构造函数中定义我的方法,但由于一些原因,我正在远离它.

Tri*_*ych 183

不,没有办法做到这一点.这基本上是反过来的范围.

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

在原型上定义的方法未在构造函数的范围内定义,并且无法访问构造函数的局部变量.

您仍然可以拥有私有变量,但是如果您希望在原型上定义的方法可以访问它们,则应该在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()); };
Run Code Online (Sandbox Code Playgroud)

  • "反向搜索范围"是带有"friend"关键字的C++功能.实际上任何函数都应该定义它的原型,因为它是它的朋友.可悲的是,这个概念是C++而不是JS :( (14认同)
  • 我没有看到这一点 - 你只是添加了一层没有做任何事情的抽象.你也可以把`secret`作为'this`的属性.JavaScript只是不支持带有原型的私有变量,因为原型绑定到调用站点上下文,而不是"创建站点"上下文. (2认同)

Sco*_*pey 61

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

长话短说,你可以用new 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;
}());
Run Code Online (Sandbox Code Playgroud)

对于所有使用ES5的现代浏览器:

您可以只使用Closures

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

或者你可以只使用Prototypes

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

不要把Closures和Prototypes混在一起

我认为你不应该把闭包变量和原型方法混在一起.你应该使用其中一个.

使用闭包访问私有变量时,原型方法无法访问变量.因此,您必须将闭包暴露在外this,这意味着您以某种方式公开它.这种方法几乎没有什么好处.

我该选哪个?

对于非常简单的对象,只需使用带闭包的普通对象即可.

如果你需要原型继承 - 继承,性能等 - 那么坚持使用"_private"命名约定,而不用担心闭包.

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

  • 遗憾的是,如果你想利用原型继承,`_private`命名约定仍然是最好的解决方案. (4认同)
  • 您链接的文章说:“ *符号类似于私人名称,但-与私人名称不同-它们**不能提供真正的隐私**。*”。实际上,如果您有实例,则可以使用Object.getOwnPropertySymbols来获取其符号。因此,这仅仅是默默无闻的隐私。 (2认同)
  • @Oriol是的,隐私是严重默默无闻的.仍然可以迭代符号,并通过`toString`推断符号的目的.这与Java或C#没有什么不同......私有成员仍可通过反射访问,但通常会被强烈模糊.这一切都强化了我的最后一点,"我不明白为什么JS开发人员努力使字段真正私密化." (2认同)

Mim*_*ght 31

当我读到这篇文章时,这听起来像是一个艰难的挑战,所以我决定想办法.我想出的是CRAAAAZY,但它完全有效.

首先,我尝试在立即函数中定义类,以便您可以访问该函数的某些私有属性.这有效并且允许您获取一些私有数据,但是,如果您尝试设置私有数据,您很快就会发现所有对象将共享相同的值.

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);
Run Code Online (Sandbox Code Playgroud)

有很多情况下这就足够了,就像你想拥有像实例之间共享的事件名这样的常量值.但实际上,它们就像私有静态变量一样.

如果您绝对需要从原型中定义的方法中访问私有命名空间中的变量,则可以尝试此模式.

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);
Run Code Online (Sandbox Code Playgroud)

我喜欢任何通过这种方式看到错误的人的反馈.

  • 您在每次构造函数调用时重新定义原型函数 (14认同)
  • @ Lu4我不确定这是真的.构造函数从闭包内返回; 唯一一次定义原型函数是第一次,即立即调用函数表达式.除了上面提到的隐私问题,这看起来对我很好(乍一看). (10认同)
  • 我想一个潜在的问题是任何实例都可以通过使用不同的实例ID访问任何其他实例私有变量.不一定是坏事...... (4认同)
  • 我想提一下,`i`已被添加到所有实例中.所以它不是完全"透明",而且"i"仍然可以被篡改. (3认同)

Jas*_*n S 18

请参阅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); }
Run Code Online (Sandbox Code Playgroud)

用例:

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
Run Code Online (Sandbox Code Playgroud)

  • 这个例子似乎是一种可怕的做法.使用原型方法的目的是让您不必为每个实例创建一个新方法.无论如何,你正在这样做.对于每种方法,您都要创建另一种方法. (47认同)
  • 为什么甚至打扰通过`set`暴露`_set`?为什么不直接将它命名为"set"? (9认同)
  • @ArmedMonkey这个概念看起来很合理,但同意这是一个不好的例子,因为显示的原型函数很简单.如果原型函数需要更长的函数,需要对"私有"变量进行简单的获取/设置访问,那么这将是有意义的. (2认同)

小智 15

我建议将"在构造函数中进行原型赋值"描述为Javascript反模式可能是个好主意.想一想.风险太大了.

你在创建第二个对象(即b)时实际做的是为所有使用该原型的对象重新定义原型函数.这将有效地重置示例中对象a的值.如果你想要一个共享变量并且你碰巧事先创建了所有的对象实例,那么它会起作用,但它感觉风险太大了.

我发现最近我正在处理的一些Javascript中的一个错误是由于这种确切的反模式.它试图在正在创建的特定对象上设置拖放处理程序,而是为所有实例执行此操作.不好.

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


Tim*_*Tim 10

@Kai

那不行.如果你这样做

var t2 = new TestClass();
Run Code Online (Sandbox Code Playgroud)

然后t2.prototypeHello将访问t的私人部分.

@AnglesCrimes

示例代码工作正常,但它实际上创建了一个由所有实例共享的"静态"私有成员.它可能不是摩根代码寻找的解决方案.

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

(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;
}());
Run Code Online (Sandbox Code Playgroud)


Edw*_*ard 6

是的,这是可能的.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;
}
Run Code Online (Sandbox Code Playgroud)

...

阅读完整的故事:

PPF设计模式

  • 但是,如果在该网站之后的某个时间点发生故障,会发生什么?那么有人应该如何看待一个例子呢?该政策已经制定,以便链接中的任何有价值的东西都可以保留在这里,而不必依赖于我们无法控制的网站. (5认同)
  • 只有链接的答案通常不赞成SO.请举例说明. (4认同)
  • @Edward,你的链接很有意思!但是,在我看来,使用原型函数访问私有数据的主要原因是防止每个对象浪费具有相同公共函数的内存.您描述的方法无法解决此问题,因为对于公共使用,原型函数需要包含在常规公共函数中.我想如果你有很多ppf组合在一个公共函数中,那么这个模式对于节省内存很有用.你用它们做别的吗? (3认同)

小智 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());
Run Code Online (Sandbox Code Playgroud)

这个例子来自我关于原型函数和私有数据的帖子,并在此有更详细的解释。


Sco*_*pey 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];
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

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

但是,有两个警告。首先,这会降低性能——每次访问私有数据时,都是一个O(n)操作,n实例数在哪里。因此,如果您有大量实例,您不会想要这样做。其次,当你完成一个实例时,你必须调用destroy; 否则,实例和数据将不会被垃圾收集,最终会导致内存泄漏。

这就是为什么我最初的回答“你不应该”是我想要坚持的。

  • @HMR 是的,您必须明确销毁私有数据。我要将这个警告添加到我的答案中。 (2认同)