Search code examples
javascriptencapsulationgetter-setterrevealing-module-pattern

partial revealing module security question, default values in signature


Flubbing around the revealing module pattern with ES6 syntax, the state of default variables seems dependent on including the trailing () (invocation) on the function.

But, wondering about accessing those underscore variables in the body of the function, some trippy stuff seemed to go on in several different variations.

Investigating some more through several half-baked, half-remembered iterations of the RM pattern; whether it had to fully be an IIFE, or could just stand alone started it off, but then it morphed into looking at how _x, _y, and _z might be accessible.

The error goes away when the trailing () is added, but the behaviour of _._x, accepting a value, that remains. Should it?

Under all this, can default values be added to the pattern using a variation of the ES6 syntax?

const _ = function (_x = 'fish', _y = 'chicken', _z = 'beef') {
    
    // console.log(_x)
    _x
    _y
    _z

    return {

        // _x,
        // _y,
        // _z,
        get x() { return this._x },
        set x(value) { this._x = value }

    }
    
}() // if you invoke it isn't broken per se., but what is the state of revealing module vars?

// _()

let x 
_._x = 'fish-ly' // strongly discouraged, but why does it sort of work?
console.log(x)
x = 'tartwell'
console.log(x)
console.log(_._x) // supposed to be private?
console.log(_._x)

x = 'o.m.g'
console.log(x)
console.log(_._x) // gives fishly

console.log(_.x) // no change but gives undefined
console.log(_.x) // no change but gives undefined
_.x[_._x] // TypeError: Cannot read property 'fishly' of undefined
console.log('x', _.x) // with call () trailing function reinstated log shows x was assigned in the statement above


// _.x = 'leafy greens'
console.log(_.x)

Remaining Question: When _.x[_._x] fires do we get a value for _.x? From the discussion below, the object appears to have taken on a property. But this syntax is not a complete assignment, and if it were it would be a value from the right side. What's happening here?


Solution

  • You've defined _ as a function. Functions, being objects, can have arbitrary key-value pairs assigned to them (unfortunately). So

    _._x = 'fish-ly'
    

    will assign a property to the _x property of the single function object (viewable by anything that wishes to reference _._x). The closure-scoped variables (not the properties) _x, _y, _z are still properly encapsulated (they'll only be changeable and viewable by what your _ function deliberately exposes), but they won't be created until the _ function gets called.

    In your snippet, you've never invoked _, so for a clearer understanding of what's going on with the exact same results, you may as well replace _ with an empty object named {}:

    const obj = {};
    
    let x;
    obj._x = 'fish-ly' // strongly discouraged, but why does it sort of work?
                       // because it's just a key-value pair on an object
    console.log(obj._x) // which are public
    
    console.log(obj.x) // no change but gives undefined
                     // because the only property is "_x", not "x"

    Once you invoke the function, the closure variables which will have just been created will be protected, as desired.

    Remaining Question: When _.x[_._x] fires we get a value for _.x. From the discussion below, the object appears to have taken on a property. But this syntax is not a complete assignment, and if it were it would be a value from the right side. What's happening here?

    Standalone expressions that don't appear to do anything are permitted. For example:

    const obj = { prop: 'val' };
    
    obj.prop;
    NaN;
    window.location;

    The last 3 lines don't do anything useful, but the interpreter does parse them as expressions (before discarding them, because nothing is being done with them). Similarly, with the standalone line

    _.x[_._x]
    

    the interpreter attempts to parse this as an expression. If it can be evaluated as an expression (and said parsing doesn't throw an error), then the statement on that line will be considered to be complete and valid (despite being useless), and the interpreter will move on to the next line.

    But evaluating _.x[_._x] as an expression fails, because it is equivalent to:

    _.x[_._x]
    _.x['fish-ly']
    // _.x is undefined, so:
    undefined['fish-ly']
    

    And trying to access any property of undefined will throw:

    undefined['fish-ly']

    As you can see, the error message is identical to the one in your snippet.