Javascript has a huge ecosystem.
At the time of writing there are 201,625 packages on the Node Package Manager (NPM) website
With a catalog of hundreds of thousands of libraries available to Node.js developers, how do you make sure it's easy to get started with any of those libraries?
By speaking the same language!
There are a handful of patterns so common in Javascript that you'll find them all in many libraries and at least one, and often many in almost every library.
Options POJO
A POJO, or Plain Old Javascript Object, is a collection of "key/value pairs", which is a concept you'll hear about all around the world of programming. Hash Tables, Lookup tables, Dictionaries, Maps, and Associative Arrays all implement the same concepts. There will be details that differ but they are roughly synonymous. In Javascript, the "key" component must be something that can be converted into a String
while the "value" can be anything (or nothing, in the case of null
). You will be able to look up the value by using the key. For example, maybe you keep information about users in an object for easily looking up the id of each nickname.
var userIdLookup = {
jake: 2,
avery: 1
};
console.log("The id of 'jake' is", userIdLookup.jake);
// output:
// 2
A POJO is most often created using the object literal syntax of {}
but can also be created using new Object
.
var userIdLookup = new Object();
userIdLookup.jake = 2;
userIdLookup.avery = 1;
console.log("The id of 'avery' is", userIdLookup.avery);
Often, you'll want to be able to configure a tool. When starting the Mocha test runner, for instance, you might want to set it up to use BDD
and pick the reporter. You will often do that using a Plain Old Javascript Object.
// include the mocha library (which was installed with npm)
var Mocha = require('mocha');
// create an instance of the Mocha Test Runner
// Use an options object
var mocha = new Mocha({
// the key is ui and the value is 'bdd'
ui: 'bdd',
// the key is reporter and the value is 'list'
reporter: 'list'
});
which is the same as:
// include the mocha library (which was installed with npm)
var Mocha = require('mocha');
// define the mocha options POJO
var mochaOptions = {
ui: 'bdd',
reporter: 'list'
};
// create an instance of the Mocha Test Runner
// Use an options object
var mocha = new Mocha(mochaOptions);
Tons of libraries use this pattern including
jQuery
$.ajax({
url: 'http://example.com',
type: 'GET',
async: true
});
Request
var request = require('request');
request({
method: 'GET',
url: 'http://example.com'
});
Twilio
var Twilio = require('twilio');
var client = new Twilio(auth, creds);
client.sendMessage({
to: '+16515556677',
from: '+14506667788',
body: 'word to your mother.'
});
Why use POJOs
By using an object to pass named arguments into a function, an arbitrary number of required and optional fields can be added without changing the signature of the function, which reduces the chance that code relying on the modified function will break.
Take for example the process of searching for a red car with four wheels using some code your coworker wrote:
var cars = findCars(4, "red");
this works for a while but things get harder when you want to want to find a compact sedan made by one of the brands you like
var cars = findCars(4, 'red', 'sedan', 'compact', 'tesla');
and things get down right impossible to read when we want to leave out search options and just find cars by tesla.
var cars = findCars(null, null, null, null, 'tesla');
That code i a mess to maintain, and it gets worse when your coworker wants to make it possible to search for just those cars that have rear wheel drive.
var cars = findCars(4, 'red', 'sedan', 'compact', true, 'tesla');
Notice that by adding this feature to the findCars function, your coworker has broken your old code!
Future you will also be wondering what that true
means when you come back to it in a couple of months.
findCars
could instead be written to take it's arguments using the options object and the rear wheel drive search features wouldn't have introduced a bug and future you will know what everything means.
var cars = findCars({
doors: 4,
color: 'red',
bodyStyle: 'sedan',
bodySize: 'compact',
manufacturer: 'tesla',
rearDriveOnly: true
});
var cars = findCars({
color: 'red',
bodyStyle: 'sedan',
manufacturer: 'tesla'
});
You'll notice that the options object allows you to call the function many different ways. It is a way, in javascript, to provide multiple signatures for a function by expecting different options depending on which action should happen. [[Aside: Some other languages allow overloading the function signature for directly. Javascript doesn't and often there will be a bit of boilerplate at the top of a function definition to facilitate flexible calls.)
Note: You might have noticed that a POJO looks similar to JSON formatted text. It turns out that JavaScript Object Notation is composed of the object and array literal syntaxes, {}
and []
respectively, and String
s. To convert between the two is a matter of using JSON.parse
and JSON.stringify
.
First Class Functions
In Javascript, functions are First Class Citizens, which means that all operations available to other objects will work with functions. Anything that can be done with any other variable in Javascript can be done with functions. In fact, functions are just callable objects.
Note: While there is no way to make an object callable without making it a function, you can apply properties to a function just like any other object. See this stackoverflow answer for more information.
Two properties of functions in particular should be pointed out. They are important because each enables a common pattern in Javascript.
- A function can be passed into another function as a parameter
- A function can be returned from another function
Callbacks
The first allows for "callbacks", which is a way to allow other code to execute and call the "callback" function when it is done.
var fs = require('fs');
fs.readFile('sample.txt', function (err, data) {
console.log("I'm done reading the file so I'm running your callback", err, data);
});
In Node.js
, callbacks are traditionally called with a first parameter of err
. This value will be null
if there is not an error and checking for an error is a common first step when writing a node.js
style callback. Let's dress up that first example a bit.
var fs = require('fs');
fs.readFile('sample.txt', function (err, data) {
if (err) {
console.error("There was an error reading sample.txt", err);
return;
}
console.log("The contents of sample.txt is: ", data);
});
Closures
In pre-ES6 Javascript, the only way to create a new scope is with a function
. A new scope offers a way to protect both the global scope and a new set of variables. By using a new scope, the global scope is left uncluttered and no one can accidentally reassign the value of your variable when their code runs.
A neat behavior of function
s in javascript is that they have access to the scope in which they were created, not the scope in which they are called.
A closure
takes advantage of a both of these behaviors. First, a quick example.
function makeGreeter (greeting) {
return function greeter (name) {
console.log(greeting + ", " + name);
}
}
var greeter = makeGreeter("Hello");
greeter("World");
// output:
// Hello World
In the example above, the value of greeting
that was passed as an argument to makeGreeter
is available to the greeter even though greeting
may seem locked into the scope of makeGreeter
.
A more real world use case is timing operations. In order to determine how long a function takes to return, you can measure the delta in a new Date
created before and after the operation. [[Note: This isn't exact as the time used to create the Date
objects is non-zero.]] It will be best if we can find a way to measure the run time of any function so as to not spend unnecessary time recreating the wheel.
function makeTimedFunction (func) {
return function callFunc (/* arguments */) {
var start = (new Date()).getTime();
// arguments holds the arguments passed to `callFunc`
// in the same order as they were. this mechanism
// allows for "passing through" arguments.
// the /* arguments */ comment is just used to indicate that
// we plan to use whatever arguments are passed in order to
// differentiate this function from one that will never be
// called with parameters. It is stylistic only.
func(arguments);
var end = (new Date()).getTime();
return (end - start);
}
}
var timedLog = makeTimedFunction(console.log);
var runTimeInMilliSeconds = timedLog("hello world");
Chaining
Chaining is a mechanism by which a developer can call multiple methods, sequentially, on an object without much boiler plate.
Chaining works because each method on an object will return the object. Let's look at an example
function makeApple() {
function setColor (color) {
apple.color = color;
return apple;
}
function setSize (size) {
apple.size = size;
return apple;
}
function setWeight (weight) {
apple.weight = weight;
return apple;
}
// hoisting will ensure this is declared in time.
var apple = {
setColor: setColor,
setSize: setSize,
setWeight: setWeight
};
return apple;
}
var apple = makeApple();
// make a large red apple of weight 8 ounces
apple.setColor('red').setSize('large').setWeight('8 oz');
console.log(apple);
// {color: 'red', size: 'large', weight: '8 oz' ...}
jQuery is notorious for chaining.
// select an element with id="p1"
$("#p1")
// set the element's color to red
.css("color", "red")
// slide the element up over a 2 second duration
.slideUp(2000)
// slide the element down over a 2 second duration, but only after the slideUp is finished.
.slideDown(2000);
SuperTest also leverages chaining to make writing tests more pleasant.
var request = require('supertest')
, express = require('express');
var app = express();
app.get('/user', function(req, res){
res.send(200, { name: 'tobi' });
});
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '20')
.expect(200)
.end(function(err, res){
if (err) {
throw err;
}
});
In the example above, the request is instructed what url to call and with what http method, the Content-Type
header's expected value is defined, the Content-Length
header's expected value is defined, the expected HTTP Status Code is declared and the request will bubble any errors it throws.
Go Do a Thing
Knowing the common patterns that other developers use when writing their libraries should prepare you to consume them. There are also other concepts that build on these other common patterns that you're now better prepared to tackle. (Such as Promises)
The best way to solidify these concepts is through repetition. Grab a package from NPM and hack on something for practice.