I remember the first time I used jQuery and the awe I felt at being able to manipulate a number of DOM elements using nothing more than a CSS selector and a few chained methods. At the time I had no idea how jQuery could parse something like div .button > a.external to find the element(s) that fit the selector or allow me to chain method after method to interact with those elements. I loved being able to write

$(...).addClass(...)

.append(...)

.attr(...)

.on(...);

Having spent a lot of time in the JavaScript world, I still find jQuery to be a very interesting library, but now for completely different reasons than when I first started using it.

One of the things I later found interesting about jQuery is that what is generated by $(...) isn’t actually an array. Typing $("html") in the console will produce output similar to

► n.fn.init [html, prevObject: n.fn.init]

And while it looks like an array because of the bracket notation used in the console, calling Array.isArray() on this output confirms it is, indeed, not. In fact, it is just an object (as suspected by the fact that it begins with n.fn.init , which is an indicator it’s the result of some constructor) and the fact that Object.prototype.toString.call() tells us it’s [object Object] .

This little oddity leads down some interesting rabbit holes, not the least of which is how jQuery could cycle through its properties to manipulate DOM elements when it’s not an array. Researching how this is possible brought me to the topic of iterators. So, for this article we are going to reverse engineer a simple version of jQuery to talk about iterability as well as some of the smaller quirks like how an object can be displayed in the console like an array.

Iterability

Before we begin we should ask: What is iterability?

Iterability is a way an object specifies how to access its data programmatically. Specifically, we mean how the JavaScript engine will access values using for...of loops, the ... (spread) operator, or Array.from() .

Some JavaScript objects have iterability built in. Strings, arrays, maps, and sets all have data easily accessible using the above techniques. So we can write the following code and get valid output:

const str = "abc";

for (const letter of str) {

console.log(letter); // prints "a", "b", "c"

} const arr = [ 1, 2, 3 ];

for (const elem of arr) {

console.log(elem); // prints 1, 2, 3

} const map = new Map();

map.set("first", 1);

map.set("second", 2);

for (const mapping of map) {

console.log(mapping); // prints ["first", 1], ["second", 2]

} const set = new Set();

set.add("first");

set.add("second");

for (const elem of set) {

console.log(elem); // prints "first", "second"

}

However, we can’t do this out of the box with a regular object like person = { name: "Albert Einstein", age: 139 } or arrayLike = { 0: "We", 1: "can't", 2: "iterate", 3: "this" } . So, how do we make plain objects iterable?

In some of the Python-esque enhancements to JavaScript over the last couple years, one of the features implemented was a Symbol.iterator global constant. By using this as the name of a method in our object, we can create a function that will specify the behavior for traversing the object’s data using for...of , etc. We will utilize this in implementing our jQuery-lite functionality.

Scoping jQuery-lite

So, what do we want to implement? To keep it simple we’ll only build a way to query the DOM given a CSS selector, and we’ll limit the methods we can perform to addClass , append , attr , and on listed above.

We will also create a function akin to $() which accepts a CSS selector string or array-like object and returns an instance of our jQuery class.

Because we are doing this to illustrate JavaScript’s iterability characteristics, we won’t use an array (or a subclass of an array) to act as the jQuery object, because these objects are inherently iterable and won’t teach us anything. Also, they open up array methods that don’t come with jQuery and don’t make any sense in respect to DOM manipulation (e.g., .reduce() ).

So, let’s first set up the class skeleton we’ll flesh out:

class JQ {

constructor(list) {

let i = 0; while (list[i] !== undefined) {

this[i] = list[i];

i++;

}

} addClass(className) { ... }

append(content) { ... }

attr(name, val) { ... }

on(event, handler) { ... }

} const $$ = input => {

if (typeof input === "string") {

const nodes = document.querySelectorAll(input);

return new JQ(nodes);

} return new JQ(input);

}

Currently this is not an iterable object, but we’ve started to lay the groundwork. In the constructor we are taking a list parameter (which is calculated by our basic $$ function and is either a list of HTML elements corresponding to a CSS selector or a prior JQ instance) and assigning each item as a property of the class JQ, using its index in list as the property key.

