Accessing private member variables from prototype-defined functions

前端 未结 25 859
孤城傲影
孤城傲影 2020-11-22 14:48

Is there any way to make “private” variables (those defined in the constructor), available to prototype-defined methods?

TestClass = function(){
    var priv         


        
相关标签:
25条回答
  • 2020-11-22 15:40

    I faced the exact same question today and after elaborating on Scott Rippey first-class response, I came up with a very simple solution (IMHO) that is both compatible with ES5 and efficient, it also is name clash safe (using _private seems unsafe).

    /*jslint white: true, plusplus: true */
    
     /*global console */
    
    var a, TestClass = (function(){
        "use strict";
        function PrefixedCounter (prefix) {
            var counter = 0;
            this.count = function () {
                return prefix + (++counter);
            };
        }
        var TestClass = (function(){
            var cls, pc = new PrefixedCounter("_TestClass_priv_")
            , privateField = pc.count()
            ;
            cls = function(){
                this[privateField] = "hello";
                this.nonProtoHello = function(){
                    console.log(this[privateField]);
                };
            };
            cls.prototype.prototypeHello = function(){
                console.log(this[privateField]);
            };
            return cls;
        }());
        return TestClass;
    }());
    
    a = new TestClass();
    a.nonProtoHello();
    a.prototypeHello();
    

    Tested with ringojs and nodejs. I'm eager to read your opinion.

    0 讨论(0)
  • 2020-11-22 15:41

    I'm late to the party, but I think I can contribute. Here, check this out:

    // 1. Create closure
    var SomeClass = function() {
      // 2. Create `key` inside a closure
      var key = {};
      // Function to create private storage
      var private = function() {
        var obj = {};
        // return Function to access private storage using `key`
        return function(testkey) {
          if(key === testkey) return obj;
          // If `key` is wrong, then storage cannot be accessed
          console.error('Cannot access private properties');
          return undefined;
        };
      };
      var SomeClass = function() {
        // 3. Create private storage
        this._ = private();
        // 4. Access private storage using the `key`
        this._(key).priv_prop = 200;
      };
      SomeClass.prototype.test = function() {
        console.log(this._(key).priv_prop); // Using property from prototype
      };
      return SomeClass;
    }();
    
    // Can access private property from within prototype
    var instance = new SomeClass();
    instance.test(); // `200` logged
    
    // Cannot access private property from outside of the closure
    var wrong_key = {};
    instance._(wrong_key); // undefined; error logged

    I call this method accessor pattern. The essential idea is that we have a closure, a key inside the closure, and we create a private object (in the constructor) that can only be accessed if you have the key.

    If you are interested, you can read more about this in my article. Using this method, you can create per object properties that cannot be accessed outside of the closure. Therefore, you can use them in constructor or prototype, but not anywhere else. I haven't seen this method used anywhere, but I think it's really powerful.

    0 讨论(0)
  • 2020-11-22 15:42

    Can't you put the variables in a higher scope?

    (function () {
        var privateVariable = true;
    
        var MyClass = function () {
            if (privateVariable) console.log('readable from private scope!');
        };
    
        MyClass.prototype.publicMethod = function () {
            if (privateVariable) console.log('readable from public scope!');
        };
    }))();
    
    0 讨论(0)
  • 2020-11-22 15:43

    When I read this, it sounded like a tough challenge so I decided to figure out a way. What I came up with was CRAAAAZY but it totally works.

    First, I tried defining the class in an immediate function so you'd have access to some of the private properties of that function. This works and allows you to get some private data, however, if you try to set the private data you'll soon find that all the objects will share the same value.

    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);

    There are plenty of cases where this would be adequate like if you wanted to have constant values like event names that get shared between instances. But essentially, they act like private static variables.

    If you absolutely need access to variables in a private namespace from within your methods defined on the prototype, you can try this pattern.

    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);

    I'd love some feedback from anyone who sees an error with this way of doing it.

    0 讨论(0)
  • 2020-11-22 15:43

    I know it has been more than 1 decade since this was was asked, but I just put my thinking on this for the n-th time in my programmer life, and found a possible solution that I don't know if I entirely like yet. I have not seen this methodology documented before, so I will name it the "private/public dollar pattern" or _$ / $ pattern.

    var ownFunctionResult = this.$("functionName"[, arg1[, arg2 ...]]);
    var ownFieldValue = this._$("fieldName"[, newValue]);
    
    var objectFunctionResult = objectX.$("functionName"[, arg1[, arg2 ...]]);
    
    //Throws an exception. objectX._$ is not defined
    var objectFieldValue = objectX._$("fieldName"[, newValue]);
    

    The concept uses a ClassDefinition function that returns a Constructor function that returns an Interface object. The interface's only method is $ which receives a name argument to invoke the corresponding function in the constructor object, any additional arguments passed after name are passed in the invocation.

    The globally-defined helper function ClassValues stores all fields in an object as needed. It defines the _$ function to access them by name. This follows a short get/set pattern so if value is passed, it will be used as the new variable value.

    var ClassValues = function (values) {
      return {
        _$: function _$(name, value) {
          if (arguments.length > 1) {
            values[name] = value;
          }
    
          return values[name];
        }
      };
    };
    

    The globally defined function Interface takes an object and a Values object to return an _interface with one single function $ that examines obj to find a function named after the parameter name and invokes it with values as the scoped object. The additional arguments passed to $ will be passed on the function invocation.

    var Interface = function (obj, values, className) {
      var _interface = {
        $: function $(name) {
          if (typeof(obj[name]) === "function") {
            return obj[name].apply(values, Array.prototype.splice.call(arguments, 1));
          }
    
          throw className + "." + name + " is not a function.";
        }
      };
    
      //Give values access to the interface.
      values.$ = _interface.$;
    
      return _interface;
    };
    

    In the sample below, ClassX is assigned to the result of ClassDefinition, which is the Constructor function. Constructor may receive any number of arguments. Interface is what external code gets after calling the constructor.

    var ClassX = (function ClassDefinition () {
      var Constructor = function Constructor (valA) {
        return Interface(this, ClassValues({ valA: valA }), "ClassX");
      };
    
      Constructor.prototype.getValA = function getValA() {
        //private value access pattern to get current value.
        return this._$("valA");
      };
    
      Constructor.prototype.setValA = function setValA(valA) {
        //private value access pattern to set new value.
        this._$("valA", valA);
      };
    
      Constructor.prototype.isValAValid = function isValAValid(validMessage, invalidMessage) {
        //interface access pattern to call object function.
        var valA = this.$("getValA");
    
        //timesAccessed was not defined in constructor but can be added later...
        var timesAccessed = this._$("timesAccessed");
    
        if (timesAccessed) {
          timesAccessed = timesAccessed + 1;
        } else {
          timesAccessed = 1;
        }
    
        this._$("timesAccessed", timesAccessed);
    
        if (valA) {
          return "valA is " + validMessage + ".";
        }
    
        return "valA is " + invalidMessage + ".";
      };
    
      return Constructor;
    }());
    

    There is no point in having non-prototyped functions in Constructor, although you could define them in the constructor function body. All functions are called with the public dollar pattern this.$("functionName"[, param1[, param2 ...]]). The private values are accessed with the private dollar pattern this._$("valueName"[, replacingValue]);. As Interface does not have a definition for _$, the values cannot be accessed by external objects. Since each prototyped function body's this is set to the values object in function $, you will get exceptions if you call Constructor sibling functions directly; the _$ / $ pattern needs to be followed in prototyped function bodies too. Below sample usage.

    var classX1 = new ClassX();
    console.log("classX1." + classX1.$("isValAValid", "valid", "invalid"));
    console.log("classX1.valA: " + classX1.$("getValA"));
    classX1.$("setValA", "v1");
    console.log("classX1." + classX1.$("isValAValid", "valid", "invalid"));
    var classX2 = new ClassX("v2");
    console.log("classX1.valA: " + classX1.$("getValA"));
    console.log("classX2.valA: " + classX2.$("getValA"));
    //This will throw an exception
    //classX1._$("valA");
    

    And the console output.

    classX1.valA is invalid.
    classX1.valA: undefined
    classX1.valA is valid.
    classX1.valA: v1
    classX2.valA: v2
    

    The _$ / $ pattern allows full privacy of values in fully-prototyped classes. I don't know if I will ever use this, nor if it has flaws, but hey, it was a good puzzle!

    0 讨论(0)
  • 2020-11-22 15:45

    Update: With ES6, there is a better way:

    Long story short, you can use the new Symbol to create private fields.
    Here's a great description: https://curiosity-driven.org/private-properties-in-javascript

    Example:

    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;
    }());
    

    For all modern browsers with ES5:

    You can use just Closures

    The simplest way to construct objects is to avoid prototypal inheritance altogether. Just define the private variables and public functions within the closure, and all public methods will have private access to the variables.

    Or you can use just Prototypes

    In JavaScript, prototypal inheritance is primarily an optimization. It allows multiple instances to share prototype methods, rather than each instance having its own methods.
    The drawback is that this is the only thing that's different each time a prototypal function is called.
    Therefore, any private fields must be accessible through this, which means they're going to be public. So we just stick to naming conventions for _private fields.

    Don't bother mixing Closures with Prototypes

    I think you shouldn't mix closure variables with prototype methods. You should use one or the other.

    When you use a closure to access a private variable, prototype methods cannot access the variable. So, you have to expose the closure onto this, which means that you're exposing it publicly one way or another. There's very little to gain with this approach.

    Which do I choose?

    For really simple objects, just use a plain object with closures.

    If you need prototypal inheritance -- for inheritance, performance, etc. -- then stick with the "_private" naming convention, and don't bother with closures.

    I don't understand why JS developers try SO hard to make fields truly private.

    0 讨论(0)
提交回复
热议问题