Inefficiencies that I’ve found on my project【JS algorithms and data structures】

03/11/2019

JavaScript

Right now I’m taking an Udemy course about JavaScript algorithms and data Structures lectured by Colt Steele. I decided to take this course because I felt necessary to learn these topics if I want to write efficient code. I thought, in the beginning, it might be tough topics for me because I’m not really confident with maths — but surprisingly, it’s going really, really interesting and I’m enjoying it very much so far. Thanks to Colt for making such great lectures!

Although I’ve completed 10% of the course, I’ve already learned so much — and at the same time, I realized that the code on my Chrome extension project is not efficient at all.

I could have been ashamed of the code I wrote, but it’s actually giving me the opportunity to work on real problems while I’m learning on the course. So, I think I can say a good job to past-myself 🙂

In the lecture, I got a super helpful list of refactoring questions that I can relate to my own project. In this article, I’m going through each questions while I’m checking code on my project.

1. Can you check the result?

 The first question is this. It seems a pretty obvious question, but it can be overlooked especially if you are a beginner. So double-check and see what the result exactly looks like.

So I checked the result of storing values in arrays by colsole.log.

// Main array which stores data of vocabularies

Array(6)
 0: {category: "cat2", id: "6", meanings: "This is meaningful.", tag: Array(1), word: "Example Word 1"}
 1: {category: "cat3", id: "5", meanings: "Lorem ipsum", tag: Array(1), word: "Example Word 2"}
 2: {category: "cat3", id: "4", meanings: "There are no meaning", tag: Array(2), word: "Example Word 3"}
 3: {category: "cat2", id: "3", meanings: "This is textarea.", tag: Array(2), word: "Example Word 4"}
 4: {category: "cat1", id: "2", meanings: "Hello World!", tag: Array(3), word: "Example Word 5"}
 5: {category: "cat3", id: "1", meanings: "This is just another example", tag: Array(1), word: "Example Word 6"}
// Array which stores category data

Array(3)
 0: "cat1"
 1: "cat2"
 2: "cat3"
// Array which stores tag data

Array(3)
 0: "tag1"
 1: "tag2"
 2: "tag3"

2. Can you derive the result differently?

 I’m not 100% sure if it would make it more efficient, but I came up with another way to store the main data when I looked back at the result.

// My main array stores data like this at the moment
 
0: {category: "cat2", id: "6", meanings: "This is meaningful.", tag: Array(1), word: "Example Word 1"}

// Probably this would be clearer what this object represents.

{ "Example Word 1" : {category: "cat2", id: "6", meanings: "This is meaningful.", tag: Array(1)} }

I’ll definitely give it a go and change this, then see how it changes the result.

3. Can you understand it at glance?

This question made me realize that my code is unreadable, and has to be improved. Firstly, a function I create was way too long and it made the function difficult to maintain. There are also a few methods that I can probably combine them as a single function.

// This is way too long!

