JavaScript arrays are “ancient” objects. They were one of the first data types added to the language, unlike Map, Proxy, WeakMap etc.
An object has two types of properties – public slots and internal slots. E.g. Map() has internal property called [[MapData]] where it keeps all it’s data. Array has no internal slots.
However, array does “process” some properties… like index. If the index is >=0, it considers them to be part of the “array”. However, if we use non positive integer values, array adds the property but does not consider them in certain scenarios, e.g. ‘array.length’ or ‘for of’.
Look at some array quirks
// Normal usage | |
console.log("const normalArray = [11, 23, 35];"); | |
const normalArray = [11, 23, 35]; | |
console.log(normalArray); | |
/* [ 11, 23, 35 ] */ | |
// Adding by index | |
console.log("normalArray[3] = 48;"); | |
normalArray[3] = 48; | |
console.log(normalArray); | |
/* [ 11, 23, 35, 48 ] */ | |
// Non positive index get added as keys | |
console.log("normalArray[-1]=5"); | |
normalArray[–1] = 5 | |
console.log(normalArray); | |
/* [ 11, 23, 35, 48, '-1': 5 ] */ | |
// Even string | |
normalArray['foo'] = 'bar'; | |
console.log("normalArray['foo'] = 'bar';") | |
console.log(normalArray); | |
/* [ 11, 23, 35, 48, '-1': 5, foo: 'bar' ] */ | |
// for of gives only positive index | |
console.log("for (let index of normalArray)"); | |
for (let element of normalArray) | |
console.log(element); | |
/* | |
11 | |
23 | |
35 | |
48 | |
*/ | |
// for in gives ALL key/value pairs | |
console.log("for (let index in normalArray)"); | |
for (let index in normalArray) | |
console.log(`${index}: ${normalArray[index]}`); | |
/* | |
0: 11 | |
1: 23 | |
2: 35 | |
3: 48 | |
-1: 5 | |
foo: bar | |
*/ | |
// It counts only positive index | |
console.log("normalArray.length"); | |
console.log(normalArray.length); | |
/* 4 */ |
Fine.. I will be careful while using arrays. Great! But what if you are writing a module/lib and you have to pass you array to the caller? What if she/he adds non-positive index?
Thankfully, we have a new type of object in JavaScript – Proxy. It does what its name says – Proxy for other objects. A Proxy can “intercept” access to another object
Let’s build a “Pure” Array
First, let’s understand Proxy
// Simple object | |
let person = { | |
id: 0, | |
name: "default", | |
} | |
// There is no validation if object is used directly | |
person.id = "Eich"; // We would like it to be positive integer | |
person.name = 3.14; // Name should ideally be non blank string | |
console.log(person); | |
// Let's setup Proxy handler with validations | |
const handler = { | |
set(object, property, value, receiver) { | |
// id should be positive integer | |
if (property == "id") { | |
if (typeof value != 'number' || value < 0) | |
throw "id should be a positive number"; | |
} | |
// name should be a non blank string | |
if (property == "name") { | |
if (typeof value != 'string' || value.trim() == "") | |
throw "name should be a non blank string"; | |
} | |
// Passed all validations, so assign to real object | |
return Reflect.set(object, property, value, receiver); | |
} | |
} | |
// Use a Proxy to person object | |
const PersonProxy = new Proxy(person, handler); | |
// These should give errors | |
PersonProxy.id = "Brendan"; | |
PersonProxy.name = 1; | |
console.log(PersonProxy); |
As we can see, Proxy can “trap” any access to the object and change the behaviour. In the case above, we are able to validate that id should be integer and name should be a string. We can build a similar Proxy for array and make sure that the index is only positive integers.
Pure Array
// Array proxy | |
const PureArray = (…args) => | |
new Proxy([…args], // Create internal array | |
// Handler with traps | |
{ | |
// Intercept assignment to array using index | |
set(target, property, value, receiver) { | |
// Allow built in properties like length | |
if (Reflect.has(target, property)) | |
return Reflect.get(target, property, receiver); | |
// Check if property is positive integer | |
const index = Number.parseInt(property); | |
if (Number.isNaN(index) || index < 0) | |
throw new Error("Index must be >= 0"); | |
// Otherwise call the internal array | |
return Reflect.set(target, property, value, receiver); | |
}, | |
// Intercept access to the array via index | |
get(target, property, receiver) { | |
// Check if its one of the built in properties | |
if (Reflect.has(target, property)) | |
return Reflect.get(target, property, receiver); | |
// Check if property is positive integer | |
const index = Number.parseInt(property); | |
if (Number.isNaN(index) || index < 0) | |
throw new Error("Index must be >= 0"); | |
// Otherwise call the internal array | |
return Reflect.get(target, property, receiver); | |
} | |
} | |
); | |
// let's create our "Pure" array | |
const myArray = PureArray(1, 2, 3); | |
console.log(myArray); | |
// Normal operations work | |
myArray[3] = 4; | |
console.log(myArray); | |
console.log(myArray.slice(2, 3)); | |
console.log(myArray.length); | |
// These don't work anymore | |
myArray[–1] = 5; | |
console.log(myArray[–3]); |
There we have it! We have used a Proxy object to intercept access to an array and allow ONLY non-positive index. For others, it will appear to be an Array and they would see no difference. All array methods will continue to work but we will ensure that only positive indexes are allows.