An optimized piece of code is any code that works optimally, i.e. code that is efficient. We say code is efficient, when it wastes neither time nor effort nor expense (expense includes computer memory). The reward for an optimised JavaScript code is generally a less buggy, smaller-sized, smoother and faster application.
In this article, I reproduce a program, which I wrote for a front-end web application to check if a number given by the user is a narcissistic number.
The flesh of the app is codified by the HTML and CSS given below.
<body>
<div class="container">
<h3>Narcissistic Number</h3>
<p>Type in a number to check if it's narcissistic</p>
<input type="number" id="inputField" onfocus="this.value=''" autofocus />
<button id="btn">evaluate</button>
<p id="displayResult"></p>
</div>
</body>
The CSS
.container {
margin: 0 auto;
width: 280px;
background-color: #fafafa;
}
p[data-status="true"] {
color: green;
}
p[data-status="false"] {
color: red;
}
The above HTML & CSS produce a beautiful application that looks like this
Now for the functioning of the app, (for the body cannot live without the mind - Morpheous), our JavaScript code that makes the app tick, is coded thus...
let displayResultEl = document.getElementById("displayResult");
let inputField = document.getElementById("inputField");
function isInteger(x) {
return x % 1 === 0;
}
let narcissistic = function() {
let strValue = inputField.value; //this is a string
if(isInteger(strValue)) {
let power = strValue.length;
let allDigits = [];
for(let i = 0; i < power; i++) {
allDigits.push(parseInt(strValue[i], 10));
}
let raisedToPowers = allDigits.map(digit =>
Math.pow(digit,power));
let total = raisedToPowers.reduce(
(sum, raisedToPower) => {
return sum + raisedToPower;
}, 0);
if(total == strValue) {
displayResultEl.dataset.status = "true";
return `TRUE! ${strValue} is a narcissitic number`;
} else {
displayResultEl.dataset.status = "false";
return `False! ${strValue} is not a narcissistic
number`;
}
} else {
displayResultEl.dataset.status = "false";
return "Use positive integers only";
}
}
let btnEl = document.getElementById("btn");
btnEl.onclick = function() {
displayResultEl.innerHTML = narcissistic();
}
const enterKey = 13;
inputField.addEventListener("keyup", function(event) {
event.preventDefault();
if(event.keyCode === enterKey) {
btnEl.click();
}
});
Summarily, what the above JavaScript code accomplishes is,
- It takes the input typed in by the user, and checks to see if it is a narcissistic number or not. It displays the result of that check.
Hurray! The app worksππ. By the way, a narcissistic number is a number that is the sum of its own digits each raised to the power of the number of digits. Narcissistic numbers include, in addition to all single digit numbers, 153, 370, 371, 407, 1634 etc.
Back to our app, the check is started either when the user hits the button on the app, or after the user has pressed the enter key on their keyboard. A magnificent triumph!
However, when you have gotten your app to do what it is intended to do, you then want to optimise for performance and maintainability. As it is, the JavaScript solution above, as most first time code solutions, is clunky and not optimised. Bugs delight in such code.
The Refactor
So what's with the above JavaScript code, and where can we optimise?
When we observe the code, we notice a couple of things
There are variables in the global space.
Global variables make codes harder to maintain, as they could be used anywhere.There is a callback function using a variable (btnEl) outside its scope.
This is a major gotcha for developers. Because of the concept of closure, reference to a variable declared outside its scope remains. This is a cause of Memory leak, which can lead to all types of nightmares as the application gets bigger.Objects declared and initialized in one outer scope are being brought into the inner local scope wholly, when perhaps the inner scope only needs one property or two. An object being used in this manner only adds up more memory usage. A destructured object allows for inner local scopes to use just those properties they need without having to bring in all kb of that object. For instance, in our code, the narcissistic function has inputField.value inside its scope. In reality, that function holds all the properties in inputField, not just value. This is unnecessary memory consumption.
There may be redundant lines of code, which only increases the time for the algorithm to run
The narcissistic function does more than one thing. It checks for the narcissistic status of the input, which is what it is set up to do. But then goes on as well to update DOM elements (a second thing). These concerns can be separated.
There is no clear pattern or definite architecture to our code. It seems anything can be anywhere.
The first step towards refactoring, and therefore optimisation of code, is observation, which is what we have done. Let us see if we can apply some improvement.
The Improvement
Picking it from (6), every code needs a discernible structure. You may call it pattern or architecture. Any name is fine by me as long as it brings in a bit of order. Let me also say, there is no one structure to rule them all. For the code above, I will like to use a module pattern, which I grasped while taking a Jonas Schmedtmann course on JavaScript.
In essence, every front-end application has its UI part (UI module), its computational part (Data Module), and its controller part (App Controller Module).
- Anything directly affecting the UI, stays inside the UI module.
- The calculations, permutations, brain work, stays inside the Data Module.
- Finally the App Controller module takes care of all event handlers, as well as acts as the intermediary between the UI and the Data modules.
This separation of concerns is captured thus...
//UI Controller
let UIController = (function() {
return {
...
}
})();
//Data Controller
let dataController = (function(){
return {
...
}
})();
// App controller
let controller = (function(dataCtrl, UICtrl) {
return {
init: function() {
console.log('Application has started');
setupEventListeners();
}
}
})(dataController, UIController);
controller.init();
You can see now, with a structure, we have solved many things at once. We will not have variables lying about in the global space anymore, they will have to fit in, in one of the module's local scopes. This clarity gives every developer confidence that they are not altering what they needn't alter.
After this improvement, you want to improve the code itself, its algorithm, remove redundant lines and also ensure that functions do only one thing.
Let us look at how our improved code looks in the UI module...
let UIController = (function() {
let DOMstrings = {
displayResult: "displayResult",
inputField: "inputField",
btn: "btn"
}
let outputStatement = function({ isNarcissistic, strValue, exponent, sum }) {
let sentence = `${strValue} is ${isNarcissistic ? '' : 'not'} a narcissistic value.\n
The sum of its own digits, each raised to the total digits count ${exponent}, is ${sum}`;
switch(isNarcissistic) {
case false:
return `No, ${sentence(false)}`;
case true:
return `Yes, ${sentence(true)}`;
default:
return "Please type in an integer"
}
}
return {
getDOMstrings: function() {
return DOMstrings;
},
getOutputStatement: function(value) {
return outputStatement(value);
}
}
})();
In the UI module,
- we hold all the DOM strings in an object, so we only need to change them in one place if the need arises.
- we have an outputStatement function that uses destructuring to pick only those properties it needs from the object passed into the function. This keeps the app light weight, as only what is needed is used
- The outputStatement function does only one thing. It outputs a statement on the screen
- The UIController is a global variable that gives other modules access to only the object it returns. Thereby effectively compartmentalizing our code, exposing only what needs to be exposed.
Let us see what out data module looks like
//Data Controller
let dataController = (function(){
let validateInput = function(strValue) {
if (isNaN(strValue)) return false;
return (strValue == parseInt(strValue, 10) && strValue % 1 === 0);
}
let narcissistic = function(strValue) {
let base;
let exponent;
let start;
let length = strValue.length;
let sum = 0;
if (strValue < 0) {
base = -1;
exponent = length - 1;
start = 1;
} else {
base = 1;
exponent = length;
start = 0;
}
for (let i = start; i < length; i++) {
sum += Math.pow(strValue[i], exponent)
}
let signedInteger = base * sum;
return {
isNarcissistic: (signedInteger == strValue),
sum: signedInteger,
exponent,
strValue
};
}
return {
checkValidInput: function(input) {
return validateInput(input);
},
checkNarcissistic: function(strValue) {
return narcissistic(strValue);
}
}
})();
The data module follows the principles we applied in the UI module
- Each function doing one thing only
- Data controller as an IIFE exposing only what needs to be exposed
Finally, let us look at our app module...
// App controller
let controller = (function(dataCtrl, UICtrl) {
let { inputField, btn, displayResult } = UICtrl.getDOMstrings();
let { getOutputStatement } = UICtrl;
let { checkValidInput, checkNarcissistic } = dataCtrl;
let inputFieldEl = document.getElementById(inputField);
let setupEventListeners = function() {
let btnEl = document.getElementById(btn);
inputFieldEl.addEventListener("keyup", keyAction);
btnEl.addEventListener("click", executeInput);
}
let keyAction = function(event) {
event.preventDefault();
const enterKey = 13;
if (event.keyCode === enterKey || event.which === enterKey) executeInput();
}
let executeInput = function() {
let strValue = inputFieldEl.value;
let isValidInput = checkValidInput(strValue);
let displayResultEl = document.getElementById(displayResult);
if (isValidInput) {
let result = checkNarcissistic(strValue);
displayResultEl.dataset.status = result.isNarcissistic ? "true" : "false";
displayResultEl.innerHTML = getOutputStatement(result);
} else {
displayResultEl.dataset.status = "false";
displayResultEl.innerHTML = getOutputStatement('NaN');
}
}
return {
init: function() {
console.log('Application has started');
setupEventListeners();
}
}
})(dataController, UIController);
controller.init();
The app controller is what brings it all together.
- Observe that the DOM elements are assigned as narrow as possible to the scope that actually needs them. This way, when you make changes to a DOM element, you're not worried it is being used somewhere else.
Phew! And so, our final code all together looks like...
//UI Controller
let UIController = (function() {
let DOMstrings = {
displayResult: "displayResult",
inputField: "inputField",
btn: "btn"
}
let outputStatement = function({ isNarcissistic, strValue, exponent, sum }) {
let sentence = `${strValue} is ${isNarcissistic ? '' : 'not'} a narcissistic value.\n
The sum of its own digits, each raised to the total digits count ${exponent}, is ${sum}`;
switch(isNarcissistic) {
case false:
return `No, ${sentence(false)}`;
case true:
return `Yes, ${sentence(true)}`;
default:
return "Please type in an integer"
}
}
return {
getDOMstrings: function() {
return DOMstrings;
},
getOutputStatement: function(value) {
return outputStatement(value);
}
}
})();
//Data Controller
let dataController = (function(){
let validateInput = function(strValue) {
if (isNaN(strValue)) return false;
return (strValue == parseInt(strValue, 10) && strValue % 1 === 0);
}
let narcissistic = function(strValue) {
let base;
let exponent;
let start;
let length = strValue.length;
let sum = 0;
if (strValue < 0) {
base = -1;
exponent = length - 1;
start = 1;
} else {
base = 1;
exponent = length;
start = 0;
}
for (let i = start; i < length; i++) {
sum += Math.pow(strValue[i], exponent)
}
let signedInteger = base * sum;
return {
isNarcissistic: (signedInteger == strValue),
sum: signedInteger,
exponent,
strValue
};
}
return {
checkValidInput: function(input) {
return validateInput(input);
},
checkNarcissistic: function(strValue) {
return narcissistic(strValue);
}
}
})();
// App controller
let controller = (function(dataCtrl, UICtrl) {
let { inputField, btn, displayResult } = UICtrl.getDOMstrings();
let { getOutputStatement } = UICtrl;
let { checkValidInput, checkNarcissistic } = dataCtrl;
let inputFieldEl = document.getElementById(inputField);
let setupEventListeners = function() {
let btnEl = document.getElementById(btn);
inputFieldEl.addEventListener("keyup", keyAction);
btnEl.addEventListener("click", executeInput);
}
let keyAction = function(event) {
event.preventDefault();
const enterKey = 13;
if (event.keyCode === enterKey || event.which === enterKey) executeInput();
}
let executeInput = function() {
let strValue = inputFieldEl.value;
let isValidInput = checkValidInput(strValue);
let displayResultEl = document.getElementById(displayResult);
if (isValidInput) {
let result = checkNarcissistic(strValue);
displayResultEl.dataset.status = result.isNarcissistic ? "true" : "false";
displayResultEl.innerHTML = getOutputStatement(result);
} else {
displayResultEl.dataset.status = "false";
displayResultEl.innerHTML = getOutputStatement('NaN');
}
}
return {
init: function() {
console.log('Application has started');
setupEventListeners();
}
}
})(dataController, UIController);
controller.init();
Observe our refactored code above...
- It consumes less memory, as objects are now destructured, and functions use the property they want without having to carry the whole weight of that object. Peek at the outputStatement function definition (line 9).
- It runs faster than our old code and even takes care of more edge cases, as refactoring exposed some bugs not previously seen.
- There is no fear of a DOM leak (which severely hampers apps). Our rewritten callbacks do not refer to any variable outside its scope. Therefore when the callback function is done, JavaScript cleans up the memory without any reference left behind(closure).
- Each function in the code does only one thing, and concerns are properly separated. Unlike the old code, now the narcissistic function only checks if it's narcissistic, and another function has the responsibility of updating the DOM. Everything is well spelt out.
- Lastly, it's beautiful to read.
I do think it's beautiful to read. Thank you dear reader for coming with me through this journey. Together we have seen a code transform from Gandalf the Grey to Gandalf the Whiteπ§. Your thoughts are very much welcome. And remember, if you have ever whispered under your breath, "For Frodo", think of me as family.
π€
You can see the full working application here
https://codepen.io/Duz/pen/oaGdmG
Top comments (2)
The pattern applied here brought in sanity and good structure to this code.
Some god level coding here, my man.
You are the first colleague that I saw used IIFE
Thank you for the kind words. It's amazing how in many different ways a problem can be solved. Credit to Udemy tutor Jonas Schmedtmann, from whom I learnt how to use the IIFE to bring structure to code.