Five Common JavaScript Misunderstandings
Over the last few years more people have been drawn to JavaScript thanks to libraries like jQuery and innovative server-side projects like Node. However, JavaScript is deceptively easy to learn for existing developers, and there are a few frustratingly awkward pitfalls for newcomers. I’ve quietly been keeping notes on some of these pitfalls, and have selected a few of my favourites in this post.
Too Many Callbacks!
New Node developers like to complain about callbacks. Deeply nested callbacks don’t read particularly well, but this isn’t necessarily JavaScript’s fault. One way to mitigate this is through flow control techniques, and chainable APIs are probably the most popular technique in this area. For example, consider jQuery where the API feels very “flat”:
$('#foo')
.slideUp(300)
.delay(800)
.fadeIn(400);
If you’re knee-deep in callbacks, check to see if the library you’re using has an alternative chainable API. A lot of popular libraries do, like Underscore.js.
Chainable APIs work by returning this from their methods. It can be quite hard to make some APIs work this way, but masking the underlying complexity is often worth it.
There are plenty of flow control libraries available through npm that solve this (and similar) style problems.
Inheritance
Experienced classical object-oriented developers often get frustrated with JavaScript’s prototypes and recreate traditional class and inheritance patterns. I think the lack of OO-related keywords is the main reason for this. However, there’s no need to shy away from prototypes, and prototypal inheritance is surprisingly easy to learn:
var user;
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log('Hello', this.name);
};
// Admin inherits from Person
function Admin(name) {
this.name = name;
}
Admin.prototype = new Person;
user = new Admin('alex');
// This method was defined on Person
user.greet();
The key line is Admin.prototype = new Person;. I only really understood this when I started to think about prototype chains: Inheritance and the prototype chain.
Scope and Callbacks in Loops
I’ve fallen foul of this a few times over the years. When looping over a set of values, people often mistakenly think an anonymous function will capture the current state of the variables in the loop:
var assert = require('assert')
, names = ['alex', 'molly', 'yuka']
, name
, i;
for (i = 0; i < names.length; i++) {
name = names[i];
if (name === 'alex') {
setTimeout(function() {
assert.equal(name, 'alex');
}, 10);
}
}
The callback will execute in the future, at which point the name will have changed because it hasn’t been bound the way it seems like it should. This will work as expected:
var assert = require('assert')
, names = ['alex', 'molly', 'yuka'];
names.forEach(function(name) {
if (name === 'alex') {
setTimeout(function() {
assert.equal(name, 'alex');
}, 10);
}
});
People often use for instead of an iterator when performance is desired, but in certain cases callback-based iteration might be more readable.
this in Nested Functions
What’s the value of this in a nested function?
var user;
function Person(name) {
this.name = name;
}
Person.prototype.nested = function() {
console.log(this);
(function() {
console.log(this);
})();
};
user = new Person('alex');
user.nested();
The first console.log will show { name: 'alex' }, but the second will show the global object (window in client-side code). This is why a lot of code sets var self = this at the start of a method.
Future Reserved Words
The ECMA standards define Future Reserved Words that include commonly used words like class, extends, and super. It’s probably a good idea to avoid using these words; check with the specifications if you’re unsure of a particular word.