Basic concepts of JavaScript

Table of contents

No heading

No headings in the article.

so let's start with this basic example...

const numbers = [1, 2, 3, 4, 5, 6];

const odds = numbers.filter((num) => num % 2);
const evens = numbers.filter((num) => num % 2 === 0);

console.log("Odd Numbers: ", odds);
console.log("Even Numbers: ", evens);

Here, odds will hold all the odd numbers from numbers array and evens will hold even numbers.

and will get the above output.

Now, if we do some changes to odds array...

odds[0] = 100;
evens[0] = 200;

console.log(numbers);
console.log("Odd Numbers: ", odds);
console.log("Even Numbers: ", evens);

Basically, odds array is derived from numbers array but filter returns a shallow copy of the numbers array.

and will get the above output.

The catch here is whatever changes we have made to the odds or evens array, it doesn't affect to numbers array which is a kind of source array of it.

Let's take another example...

const data = {
    name: 'Tony Stark', 
    age: 32, 
    languages:{
        speaking: ['Gujarati', 'Hindi', 'English'],
        programming: ['JavaScript', 'Python', 'C++']
    }
};

const copyData = data;

copyData.name = 'Iron Man';
console.log(copyData);
console.log(data);

Here, if you will see, the changes we have made to copyData, also get affected into our original data object, as we will get shallow copy with the equal operator.

That's what I want to explain here, in our first example of the odds and evens array, we saw that odds and evens array get updated but not the original numbers array but here both the objects (original and copied object) get affected by changes we have made to copyData.

The reason behind this behaviour is the mutable and Immutable concept of JavaScript.

In our odds - evens example, we are dealing with primitive datatypes of JavaScript and primitive datatypes values are Immutable means in simpler words, non-updatable values.

whereas in the second example, we are dealing with an object which are mutable and the equal to operator returns a shallow copy, the change gets reflected in both places.

Now, this is the problem like our changes get reflected in all the places and that causes serious issues so what is the solution ?!

well, we have to break the reference or in other words, we have to do Deep Copy rather than shallow copy.

Now, there are many ways to break the references...

// const copyData = data;
const copyData = Object.assign({}, data);

copyData.name = 'Iron Man';

console.log(copyData);
console.log(data);

as you can see, no changes get reflected in our original data object as we have break the reference using Object.assign method.

Now let's make one more change to this...

copyData.name = 'Iron Man';
copyData.languages.speaking[0] = 'Pastun';

console.log(copyData);
console.log(data);

Now, we are making some changes to the languages array and you will see again our changes in the languages array get reflected in our original data source also...

The reason for such a behaviour is our languages array holds some more array and properties which is one more level of nesting and Object.assign method can't break all the nested levels of references.

As a solution, we will break references like this...

// const copyData = data;
// const copyData = Object.assign({}, data);
const copyData = JSON.parse(JSON.stringify(data));

copyData.name = 'Iron Man';
copyData.languages.speaking[0] = 'Pastun';

console.log(copyData);
console.log(data);

Now, you will see again our original data object did not get affected...

JSON.parse(JSON.stringify(()) will perform Deep Copy internally and break the references for all nested-level data.

Let's take another similar example...

const data = [
    {
        name: 'Tony Stark', age: 32, languages: {
            speking: ['Gujarati', 'Hindi', 'English'],
            programming: ['JavaScript', 'Python', 'C++']
        }
    },
    {
        name: 'Steve Rogers', age: 132, languages: {
            speking: ['Gujarati', 'Hindi', 'English'],
            programming: ['JavaScript', 'Java', 'C++']
        }
    },
    {
        name: 'Thor', age: 1500, languages: {
            speking: ['Asgardian', 'Hindi', 'English'],
            programming: []
        }
    },
    {
        name: 'Peter Parker', age: 23, languages: {
            speking: ['Gujarati', 'Hindi', 'English'],
            programming: ['JavaScript', 'Java', 'Python']
        }
    }
];

const filteredData = data.filter(d => d.age < 30);

console.log(filteredData);

We have data array and when we filter it based on age, will get the above output. pretty straightforward right?

But, here let's make some changes...

filteredData.forEach(data => {
    data.name = 'New Name'
});

console.log("Printing Filtered data--------------------");
console.log(filteredData);
console.log("Printing Original Data------------")
console.log(data);

Here, we are updating the name of all objects of filteredData array and now if you see the output...

The name property of filteredData is updated which is ok but if you see carefully, at our original array, data, from which filteredData was calculated, in that array, name property also gets updated the same as filteredData.

And this issue is known to us from the above example so let's break the reference.

Now, there are many ways to break the references...

const filteredData = Object.assign([], data.filter(d => d.age < 30)));
// const filteredData2 = [...data.filter(d => d.age < 30)];  // spread operator...

filteredData.forEach(data => {
    data.name = 'New Name';
});

console.log("Printing Filtered data--------------------");
console.log(filteredData);
console.log("Printing Original Data------------")
console.log(data);

Here, if you see the output, still we are facing the same issue. Our changes get reflected in filteredData as well as in data array too.

And the issue here is the same as the above example.

In the above example, when we made changes in the languages array, we are facing the same issue as we are facing here, in our data, we have an array it's having nested level information, and as a solution, we have to break all levels of references as we did above.

const filteredData = JSON.parse(JSON.stringify(data.filter(d => d.age < 30)));
const filteredData2 = lodash.cloneDeep(data.filter(d => d.age < 30));  // with external library lodash....

filteredData.forEach(data => {
    data.name = 'New Name';
    data.languages.speking[0] = 'Marathi';
});

console.log("Printing Filtered data--------------------");
console.log(filteredData);
console.log("Printing Original Data------------")
console.log(data);

Here, things to remember are, we have multiple ways to break the references but it depends on our data and situations, how to use and which way to use to break the reference.

When we have multiple levels of nesting properties and data, JSON.parse(JSON.stringify()) is the ideal way to break the reference and when we have linear data like no nesting properties, then Object.assign and spread operator can do our job !!

Let's see the same behaviour of data references with functions...

let's take a simple example of calling a function with arguments...

function increaseNumber(number) {
    number++;
    console.log(number);
}

const num = 5;
increaseNumber(num);
console.log(num);

Here, the changes made inside function not reflecting the original value of the variable and the same primitive datatypes and Immutable concepts implies here.

But if we perform the same kind of thing with Objects or Arrays, it will behave same as the above examples...

function printObject (object) {
    const keys = Object.keys(object);

    for (const key of keys) {
        if (key === 'name') {
            object[key] += 'Some Additional Text.';
        }
        console.log(object[key]);
    }
}

const data = {
    name: 'Tony Stark',
    age: 32,
    languages:{
        speaking: ['Gujarati', 'Hindi', 'English'],
        programming: ['JavaScript', 'Python', 'C++']
    }
};

printObject(data);
console.log(data);

and again as a solution, we have to break the references before using any object or array inside the function. and here also we have to take care of which method to use for breaking references depending on the data we are getting.

function printObject (object) {
    const obj = Object.assign({}, object);
    const keys = Object.keys(obj);

    for (const key of keys) {
        if (key === 'name') {
            obj[key] += '!!';
        }
        console.log(obj[key]);
    }
}

const data = {
    name: 'Tony Stark',
    age: 32,
    languages:{
        speaking: ['Gujarati', 'Hindi', 'English'],
        programming: ['JavaScript', 'Python', 'C++']
    }
};

printObject(data);
console.log(data);

You can try that nested property example with JSON.parse(JSON.stringify()).

This basic things will behave same in functions so we have to take care of this things while working with functions.