From IIFEs to CommonJS to ES6 Modules
I’ve taught JavaScript for a long time to a lot of people. Consistently the most commonly under-learned aspect of the language is the module system. There’s a good reason for that. Modules in JavaScript have a strange and erratic history. In this post, we’ll walk through that history and you’ll learn modules of the past to better understand how JavaScript modules work today.
Before we learn how to create modules in JavaScript, we first need to understand what they are and why they exist. Look around you right now. Any marginally complex item that you can see is probably built using individual pieces that when put together, form the item.
Let’s take a watch for example.
A simple wristwatch is made up of hundreds of internal pieces. Each has a specific purpose and clear boundaries for how it interacts with the other pieces. Put together, all of these pieces form the whole of the watch. Now I’m no watch engineer, but I think the benefits of this approach are pretty transparent.
Reusability
Take a look at the diagram above one more time. Notice how many of the same pieces are being used throughout the watch. Through highly intelligent design decisions centered on modularity, they’re able to re-use the same components throughout different aspects of the watch design. This ability to re-use pieces simplifies the manufacturing process and, I’m assuming, increases profit.
Composability
The diagram is a beautiful illustration of composability. By establishing clear boundaries for each individual component, they’re able to compose each piece together to create a fully functioning watch out of tiny, focused pieces.
Leverage
Think about the manufacturing process. This company isn’t making watches, it’s making individual components that together form a watch. They could create those pieces in house, they could outsource them and leverage other manufacturing plants, it doesn’t matter. The most important thing is that each piece comes together in the end to form a watch - where those pieces were created is irrelevant.
Isolation
Understanding the whole system is difficult. Because the watch is composed of small, focused pieces, each of those pieces can be thought about, built and or repaired in isolation. This isolation allows multiple people to work individually on the watch while not bottle-necking each other. Also if one of the pieces breaks, instead of replacing the whole watch, you just have to replace the individual piece that broke.
Organization
Organization is a byproduct of each individual piece having a clear boundary for how it interacts with other pieces. With this modularity, organization naturally occurs without much thought.
We’ve seen the obvious benefits of modularity when it comes to everyday items like a watch, but what about software? Turns out, it’s the same idea with the same benefits. Just how the watch was designed, we should design our software separated into different pieces where each piece has a specific purpose and clear boundaries for how it interacts with other pieces. In software, these pieces are called modules . At this point, a module might not sound too different from something like a function or a React component. So what exactly would a module encompass?
Each module has three parts - dependencies (also called imports), code, and exports.
imports
code
exports
Dependencies (Imports)
When one module needs another module, it can import
that module as a dependency. For example, whenever you want to create a React component, you need to import
the react
module. If you want to use a library like lodash
, you’d need import
the lodash
module.
Code
After you’ve established what dependencies your module needs, the next part is the actual code of the module.
Exports
Exports are the “interface” to the module. Whatever you export from a module will be available to whoever imports that module.
Enough with the high-level stuff, let’s dive into some real examples.
First, let’s look at React Router. Conveniently, they have a modules folder. This folder is filled with… modules, naturally. So in React Router, what makes a “module”. Turns out, for the most part, they map their React components directly to modules. That makes sense and in general, is how you separate components in a React project. This works because if you re-read the watch above but swap out “module” with “component”, the metaphors still make sense.
Let’s look at the code from the MemoryRouter
module. Don’t worry about the actual code for now, but focus on more of the structure of the module.
// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";
// code
class MemoryRouter extends React.Component {
history = createMemoryHistory(this.props);
render() {
return (
<Router
history={this.history}
children={this.props.children}
/>;
)
}
}
// exports
export default MemoryRouter;
You’ll notice at the top of the module they define their imports, or what other modules they need to make the MemoryRouter
module work properly. Next, they have their code. In this case, they create a new React component called MemoryRouter
. Then at the very bottom, they define their export, MemoryRouter
. This means that whenever someone imports the MemoryRouter
module, they’ll get the MemoryRouter
component.
Now that we understand what a module is, let’s look back at the benefits of the watch design and see how, by following a similar modular architecture, those same benefits can apply to software design.
Reusability
Modules maximize reusability since a module can be imported and used in any other module that needs it. Beyond this, if a module would be beneficial in another program, you can create a package out of it. A package can contain one or more modules and can be uploaded to NPM to be downloaded by anyone. react
, lodash
, and jquery
are all examples of NPM packages since they can be installed from the NPM directory.
Composability
Because modules explicitly define their imports and exports, they can be easily composed. More than that, a sign of good software is that is can be easily deleted. Modules increase the “delete-ability” of your code.
Leverage
The NPM registry hosts the world’s largest collection of free, reusable modules (over 700,000 to be exact). Odds are if you need a specific package, NPM has it.
Isolation
The text we used to describe the isolation of the watch fits perfectly here as well. “Understanding the whole system is difficult. Because (your software) is composed of small, focused (modules), each of those (modules) can be thought about, built and or repaired in isolation. This isolation allows multiple people to work individually on the (app) while not bottle-necking each other. Also if one of the (modules) breaks, instead of replacing the whole (app), you just have to replace the individual (module) that broke.”
Organization
Perhaps the biggest benefit in regards to modular software is organization. Modules provide a natural separation point. Along with that, as we’ll see soon, modules prevent you from polluting the global namespace and allow you to avoid naming collisions.
At this point, you know the benefits and understand the structure of modules. Now it’s time to actually start building them. Our approach to this will be pretty methodical. The reason for that is because, as mentioned earlier, modules in JavaScript have a strange history. Even though there are “newer” ways to create modules in JavaScript, some of the older flavors still exist and you’ll see them from time to time. If we jump straight to modules in 2018, I’d be doing you a disservice. With that said, we’re going to take it back to late 2010. AngularJS was just released and jQuery is all the rage. Companies are finally using JavaScript to build complex web applications and with that complexity comes a need to manage it - via modules.
Your first intuition for creating modules may be to separate code by files.
// users.js
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
// dom.js
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
The full code can be found here .
OK. We’ve successfully separated our app into its own files. Does that mean we’ve successfully implemented modules? No. Absolutely not. Literally, all we’ve done is separate where the code lives. The only way to create a new scope in JavaScript is with a function. All the variables we declared that aren’t in a function are just living on the global object. You can see this by logging the window
object in the console. You’ll notice we can access, and worse, change addUsers
, users
, getUsers
, addUserToDOM
. That’s essentially our entire app. We’ve done nothing to separate our code into modules, all we’ve done is separate it by physical location. If you’re new to JavaScript, this may be a surprise to you but it was probably your first intuition for how to implement modules in JavaScript.
So if file separation doesn’t give us modules, what does? Remember the advantages to modules - reusability, composability, leverage, isolation, organization. Is there a native feature of JavaScript we could use to create our own “modules” that would give us the same benefits? What about a regular old function? When you think of the benefits of a function, they align nicely to the benefits of modules. So how would this work? What if instead of having our entire app live in the global namespace, we instead expose a single object, we’ll call it APP
. We can then put all the methods our app needs to run under the APP
, which will prevent us from polluting the global namespace. We could then wrap everything else in a function to keep it enclosed from the rest of the app.
// App.js
var APP = {}
// users.js
function usersWrapper () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
}
usersWrapper()
// dom.js
function domWrapper() {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
}
domWrapper()
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="app.js"></script> <script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
The full code can be found here .
Now if you look at the window
object, instead of it having all the important pieces of our app, it just has APP
and our wrapper functions, usersWrapper
and domWrapper
. More important, none of our important code (like users
) can be modified since they’re no longer on the global namespace.
Let’s see if we can take this a step further. Is there a way to get rid of our wrapper functions? Notice that we’re defining and then immediately invoking them. The only reason we gave them a name was so we could immediately invoke them. Is there a way to immediately invoke an anonymous function so we wouldn’t have to give them a name? Turns out there is and it even has a fancy name - Immediately Invoked Function Expression
or IIFE
for short.
IIFE
Here’s what it looks like.
(function () {
console.log('Pronounced IF-EE')
})()
Notice it’s just an anonymous function expression that we’ve wrapped in parens ().
(function () {
console.log('Pronounced IF-EE')
})
Then, just like any other function, in order to invoke it, we add another pair of parens to the end of it.
(function () {
console.log('Pronounced IF-EE')
})()
Now let’s use our knowledge of IIFEs to get rid of our ugly wrapper functions and clean up the global namespace even more.
// users.js
(function () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
})()
// dom.js
(function () {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
})()
The full code can be found here .
chef’s kiss . Now if you look at the window
object, you’ll notice the only thing we’ve added to it is APP
, which we use as a namespace for all the methods our app needs to properly run.
Let’s call this pattern the IIFE Module Pattern .
What are the benefits to the IIFE Module Pattern? First and foremost, we avoid dumping everything onto the global namespace. This will help with variable collisions and keeps our code more private. Does it have any downsides? It sure does. We still have 1 item on the global namespace, APP
. If by chance another library uses that same namespace, we’re in trouble. Second, you’ll notice the order of the <script>
tags in our index.html
file matter. If you don’t have the scripts in the exact order they are now, the app will break.
Even though our solution isn’t perfect, we’re making progress. Now that we understand the pros and cons to the IIFE module pattern, if we were to make our own standard for creating and managing modules, what features would it have?
Earlier our first instinct for the separation of modules was to have a new module for each file. Even though that doesn’t work out of the box with JavaScript, I think that’s an obvious separation point for our modules. Each file is its own module. Then from there, the only other feature we’d need is to have each file define explicit imports (or dependencies) and explicit exports which will be available to any other file that imports the module.
Our Module Standard
1) File based
2) Explicit imports
3) Explicit exports
Now that we know the features our module standard will need, let’s dive into the API. The only real API we need to define is what imports and exports look like. Let’s start with exports. To keep things simple, any information regarding the module can go on the module
object. Then, anything we want to export from a module we can stick on module.exports
. Something like this
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
module.exports.getUsers = getUsers
This means another way we can write it is like this
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
module.exports = {
getUsers: getUsers
}
Regardless of how many methods we had, we could just add them to the exports
object.
// users.js
var users = ["Tyler", "Sarah", "Dan"]
module.exports = {
getUsers: function () {
return users
},
sortUsers: function () {
return users.sort()
},
firstUser: function () {
return users[0]
}
}
Now that we’ve figured out what exporting from a module looks like, we need to figure out what the API for importing modules looks like. To keep this one simple as well, let’s pretend we had a function called require
. It’ll take a string path as its first argument and will return whatever is being exported from that path. Going along with our users.js
file above, to import that module would look something like this
var users = require('./users')
users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]
Pretty slick. With our hypothetical module.exports
and require
syntax, we’ve kept all of the benefits of modules while getting rid of the two downsides to our IIFE Modules pattern.
As you probably guessed by now, this isn’t a made up standard. It’s real and it’s called CommonJS.
The CommonJS group defined a module format to solve JavaScript scope issues by making sure each module is executed in its own namespace. This is achieved by forcing modules to explicitly export those variables it wants to expose to the “universe”, and also by defining those other modules required to properly work.
- Webpack docs
If you’ve used Node before, CommonJS should look familiar. The reason for that is because Node uses (for the most part) the CommonJS specification in order to implement modules. So with Node, you get modules out of the box using the CommonJS require
and module.exports
syntax you saw earlier. However, unlike Node, browsers don’t support CommonJS. In fact, not only do browsers not support CommonJS, but out of the box, CommonJS isn’t a great solution for browsers since it loads modules synchronously. In the land of the browser, the asynchronous loader is king.
So in summary, there are two problems with CommonJS. First, the browser doesn’t understand it. Second, it loads modules synchronously which in the browser would be a terrible user experience. If we can fix those two problems, we’re in good shape. So what’s the point of spending all this time talking about CommonJS if it’s not even good for browsers? Well, there is a solution and it’s called a module bundler.
Module Bundlers
What a JavaScript module bundler does is it examines your codebase, looks at all the imports and exports, then intelligently bundles all of your modules together into a single file that the browser can understand. Then instead of including all the scripts in your index.html file and worrying about what order they go in, you include the single bundle.js
file the bundler creates for you.
app.js ---> | |
users.js -> | Bundler | -> bundle.js
dom.js ---> | |
So how does a bundler actually work? That’s a really big question and one I don’t fully understand myself, but here’s the output after running our simple code through Webpack, a popular module bundler.
The full code can with CommonJS and Webpack can be found here . You’ll need to download the code, run “npm install”, then run “webpack”.
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(
exports,
name,
{ enumerable: true, get: getter }
);
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string')
for(var key in value)
__webpack_require__.d(ns, key, function(key) {
return value[key];
}.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({
/***/ "./dom.js":
/*!****************!*\
!*** ./dom.js ***!
\****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval(`
var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
function addUserToDOM(name) {\n
const node = document.createElement(\"li\")\n
const text = document.createTextNode(name)\n
node.appendChild(text)\n\n
document.getElementById(\"users\")\n
.appendChild(node)\n}\n\n
document.getElementById(\"submit\")\n
.addEventListener(\"click\", function() {\n
var input = document.getElementById(\"input\")\n
addUserToDOM(input.value)\n\n
input.value = \"\"\n})\n\n
var users = getUsers()\n
for (var i = 0; i < users.length; i++) {\n
addUserToDOM(users[i])\n
}\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),
/***/ "./users.js":
/*!******************!*\
!*** ./users.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {
eval(`
var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
function getUsers() {\n
return users\n}\n\nmodule.exports = {\n
getUsers: getUsers\n
}\n\n//# sourceURL=webpack:///./users.js?`);})
});
You’ll notice that there’s a lot of magic going on there (you can read the comments if you want to know exactly what’s happening), but one thing that’s interesting is they wrap all the code inside of a big IIFE. So they’ve figured out a way to get all of the benefits of a nice module system without the downsides, simply by utilizing our old IIFE Module Pattern.
What really future proofs JavaScript is that it’s a living language. TC-39, the standards committee around JavaScript, meets a few times a year to discuss potential improvements to the language. At this point, it should be pretty clear that modules are a critical feature for writing scalable, maintainable JavaScript. In ~2013 (and probably long before) it was dead obvious that JavaScript needed a standardized, built in solution for handling modules. This kicked off the process for implementing modules natively into JavaScript.
Knowing what you know now, if you were tasked with creating a module system for JavaScript, what would it look like? CommonJS got it mostly right. Like CommonJS, each file could be a new module with a clear way to define imports and exports - obviously, that’s the whole point. A problem we ran into with CommonJS is it loads modules synchronously. That’s great for the server but not for the browser. One change we could make would be to support asynchronous loading. Another change we could make is rather than a require
function call, since we’re talking about adding to the language itself, we could define new keywords. Let’s go with import
and export
.
Without going too far down the “hypothetical, made up standard” road again, the TC-39 committee came up with these exact same design decisions when they created “ES Modules”, now the standardized way to create modules in JavaScript. Let’s take a look at the syntax.
ES Modules
As mentioned above, to specify what should be exported from a module you use the export
keyword.
// utils.js
// Not exported
function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}
// Exported
export function first (arr) {
return arr[0]
}
// Exported
export function last (arr) {
return arr[arr.length - 1]
}
Now to import first
and last
, you have a few different options. One is to import everything that is being exported from utils.js
.
import * as utils from './utils'
utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3
But what if we didn’t want to import everything the module is exporting? In this example, what if we wanted to import first
but not last
? This is where you can use what’s called named imports
(it looks like destructuring but it’s not).
import { first } from './utils'
first([1,2,3]) // 1
What’s cool about ES Modules is not only can you specify multiple exports, but you can also specify a default
export.
// leftpad.js
export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}
When you use a default
export, that changes how you import that module. Instead of using the *
syntax or using named imports, you just use import name from './path'
.
import leftpad from './leftpad'
Now, what if you had a module that was exporting a default
export but also other regular exports as well? Well, you’d do it how you’d expect.
// utils.js
function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}
// regular export
export function first (arr) {
return arr[0]
}
// regular export
export function last (arr) {
return arr[arr.length - 1]
}
// default export
export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}
Now, what would the import syntax look like? In this case, again, it should be what you expect.
import leftpad, { first, last } from './utils'
Pretty slick, yeah? leftpad
is the default
export and first
and last
are just the regular exports.
What’s interesting about ES Modules is, because they’re now native to JavaScript, modern browsers support them without using a bundler. Let’s look back at our simple Users example from the beginning of this tutorial and see what it would look like with ES Modules.
The full code can be found here .
// users.js
var users = ["Tyler", "Sarah", "Dan"]
export default function getUsers() {
return users
}
// dom.js
import getUsers from './users.js'
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
Now here’s the cool part. With our IIFE pattern, we still needed to include a script to every JS file (and in order, none the less). With CommonJS we needed to use a bundler like Webpack and then include a script to the bundle.js
file. With ES Modules, in modern browsers, all we need to do is include our main file (in this case dom.js
) and add a type='module'
attribute to the script tab.
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users">
</ul>
<input id="input" type="text" placeholder="New User"></input>
<button id="submit">Submit</button>
<script type=module src='dom.js'></script> </body>
</html>
Tree Shaking
There’s one more difference between CommonJS modules and ES Modules that we didn’t cover above.
With CommonJS, you can require
a module anywhere, even conditionally.
if (pastTheFold === true) {
require('./parallax')
}
Because ES Modules are static, import statements must always be at the top level of a module. You can’t conditionally import them.
if (pastTheFold === true) {
import './parallax' // "import' and 'export' may only appear at the top level"
}
The reason this design decision was made was because by forcing modules to be static, the loader can statically analyze the module tree, figure out which code is actually being used, and drop the unused code from your bundle. That was a lot of big words. Said differently, because ES Modules force you to declare your import statements at the top of your module, the bundler can quickly understand your dependency tree. When it understands your dependency tree, it can see what code isn’t being used and drop it from the bundle. This is called Tree Shaking or Dead Code Elimination.
There is a stage 4 proposal for dynamic imports which will allow you to conditionally load modules via import().
I hope diving into the history of JavaScript modules has helped you gain not only a better appreciation for ES Modules, but also a better understanding of their design decisions.
(Bonus)JavaScript Composition vs Inheritance
This post is designed to be read after you read JavaScript Inheritance and the Prototype Chain .
Previously we looked at how to accomplish inheritance in JavaScript using both ES5 and ES6. In our example, we abstracted the common features amongst every animal (name, energy, eat, sleep, and play) to an Animal
base class. Then, whenever we wanted to create an individual type of animal (Dog, Cat, etc.), we created a subclass for that type.
class Animal {
constructor(name, energy) {
this.name = name
this.energy = energy
}
eat(amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
sleep() {
console.log(`${this.name} is sleeping.`)
this.energy += length
}
play() {
console.log(`${this.name} is playing.`)
this.energy -= length
}
}
class Dog extends Animal {
constructor(name, energy, breed) {
super(name, energy)
this.breed = breed
}
bark() {
console.log('Woof Woof!')
this.energy -= .1
}
}
class Cat extends Animal {
constructor(name, energy, declawed) {
super(name, energy)
this.declawed = declawed
}
meow() {
console.log('Meow!')
this.energy -= .1
}
}
And without the code, we can visualize our class structure like this
Animal
name
energy
eat()
sleep()
play()
Dog
breed
bark()
Cat
declawed
meow()
This worked well as it allowed us to minimize code duplication and maximize code reuse.
Let’s take this a step further and pretend we’re building software for “Farm Fantasy” - a massively multiplayer online (MMO) role-playing game where you do the exact same thing a farmer does, except, you know, online and you pay to do it.
Now that we’re creating an MMO, we’re going to need to have users. We can update our class structure now to look like this
User
email
username
pets
friends
adopt()
befriend()
Animal
name
energy
eat()
sleep()
play()
Dog
breed
bark()
Cat
declawed
meow()
The examples above are textbook examples of classes and inheritance. Sadly, unlike in the classroom, real-world software development isn’t always so predictable.
Let’s say 6 months after building out our initial class structure, our project manager decides we need to change some things. Users love the app and the ability to pay to be a pretend farmer, but they want a more real-life experience. Right now, only instances of Animal
have the ability to eat
, sleep
, and play
. The users are demanding that they also have those same features.
Alright, no issue. We just need to adjust our class structure around a little bit.
... 🤔
I guess we could abstract the common properties to another parent class and have one more step of inheritance
FarmFantasy
name
play()
sleep()
eat()
User
email
username
pets
friends
adopt()
befriend()
Animal
energy
Dog
breed
bark()
Cat
declawed
meow()
That works, but it’s incredibly fragile. There’s even a name for this anti-pattern - God object.
And just like that, we see the biggest weakness with inheritance. With inheritance, you structure your classes around what they are , a User
, an Animal
, a Dog
, a Cat
- all of those words encapsulate a meaning centered around what those things are . The problem with that is a User
today will probably be different than a User
in 6 months. Inheritance makes us turn a blind eye to the inevitable fact that our class structure will most likely change in the future, and when it does, our tightly coupled inheritance structure is going to crumble.
The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong. Creator of Erlang.
So if inheritance is such a problem, how do we get the same functionality while minimizing some of the downsides? Rather than thinking in terms of what things are , what if we think in terms of what things do ? Let’s take a Dog for example. A Dog is a sleeper, eater, player, and barker. A Cat is a sleeper, eater, player, and meower. A User is a sleeper, eater, player, adopter, and friender. Now let’s transform all of these verbs into functions.
const eater = () => ({})
const sleeper = () => ({})
const player = () => ({})
const barker = () => ({})
const meower = () => ({})
const adopter = () => ({})
const friender = () => ({})
Do you see where we’re going with this? Instead of having these methods defined (and coupled) to a particular class, if we abstract them into their own functions, we can now compose them together with any type that needs them.
Let’s take a closer look at one of our methods again, eat
.
eat(amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
Notice that eat
logs to the console then increases the energy
property on the instance by the amount
argument. Now the question we need to answer is how we can operate on a specific instance from a one-off function? Well, what if we just pass it in when we invoke the function? Seems simple enough.
const eater = (state) => ({
eat(amount) {
console.log(`${state.name} is eating.`)
state.energy += amount
}
})
Now we can follow this same pattern for each one of our functions.
...
const sleeper = (state) => ({
sleep(length) {
console.log(`${state.name} is sleeping.`)
state.energy += length
}
})
const player = (state) => ({
play() {
console.log(`${state.name} is playing.`)
state.energy -= length
}
})
const barker = (state) => ({
bark() {
console.log('Woof Woof!')
state.energy -= .1
}
})
const meower = (state) => ({
meow() {
console.log('Meow!')
state.energy -= .1
}
})
const adopter = (state) => ({
adopt(pet) {
state.pets.push(pet)
}
})
const friender = (state) => ({
befriend(friend) {
state.friends.push(friend)
}
})
Now whenever a Dog, Cat, or User needs to add the ability to do any of the functions above, they merge the object they get from one of the functions onto their own object.
Let’s see what that looks like. We’ll start with a Dog. Earlier we defined a Dog by what it does, a Dog is a sleeper
, eater
, player
, and barker
.
function Dog (name, energy, breed) {
let dog = {
name,
energy,
breed,
}
return Object.assign(
dog,
eater(dog),
sleeper(dog),
player(dog),
barker(dog),
)
}
const leo = Dog('Leo', 10, 'Goldendoodle')
leo.eat(10) // Leo is eating
leo.bark() // Woof Woof!
Inside of Dog
, we create the “instance” using a plain old JavaScript object. Then we use Object.assign
to merge the dog’s state with all of the methods a dog should have - each defined by what a dog does , not what it is .
Now how would we create a Cat
class? Earlier we defined a Cat as a sleeper
, eater
, player
, and meower
.
function Cat (name, energy, declawed) {
let cat = {
name,
energy,
declawed,
}
return Object.assign(
cat,
eater(cat),
sleeper(cat),
player(cat),
meower(cat),
)
}
Now, what about a User
? Earlier we ran into issues when we needed to refactor our class structure so that users could also sleep
, eat
, and play
. Now that we’ve decoupled our functions from the class hierarchy, this is trivial to do.
function User (email, username) {
let user = {
email,
username,
pets: [],
friends: []
}
return Object.assign(
user,
eater(user),
sleeper(user),
player(user),
adopter(user),
friender(user),
)
}
To really test our theory, what if we wanted to give all dogs the ability to add friends as well. This wasn’t in our initial requirement, but with composition it’s pretty straight forward.
function Dog (name, energy, breed) {
let dog = {
name,
energy,
breed,
friends: []
}
return Object.assign(
dog,
eater(dog),
sleeper(dog),
player(dog),
barker(dog),
friender(dog),
)
}
By favoring composition over inheritance and thinking in terms of what things do rather than what things are , you free yourself of fragile and tightly coupled inheritance structures.
You may have noticed I’m using what we previously referred to as the “Functional Instantiation” pattern. This is mostly for preference since we’re not involving the prototype at all. If for some reason you really liked the this and new keyword, you could use the following pattern.
function Cat (name, energy, declawed) {
this.name = name
this.energy = energy
this.declawed = declawed
return Object.assign(
this,
eater(this),
sleeper(this),
player(this),
meower(this),
)
}
const charles = new Cat('Charles', 10, false)
(Bonus) JavaScript Array Methods you should know
As JavaScript developers, we’re constantly dealing with Arrays. The most impactful skill set you can master in regards to Arrays in JavaScript is getting comfortable with most of the methods on Array.prototype
. This post is a collection of Array methods I think are crucial to know if you’re a JavaScript developer.
.concat
Used to merge two or more arrays. What’s nice about concat
is it doesn’t mutate the original array but instead returns a new array.
const oldFriends = ['Jake', 'Mikenzi', 'Jessica']
const newFriends = ['Merrick', 'Cash']
const friends = oldFriends.concat(newFriends)
oldFriends // Jake, Mikenzi, Jessica
newFriends // Merrick, Cash
friends // Jake, Mikenzi, Jessica, Merrick, Cash
Because concat
doesn’t modify the original array and instead returns a new array, it’s commonly used in React apps, Redux apps, or anywhere else mutations are frowned upon.
addFriend(friend) {
this.setState((prevState) => ({
friends: prevState.concat([friend])
}))
}
.every
Used to determine if every element in an array passes a test specified by a given function. The function passed to every
gets invoked once for each element in the array. As soon as that function returns a falsy value, every
will stop executing and return false
. If the function passed to every
never returns a falsy value, then every
will return true.
const ages = [18,21,28,34,22]
const groupCanVote = ages.every((age) => {
return age >= 18
})
const groupCanDrink = ages.every((age) => {
return age >= 21
})
console.log(groupCanVote) // true
console.log(groupCanDrink) // false
.fill
Defined, fill
“fills all the elements of an array from a start index to an end index with a static value”. It sounds pretty useless, but I’ve found one scenario where it’s pretty helpful.
As you start to make your code more functional, you naturally start to avoid using regular for loops in favor of .map
, .forEach
, etc. Let’s say our use case was we wanted to execute a function an arbitrary amount of times, say 10. How could we do this and avoid using a for loop? Your first intuition might be to use an Array constructor with .map.
Array(10).map(() => {
return doThing()
})
You’ll notice if you run that code it won’t work. When you do Array(10)
you’re creating an array of 10 unset or empty
values. When you use any of the array methods, they won’t work on an array with empty values. This is where .fill
comes into play. If you do Array(10).fill()
, you’ll then get an array with 10 undefined
, not empty items.
Array(10).fill().map(() => {
return doThing()
})
Not super useful, but good to know it exists if you need it.
.filter
Creates a new array after filtering out elements that don’t pass a test specified by a given function.
const tweets = [
{ id: 1, stars: 13, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, stars: 87, text: 'Tech conferences are too expensive.' },
{ id: 3, stars: 51, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, stars: 19, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
const popularTweets = tweets.filter((tweet) => {
return tweet.stars > 50
})
filter
is popular in React and Redux code bases (or anywhere where immutability is required) since it doesn’t modify the original array, instead, it returns a new array.
removeItem(index) {
this.setState((prevState) => ({
todos: prevState.todos.filter((item, i) => i !== index)
}))
}
.find
Allows you to find
the first element in an array which satisfies a test specified by a given function.
const tweets = [
{ id: 1, stars: 13, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, stars: 87, text: 'Tech conferences are too expensive.' },
{ id: 3, stars: 51, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, stars: 19, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
const tweet = tweets.find((t) => t.id === 3)
console.log(tweet) // {id: 3, stars: 51, text: "Clean code is subjective. Optimize for deletion."}
.findIndex
Similar to .find
, but instead of returning the element, it returns the index where the element is located.
const tweets = [
{ id: 1, stars: 13, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, stars: 87, text: 'Tech conferences are too expensive.' },
{ id: 3, stars: 51, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, stars: 19, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
const index = tweets.findIndex((t) => t.id === 3)
console.log(index) // 2
.forEach
Invokes a provided function once for each item in the array. Note .forEach
is similar to .map
except .map
returns a new array, .forEach
has no return value. Generally, you can use forEach
as a replacement for a for loop.
const friends = ['Jake', 'Mikenzi', 'Jacob']
friends.forEach((friend) => addToDOM(friend))
.includes
Determines if a particular value is found in an array. Note: We used to use indexOf
for this same behavior but as of ES6, includes
is more intuitive.
const friends = ['Jake', 'Mikenzi', 'Jacob']
friends.includes('Jake') // true
friends.includes('Karl') // false
.indexOf
Returns the index of a particular value. If that value doesn’t exist in the array, it returns -1.
const friends = ['Jake', 'Mikenzi', 'Jacob']
friends.indexOf('Jake') // 0
friends.indexOf('Karl') // -1
.join
Turns (or joins) all elements of an array into a string and returns it. By default, when converted into a string, each item in the array will be separated by a comma. You can change that by passing in a custom separator as the first argument.
const splitSentence = ['learn', 'react', 'at', 'ui.dev']
splitSentence.join() // learn,react,at,ui.dev
splitSentence.join(' ') // learn react at ui.dev
.map
Creates a new array with the result of calling a provided function on every element in the original array. When should you use .map
? I use it when I need to create a new array, based on a previous array. In the example below, I want to create a new array, tweetIds
, based on the original array, tweets
.
const tweets = [
{ id: 1, stars: 13, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, stars: 87, text: 'Tech conferences are too expensive.' },
{ id: 3, stars: 51, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, stars: 19, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
const tweetIds = tweets.map((tweet) => tweet.id) // [1,2,3,4]
In React, .map
is used a lot along with JSX to create an unordered list.
render () {
return (
<ul>
{this.state.todos.map((todo) => {
return <li key={todo.id}>{todo.text}</li>
})}
</ul>
)
}
.pop
Removes the last element from an array and returns it. Favor using .filter
instead of .pop
as mutations are .
const friends = ['Jake', 'Mikenzi', 'Karl']
const removedFriend = friends.pop()
console.log(removedFriend) // Karl
console.log(friends) // ['Jake', 'Mikenzi']
.push
Adds an item to the end of the array and returns the array’s new length. Favor using .concat
instead of .push
as mutations are .
const friends = ['Jake', 'Mikenzi']
friends.push('Jordyn') // 3
console.log(friends) ['Jake', 'Mikenzi', 'Jordyn']
.reduce
.reduce
holds the keys to the universe. If you master it, you’ll be able to do pretty much anything you want with arrays. Before you even look at the API, it’s important to understand why .reduce
exists. The idea of .reduce
is that you can take an array and transform it into anything else - another array, an object, an integer, literally anything. Why would you ever want to do that? Look at every single example on this whole page. In each one, we’re taking an array and transforming it in some way - mostly to another array. Let’s look at some other common ways you’d transform an array.
[1,2,3] -> sum-> 6
---
[
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
] -> Just the names -> ['Tyler', 'Mikenzi', 'Blaine']
---
[
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
] -> Length and age count -> { users: 3, ageTotal: 84}
---
[
{ id: 1, stars: 13, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, stars: 87, text: 'Tech conferences are too expensive.' },
{ id: 3, stars: 51, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, stars: 19, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
-> Remove the stars property ->
[
{ id: 1, text: 'Turns out "git reset --hard HEAD^" was a terrible idea.' },
{ id: 2, text: 'Tech conferences are too expensive.' },
{ id: 3, text: 'Clean code is subjective. Optimize for deletion.' },
{ id: 4, text: 'Maybe the real benefit of open source was the friendships we made along the way?' },
]
Here’s how I think about whether I should use .reduce
or not.
1) Am I transforming an array into another array
just by removing some elements? Use .filter
2) Am I transforming an array into another array? Use .map
3) Am I transforming an array into something other than another array? Use .reduce
Now that we understand what .reduce
is used for, let’s take a look at the API. Don’t worry if this is confusing, with great power comes great initial confusion. Google “reduce JavaScript” and read the first two pages of results. It’ll be worth it - promise.
I’ll first show you how to accomplish our goal of summing up an array of number into a single integer, then we’ll walk through it.
function sum (arr) {
return arr.reduce((total, num) => {
return total + num
}, 0)
}
sum([1,2,3]) // 6
sum([5,5,5]) // 15
Remember, the goal here is to take the array that’s being passed into the sum
function and transform it into a single integer, which is the summation of all the numbers in the array.
The first thing you’ll notice is that we pass .reduce
two arguments. The first is a function that will be invoked for every element in the array. The second is what we’ll call the “initial value”. In our example, because we’re adding all the numbers together, we want the initial value to be 0 (so we don’t run into any NaN problems). If we were creating a new object, the initial value would be an empty object. If we were creating an array, it would be an empty array.
Next, we need to figure out what the parameters are for the function we pass to .reduce
. In our example, we named them total
and num
. Remember, this function is going to be called for each element in the array. The reason we named it num
is because we’re reducing over an array full of integers and for each iteration, num
is going to be whatever the number is in the array. We can see this in the example below.
function sum (arr) {
return arr.reduce((total, num) => {
console.log(num)
}, 0)
}
sum([1,2,3])
// 1
// 2
// 3
That makes sense. The next thing we need to figure out is total
. This is where it gets a little mind-bendy. In our example, total
is going to initially be 0, since that’s what we set our initial value to. After that, total
is going to be whatever the previous iteration returned. We can see this clearly in the example below.
function sum (arr) {
return arr.reduce((total, num) => {
console.log(total)
return Date.now()
}, 0)
}
sum([1,2,3])
The first time our callback function runs, it logs 0 to the console. Again, that’s because we set the initial value to 0. Then, for each new time the function runs, we get a timestamp in the console because that’s what we’re returning.
Now let’s take this knowledge and look back at our initial solution.
function sum (arr) {
return arr.reduce((total, num) => {
return total + num
}, 0)
}
sum([1,2,3])
The very first time the callback function runs, total
will be 0 and num
will be 1. Then, the next iteration, total
will be 1 and num will be 2
. The reason total
is one is because we previously returned 0 + 1
. Then, total
will be 3 and num
will be 3. Then, there are no more elements in the array so what gets returned is 6. We can see this in the diagram below.
Initial Value: 0
First iteration:
total: 0
num: 1
Second iteration:
total: 1
num: 2
Third iteration:
total: 3
num: 3
No more elements in the array, return 3 + 3 which is 6.
Let’s look at the example where we convert an array of objects into another object.
[
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
] -> Length and age count -> { users: 3, ageTotal: 84}
First, what do we want the initial value to be? We want it to be an object with a users
property as well as an ageTotal
property, both set to 0
.
function getUserData (users) {
return users.reduce(() => {
}, { users: 0, ageTotal: 0 })
}
Now we need to figure out what each iteration looks like. We know what we want to return is the initial object, adding 1 to the users
property and adding whatever the user’s age is to the ageTotal
property.
function getUserData (users) {
return users.reduce((data, user) => {
data.users += 1
data.ageTotal += user.age
return data
}, { users: 0, ageTotal: 0 })
}
const users = [
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
]
getUserData(users) // { users: 3, ageTotal: 84 }
Impressive, yeah? Odds are this is still a bit fuzzy. The best thing you can do is take an array, and practice transforming it into anything you can think of.
.reverse
Reverses the order of an array. It’s important to remember that this method mutates the original array.
const letters = ['a', 'b', 'c']
letters.reverse()
console.log(letters) // ['c', 'b', 'a']
The most common use case I see for .reverse
is the interview question, “how do you reverse a string?”
const string = 'I like JavaScript'
const arr = string.split('') // Convert the string into an array
// Then reverse the array and join it back to a string.
arr.reverse().join('') // "tpircSavaJ ekil I"
.shift
Removes the first element from an array and returns it. Favor using .filter
instead of .shift
as mutations are .
const friends = ['Karl', 'Mikenzi', 'Jake']
const removedFriend = friends.shift()
console.log(removedFriend) // Karl
console.log(friends) // ['Mikenzi', 'Jake']
.slice
Allows you to create a new array from a portion of an existing array. It also doesn’t modify the original array.
.slice
takes two arguments. The first argument is the index of the beginning item you want to grab in the array and the second argument is the index of the end item you want to grab in the array, not inclusive. So for example, if you do .slice(1,4)
, the array that is returned will have whatever elements were in the 1st, 2nd, 3rd index in the original array, not the 4th.
const friends = ['Jake', 'Mikenzi', 'Jordyn', 'Cash', 'Leo']
const bestFriends = friends.slice(1,4)
console.log(bestFriends) // ['Mikenzi', 'Jordyn', 'Cash']
.some
Used to determine if any element in an array passes a test specified by a given function. The function passed to some
gets invoked once for each element in the array. As soon as that function returns a truthy value, some
will stop executing and return true
. If the function passed to some
never returns a truthy value, then some
will return false.
const ages = [6, 14, 12, 22, 13]
const hasAdultSupervision = ages.some((age) => {
return age >= 21
})
const canRentCar = ages.some((age) => {
return age >= 25
})
console.log(hasAdultSupervision) // true
console.log(canRentCar) // false
.sort
Allows you to sort the elements of an array. This one is more complex than I’d like it to be. Here’s my rule of thumb that works _most of the time_™. If what you’re sorting is based on string values, you can call .sort()
and you’d get what you’d expect.
const friends = ['Jake', 'Jacob', 'Mikenzi', 'Alex']
friends.sort()
console.log(friends) // ["Alex", "Jacob", "Jake", "Mikenzi"]
If you’re wanting to sort an array of integers, you can pass in a function and return the first argument minus the second argument.
const ages = [21, 19, 35, 38, 18, 23]
ages.sort((a,b) => a - b)
console.log(ages) // [18, 19, 21, 23, 35, 38]
Now say we had a more complex array, something like this array of objects.
const users = [
{ name: 'Jim', age: 28 },
{ name: 'Alex', age: 32 },
{ name: 'Mikenzi', age: 26 },
{ name: 'Christina', age: 42 },
]
To sort by the age, which is an integer, you would use the same pattern we used above, a - b
.
users.sort((a,b) => a.age - b.age)
To sort by name, you’d use a similar pattern, but instead of the minus sign, you’d use a greater than sign.
users.sort((a, b) => a.name > b.name)
Note that .sort
is mutative so each time you invoking it you’re modifying the original array.
.splice
Allows you to add and or remove items from anywhere inside of an array. It’s mutative so favor using another method that isn’t mutative like .slice
, .map
, .filter
or the spread operator.
The API is a bit funky as well. The first argument specifies where to start, the second argument specifies how many elements to remove, and the third argument specifies what to add.
const friends = ['Jake', 'Karl', 'Mikenzi']
const removedItems = friends.splice(1, 1, 'Jordyn')
// Start at index 1, remove 1 item then add Jordyn
console.log(removedItems) // ['Karl']
console.log(friends) // ['Jake', 'Jordyn', 'Mikenzi']
.unshift
Adds one or more elements to the beginning of an array and returns the array’s new length.
const ages = [22,27,29]
ages.unshift(20) // 4
console.log(ages) // [20,22,27,29]
Favor using .concat
or the Array spread operator instead of .shift
as mutations are .
const ages = [22,27,29]
const newAges = [20].concat(ages)
console.log(ages) // [22,27,29]
console.log(newAges) // [20,22,27,29]
const ages = [22,27,29]
const newAges = [20, ...ages]
console.log(ages) // [22,27,29]
console.log(newAges) // [20,22,27,29]
Bonus
.from
.from
isn’t an array method on Array.prototype
but instead is a static method on the Array
class. So you access it with Array.from()
.
.from
is used to create a new array from an “array-like” or iterable object.
The most common use case where you see an “array-like” object is with the “arguments” keyword inside of a function.
function sumArgs() {
console.log(arguments)
return arguments
.reduce((total, ele) =>
total + ele,
0
) // Error!
}
sumArgs(1,2,3,4,5) // [1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]
Notice that arguments
looks like an array, but it also has some extra properties on it. So it’s an “array-like” object.
Even though it’s “array-like”, that doesn’t mean it’s an instance of Array
which means it doesn’t have access to the methods on Array.prototype
.
This is where Array.from
comes into play. We can pass our array-like object to Array.from
and what we’ll get back is an actual instance of Array
.
function sumArgs() {
return Array.from(arguments)
.reduce((total, ele) => total + ele, 0)
}
sumArgs(1,2,3) // 6
(Bonus) Creating your own JavaScript Array
For the record, this is purely for educational purposes. There are roughly 0 other benefits to creating and using your own arrays in JavaScript.
When you’re first learning anything new, it’s hard to see the bigger picture. Generally your focus is on how to use the thing rather than how the thing works . Take a car for example. When you first start driving, you’re not worried about how the engine works. Instead, you’re just trying not to crash and die.
When you first started out with JavaScript, odds are one of the first data structures you learned was an array. Your concern was most likely memorizing the array API and how you’d use it, not how it actually works. Since that day, have you ever taken a step back and really thought about how arrays work? Probably not, and that’s fine. But today all of that is going to change. The goal here is to take the knowledge and patterns you’ve learned in this course and to use them to re-create a small portion of the JavaScript array API.
Here’s the end result we’re going for.
const friends = array('Jordyn', 'Mikenzi')
friends.push('Joshy') // 3
friends.push('Jake') // 4
friends.pop() // Jake
friends.filter((friend) => friend.charAt(0) !== 'J') // ['Mikenzi']
friends // { 0: 'Jordyn', 1: 'Mikenzi', 2: 'Joshy', length: 3, push: fn, pop: fn, filter: fn }
We first need to think about what an Array in JavaScript actually is. The good news is we don’t need to think too hard since we can use JavaScript’s typeof
operator.
const arr = []
typeof arr // "object"
Turns out an array was really just an object all along . An array is just an object with numerical keys and a length property that’s managed automatically for you. Instead of manually adding or removing values from the object, you do it via the array API, .push
, .pop
, etc. This becomes even more clear when you look at how you use bracket notation on both objects and arrays to access values.
const friendsArray = ['Jake', 'Jordyn', 'Mikenzi']
const friendsObj = {0: 'Jake', 1: 'Jordyn', 2: 'Mikenzi'}
friendsArray[1] // Jordyn
friendsObj[1] // Jordyn
It’s a little weird to have an object with numerical keys (since that’s literally what an array is for), but it paints a good picture that arrays really are just fancy objects. With this in mind, we can take the first step for creating our array
function. array
needs to return an object with a length property that delegates to array.prototype
(since that’s where we’ll be putting all the methods). As we’ve done in previous sections, we can use Object.create
for this.
function array () {
let arr = Object.create(array.prototype)
arr.length = 0
return arr
}
That’s a good start. Since we’re using Object.create to delegate failed lookups to array.prototype
, we can no add any methods we want shared across all instances to array.prototype
. If that’s still a little fuzzy, read A Beginner’s Guide to JavaScript’s Prototype.
Now before we move onto the methods, we first need to have our array
function accept n amount of arguments and add those as numerical properties onto the object. We could use JavaScript’s spread operator to turn arguments
into an array, but that feels like cheating since we’re pretending we’re re-creating arrays. Instead, we’ll use a trusty for in
loop to loop over arguments
and add the keys/values to our array and increment length
.
function array () {
let arr = Object.create(array.prototype)
arr.length = 0
for (key in arguments) {
arr[key] = arguments[key]
arr.length += 1
}
return arr
}
const friends = array('Jake', 'Mikenzi', 'Jordyn')
friends[0] // Jake
friends[2] // Jordyn
friends.length // 3
So far, so good. We have the foundation for our array
function.
Now as we saw above, we’re going to implement three different methods, push
, pop
, and filter
. Since we want all the methods to be shared across all instances of array
, we’re going to put them on array.prototype
.
array.prototype.push = function () {
}
array.prototype.pop = function () {
}
array.prototype.filter = function () {
}
Now let’s implement push
. You already know what .push
does, but how can we go about implementing it. First, we need to figure out a way to operate on whatever instance invokes push
. This is where the this
keyword will come into play. Inside of any of our methods, this
is going to reference the instance which called the specific method.
...
array.prototype.push = function () {
console.log(this)
}
const friends = array('Jake', 'Jordyn', 'Mikenzi')
friends.push() // {0: "Jake", 1: "Jordyn", 2: "Mikenzi", length: 3}
Now that we know we can use the this
keyword, we can start implementing .push
. There are three things .push
needs to do. First, it needs to add an element to our object at this.length
, then it needs to increment this.length
by one, and finally, it needs to return the new length of the “array”.
array.prototype.push = function (element) {
this[this.length] = element
this.length++
return this.length
}
Next, is .pop
. .pop
needs to do three things as well. First it needs to remove the “last” element, or the element at this.length - 1
. Then it needs to decrement this.length
by one. Lastly, it needs to return the element that was removed.
array.prototype.pop = function () {
this.length--
const elementToRemove = this[this.length]
delete this[this.length]
return elementToRemove
}
Our last method we’re going to implement is .filter
. .filter
creates a new array after filtering out elements that don’t pass a test specified by a given function. Like we saw earlier, we can iterate over every key/value pair in the “array” by using a for in
loop. Then for each key/value pair in the “array”, we’ll call the callback function that was passed in as the first argument. If the result of that invocation is truthy, we’ll push that into a new “array” which we’ll then return after we’ve iterated over the entire “array” instance.
array.prototype.filter = function (cb) {
let result = array()
for (let index in this) {
if (this.hasOwnProperty(index)) { // Avoid prototype methods
const element = this[index]
if (cb(element, index)) {
result.push(element)
}
}
}
return result
}
At first glance, our implementation of .filter
above looks like it should work. Spoiler alert, it doesn’t. Can you think of why it doesn’t? Here’s a hint - it has nothing to do with .filter
. Our code for .filter
is actually correct, it’s our array
constructor function that is where the issue is. We can see the bug more clearly if we step through a use case for our .filter
function.
const friends = array('Jake', 'Jordyn', 'Mikenzi')
friends.filter((friend) => friend.charAt(0) !== 'J')
/* Breakdown of Iterations*/
1) friend is "Jake". The callback returns false
2) friend is "Jordyn". The callback returns false
3) friend is "Mikenzi". The callback returns true
4) friend is "length". The callback throws an error
Ah. We’re using a for in
loop which by design loops over all enumerable properties of the object. In our array
function we just set length
by doing this.length = 0
. That means length
is an enumerable property and, as we saw above, will show up in for in
loops. You may have never seen this before, but the Object
class has a static method on it called defineProperty
which allows you to add a property on an object and specify if that property should be enumerable
or not. Let’s modify our array
function to use it so we can set length
to not be enumerable
.
function array () {
let arr = Object.create(array.prototype)
Object.defineProperty(arr, 'length', {
value: 0,
enumerable: false,
writable: true,
})
for (key in arguments) {
arr[key] = arguments[key]
arr.length += 1
}
return arr
}
Perfect.
Here is all of our code all together, including our example use cases from the beginning of the article.
function array () {
let arr = Object.create(array.prototype)
Object.defineProperty(arr, 'length', {
value: 0,
enumerable: false,
writable: true,
})
for (key in arguments) {
arr[key] = arguments[key]
arr.length += 1
}
return arr
}
array.prototype.push = function (element) {
this[this.length] = element
this.length++
return this.length
}
array.prototype.pop = function () {
this.length--
const elementToRemove = this[this.length]
delete this[this.length]
return elementToRemove
}
array.prototype.filter = function (cb) {
let result = array()
for (let index in this) {
if (this.hasOwnProperty(index)) {
const element = this[index]
if (cb(element, index)) {
result.push(element)
}
}
}
return result
}
let friends = array('Jordyn', 'Mikenzi')
friends.push('Joshy') // 3
friends.push('Jake') // 4
friends.pop() // Jake
friends.filter((friend) => friend.charAt(0) !== 'J') // { 0: "Mikenzi", length: 1 }
Nice work! Even though this exercise doesn’t have any practical value, I hope it’s helped you understand a little bit more about the JavaScript language.
Outro
If you made it this far, I’m incredibly humbled that you’d take the time to go through this course. As you can imagine, it was no small task to create, but I’m hopeful it’s been worth your time. The question now is, where do you go from here? As always, the answer is “Go build stuff!”. This course was full of some stuff you’ll use every day and some stuff you may never use at all (looking at you Prototypal Inheritance). Regardless, you won’t truly master JavaScript until you take what you’ve learned in this course and apply it to a real life scenario.
Last thing. If you’d like to leave a review for the course, it would mean a lot to us. You can do that below.