2

I have a sealed object with an array member on which I want to prevent direct pushes.

var myModule = (function () {
    "use strict";
    var a = (function () {
        var _b = {},
            _c = _c = "",
            _d = [];
        Object.defineProperty(_b, "c", {
            get: function () { return _c; }
        });
        Object.defineProperty(_b, "d", {
            get { return _d; }
        });
        _b.addD = function (newD) {
            _d.push(newD);
        };
        Object.seal(_b);
        return _b;
    }());
    var _something = { B: _b };
    return {
        Something: _something,
        AddD: _b.addD
    };
}());

myModule.Something.c = "blah"; // doesn't update = WIN!!
myModule.AddD({}); // pushed = WIN!
myModule.Something.d.push({}); // pushed = sadness

How can I prevent the push?

UPDATE:

Thanks for all the thoughts. I eventually need the JSON to send to the server. It looks like I might need to use an object for the array then figure out a way to generate and return the JSON needed, or change _something to use .slice(). Will play and report.

6
  • You could override the push prototype, but I wouldn't recommend it. Why not make it private variable? Commented Sep 8, 2015 at 20:03
  • 4
    It's less efficient, but you could make your getters return a copy of the array, so that pushing onto it won't have any effect on the object. Commented Sep 8, 2015 at 20:09
  • 2
    What @Barmar said. Or, analyze why you want to even allow access to d in the first place. Maybe instead you could provide methods like forEachD(...) or something, so people can still do the things that make sense, but not the things that don't. Commented Sep 8, 2015 at 20:11
  • 1
    for d: get { return _d.slice(); } should do what you want safe and cheap Commented Sep 8, 2015 at 20:12
  • 1
    FYI: push isn't the only way of adding data into the array, you'd need to cater for splice and unshift, if removing is an issue, then you'll also need to look out for shift & pop - basically, there are many ways for someone to alter the array so returning a copy as @Barmar suggested may be best unless you require it be a persistent instance for something like angular. Commented Sep 8, 2015 at 20:15

3 Answers 3

2

you could override the push method:

var _d = [];
_d.__proto__.push = function() { return this.length; }

and when you need to use it in your module, call Array.prototype.push:

_b.addD = function (newD) {
    Array.prototype.push.call(_d, newD);
};
Sign up to request clarification or add additional context in comments.

7 Comments

Just keep in mind if you DO decide to do this, you will be defining the push method for all arrays... thereby not enabling you to push to any arrays. One trick you could do is to add a super class to your particular array and overwrite THAT prototype so that normal arrays still have regular functionality
Wouldn't it be better to assign to arr.push()?
@molow is correct. I am so used to writing in other languages that refer to arrays as arr. I believe this will do what you want it to do. However, the same thing applies in that you will no longer be able to push to this array. Essentially you will lock it.
It's worth noting that this solution and arr.push = function(){} will both still not stop you using the push function on that array
My point is exactly that, the code in your addD could be run outside his container object as Array.prototype.push.call(myModule.Something.d, newD). I'm not saying any of your code is invalid, it's just worth knowing that it's not waterproof
|
1

I haven't done any performance tests on this, but this certainly helps to protect your array.

(function(undefined) {
    var protectedArrays = [];
    protectArray = function protectArray(arr) {
        protectedArrays.push(arr);
        return getPrivateUpdater(arr);
    }
    var isProtected = function(arr) {
        return protectedArrays.indexOf(arr)>-1;
    }
    var getPrivateUpdater = function(arr) {
        var ret = {};
        Object.keys(funcBackups).forEach(function(funcName) {
            ret[funcName] = funcBackups[funcName].bind(arr);
        });
        return ret;
    }

    var returnsNewArray = ['Array.prototype.splice'];
    var returnsOriginalArray = ['Array.prototype.fill','Array.prototype.reverse','Array.prototype.copyWithin','Array.prototype.sort'];
    var returnsLength = ['Array.prototype.push','Array.prototype.unshift'];
    var returnsValue = ['Array.prototype.shift','Array.prototype.pop'];

    var funcBackups = {};
    overwriteFuncs(returnsNewArray, function() { return []; });
    overwriteFuncs(returnsOriginalArray, function() { return this; });
    overwriteFuncs(returnsLength, function() { return this.length; });
    overwriteFuncs(returnsValue, function() { return undefined; });

    function overwriteFuncs(funcs, ret) {
        for(var i=0,c=funcs.length;i<c;i++)
        {
            var func = funcs[i];
            var funcParts = func.split('.');
            var obj = window;
            for(var j=0,l=funcParts.length;j<l;j++)
            {
                (function() {
                    var part = funcParts[j];
                    if(j!=l-1) obj = obj[part];
                    else if(typeof obj[part] === "function")
                    {
                        var funcBk = obj[part];
                        funcBackups[funcBk.name] = funcBk;
                        obj[part] = renameFunction(funcBk.name, function() {
                            if(isProtected(this)) return ret.apply(this, arguments);
                            else return funcBk.apply(this,arguments);
                        });
                    }
                })();
            }
        }
    }
    function renameFunction(name, fn) {
        return (new Function("return function (call) { return function " + name +
            " () { return call(this, arguments) }; };")())(Function.apply.bind(fn));
    };
})();

You would use it like so:

var myArr = [];
var myArrInterface = protectArray(myArr);
myArr.push(5); //Doesn't work, but returns length as expected
myArrInterface.push(5); //Works as normal

This way, you can internally keep a copy of the interface that isn't made global to allow your helper funcs to modify the array as normal, but any attempt to use .push .splice etc will fail, either directly, or using the .bind(myArr,arg) method.

It's still not completely watertight, but a pretty good protector. You could potentially use the Object.defineProperty method to generate protected properties for the first 900 indexes, but I'm not sure of the implications of this. There is also the method Object.preventExtensions() but I'm unaware of a way to undo this effect when you need to change it yourself

Comments

1

Thank you, dandavis!

I used the slice method:

var myModule = (function () {
    "use strict";
    var a = (function () {
        var _b = {},
            _c = _c = "",
            _d = [];
        Object.defineProperty(_b, "c", {
            get: function () { return _c; }
        });
        Object.defineProperty(_b, "d", {
            get { return _d.slice(); } // UPDATED
        });
        _b.updateC = function (newValue) {
            _c = newValue;
        };
        _b.addD = function (newD) {
            _d.push(newD);
        };
        Object.seal(_b);
        return _b;
    }());
    var _something = { B: _b };
    return {
        Something: _something,
        AddD: _b.addD
    };
}());

myModule.Something.c = "blah"; // doesn't update = WIN!!
myModule.AddD({}); // pushed = WIN!
myModule.Something.d.push({}); // no more update = happiness

This allows me to protect from direct push calls enforcing some logic.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.