'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 1:[1]

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

Solution 2:[2]

Reviewing this, 2 years on, a possible solution for this is to return a copy of the array, via the property accessor. Whether it is the best approach depends on various factors (e.g. expected array size, etc.)

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

This means that calling .push on the array would have no affect on the original _arr array and the addArrValue method would be the only way to mutate the "private" _arr variable.

var x = new ClassName();

x.arr.push("newValue"); // Silently fails as it mutates a copy of _arr

console.log(x.arr); // []

x.addArrValue("hi");

console.log(x.arr); // ["hi"];

Solution 3:[3]

Not defineProperty, but Nowadays can use es6 Proxy

not the best one imo, just an alternative, but really customizable I'd say

var _arr;

_arr = [];

function ClassName () {
    var props = {};

    // ... other properties ...

    this.arr = new Proxy(_arr, {
      get(obj, prop){
        console.log("prop:", prop.toString());
        if(typeof prop === "symbol"){
          return obj[prop];
        }

        // either block functions like push, pop
        switch(prop){
        // or other mutable methods
        case "push":
        case "pop":
        case "shift":
        case "unshift":
          // you can throw error
          return () => {};
        case "length":
          return obj[prop];
        }

        if(Number.isInteger(Number(prop))){ // this is not a good way, just for example
          return obj[prop];
        }

        /*
        */
        // idk which is desired
        return obj[prop]; // default behaviour just in case
      },
      set(target, key, value){
        // you can throw error
        return false;
      }
    });

    Object.seal(this);
};

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

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

    _arr.push(value);
}


let x = new ClassName();
console.log(x.arr);
x.addArrValue(2);
console.log(x.arr);
console.log(x.arr.toString());
console.log(x.arr instanceof Array); // true
console.log(Array.isArray(x.arr)) // true
console.log(x.arr.class); // undefined

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Graeme Wicksted
Solution 2 Ash Clarke
Solution 3