Every developer sometimes makes mistakes when writing applications. This is a basic part of the development process. In particular, there are some errors that will be made more often than others. It would be good to know them and prevent them before they do. Once we do that, we will develop the application a lot easier. Here are 10 common mistakes that JavaScript developers often encounter:
Wrong reference to this
The coding techniques and designs in JavaScript are becoming more and more sophisticated, and with the increase of self-referencing scopes in callbacks and closures, a source of confusion about this / that.
Consider the code below:
1 2 3 4 5 6 7 | Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); }; |
Executing the above code will result in an error:
1 2 | Uncaught TypeError: undefined is not a function |
Why?
We need to consider the context . The cause of the above error is because, when you call the function setTimeout()
is actually here you call the function window.setTimeout()
. As a result, the function you pass into setTimeout()
is defined in the context of the window object. this
here is a window object and it doesn’t have the clearBoard()
function clearBoard()
The traditional solution is to store references to this
in a variable and can be used by closures.
1 2 3 4 5 6 7 8 | Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); }; |
In a more modern way, you can use bind()
to pass a correct reference:
1 2 3 4 5 6 7 8 9 | Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! }; |
Think there is block-level scope
One mistake developers make is to assume that Javascript creates a new scope for each code block. Although this is true in many other programming languages, it is not true with Javascript.
For example, consider the following code:
1 2 3 4 5 | for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output? |
If you thought console.log()
would print undefined
or throw an error, you’re wrong. Believe it or not, it will print the output as 10. Why?
In most other languages, the above code will fail because the variable i
can only be used in the block of the for
loop. However, in JavaScript, the variable i
is still in scope even after the loop for
ending and retain its value after exiting the loop. This behavior is called variable hoisting.
It is worth noting that block scope is supported in Javascript via the let
keyword.
Memory leak
Memory leak is an almost unavoidable problem in Javascript if you are not consciously avoiding it. There are many ways that can leak memory, so we will only highlight some common cases.
Example 1: Loose reference to objects no longer exist.
Consider the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second |
If you execute the above code and monitor the memory used, you will see a large amount of memory leak, 1MB per second. That looks like we’re leaking longStr
every time replaceThing
is called. But why?
Please take a closer look.
Each theThing
object contains 1MB longStr
object. Every second, when we call replaceThing
, it holds a reference to theThing
object first in priorThing
. But perhaps this is not a problem, because every run through, previous references to priorThing
will be canceled (when priorThing
is reassigned via priorThing = theThing;
). Moreover, it is only referenced in the body of the replaceThing
function and in the unused
function is never used.
So why is there a memory leak here?
To understand what happens, we need a better understanding of how things work in Javascript. If both functions defined in replaceThing
actually use priorThing
, then it is important that both receive the same object, even if priorThing
is reassigned, both functions still use the same lexical environment. But as soon as a variable is used by any closure, it ends in the lexical environment shared by all closures within that scope. And it leads to memory leak.
Example 2: Circle reference
1 2 3 4 5 6 | function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } } |
Here, onClick
has a closure that holds the reference to the element (via element.nodeName
). By assigning onClick
to element.click
, a circular reference is created: element
-> onClick
-> element
-> onClick
-> element
…
Even if the element
is removed from the DOM, the above reference will still prevent the element
and onClick
being cleaned, and thus, lead to memory leaks.
Preventing memory leaks: What you need to know
Memory management in Javascript depends mainly on object access.
The following objects are said to be accessible and are considered to be root:
- Objects are referenced from anywhere in the current call stack (all local variables, parameters in the function being called, variables in the closure scope)
- All global variables.
Objects are kept in memory at least as long as they are accessible from any root via a reference or a series of references.
Confused when compared by
One of the advantages of Javascript is to automatically cast a boolean value when referring to a boolean context. But sometimes this convenience is confusing. For example, some of the following are often annoying to Javascipt developers:
1 2 3 4 5 6 7 8 9 10 | // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" trn" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ... |
As the example described above, cast rules are sometimes unclear, so it’s better to use ===
instead of ==
.
One more thing to note is that comparing everything with NaN
will always return false
. Therefore, to determine a value of NaN
to use an available global function isNaN()
:
1 2 3 4 | console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true |
DOM manipulation is not effective
Javascript provides the ability to manipulate DOM elements quite easily, but there is no incentive to do so effectively.
A common example is adding a series of DOM elements at once. Adding a DOM element is a costly operation, so the simultaneous addition of DOM elements is inefficient and may not work properly.
An effective alternative when more DOM elements are needed is to use document fragments
. It will improve both efficiency and performance.
For example:
1 2 3 4 5 6 7 8 9 | var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true)); |
Using the wrong function definition in the for
loop
Track the following code:
1 2 3 4 5 6 7 8 | var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; } |
In the above code, if we have 10 input
elements, when clicking on any element, we will get the message “This is element # 10”. This is because when the onclick
is triggered on any element, the for
loop is then completed and the value of i
is 10. The following is how to fix the above code to achieve the desired behavior:
1 2 3 4 5 6 7 8 9 10 11 | var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); } |
With the above fix, makeHandler
is executed immediately each time through the loop, taking the parameter i + 1
and binding it to the scope of the variable num
. This will ensure that each onclick
event will get the appropriate i
value through this num
variable.
Do not thoroughly apply the prototype inheritance
A large number of Javascript developers do not fully understand and take full advantage of the features of prototype inheritance.
For example:
1 2 3 4 5 6 7 8 | BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } }; |
It looks pretty simple. If passed in the name
, it will be used, otherwise the name will be assigned as “default”.
1 2 3 4 5 6 | var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique' |
But if we do the following:
1 2 | delete secondObj.name; |
then you will get:
1 2 | console.log(secondObj.name); // -> Results in 'undefined' |
But it would be better if it was the “default” default value. This can be achieved by leveraging prototype inheritance:
1 2 3 4 5 6 7 8 | BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default'; |
In this way, BaseObject
inherits the name
attribute from the prototype
object. If the name
attribute is removed from an instance of BaseObject
, the prototype chain will be searched and the name
will be taken from the prototype
object and set to “default”. So we will have:
1 2 3 4 5 6 | var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default' |
Create incorrect references to instance methods
Let’s define an object, and create an instance of it:
1 2 3 4 5 6 7 8 | var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); |
Now, for convenience, make a reference to whoAmI
and we can call it by whoAmI()
instead of obj.whoAmI()
.
1 2 | var whoAmI = obj.whoAmI; |
To make sure everything matches, print out the value of whoAmI
:
1 2 | console.log(whoAmI); |
Result:
1 2 3 4 | function () { console.log(this === window ? "window" : "MyObj"); } |
Ok, looks good.
But now, look at the difference when we call obj.whoAmI()
and whoAmI()
:
1 2 3 | obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!) |
What is wrong here.
When we perform the assignment var whoAmI = obj.whoAmI;
, whoAmI
variable is defined in global namespace. As a result, the value of this
is window
, not the object
instance of MyObject
.
Therefore, if we need to make a reference to a method of an object, we need to make sure that it is done in the namspace object to hold the value of this
. For example:
1 2 3 4 5 6 7 8 9 10 11 12 | var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected) |
Use string as the first parameter of setTimeout
or setInterval
Passing the first parameter to setTimeout
or setInterval
a string value is not an error. The problem here is about performance. If we pass such a string parameter, it will be sent to the constructor function and converted into a new function. The process is slow and inefficient, and really it’s not necessary.
Instead of writing the following:
1 2 3 | setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000); |
Please convert it into:
1 2 3 4 5 6 | setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000); |
Do not use “strict mode”
“strict mode” is a way to enforce parsing and error handling more strictly with Javascript code at runtime.
Here are some key benefits of using “strict mode”:
- Making debugging easier: bugs that can be ignored or fail silently will be thrown exceptions.
- Prevent from accidentally creating global variables: If “strict mode” is not used, assigning values to an undefined variable will automatically create a global variable. This is a fairly common mistake in Javascript. With “strict mode”, doing so will throw an exception.
- Prevent reference to
this
as null or undefined. - Duplicate names of attributes of an object or parameters of a function are not allowed.
- Throw error when using wrong
delete
.delete
cannot be used with non-configurable properties. When not using “strict mode”, such behavior will fail quietly without our knowledge. But with “strict mode” the error will be reported.
Refer
https://www.toptal.com/javascript/10-most-common-javascript-mistakes