Friday, 3 April 2015

JavaScript is an easy language to get started with, but to achieve mastery takes a lot of effort. Beginners often make a few well-known mistakes that come back and bite them when they least expect. To find which these mistakes are, keep reading!

1. Missing curly braces

One practice, which JavaScript beginners are often guilty of, is omitting curly braces after statements like ifelsewhile and for. Although it is allowed, you should be extra careful, because this practice can often conceal problems and be the source of bugs. See the below example:
JS

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // This code doesn't do what the indentation implies!
    if(name === undefined)
        console.log('Please enter a username!');
        fail();
    // The following line is never reached:
    success(name);
}
function success(name){
    console.log('Hello, ' + name + '!');
}
function fail(){
    throw new Error("Name is missing. Can't say hello!");
}
RUN
Although the fail() call is indented and looks as if it belongs to the if statement, it does not. It is always called. So it is a good practice to always surround blocks of code with curly braces, even if there is only one statement involved.

2. Missing semicolons

When JavaScript is parsed, there is a process known as automatic semicolon insertion. As the name suggests, the parser is happy to insert missing semicolons for you. The purpose of this feature is to make JavaScript more approachable and easier to write by beginners. However, you should always include semicolons, because there are dangers in omitting them. Here is an example:
JS

1
2
3
4
5
6
7
8
9
10
11
// This code results in a type error. Adding semicolons will fix it.
console.log('Welcome the fellowship!')
['Frodo', 'Gandalf', 'Legolas', 'Gimli'].forEach(function(name){
    hello(name)
})
function hello(name){
    console.log('Hello, ' + name + '!')
}
RUN
Because there is a semicolon missing on line 3, the parser assumes that the opening bracket on line 5 is an attempt to access a property using the array accessor syntax (see mistake #8), and not a separate array, which is not what was intended and results in a type error. The fix is simple – always write semicolons.
Some experienced JavaScript developers prefer to leave out semicolons, but they are perfectly aware of the bugs that this might cause and know how to prevent them.

3. Not understanding type coercion

JavaScript is dynamically typed. This means that you don’t need to specify a type when declaring a new variable, and you can freely reassign or convert its value. This makes JavaScript much easier to write than something like C# or Java, but you open the doors for potential bugs that in other languages are caught during the compilation step. Here is an example:
JSHTML

1
2
3
4
5
6
7
8
9
10
11
12
// Listen for the input event on the textbox
var textBox = document.querySelector('input');
textBox.addEventListener('input', function(){
    // textBox.value holds a string. Adding 10 appends 
    // the string '10', it doesn't perform an addition..
    console.log(textBox.value + ' + 10 = ' + (textBox.value + 10));
});
RUN
The problem can be easily fixed by using parseInt(textBox.value, 10) to turn the string into a number before adding 10 to it. Depending on how you use a variable, the runtime might decide that it should be converted in one type or another. This is known as type coercion. To prevent types from being implicitly converted when comparing variables in if statements, you can usestrict equality checks (===).

4. Forgetting var

Another practice that beginners are guilty of, is forgetting to use the var keyword when declaring variables. JavaScript is very permissive, and the first time it sees that you’ve used a variable without the var statement, it will silently declare it for you globally. This can be the source of subtle bugs. Here is an example, which also shows a different bug – missing a comma when declaring multiple variables at once:
JS

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1, b = 2, c = 3;
function alphabet(str){
    var a = 'A', b = 'B'    // Oops, missing ',' here!
        c = 'C', d = 'D';
    return str + ' ' + a + b + c + '…';
}
console.log( alphabet("Let's say the alphabet!") );
// Oh no! Something went wrong! c has a new value!
console.log(a, b, c);
RUN

When the parser reaches line 4, it will insert a semicolon automatically, and then interpret the cand d declarations on line 5 as global. This will cause the value of the outer c variable to change. Read about more JavaScript gotchas here.

5. Arithmetic operations with floats

This mistake is true for nearly every programming language out there, including JavaScript. Due to the way floating point numbers are represented in memory, arithmetic operations are not as precise as you’d think. Here is an example:
JS

1
2
3
4
5
6
7
var a = 0.1, b = 0.2;
// Surprise! this is false:
console.log(a + b == 0.3);
// Because 0.1 + 0.2 does not produce the number that you expect:
console.log('0.1 + 0.2 = ', a + b);
RUN
To work around this problem, you should not use use decimals if you need absolute correctness – use whole numbers, or if you need to work with money, use a library likebignumber.js.

6. Using constructors over literals

When Java and C# programmers start writing JavaScript, they often prefer to create objects using constructors: new Array()new Object()new String(). Although they are perfectly supported, it is recommended to use the literal notations: []{}"", because the constructor functions have subtle peculiarities:
JS

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var elem4 = new Array(1,2,3,4);
console.log('Four element array: ' + elem4.length);
// Create an array with one element. It doesn't do what you think it does:
var elem1 = new Array(23);
console.log('One element array? ' + elem1.length);
/* String objects also have their warts */
var str1 = new String('JavaScript'),
    str2 = "JavaScript";
// Strict equality breaks:
console.log("Is str1 the same as str2?", str1 === str2);
RUN
The solution is simple: try to always use the literal notation. Besides, JS arrays don’t need to know their length in advance.

7. Not understanding how scopes work

A difficult concept for beginners to understand is JavaScript’s scoping rules and closures. And rightfully so:
JS

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for(var i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i+1);
    }, 100*i);
}
/* To fix the bug, wrap the code in a self-executing function expression:
for(var i = 0; i < 10; i++){
    (function(i){
        setTimeout(function(){
            console.log(i+1);
        }, 100*i);
    })(i);
}               
*/
RUN
Functions retain visibility to variables in their parent scopes. But because we are delaying the execution with a setTimeout, when the time comes for the functions to actually run, the loop has already finished and the i variable is incremented to 11.
The self executing function in the comment works, because it copies the i variable by value and keeps a private copy for each timeout function. Read more about scopes here and here.