But how do we make it iterable? Simply add our Symbol.iterator method to our JQ class:

[Symbol.iterator]() {

let i = 0;

const _this = this; return {

next() {

return _this[i]

? { done: false, value: _this[i++] }

: { done: true };

}

}

}

We start by creating a closure which will store a variable i and a reference to this that we will use to iterate through all the JQ instance’s properties that we want to cycle through in our other methods.

The real magic, however, happens in the return clause. The object we are returning has a next() method that the JavaScript engine will continually call so long as next() returns an object with a done: false property. That same object will contain a value from our JQ instance, and when we finally run out of values to pick out, we flag done: true .

So, let’s use our new iterability functionality to add logic to our remaining methods

Utilizing Iterability

The code we will use to flesh out or JQ methods is pretty simple. The way I am writing it will show two ways that our iterability will be used, and while it might not be the best for production purposes, it’s good for pedagogy. Our fleshed out class would look like this:

class JQ {

constructor(list) {

let i = 0; while (list[i] !== undefined) {

this[i] = list[i];

i++;

}

} addClass(className) {

for (const elem of this) {

elem.classList.add(className);

} return $$([ ...this ]);

} append(content) {

for (const elem of this) {

elem.append(content.cloneNode());

} return $$([ ...this ]);

} attr(name, val) {

for (const elem of this) {

elem.setAttribute(name, val);

} return $$([ ...this ]);

} on(event, handler) {

for (const elem of this) {

elem.addEventListener(event, handler);

} return $$([ ...this ]);

} [Symbol.iterator]() {

let i = 0;

const _this = this; return {

next() {

return _this[i]

? { done: false, value: _this[i++] }

: { done: true };

}

}

}

}

Each method uses the for...of loop and the ... operator. Because we have our iterator method, both of these work. That’s really all there is to making your objects iterable, but there are a couple more interesting things we can do with our class.

Bonus: Array Spoofing

Right now, if we log a JQ instance to the console, it will print out as an object typically would. A jQuery object, as we said earlier, does not. So how do we go about coercing an object to print out as an array?

There are only two requirements that must be met in order to make that happen: a length property and a splice() method. So let’s add those quickly. To keep things short, we’ll only modify the constructor (to add the length) and add the splice method.

class JQ {

constructor(list) {

let i = 0; while (list[i] !== undefined) {

this[i] = list[i];

i++;

}



this.length = i;

}



splice(...args) {

return [ ...this ].splice(...args);

}

}

That’s all we have to do! Now it will print out as

► JQ [ ... ]

Bonus: Using Generators

We can simplify the amount of code our class uses by switching out our Symbol.iterator method for a generator function. Simply put, a generator function returns a generator object which implements the iterator rules that we manually implemented above.

There are three things that are special about generators:

They use the yield keyword to pass values to the calling function. They remember their state in between calls. They use a * in their declaration.

So let’s do the swap.

class JQ {

constructor(list) {

let i = 0; while (list[i] !== undefined) {

this[i] = list[i];

i++;

}

}



*generator() {

for (let i = 0; i < this.length; i++) {

yield this[i];

}

}

}

Right now, however, all our previous methods are broken, since they require an iterator to perform their values, and generators are not iterators even though they implement them. If we attempted to call .addClass() on a JQ object now, we’d get the following error:

Uncaught TypeError: this is not iterable

So what do we need to do? We need to modify our other methods to use our *generator() . In general if we do const genObj = generator() , we will get an object that is iterable. So we will add one extra line of code to and change two lines of code in each of our methods.

class JQ {

constructor(list) {

let i = 0; while (list[i] !== undefined) {

this[i] = list[i];

i++;

}

}



*generator() {

for (let i = 0; i < this.length; i++) {

yield this[i];

}

} addClass(className) {

const iterable = this.generator();



for (const elem of iterable) {

elem.classList.add(className);

} return $$([ ...this.generator() ]);

}

}

We call this.generator() to get an iterable generator object, which allows us to use the for...of loop. We also have to call it again in our return statement, because generator objects are one-time use objects, and if we reuse it we will have no data supplied to the $$ function.

And that’s how we can use generator functions to simplify our own iterability logic (at the expense of a little more code in our methods).