DEV Community

Cover image for Let's build a garage!
Amin
Amin

Posted on

Let's build a garage!

Behind this non-techy title hides a little trick in JavaScript that will make you love loops.

Let's say you have a garage. You wanted to manage your garage in JavaScript obviously because JavaScript is the best language for managing vehicles in a garage. Not convinced? Well, famous people are.

Not only JavaScript is the best language in the world, but it has allowed me to increase the management capability of my motorcycle garage by +99999%. ­— Keanu Reeves, 2019.

Okay, now that you are convinced, let's get started with a little bit of code.

The initial setup

We will write a simple, yet powerful garage class that will hold all of our vehicles.

"use strict";

class Garage {
  constructor() {
    this.vehicles = [];
  }

  add(vehicle) {
    this.vehicles.push(vehicle);
  }
}
Enter fullscreen mode Exit fullscreen mode

And then, we will need to instanciate a new garage to store our vehicles.

const garage = new Garage();
Enter fullscreen mode Exit fullscreen mode

Now, we can store our vehicles inside of our garage.

garage.add("Triumph Street Triple");
garage.add("Mazda 2");
garage.add("Nissan X-Trail");
Enter fullscreen mode Exit fullscreen mode

And what about looping over them to list all of our vehicles?

for (const vehicle of garage.vehicles) {
  console.log(vehicle);
}
Enter fullscreen mode Exit fullscreen mode

We can already see the result of our script by using Node.js.

$ node main.js
Triumph Street Triple
Mazda 2
Nissan X-Trail
Enter fullscreen mode Exit fullscreen mode

Great! or is it?

To complexity and beyond!

Most of the time, our classes will be more complex that this simple example. Let's say that our garage is now making a clear distinction between motorcycles and cars. While still holding them all together. Gotta listen to the orders of the garage holder, right?

  constructor() {
-   this.vehicles = [];
+   this.cars = [];
+   this.motorcycles = [];
  }
Enter fullscreen mode Exit fullscreen mode

We may also need to change a bit our add method to reflect the distinction as well.

- add(vehicle) {
-   this.vehicles.push(vehicle);
- }
+ addMotorcycle(motorcycle) {
+   this.motorcycles.push(motorcycle);
+ }
+
+ addCar(car) {
+   this.cars.push(car);
+ }
Enter fullscreen mode Exit fullscreen mode

As well as the way we add vehicles into the garage.

- garage.add("Triumph Street Triple");
+ garage.addMotorcycle("Triumph Street Triple");
- garage.add("Mazda 2");
+ garage.addCar("Mazda 2");
- garage.add("Nissan X-Trail");
+ garage.addCar("Nissan X-Trail");
Enter fullscreen mode Exit fullscreen mode

We can now run our script. This should work as intended, right?

$ node main.js
for (const vehicle of garage.vehicles) {
                             ^

TypeError: garage.vehicles is not iterable
Enter fullscreen mode Exit fullscreen mode

What's wrong?

You see, we now have removed the garage.vehicles property and instead we have two properties that holds our vehicles. We could have made two loops and loop over these two properties. We could even merge the two arrays into one and loop over it. Why not, let's do it!

- for (const vehicle of garage.vehicles) {
+ for (const vehicle of [...garage.motorcycles, ...garage.cars]) {
    console.log(vehicle);
  }
Enter fullscreen mode Exit fullscreen mode

Let's test this out:

$ node main.js
Triumph Street Triple
Mazda 2
Nissan X-Trail
Enter fullscreen mode Exit fullscreen mode

Yay! Working as intended. But it made our syntax less readable than before, less natural. Now imagine our garage increased in popularity and people from around the country want repairs for their bicycle, bus, trucks, ... Will you keep doing that? Of course yes! I mean no!

Do you have a moment to talk about our lord and savior, Iterator Protocol?

There is this strange world of iterator hidden under the JavaScript language. The saying says that once you go in there, you never really come back as one. You are part of something greater. You are part of the JavaScript language now. You feel whole, but you also feel connected to the inner system calls made by the JavaScript engine. But before feeling this power, we will need to refactor our code a bit.

  class Garage {
+   *[Symbol.iterator]() {
+     for (const vehicle of [...garage.motorcycles, ...garage.cars]) {
+       yield vehicle;
+     }
+   }
  }
Enter fullscreen mode Exit fullscreen mode

Okay! But wasn't what we did earlier? With a bit of new syntax? Yes of course, but we now are able to use a syntax that is more readable. Maybe not natural because we do not iterate over objects often, but it now allows us to iterate the object with the for...of loop and the triple-dot-syntax.

The star tells the engine that our function is now a generator function. A special kind of function that will help us return something that is compliant with the iterator protocol. The symbol will allow the iteration of our instances with the for loop (and the triple-dot-syntax) as well as use our instance in all methods that take an iterable as their argument. For instance, we would now be able to do something like:

Array.from(garage).map(vehicle => console.log(vehicle));
Enter fullscreen mode Exit fullscreen mode

And that would work fine!

Usage

Now that everything is setup, we can go back to our first initial definition of the for loop.

- for (const vehicle of [...garage.motorcycles, ...garage.cars]) {
+ for (const vehicle of garage) {
    console.log(vehicle);
  }
Enter fullscreen mode Exit fullscreen mode

Will it work? (spoiler: it will)

$ node main.js
Triumph Street Triple
Mazda 2
Nissan X-Trail
Enter fullscreen mode Exit fullscreen mode

But wait, there is more!

Now that we are using this new protocol and the iterator symbol, we can do cool things like looping over it without for loop:

+ [...garage].map(vehicle => console.log(vehicle));
- for (const vehicle of garage) {
-   console.log(vehicle);
- }
Enter fullscreen mode Exit fullscreen mode

This can be great to filter out vehicles by names for instance:

- [...garage].map(vehicle => console.log(vehicle));
+ [...garage]
+   .filter(vehicle => vehicle.toLowerCase().includes("triumph"))
+   .map(vehicle => console.log(vehicle));
Enter fullscreen mode Exit fullscreen mode

Running this would gives us only Triumph motorcycles (though, we only have one, these bikes are pretty expensive you know!).

$ node main.js
Triumph Street Triple
Enter fullscreen mode Exit fullscreen mode

The end

That is all there is for now folks! If you are interested about this subject, you can check out the documentation about the Symbol.iterator and the Iterator Protocol as well.

You can play with that example online here.

Will you use that feature? Do you think it helps or adds more complexity to your apps? Let me know in the comment section!

Top comments (2)

Collapse
 
domters profile image
Mike Peters • Edited

It is much more simple than to build real garage:) When building a garage, it's important to consider the quality and functionality of the garage door. I recently had to made garage door track replacement, and I can attest to the importance of hiring a professional garage door repair service(source link). Not only did they have the necessary tools and expertise, but they also helped me understand how to maintain my garage door for optimal performance. Investing in a high-quality garage door and keeping up with regular maintenance can save you time and money in the long run.

Collapse
 
aminnairi profile image
Amin

Haha yeah, maybe one day we will have a JavaScript Web API for that 😅