Types in Javascript
Types in Javascript
Every programming language has various types which are different representations of data, such as numbers, strings, booleans, and so on. In Javascript, there are 7 types:
number: 5
boolean: true
string: 'Hello'
undefined: undefined
null: null
symbol: Symbol('a symbol')
object: {}
We can always determine the type of our data by using the typeof operator. The results for each typeof will be shown in comments on the right:
number: 5 //typeof 5: number
boolean: true //typeof true: boolean
string: 'Hello' //typeof 'Hello': string
undefined: undefined //typeof undefined: undefined
null: null //typeof null: object
symbol: Symbol('a symbol') //typeof Symbol('a symbol'): symbol
object: {} //typeof {}: object
As we look through these typeofs, we will get the expected data types and it will be very easy to understand until we reach null. Null resulting as an object is actually a bug in the javascript language. It should actually be a null type. This was proposed for a fix but rejected as there are many, many legacy programs that rely on this bug so if it were fixed, these programs would no longer function correctly.
In the case of undefined and null, there is an important differentiation. The undefined type simply means that there is a variable here, but it has not yet been defined such as when variables are hoisted, or when a function does not return anything. Null on the other hand means that there is no value.
What about other examples with typeof?
typeof [] // 'object'
typeof function(){} // 'function'
This looks like we have another type here with function. Actually, when it comes down to it, function is just an Object. So are arrays. So even though we are given the type 'function' here, it is still just an object. We can interact with functions as if they're objects too.
function a() {
return 10;
}
a.a = 15;
console.log(a.a);
So we can add properties to our functions as if they're objects.
Of all of our types, we have two main categories. Primitive types and non-primitive types. All of the types other than Object types are primitives.
Primitive types:
- number
- boolean
- string
- undefined
- null
- symbol
Non-primitive types:
- object
- array
- function
Primitive types are types that represent only a single value so in memory it will only have a single value, and cannot be broken down into any smaller parts. Non-primitive types don't contain the actual value directly. Instead it has a reference to somewhere else in memory where the value is stored. There are many different types of objects that come with the language which can be seen here, such as NaN, Error, Date, and so on. Under the hood these are all objects.
You might notice that on this list of various objects we see types we appear to have already covered as primitives such as Boolean, String, and Number. In this case, these are actually just primitive value wrappers, hence why they are capitalised unlike the primitives. This allows us to use various methods on our primitive values very easily.
// get boolean value true as string
true.toString(); // returns 'true'.
// under the hood operates similarly to this:
Boolean(true).toString();
Pass by Value and Pass by Reference
In Javascript, primitive types in Javascript are immutable, meaning that in order to change the value of these types, we just remove the primitive type and create a new value in memory, we can't actually modify them directly.
When using primitive types the variable contains the memory address to the value we've assigned it. This is what we call 'Pass by Value', which simply means that when we use the variable, we are simply looking at only the value held in memory. These primitive types do not know of the existence of any other primitive type values.
var a = 10; // creates value in memory. Variable a contains the memory address for where the value of 10 exists
a = 5; // removes previous address in memory and assigns new memory containing the address to our value of 5
var b = 15; // creates value in memory just like a did previously. Does not know about the existence of a.
var c = b; // copies the primitive type value from b as if we were writing c = 15.
c++; // increment 15 by 1
If we were to log all of the above variables, we would have each of these variables with their own distinct values. Changing b after assigning c to its value would not change c.
Objects on the other hand use what we call 'Pass by Reference'.
let obj1 = { name: 'John', age: 43 }; // create and point to address in memory of our object
let obj2 = obj1; // point to the address in memory where we can find obj1
obj2.age = 86; // change value of obj2 age property
console.log(obj1);
console.log(obj2);
Here we'll note that when we changed the values of obj2, the values of obj1 also changed with it. This is because of Pass by Reference. When we assigned obj2 to obj1, they are both pointing to the same reference in memory. This means that we are saving memory by not cloning objects, but with the potential side effect of accidentally changing the objects values. We can also see this with arrays as they are objects types too.
var c = [1,2,3];
var d = c;
d.push(500);
console.log(c);
console.log(d);
We'll see here that we've modified the array for both variables just the same as our previous objects. So what if we actually want to create a clone of our object separate from the previous one? There are various ways to do this but with our array a simple way to achieve this is the concat method.
var c = [1,2,3];
var d = [].concat(c);
d.push(500);
console.log(c);
console.log(d);
Now these are two separate arrays, but are taking up more space in memory. For non-array objects this is a little different. With modern javascript we can use the Object.assign method, as well as the spread operator.
let obj1 = { name: 'John', age: 43 };
let clone = Object.assign({}, obj1) // add the values in obj1 to the empty object,
let clone2 = {...obj1}; // add the values in obj1 to a new automatically created empty object
clone.age = 86;
console.log(obj1);
console.log(clone);
console.log(clone2);
This shouldn't be too difficult to follow and understand, but what if we make it much more complicated and we have other objects inside our object?
let obj1 = { name: 'John', age: 43, likes: {colours: 'red'} };
let clone = Object.assign({}, obj1) // add the values in obj1 to the empty object,
let clone2 = {...obj1}; // add the values in obj1 to a new automatically created empty object
obj1.likes.colours = 'blue';
console.log(obj1);
console.log(clone);
console.log(clone2);
As we can see here, all of our objects have the same value for our likes object. This is because we only performed a shallow clone, which means we only cloned the first level of our object (the object memory address) - but we had a second level in our object leading to another object, which means it works as a Pass by Reference. The solution to this is by performing deep cloning. We can do this easily with Json.
let obj1 = { name: 'John', age: 43, likes: {colours: 'red'} };
let clone = Object.assign({}, obj1) // add the values in obj1 to the empty object,
let clone2 = {...obj1}; // add the values in obj1 to a new automatically created empty object
let deepClone = JSON.parse(JSON.stringify(obj1));
obj1.likes.colours = 'blue';
console.log(obj1);
console.log(clone);
console.log(clone2);
console.log(deepClone);
Now we have successfully cloned the original object into another separate object. In this case, we're using JSON to convert all of our objects properties and values into strings and then parsing them back into values. This is a slow process if we're cloning a huge object with many nested values, so it's probably not something you will see too often.