javascriptarrayspropertiesnative-methods

How can I use `defineProperty` to create a readonly array property?


I have the following as a part of a module (names simplified for purpose of question):

In "module.js":

var _arr;

_arr = [];

function ClassName () {
    var props = {};

    // ... other properties ...

    props.arr = {
        enumerable: true,
        get: function () {
            return _arr;
        }
    };

    Object.defineProperties(this, props); 

    Object.seal(this);
};

ClassName.prototype.addArrValue = function addArrValue(value) {

    // ... some code here to validate `value` ...

    _arr.push(value);
}

In "otherfile.js":

var x = new ClassName();

With the implementation above, and the sample code below, adding values to arr can be achieved in two ways.

// No thank you.
x.arr.push("newValue"); // x.arr = ["newValue"];

// Yes please!
x.addArrValue("newValue"); // Only this route is desired.

Does anyone know how to achieve a readonly array property?

Note: writeable is false by default and no difference is observed if I explicitly set it.


Solution

  • Object.freeze() will do what you're asking (on browsers properly implementing the spec). Attempts to modify the array will either fail silently or throw TypeError when in strict mode.

    The easiest solution is to return a new frozen copy (freeze is destructive):

    return Object.freeze(_arr.slice());
    

    However, if more reading than writing is expected, lazy-cache the most recently accessed frozen copy and purge upon write (since addArrValue controls writes)

    Lazy caching read-only copy using modified original example:

    "use strict";
    const mutable = [];
    let cache;
    
    function ClassName () {
        const props = {};
    
        // ... other properties ...
    
        props.arr = {
            enumerable: true,
            get: function () {
                return cache || (cache = Object.freeze(mutable.slice());
            }
        };
    
        Object.defineProperties(this, props); 
    
        Object.seal(this);
    };
    
    ClassName.prototype.addArrValue = function addArrValue(value) {
    
        // ... some code here to validate `value` ...
    
        mutable.push(value);
        cache = undefined;
    }
    

    Lazy caching read-only copy using ES2015 classes:

    class ClassName {
        constructor() {
            this.mutable = [];
            this.cache = undefined;
            Object.seal(this);
        }
    
        get arr() {
            return this.cache || (this.cache = Object.freeze(this.mutable.slice());
        }
    
        function addArrValue(value) {
            this.mutable.push(value);
            this.cache = undefined;
        }
    }
    

    A "transparent" re-usable class hack (rarely required):

    class ReadOnlyArray extends Array {
        constructor(mutable) {
            // `this` is now a frozen mutable.slice() and NOT a ReadOnlyArray
            return Object.freeze(mutable.slice()); 
        }
    }
    
    const array1 = ['a', 'b', 'c'];
    const array2 = new ReadOnlyArray(array1);
    
    console.log(array1); // Array ["a", "b", "c"]
    console.log(array2); // Array ["a", "b", "c"]
    array1.push("d");
    console.log(array1); // Array ["a", "b", "c", "d"]
    console.log(array2); // Array ["a", "b", "c"]
    //array2.push("e"); // throws
    
    console.log(array2.constructor.name); // "Array"
    console.log(Array.isArray(array2));   // true
    console.log(array2 instanceof Array); // true
    console.log(array2 instanceof ReadOnlyArray); // false
    

    A proper re-usable class:

    class ReadOnlyArray extends Array {
        constructor(mutable) {
            super(0);
            this.push(...mutable);
            Object.freeze(this);
        }
        static get [Symbol.species]() { return Array; }
    }
    
    const array1 = ['a', 'b', 'c'];
    const array2 = new ReadOnlyArray(array1);
    
    console.log(array1); // Array ["a", "b", "c"]
    console.log(array2); // Array ["a", "b", "c"]
    array1.push("d");
    console.log(array1); // Array ["a", "b", "c", "d"]
    console.log(array2); // Array ["a", "b", "c"]
    //array2.push("e"); // throws
    
    console.log(array2.constructor.name); // "ReadOnlyArray"
    console.log(Array.isArray(array2));   // true
    console.log(array2 instanceof Array); // true
    console.log(array2 instanceof ReadOnlyArray); // true