Mariusz Rajczakowski
Software Engineer
8 min read | 6 months ago

Free yourself from var. Use let and const instead!

New syntax

In order to bring ES2015 syntax to your project, let’s first familiarize yourself with a new way of declaring variables.

Two new keywords were introduced with that specification: let and const.

You might wonder why to indroduce new ways of declaring variables, if we have been doing just fine with var keyword.

Well, those two new keywords differ from var, because they behave differently in a block scope.

Using them actually will make your code less error prone. Let's see var vs let comparison first (as const behaves the same in this context).

Let vs var

Scope

Global scope (if they are not declared within a function)

Unlike var, let statement does NOT pollute global object


            //browser
            var a = 1;
            let b = 2;
            console.log(window.a); //it will print 1
            console.log(window.b); //it will print undefined
            //node
            var a = 1;
            let b = 2;
            console.log(global.a); //it will print 1
            console.log(global.b); //it will print undefined
            

Let bindings apply to the current scope which might give some problems in switch statement like this:


            let x = 1;
            switch(x){
              case 0:
                 let foo;
                 break;
              case 1:
                 let foo; //SyntaxError
                 break;
            }
            //but you can fix it by enclosing each case with curly brackets:
            switch(x){
              case 0: {
                let foo;
                 break;
              }
              case 1: {
                let foo; //no problem
                break;
              }
            }
            

Var vs Let in block scope


            var foo = 1;
            var bar = 2;

            if(true){
              var foo = 100; //this will change the value of global variable foo
              let bar = 200; // this is block scoped and not affect globally initialized bar
              console.log(foo); // 100
              console.log(bar); // 200
            }
            console.log(foo); // 100
            console.log(bar); // 2
            

Function scope

Both bindings behave similarly within a function scope except for variable hoisting.


            /* both foo and bar are not available here and will throw ReferenceError */

            function test(){
              /* foo is undefined here */
              /* bar will throw ReferenceError */
              var foo = 1;
              let bar = 2;
              /* foo and bar available here*/
            }

            /* both foo and bar are not available here and will throw ReferenceError */
            

Redeclaration


            var foo = 1;
            var foo = 2; // no error is thrown, instead value of foo is 2 now
            let bar = 1;
            let bar = 2; // SyntaxError is thrown because variable within the same scope has been already declared
            

Workarounds with closures

In ES5 closures was a workaround to allow memoization of counter within for loop. That is no longer a case with let binding as its block scoped and declared separately for each loop iteration.


            //Problem: createDynamicMenu function dynamically
            //create 4 li elements and append it to unordered list
            //however when you click on each button you will see in your console
            //'Item 5 clicked' instead of 'Item i clicked' for each iteration

             var list = document.getElementById('list'); //get ul element with id list

            function createDynamicMenu(){
              for(var i = 1; i < 5; i++){
                var element = document.createElement('li');
                element.append(document.createTextNode('Button '+i));
                element.onclick = function(){
                  console.log('Item '+i+' clicked');
                  //when createDynamicMenu() has been invoked and 'i' will equal 5,
                  // hence each onclick listener will print 'Item 5 clicked'
                }
                list.append(element);
              }
            }
            createDynamicMenu();

            //Solution with closures
            function createDynamicMenu(){
              for(var i = 1; i < 5; i++){
                var element = document.createElement('li');
                element.append(document.createTextNode('Button '+i));
                element.onclick = (function(i){
                  return function(){
                    console.log('Item '+i+' clicked');
                  }
                })(i);
                //closure 'remembers' 'i' counter at the time it was defined
                //and for each 'i' onclick listener will
                //print out correctly 'Item i clicked'
                list.append(element);
              }
            }

            //Solution with let binding
            function createDynamicMenu(){
              for(let i = 1; i < 5; i++){
                var element = document.createElement('li');
                element.append(document.createTextNode('Button '+i));
                element.onclick =  function(){ console.log('Item '+i+' clicked'); }
                // let 'i' is block scoped and created separetely for each iteration,
                // hence each onclick listener will print out correct value
                list.append(element);
              }
            }
            