8. Using eval

Eval is evil. It is considered a bad practice, and most of the times when you use it, there is a better and faster approach.
JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Using eval to access properties dynamically */
var obj = {
    name: 'Foo Barski',
    age: 30,
    profession: 'Programmer'
};
// Which property to access?
var access = 'profession';
// This is a bad practice. Please don't do it:
console.log( eval('obj.name + " is a " + obj.' + access) );
// Instead, use array notation to access properties dynamically:
console.log( obj.name + " is a " + obj[access]);
/* Using eval in setTimout */
RUN
Code inside eval is a string. Debug messages arising from eval blocks are incomprehensible and you have to juggle with escaping single and double quotes. Not to mention that it is slower than regular JavaScript. Don’t use eval unless you know what you are doing.

9. Not understanding asynchronous code

Something that is unique to JavaScript is that nearly everything is asynchronous, and you need to pass callback functions in order to get notified of events. This doesn’t come intuitively to beginners, and they quickly find themselves scratching their heads on a bug that is difficult to understand. Here is an example, in which I use the FreeGeoIP service to fetch your location by IP:
JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var userData = {};
// Fetch the location data for the current user.
load();
// Output the location of the user. Oops, it doesn't work! Why?
console.log('Hello! Your IP address is ' + userData.ip + ' and your country is ' + userData.country_name);
// The load function will detect the current visitor's ip and location with ajax, using the
// freegeoip service. It will place the returned data in the userData variable when it's ready.
function load(){
    $.getJSON('http://freegeoip.net/json/?callback=?', function(response){
        userData = response;
        // Uncomment this line to see what is returned:
        // console.log(response);
    });
RUN
Even though the console.log comes after the load() function call, it is actually executed before the data is fetched.

10. Misusing event listeners

Let’s say that you want to listen for clicks on a button, but only while a checkbox is checked. Here is how a beginner might do it (using jQuery):
JSHTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var checkbox = $('input[type=checkbox]'),
    button = $('button');
// We want to listen for clicks only when the checkbox is marked.
checkbox.on('change', function(){
    // Is the checkbox checked?
    if(this.checked){
        // Listen for clicks on the button. 
        button.on('click', function(){
            // This alert is called more than once. Why?
            alert('Hello!');
        });
    }
RUN
This is obviously wrong. Ideally, you should listen for an event only once, like we did with the checkbox’s change event. Repeatedly calling button.on('clicked' ..) results in multiple event listeners that are never removed. I will leave it as an exercise for the reader to make this example work :)

Conclusion

The best way to prevent mistakes like these from happening is to use JSHint. Some IDEs offer built-in integration with the tool, so your code is checked while you write. I hope that you found this list interesting. If you have any suggestions, bring them to the comment section!

No comments:

Post a Comment

Alternative content