Since NodeList
objects are snapshots (they don't track the contents of the DOM the way an HTMLCollection
does), the contents of the NodeList
are static, which makes supporting []
indexing easy: just assign to the indexes. Arrays are just objects with a couple of additional behaviors (a dynamic length
property, adjusting length
when you assign via []
, and of course Array.prototype
). The same thing that makes array[0]
work is what makes anyObject["property name"]
work.
To make something that looks like a NodeList
, offhand I think you need to:
- Put
NodeList.prototype
in its prototype chain so instanceof
works
- Support
item
- Support indexing (in this case, just by assigning to those properties)
- Support
length
as an accessor with a getter and no setter rather than a non-writable data property (in case anyone looks)
For instance (see comments):
// Constructor
function PseudoNodeList(arrayLike) {
const length = arrayLike.length;
// Define `length` -- slight difference with `NodeList` here, this is
// defined on the object itself, but `NodeList` has it on the prototype
Object.defineProperty(this, "length", {
get() {
return length;
},
enumerable: true, // Oddly, it is on `NodeList.prototype`
configurable: true,
});
// Copy the indexed entries
Object.assign(this, Array.from(arrayLike));
// (Instead of the statement above, you could use a `for` loop, which
// would likely be faster -- you did mention performance)
}
// Make `instanceof` work, and inherit the implementations of
// [Symbol.iterator] and other methods -- though you'll want to test that
// Safari and Firefox are okay with inheriting them, I only checked on
// Chromium-based browsers (Chromium, Chrome, Brave, Edge, Opera I think).
// They may be more picky about `this`.
PseudoNodeList.prototype = Object.create(NodeList.prototype);
// Fix `constructor` property
PseudoNodeList.prototype.constructor = PseudoNodeList;
// Add item method
Object.defineProperty(PseudoNodeList.prototype, "item", {
value(index) {
if (index < 0 || index >= this.length) {
return null;
}
return this[index];
},
enumerable: true, // Oddly, it is on `NodeList.prototype`
configurable: true,
});
Live Example:
// Constructor
function PseudoNodeList(arrayLike) {
const length = arrayLike.length;
// Define `length` -- slight difference with `NodeList` here, this is
// defined on the object itself, but `NodeList` has it on the prototype
Object.defineProperty(this, "length", {
get() {
return length;
},
enumerable: true, // Oddly, it is on `NodeList.prototype`
configurable: true,
});
// Copy the indexed entries
Object.assign(this, Array.from(arrayLike));
// (Instead of the statement above, you could use a `for` loop, which
// would likely be faster -- you did mention performance)
}
// Make `instanceof` work, and inherit the implementations of
// [Symbol.iterator] and other methods -- though you'll want to test that
// Safari and Firefox are okay with inheriting them, I only checked on
// Chromium-based browsers (Chromium, Chrome, Brave, Edge, Opera I think).
// They may be more picky about `this`.
PseudoNodeList.prototype = Object.create(NodeList.prototype);
// Fix `constructor` property
PseudoNodeList.prototype.constructor = PseudoNodeList;
// Add item method
Object.defineProperty(PseudoNodeList.prototype, "item", {
value(index) {
if (index < 0 || index >= this.length) {
return null;
}
return this[index];
},
enumerable: true, // Oddly, it is on `NodeList.prototype`
configurable: true,
});
// ======= Using it:
const p = new PseudoNodeList(
document.querySelectorAll(".example")
);
console.log(`p instanceof NodeList? ${p instanceof NodeList}`);
console.log(`p.length = ${p.length}`);
console.log(`p.keys():`, [...p.keys()]);
console.log(`p.values():`, [...p.values()]);
console.log(`p.entries():`, [...p.entries()]);
// Turn all of them green via iteration
for (const el of p) {
el.style.color = "green";
}
// Use `item` method to make the first match `font-size: 20px`
p.item(0).style.fontSize = "20px";
// Use indexed property to make the first match `font-style: italic`
p[1].style.fontStyle = "italic";
<div>one</div>
<div class="example">two</div>
<div>one</div>
<div class="example">three</div>
I didn't take the approach of subclassing Array
(which would certainly be another way to go) because I didn't want to hide all the array methods that NodeList
doesn't have.
If you wanted to make the []
indexing dynamic (you don't need to for a NodeList
stand-in, since again those are static), you could use a Proxy
.