'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 |