Hoisting

Var bindings are subject to variable hoisting which means they are moved to the top of the current execution context. This rule does not apply to let bindings, which means that they cannot be used before it's initialization


            console.log(zebra);   // undefined - does not throw an error even if its
                                  // above the initialization statement
            console.log(letItBe); // throws Reference errors as let is
                                  // not subject to variable hoisting
            var zebra = 'test';
            let letItBe = 'test';
            

Const

Const behaves the same like let in the examples above.

The main difference is that value of a constant cannot be redeclared and variable identifier cannot be re-assigned. Const keyword create a read-only reference to a value.


            const ZEBRA; //will throw SyntaxError as ZEBRA has not been initialized

            const NUMBER_OF_WHEELS = 4;  // perfectly valid initialization
            const NUMBER_OF_DOORS = 4, NUMBER_OF_WINGS = 2; // perfectly valid initializations

            NUMBER_OF_DOORS = 3; // reassignment will throw an error
            const NUMBER_OF_DOORS = 6;  // redeclaration will throw an error
            

Immutability? Not really...

The value types re-assignments works as intended (throws an error), however when it comes reference types like an array or object they can be modified without throwing an error (not immutable)


            const ITEMS = [1,2,3];
            const ITEMS = [4,5,6]; //it will throw SyntaxError
            ITEMS = [7,8,9]; //  it will throw TypeError
            //however...
            ITEMS.push(4); //works ok - ITEMS equals [1,2,3,4]
            ITEMS.pop(); //works ok - ITEMS equals [1,2,3]
            ITEMS[3] = '100'; // works ok - ITEMS equals [1,2,3,100]

            const USER = { name: 'John', age: 22 };
            const USER = { name: 'Tom', age: 32}; //re-declaration will throw SyntaxError
            USER = {name: 'Tom', age: 32} // // re-assignment will throw TypeError
            //but...
            USER.name = 'Tom'// works ok - USER equals { name: 'Tom', age: 22 };
            delete USER.name //works ok - USER equals { age: 22 };
            

Solution for immutability

Use external libraries such as immutable.js or Object.freeze() (natively supported in ES5)


            //Object.freeze() usage
            const ITEMS = [1,2,3];
            Object.freeze(ITEMS); // ITEMS are now immutable
            ITEMS.push(4); //it will throw TypeError
            ITEMS[3] = '100'; // it does not mutate the original array but neither throw an error
            //but it doesn't work with nested object, so you must apply this recursively
            const USER = {
              name: 'Thomas',
              address: {
                street: 'Akacjowa 4',
                city: 'Warszawa'
              }
            }
            Object.freeze(USER);
            USER.name = 'John'; // fails silently and value is not changed
            USER.address.city = 'London'; // value has been changed and now equals 'London'

            //solution:
            function deepFreeze(obj){
              var props = Object.getOwnPropertyNames(obj);
              props.forEach(function(name){
                 var prop = obj[name];
                 if(prop !== null && typeof prop === 'object'){
                   deepFreeze(prop); //apply freezing recursively
                 }
              });
              return Object.freeze(obj);
            }
            deepFreeze(USER);
            USER.address.city = 'Rzeszow'; // fails silently and value is not changed
            

Usage


            //use camel case syntax for lets
            let myVar = 'foo';
            let itMightChange = 3;

            //use upper case for const
            const DEFAULT_NUMBER = 1;
            const NUMBER_OF_WHEELS = 4;
            

Var vs Let vs Const

If you are using ES2015 (ES6) use const by default.

If you think that current variable might be re-assigned in the future use let instead.

You should forget about var, and in most cases all your var keywords could be replaced with let straight away!

References
  1. https://developer.mozilla.org
  2. icons for main picture: https://dribbble.com/jucha

Share:



Warning! This site uses cookies
By continuing to browse the site, you are agreeing to our use of cookies. Read our privacy policy