开发者

Private variables in inherited prototypes

I think I have misunderstood how Javascript prototypal inheritance works. Specifically, the prototypes internal variables seem to be shared between multiple different sub-objects. It is easiest to illustrate with code:

var A = function()
{
  var internal = 0;
  this.increment = function()
  {
    return ++internal;
  };
};

var B = function() {};
// inherit from A
B.prototype = new A;

x = new B;
y = new B;

$('#hello').text(x.increment() + " - " + y.increment());​

This outputs 1 - 2 (test it on JSBin), while I fully expected the result to be 1 - 1, since I wanted two separate objects.

How can I make sure that the A object isn't shared object between multiple instances of B?

Update: This article highlights some of the issues:

The problem is that the scope each approach uses to create a private variable, which works fine, is also the closure, in action, that results i开发者_JAVA技巧n if you change a private variable for one object instance, it is being changed for all. I.e. it’s more like a private static property, than an actual private variable.

So, if you want to have something private, more like a non-public constant, any of the above approaches is good, but not for actual private variables. Private variables only work really well with singleton objects in JavaScript.

Solution: As per BGerrissen's answer, changing the declaration of B and leaving of the prototype works as intended:

var B = function() { A.apply(this, arguments); };


Private members are tricky using prototypical inheritance. For one, they cannot be inherited. You need to create private members in each individual constructor. You can do this by either applying the super constructor in the subclass or create a decorator.

Decorator example:

function internalDecorator(obj){
    var internal = 0;
    obj.increment = function(){
        return ++internal;
    }
} 

var A = function(){
    internalDecorator(this);
}
A.prototype = {public:function(){/*etc*/}}

var B = function(){
    internalDecorator(this);
}
B.prototype = new A(); // inherits 'public' but ALSO redundant private member code.

var a = new B(); // has it's own private members
var b = new B(); // has it's own private members

This is just a variation of the super constructor call, you can also achieve the same by calling the actual super constructor with .apply()

var B = function(){
    A.apply(this, arguments);
}

Now by applying inheritance through B.prototype = new A() you invoke needless constructor code from A. A way to avoid this is to use Douglas Crockfords beget method:

Object.beget = function(obj){
    var fn = function(){}
    fn.prototype = obj;
    return new fn(); // now only its prototype is cloned.
}

Which you use as follows:

B.prototype = Object.beget(A.prototype);

Of course, you can abandon inheritance altogether and make good use of decorators, at least where private members are needed.


You need to forget the idea of classes. There isn't really such a thing in JavaScript as an 'instance of B'. There is only 'some object you obtained by calling the constructor function B'. An object has properties. Some are its "own" properties, others are included by searching the prototype chain.

When you say new A, you're creating one object. Then you assign it as the prototype for B, which means that every call to new B produces a new object that has the same direct prototype, and hence the same counter variable.

In Tim Down's answer, two alternatives are shown. His incrementPublic uses inheritance, but makes the counter variable public (i.e. gives up encapsulation). Whereas incrementInternal makes the counter private but succeeds by moving the code into B (i.e. gives up inheritance).

What you want is a combination of three things:

  • inheritable behaviour - so it must be defined in A and require no code in B aside from setting the prototype
  • private data, stored in closure-local variables
  • per-instance data, stored in this.

The problem is the contradiction between the last two. I would also say that inheritance is of limited value in JS anyway. It's better to treat it as a functional language:

// higher-order function, returns another function with counter state
var makeCounter = function() {
  var c = 0;
  return function() { return ++c; };
};

// make an object with an 'increment' method:
var incrementable = {
  increment: makeCounter()
};

Personally I tend to avoid constructor functions and prototype inheritance most of the time. They are far less useful in JS than people from an OO background assume.

Update I'd be wary of the statement you quoted in your update:

Private variables only work really well with singleton objects in JavaScript.

That's not really true. An object is just a 'dictionary' of properties, any of which may be functions. So forget objects and think about functions. You can create multiple instances of a function according to some pattern, by writing a function that returns a function. The makeCounter example about is just a simple example of this. makeCounter is not a "singleton object" and doesn't have to be limited to use in singleton objects.


The point of the prototype is that it is shared between multiple objects (i.e. those that are created by the same constructor function). If you need variables that are not shared between objects that share a prototype, you need to keep those variables within the constructor function for each object. Just use the prototype for sharing methods.

In your example, you can't do exactly what you want using prototypes. Here's what you could do instead. See Daniel Earwicker's answer for more explanation that there's no point me now replicating here.

var A = function() {};

A.prototype.incrementPublic = function()
{
    return ++this.publicProperty;
};

var B = function()
{
    this.publicProperty = 0;
    var internal = 0;
    this.incrementInternal = function()
    {
      return ++internal;
    };
};

B.prototype = new A();

var x = new B(), y = new B();
console.log(x.incrementPublic(), y.incrementPublic()); // 1, 1
console.log(x.incrementInternal(), y.incrementInternal()); // 1, 1


I just found other tricky solution, without exporting any methods/variables to public objects.

function A(inherit) {
    var privates = { //setup private vars, they could be also changed, added in method or children
        a: 1,
        b: 2,
        c: 3
    };
    //setup public methods which uses privates
    this.aPlus = bindPlus("aPlus", this, privates); //pass method name as string!
    this.aGet = bindPlus("aGet", this, privates);
    if (inherit) {
        return privates;
    }
}
A.prototype.aPlus = function () {
    var args = getArgs(arguments),
        //self is "this" here 
        self = args.shift(),
        privates = args.shift(),
        //function real arguments
        n = args.shift();
    return privates.a += n;
};

A.prototype.aGet = function (n) {
    var args = getArgs(arguments),
        self = args.shift(),
        privates = args.shift();
    console.log(this, self, privates);
    return privates.a;
};

//utilites
function getArgs(arg) {
    return Array.prototype.slice.call(arg);
}

function bindPlus(funct, self, privates) {
    return function () {
        return Object.getPrototypeOf(self)[funct].bind(this, self, privates).apply(null, arguments);
    };
}

//inherited 
function B(inherit) {
    var privates = Object.getPrototypeOf(this).constructor.call(this, true);
    privates.d = 4;
    this.dGet = bindPlus("dGet", this, privates);
    if (inherit) {
        return privates;
    }
}

B.prototype = Object.create(A.prototype);
B.constructor = B;

B.prototype.aGet = function () {
    var args = getArgs(arguments),
        self = args.shift(),
        privates = args.shift();
    console.warn("B.aGet", this, privates);
    return privates.a;
};

B.prototype.dGet = function () {
    var args = getArgs(arguments),
        self = args.shift(),
        privates = args.shift();
    console.warn("B.dGet", this, privates);
    return privates.d;
};


// tests
var b = new B();
var a = new A();

//should be 223
console.log("223 ?",b.aPlus(222));

//should be 42
console.log("41",a.aPlus(222));

More test and samples here: http://jsfiddle.net/oceog/TJH9Q/

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