- 2.1. Function-scoped variables
- 2.2. Inadvertent sharing via a closure
- 2.7. for...in is weird
JavaScript has many pitfalls. This post examines whether they make JavaScript “unfixable” as a language – as some argue. To do so, it separates the pitfalls into two categories: The ones that become harmless after learning about them and the ones that don’t. We’ll also look at how the upcoming ECMAScript 6 fixes most problems.
Warning: If you are new to JavaScript then don’t let this post be your introduction to it. Consult other material first.
Major JavaScript pitfalls
There are two pitfalls in JavaScript that frequently trip up even experienced programmers.Dynamic this
Using a function inside a method is problematic, because you can't access the method’s this. This might be fixed in ECMAScript 6 via block lambdas (that don’t have their own this):
var obj = {
name: "Jane",
friends: ["Tarzan", "Cheeta"],
printFriends: function () {
// forEach argument is block lambda => can omit parentheses
this.friends.forEach {
| friend |
console.log(this.name+" knows "+friend);
}
}
}
Subtyping is difficult
While implementing a single type via a constructor is something that can be learned, creating a sub-constructor is too complicated [1]. As a result, numerous inheritance libraries have sprung up that lead to greatly varying coding styles, making life unnecessarily hard for humans and IDEs. And libraries are limited with regard to what they can do about super-calls. ECMAScript 6 will probably bring us some kind of inheritance operator and super-calls [2]:
var Employee = Person <| function (name, title) {
super.constructor(name);
this.title = title;
}
Employee.prototype .= {
describe() {
return super.describe() + " (" + this.title + ")";
}
};
Additionally, there might be class literals which would make things even simpler.
Minor JavaScript pitfalls
Apart from the big ones, there are several minor pitfalls that people frequently complain about. It is obviously not ideal that those exist, but you can learn and accept them. And once you have, they are unlikely to bite you in the future. The following list is not exhaustive, but covers the more ugly ones.Function-scoped variables
JavaScript’s var statement is function-scoped [3], even in nested blocks, the variables declared by it exist in the complete (innermost enclosing) function.
function createInterval(start, end) {
// Variable tmp already exists here
console.log(tmp); // undefined
if (start > end) {
var tmp = start;
start = end;
end = tmp;
}
return [start, end];
}
You quickly learn to use a pattern called immediately-invoked function expression (IIFE). Not pretty, but it works.
function createInterval(start, end) {
// Variable tmp does not exist here,
// accessing it would cause an error
if (start > end) {
(function () { // IIFE: open
var tmp = start;
start = end;
end = tmp;
}()); // IIFE: close
}
return [start, end];
}
ECMAScript 6 will have block-scoped variables via the let statement.
function createInterval(start, end) {
// Variable tmp does not exist here
if (start > end) {
let tmp = start;
start = end;
end = tmp;
}
return [start, end];
}
But with ECMAScript 6’s destructuring assignment, you don’t even need a temporary variable to do the swapping:
[start, end] = [end, start];
Inadvertent sharing via a closure
Functions you create in a given context stay connected to the variables in that context, even after leaving the context. For example, every function in the array result below will return 10, because that is the final value of i.
var result = [];
for(var i=0; i < 10; i++) {
result.push(function () { return i })
}
console.log(result[5]()); // 10, not 5
To avoid sharing, you need to make a copy, via an IIFE with a parameter:
var result = [];
for(var i=0; i < 10; i++) {
(function (i) { // copied i
result.push(function () { return i })
}(i)); // original i
}
console.log(result[5]()); // 5
Inadvertent sharing is not a quirk, it’s how things should normally work. I find that I normally notice quite easily when it happens and do the requisite copying. But it can foil beginners. Which is why Dart creates a fresh copy of the iteration value for each loop iteration. ECMAScript 6’s for...of loop will probably do the same. If you use an iteration method where the iteration value is passed to a function, then sharing won’t be a problem, either:
range(0, 10).map(function (i) {
result.push(function () { return i })
});
console.log(result[5]()); // 5
function range(start, end) {
var arr = [];
for(; start<end; start++) {
arr.push(start);
}
return arr;
}
Extracted methods can’t use this
You have to learn to use bind(). Example:
function repeat(n, func) {
for(var i = 0; i < n; i++) {
func();
}
}
var counter = {
count: 0,
inc: function () {
this.count++;
}
}
// The second argument can’t be just counter.inc
repeat(2, counter.inc.bind(counter));
Creating global variables via this
Before ECMAScript 5, if you made a function call to a constructor (because you forgot new) or a method (see previous item), then this would lead to global variables being read or created:
> function Point(x,y) { this.x = x; this.y = y }
> Point(12, 7)
undefined
> x
12
ECMAScript 5 strict mode [4] fixes that problem:
> function Point(x,y) { "use strict"; this.x = x; this.y = y }
> Point(12, 7)
TypeError: Cannot set property 'x' of undefined
Automatic creation of globals
Before ECMAScript 5, writing to a variable that didn’t exist, yet, automatically created a global variable:
> function f() { foobar = "hello" }
> f()
> foobar
'hello'
With strict mode, you get an error:
> function f() { "use strict"; foobar = "hello" }
> f()
ReferenceError: foobar is not defined
Comparison via == is weird
On the web, there are whole corpora of strange things you can do with ==. Simple solution: Don’t use it. Ever [5]. The strict equality operator === is very reliable.for...in is weird
The for...in loop is a strange beast:- Objects: It iterates over all property names, including (enumerable) inherited ones.
> var proto = { protoProp: true }; > var obj = Object.create(proto); > obj.objProp = true; > for(var propName in obj) { console.log(propName) } objProp protoPropIf, say, Object.prototype.toString wasn’t non-enumerable, it would also show up above, because by default, all objects inherit from Object.prototype. - Arrays: for...in seems to iterate over the array indices, but that is only because the length property is not enumerable. It actually iterates over all properties of an array.
> var arr = [ "a", "b" ]; > arr.hello = true; > for(var propName in arr) { console.log(propName) } 0 1 helloFor arrays, it would make much more sense to iterate (only) over the array elements and not their indices. ECMAScript 6’s for...of loop will do that.
var arr = [ "apple", "pear", "orange" ];
// You probably only want elem, but you have index, too
arr.forEach(function(elem, index) {
console.log(elem);
});
To iterate over the property names of an object, you can use:
Object.keys({ first: "John", last: "Doe" }).
forEach(function (propName) {
console.log(propName);
});
Array-like objects
Some objects in JavaScript are array-like, they have a length property and indexed access, but none of the array methods. You therefore need to borrow array methods via call(). The special variable arguments is array-like:
function printArguments() {
console.log(Array.prototype.join.call(arguments, "; "));
}
Not pleasant, but learnable. And you get errors quickly if you are doing something wrong. At least in ECMAScript 6, you won’t have to use arguments, any more:
function printArguments(...args) {
console.log(args.join("; "));
}
As a side benefit, function arity can now be automatically checked by the language.
3 comments:
Typo: in 2.6) "One the web" - should be "On the web"
Fixed. Thanks.
Cool stuff. I think "1.1 Dynamic this" is no more major than function-scoped variables. Once I learned the following boilerplate, it stopped biting me:
var t = this;
// use "t" instead of "this" now...
Of course, "t" could be named differently. If implementing Cat.prototype.purr, I would instead write var cat = this.
Post a Comment