mhermher
mhermher

Reputation: 137

Unsettable & Unwritable properties are still mutable

I am trying to create a property within a constructor function which is immutable except through a prototype function. I am trying to go off MDN documentation of this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties. But there does not seem to be a way to make a property completely immutable. Consider a simple example:

function Test(){
    Object.defineProperties(this,{
        elems : { value : [] }
    })
}
Test.prototype.addElem = function(newElem){
    if (this.elems.indexOf(newElem) == -1){
        this.elems.push(newElem);
    }
};

which works fine in most cases (not assignable):

>a = new Test()
Object { , 1 more… }
>a.elems
Array [  ]
>a.elems = 10
10
>a.elems
Array [  ]

Unfortunately, it is still mutable. Consider:

>a.elems.push(10)
1
>a.elems
Array [ 10 ]

I am sure they are other functions (array or object methods?) that will change the value of a non-writeable & non-settable property. Push was just the one I ran into. Is there a way to accomplish this? I know that one possible solution is :

function Test(){
    var elems = [];
    this.addElem = function(newElem){
        if (elems.indexOf(newElem) == -1){
            elems.push(newElem);
        }
    }
}

But I have read this is memory-inefficient especially when there are many instances of the "class". Also, what I am working on may have many methods like this, so I am even more worried about memory considerations.

Any ideas? I am not super knowledgeable about all the intricacies of JS prototyping.

Upvotes: 2

Views: 4130

Answers (2)

twernt
twernt

Reputation: 20591

In JavaScript, objects are extensible by default, but if you're able to take advantage of ES5, you should be able to use the Object.seal() or Object.freeze() methods to get immutable properties.

The MDN docs for Object.freeze() have an example that shows how to recursively freeze ("deepFreeze") all of the properties of an object, effectively making it completely immutable.

Here's a proof of concept that combines the code in the question with the code from the docs:

function Test() {
    Object.defineProperties(this, {
        elems : { value : [] }
    })
}

Test.prototype.addElem = function(newElem) {
    if (this.elems.indexOf(newElem) == -1) {
        this.elems.push(newElem);
    }
};

function deepFreeze(obj) {

  // Retrieve the property names defined on obj
  var propNames = Object.getOwnPropertyNames(obj);

  // Freeze properties before freezing self
  propNames.forEach(function(name) {
    var prop = obj[name];

    // Freeze prop if it is an object
    if (typeof prop == 'object' && prop !== null)
      deepFreeze(prop);
  });

  // Freeze self (no-op if already frozen)
  return Object.freeze(obj);
}

a = new Test();

a.elems.push(1);
console.log(a.elems); // [1]

deepFreeze(a);

a.elems.push(2);

console.log(a.elems); // Error

In FireBug, the a.elems.push() after the object is "deep frozen" returns a TypeError exception, indicating the property is not writable;

TypeError: can't define array index property past the end of an array with non-writable length

The Safari inspector also returns a TypeError exception:

TypeError: Attempted to assign to readonly property.

Upvotes: 2

Ron Sims II
Ron Sims II

Reputation: 646

You can largely accomplish this with the help of a closure. This is how you achieve privacy in JavaScript.

In a nutshell you create a variable inside of a function and have that function return an object that contains setters/getters.

In my example the foo function contains a _foo variable that can only be set by the methods in the object returned from function foo. You are effectively creating an API to the var held withing the function foo's scope.

var foo = function(config){

    if (!config) {
        config = {};
    }
//enclosed variable
    var _foo = {
        bar: []
    };

    if (config.bar) {//All item to be initialized with data
        _foo.bar = config.bar;
    }

    var fooAPI = {
        addBarItem: function(val){
            _foo.bar.push(val);
            return _foo.bar.length - 1;//return idenx of item added
        },
        removeBarItem: function(index) {
            return _foo.bar.slice(index, 1);//return the removed item
        },
        getBarItem: function(index) {
            return _foo.bar[index];//return the removed item
        },
        emptyBarItems: function() {
            return _foo.bar.slice(0, _foo.bar.length);//return all removed
        },
    getBarItems: function(){
        //clone bar do not return reference to it in order to keep private
        var newBar = [];
         _foo.bar.forEach(function(item){
             newBar.push(item);
            });
            return newBar;
        }
    };

    return fooAPI;
};

var myFoo = new foo({bar: ['alpha', 'beta', 'gamma']});
console.log(myFoo.getBarItems());

Upvotes: 0

Related Questions