function displayWords(root, location, storage) {

    // O(n)
    for( let i = 0; i < storage.length; i++ ) {
        // Build main-area with words
        // Main <ul> 
        const wordUl = document.getElementById('words-ul')
        // Message if there's no data
        const message = document.createElement('p')
        message.textContent = ('Please add tags from side bar.')
        
        if ( storage === undefined || storage === null ) {
            wordUl.appendChild(message)   
        }
        // <li>
        const wordLi = document.createElement('li')
        wordLi.setAttribute('class', 'flexbox')

        // First <p> word
        const wordPara1 = document.createElement('p')
        wordPara1.setAttribute('class', 'main-word')
        wordPara1.setAttribute('id', `main-word-${i}`)
        wordPara1.textContent = storage[i].word
        // <div> for tag's <span>
        const tagDiv = document.createElement('div')
        tagDiv.setAttribute('class', 'tagDiv')
        tagDiv.setAttribute('id', `tagDiv${i}`)

        wordUl.appendChild(wordLi)
        wordLi.appendChild(wordPara1)
        wordLi.appendChild(tagDiv)

        // Tag's <span> O(n2)
        for (let j = 0; j < storage[i].tag.length; j++ ) {

            const divTag = document.getElementById(`tagDiv${i}`)
            const tagSpan = document.createElement('span')
            tagSpan.textContent = storage[i].tag[j]
            tagSpan.setAttribute('class', 'main-tag');
            divTag.appendChild(tagSpan)

        }
        // Second <p> meaning
        const wordPara2 = document.createElement('p')
        wordPara2.textContent = storage[i].meanings
        wordPara2.setAttribute('class', 'main-meaning')

        wordLi.appendChild(wordPara2)

        // Edit button
        const editForm = document.createElement('form')
        editForm.setAttribute('id',`edit-submit-form${i}`)
        
        const editSubmit = document.createElement('button')
        editSubmit.setAttribute('type', 'submit')
        editSubmit.setAttribute('class', 'edit-submit-btn')

        const fontAwesome = document.createElement('i')

        wordLi.appendChild(editForm)
        editForm.appendChild(editSubmit)
        editSubmit.appendChild(fontAwesome)

        wordLi.addEventListener('mouseenter', () => {
            fontAwesome.setAttribute('class', 'fas fa-edit')
        }, false);

        wordLi.addEventListener('mouseleave', () => {
            fontAwesome.removeAttribute('class')
        }, false);

        // Pull down word-edit form
        document.getElementById(`edit-submit-form${i}`).onsubmit = () => {
            
            const input = document.getElementById('add-word-input')
            input.checked = true;
            // Change ID so the form can send different way
            const editWordForm = document.getElementById('add-word-form')
            editWordForm.id = 'edit-word-form'
            // Display data related to the button that is submitted
            //// ID
            document.getElementById('id-sender').value = storage[i].id
            //// word
            document.getElementById('vocabulary').value = storage[i].word
            //// meaning
            document.getElementById('meaning-textarea').value = storage[i].meanings
            //// tag
            chrome.storage.local.get({ tagData:[] }, function(items) {

                // O(n2)
                for (let k = 0; k < items.tagData.length; k++ ) {

                    const a = items.tagData[k]
                    //O(n3)
                    for (let l = 0; l < storage[i].tag.length; l++ ) {

                        const b = storage[i].tag[l]

                        if ( a === b ) {
                            document.getElementById(`tag${k}-input`).checked = true
                        }

                    }
                    
                }
            });
            //// category
            chrome.storage.local.get({ catData:[] }, function(items) {

                for (let k = 0; k < items.catData.length; k++ ) {

                    const a = items.catData[k]

                    const b = storage[i].category

                    if ( a === b ) {
                        document.getElementById(`cat${k}-input`).checked = true
                    }
                    
                }
            });

            document.getElementById('cancel-form').onsubmit = () => {

                if ( document.getElementById('edit-word-form') ) {
                    document.getElementById('edit-word-form').id = 'add-word-form'
                    // Close the pull down window
                    input.checked = false;
                } else {
                    input.checked = false;
                }

            }

            // Set data into storage.local
            document.getElementById('edit-word-form').onsubmit = () => {

                let editedWord = { id:'', word:'', tag:[], category:'', meanings:'' };

                editedWord.id = document.getElementById('id-sender').value
                editedWord.word = document.getElementById('vocabulary').value
                editedWord.meanings = document.getElementById('meaning-textarea').value

                // Push tag
                const tagEl = document.getElementById('tag-container');
                const tags = tagEl.getElementsByTagName('input');
                for (let l=0, len=tags.length; l<len; l++ ) {
            
                    if (tags[l].checked) {
        
                        const nTag = document.getElementById(`tag${l}-label`).textContent;
                        editedWord.tag.push(nTag);
        
                    }
        
                }

                // Push category
                const catEl = document.getElementById('category-container');
                const cat = catEl.getElementsByTagName('input');
                for (let m=0, len=cat.length; m<len; m++ ) {
                    
                    if (cat[m].checked) {

                        let checkedSpan = document.getElementById(`cat${m}-span`).textContent;
                        editedWord.category = checkedSpan;

                    }

                }

                for (let n = 0; n < location.length; n++ ) {
                    if ( location[n].id == editedWord.id ) {
                        location[n] = editedWord
                        chrome.storage.local.set(root)
                        alert('Successfully edited!')
                    } 
                }
                
                const putBackWordForm = document.getElementById('edit-word-form')
                putBackWordForm.id = 'add-word-form'

            }
            return false;
  
        };


    }

}

Oh no. I hope this is the last time that I create such a long single function.. 🙁 I’m going to separate it as much as I can, so hopefully, it will be much easier to understand.

4. Can you improve the performance of your solution?

This question spotted some problems with my project.

First problem is that I am using ‘unshift’ to store data in an array.

if(items.wordInfo === undefined ) {

    items.wordInfo[0] = newWord;

} else {

    items.wordInfo.unshift(newWord); 

}

chrome.storage.local.set(items);

When we store data in an array by using unshift, each index in the array which already in the storage has to be changed one by one. That means its time complexity would be O(n) —  which means their run time grows as the amount of stored data grows. 

In my project, I’ve used unshift in a function only because I wanted to display stored data in reverse order. So this part can be definitely improved by using push instead, which time complexity is always constant.

The second problem I’ve found is that I’m using a pretty deep nested loop to display the stored data.

The deepest nested loop was O(n*m).

function displayWords(root, location, storage) {

    // O(n)
    for( let i = 0; i < storage.length; i++ ) {

        // Build main-area with values in the array

        // O(n*m)
        for (let j = 0; j < storage[i].tag.length; j++ ) {

           // Display tag data which stored in the each array

        }
        
        // Pull down word-edit form function
        document.getElementById(`edit-submit-form${i}`).onsubmit = () => {
    
            //// tag
            chrome.storage.local.get({ tagData:[] }, function(items) {

                // O(n)
                for (let k = 0; k < items.tagData.length; k++ ) {

                    const a = items.tagData[k]
                    //O(n*m)
                    for (let l = 0; l < storage[i].tag.length; l++ ) {

                        const b = storage[i].tag[l]

                        if ( a === b ) {
                            document.getElementById(`tag${k}-input`).checked = true
                        }

                     }
                    
                 }

            });
  
        };

    }

}

The problem was, when I thought about extracting each value from a multi-dimensional array, I could only come up with the idea by using for loop and go through each data one by one. I haven’t come up with a better idea yet, but I’m sure that I can improve the performance by using other operations like filter.

Conclusion(for now!)

The questions I went through are here:

  • Can you check the result?
  • Can you derive the result differently?
  • Can you understand it at glance?
  • Can you improve the performance of your solution?

Still a few more questions ahead, but I found those problems to solve -- so I’m going to give good research about it. And then I can update my project for the better.

I didn't realize all of these inefficiencies until I started this course -- an Udemy course about JavaScript algorithms and data Structures. I definitely recommend this course to anyone who is learning JavaScript. It's super fun! 😉