JavaScript gotchas
JavaScript is one of the most widely used programming languages. It can be found in Pega Platform internals too. Despite being popular, JavaScript is also said to be the world's most misunderstood programming language.
Its syntax may look familiar for engineers with experience in Java or C#.
However, the results and side effects of JavaScript code are not always easy to predict.
Let's explore some of the most common JavaScript gotchas and misconceptions:
0. Language name
There are several names that people use to refer to specific versions of JavaScript.
In short:
JavaScript 5th edition == ES5 == ECMAScript 5
JavaScript 6th edition == ES6 == ECMAScript 2015
JavaScript 7th edition == ES7 == ECMAScript 2016
...
1. Unintended global variables
If you assign a value to an undeclared variable, JavaScript will not throw an error. Instead, it will create (or override) a global variable with such name.
function f(heavyObject){
myConfig = heavyObject; // undeclared variable becomes a global variable holding a heavy object
if(myConfig.isValid){
// ...
}
}
So forgetting the 'var' keyword or misspelling a variable name may cause a memory leak or breaking the global state of the application.
2. Unexpected scope of variables
For years variables were declared with 'var' keyword in JavaScript. Since ES6 there are 3 keywords for declaring variables:
- const (block scope)
- let (block scope)
- var (function scope)
Variables declared with 'const' or 'let' have the well-known block scope. They are valid from the place of declaration to the end of the block in which they are declared. They can be shadowed by another variable declaration with the same name.
function f(){
let x = 123;
console.log("Outer block scope, x=" + x);
// y does not exist
if(true){
let x = 4; // inner-scope declaration shadows outer-scope declaration
let y = 5;
console.log(" Inner block scope, x=" + x);
console.log(" Inner block scope, y=" + y);
}
console.log("Outer block scope, x=" + x);
// y does not exist
}
f();
Output:
Outer block scope, x=123
Inner block scope, x=4
Inner block scope, y=5
Outer block scope, x=123
On the other hand, variables declared with the 'var' keyword behave differently. They have function scope. Even if you try to redeclare a variable name several times in different block scopes, there will be just one variable with a given name per function. What's more, JavaScript moves all declarations of function-scoped variables to the beginning of the function. Therefore, function-scoped variables exist from the very first line of the function, even if they are declared later in an inner block.
function f(){
var x = 123;
console.log("Outer block scope, x=" + x);
console.log("Outer block scope, y=" + y); // local y already exists, but its value is undefined
if(true){
var x = 4; // redeclaration is ignored, there is still only one x variable
var y = 5; // declaration is automatically moved to the beginning of the function
console.log(" Inner block scope, x=" + x);
console.log(" Inner block scope, y=" + y);
}
console.log("Outer block scope, x=" + x);
console.log("Outer block scope, y=" + y);
}
f();
Output:
Output:
Outer block scope, x=123
Outer block scope, y=undefined
Inner block scope, x=4
Inner block scope, y=5
Outer block scope, x=4
Outer block scope, y=5
3. Comparing with == operator
The == operator is tricky. It tries to compare values even if their type is different.
Examples:
"0" == 0 // -> true
0 == "" // -> true
"0" == "" // -> false
false == 0 // -> true
!false == !0 // -> true
false == "0" // -> true
!false == !"0" // -> false
3 == "3" // -> true
3 == "003" // -> true
"3" == "003" // -> false
null == undefined // -> true
null == 0 // -> false
false == 0 // -> true
It's usually better to use === operator that compares both value and its type.
4. Checking if value exists
Checking if a variable or property has a value is simple in Java and C#. You just make sure that the value is not null.
Things are more complicated in JavaScript. You need to check that the value is neither null nor undefined.
if(localVar !== null && localVar !== undefined){
/* do something with localVar */
}
An alternative solution is using != operator, that will work for both null and undefined. But in general people don't like != and == operators in JavaScript.
if(localVar != null){
/* do something with localVar */
}
The above two code examples might not work in case of global variables. If the global variable does not exist, then trying to read its value will throw ReferenceError.
You can check if the global variable exists with the following code:
if(typeof globalVariable !== "undefined" && globalVariable !== null){
/* do something with globalVariable */
}
In case of objects, accessing a property that does not exist will not throw an Error. It will return undefined.
var person = {name:"John", surname:"Smith"};
console.log(person.age); // -> undefined
However, some properties may be "inherited" from the object prototype.
var person = {name:"John", surname:"Smith"};
console.log(person.toString); // -> ƒ toString() { [native code] }
If you are not interested in inherited properties, you can filter them out with hasOwnProperty function.
var person = {name:"John", surname:"Smith"};
console.log(person.hasOwnProperty("name")); // -> true
console.log(person.hasOwnProperty("toString")); // -> false
There is also a short way to check properties.
if(person.name){
/* do something with person.name */
}
This can be tricky though. The condition will be false not only for null and undefined. It will also be false for any "falsy" value like 0, "", false, null, NaN, etc.
5. What is 'this'?
In JavaScript the meaning of 'this' keyword depends on the way the function was invoked.
If the function is invoked with the new keyword, then 'this' points to a new empty object.
If the function is invoked as an object member, then 'this' points to the that object.
If the function is invoked as an ordinary function, then 'this' points to the global window object.
Let's see an example:
function Person(name){
this.personName = name;
this.sayHello = function(){
return "Hello, I'm " + this.personName;
};
}
var p = new Person("John");
p.personName; // -> "John"
p.sayHello(); // -> "Hello, I'm John"
Let's change the person's name, to ensure that sayHello function can see the change.
p.personName = "George";
p.sayHello(); // -> "Hello, I'm George"
Let's copy the sayHello function to a different object
var cat = {
personName: "Garfield"
}
cat.saySomething = p.sayHello;
cat.saySomething(); // -> "Hello, I'm Garfield"
Now, let's store the sayHello function in a variable. When we call it, 'this' will point to the global window object. Window does not have a personName property, so it will be undefined.
var f = p.sayHello;
f(); // -> "Hello, I'm undefined"
Let's call the Person function without the 'new' keyword. 'this' should again point to the global window object;
var p2 = Person("The Global Window Object");
This time no new object was created, so p2 is undefined.
p2; // -> undefined
Let's check the window object:
window.personName; // -> "The Global Window Object"
Let's call the f function again. 'this' will point to window object.
f(); // -> "Hello, I'm The Global Window Object"
6. Wasting memory while creating objects
There are several ways you can create objects in JavaScript.
You can create a constructor method that sets all properties and methods, e.g.
function Person(name){
this.personName = name;
this.sayHello = function(){
return "Hello, I'm " + this.personName;
};
}
Please note that each time you create a new Person, a new copy of the sayHello function is created. Each object has a separate (though exactly the same) implementation. So we are wasting memory for several copies of the same function.
var p1 = new Person("Jessica");
var p2 = new Person("Victoria");
p1.sayHello === p2.sayHello // -> false;
The same thing happens when you create a method that returns objects as literals
function createPerson(name){
return {
personName: name,
sayHello: function(){
return "Hello, I'm " + this.personName;
}
}
}
var p3 = createPerson("Jim");
var p4 = createPerson("Denzel");
p3.sayHello === p4.sayHello; // -> false
The solution is to define the method in prototype object:
function Person(name){
this.personName = name;
}
Person.prototype.sayHello = function(){
return "Hello, I'm " + this.personName;
};
var p5 = new Person("Amanda");
var p6 = new Person("Alex");
p5.sayHello === p6.sayHello // -> true
Now, instances of the Person class share the same single implementation of sayHello function.