Rethinking JS [short notes]
Shihabudheen US
Posted on August 10, 2020
Mental model 🧠
- Mental models are how we think 🤔 about something
- Slow and fast thinking
- Slow thinking is laborious, front lobe
- ⚡️ Fast thinking is less tiring and most often preferred(default)
- Mental models are essential to write good code, easy to reason about and less prone to errors
Context
You are on the JS asteroid in the space. You see stars⭐️, planets🪐 and asteroids ☄️ floating in space 🌌.
Values and Expressions
Values
- values are things. They are like numbers in Math, words in a sentence and dots in geometry. It is a thing 🧱. We can't 🚫 do much to them, but we can do things with them
- there are two types of values in JS. Primitive and Composite
- Primitive values are numbers and strings(and few more). They are like far distant stars and we can only look and refer them, but we can't change them or affect them.
- Composite values are different. We can manipulate them from code. Like functions and objects. They are like rocks closer to the asteroid that we are on.
Expression
- expressions are kind of questions ❓ that we ask JS. The expressions always result in values.
typeof
to know the type of value we can use typeof
operator.
typeof _value
will give us the type of the value as string.
The types can be,
Primitive
- undefined (undefined)
- null (object)
- number (number)
- bigint
- symbol
- string
- boolean
Composite
- object (object)
- function (function)
Primitives are immutable
In JS, primitives are immutable. For example
let name='yikes'
name[0]='l' // can't change
console.log(name) // 'yikes'
Even though string appears to be similar to an array, which is not a primitive we might have an intuition that we can mutate or change it. But in practice, we can't since the strings are primitive. This also applies to all the primitives.
let number=10
number.value='ten'
console.log(number) // 10
Since the addition of a property is also a kind of mutation, this too is not allowed on Primitives.
Variables
Variables are like wires. We can connect the variables to values. In order to connect a variable wire to a value, we use assignment statements.
let x='Shihab'
Now the variable wire x is connected to string value Shihab. The RHS of an assignment is always an expression.
let world='World'
let say='Hello '+ world
Since we are asking JS, what is 'Hello '+world
it is an expression which resolves to a value 'Hello World'
.
The RHS of let x='Shihab'
is also an expression, since it also resolves to a value 'Shihab'. We call it literlas since we write down the exact value.
In JS, we always pass the value and not the variable itself. We cannot change what the variable points to, but at times we can change the value itself.
let num=10
function double(x){
x=x*2
}
double(num) // here we pass the value 10
// and not the reference to it
console.log(num) // 10
let arr=[10,20]
function mutate(input){
input[0]=30
}
mutate(arr)
console.log(arr) // [30,20]
This is because we pass the value of arr which is [10,20]. Since arrays are mutable, we were able to mutate the value. And the function cannot change the value arr was wired to, thus we get [30,20] when trying to print arr.
Counting Values
We should always think, values as having a precise count.
Undefined ----> Undefined [1]
null -----> null
Boolean -----> true or false [2]
Number ----> 18 quintillion [...]
BigInit ---> Use for arbitrary precision and no round-off. Mainly used in financial calculations.
String ---> A string for each conceivable string that exists in the universe. A string has properties but it is not as same as other objects. Since the string is primitive it is immutable.
Symbols ---> recently new
Objects ---> Each time it creates a brand new Object
Function ---> Each function expressions are distinct. Like any other things in JS, functions are expressions too. When it is called with () [Call expression] JS resolves it to the return value of it. If not, it resolves to function expression or body. Function are also Objects, but special objects. Whatever you can do with Objects can be done with functions too. But what makes function different is, the can be invoked.
In this we way, we have can better place and point our variables to values. In our model, there should be only two booleans, and one undefined
and null
. All the time, when a primitive is being referred, JS actually summons them. But in the case of Objects {} and functions (), it creates a brand new value for us.
Equality in JS
In JS there are mainly 3 types of equalities
- Same Value
Object.is()
- Strict equality
===
- Loose equality
==
Same Value
Same value returns true
is we are pointing to the same values.
Strict Value
It is same as Object.is()
expect for
NaN === NaN // false
0 === -0 // true
-0 === 0
To test, if a number is NaN
we can use Number.isNaN()
or num !== num
.
Loose Equality
It just compares the sameness of values.
2=='2'
true==0
Properties
Properties are similar to variables. They also point to values, but they start from an Object and they belong to it.
let sherlock={
surname:'Homes',
address:{
city:'London'
}
}
Even though it seems like a single object is being created there are actually two distinct objects here. An object can never reside inside another object, even though it might seem nested from code.
let sherlock={
surname:'Holmes',
age:64
}
Rules of reading a property
console.log(sherlock.age)
Properties will have names, which are strings basically. They must be unique within an object,i.e. an object cannot have two keys with the same name. The names are case sensitive too.
These rules look roughly like this:
Figure out the value of the part before the dot (.).
If that value is
null
orundefined
, throw an error immediately.Check whether a property with that name exists in our object.
a. If it exists, answer with the value this property points to.
b. If it doesn’t exist, answer with the undefined
value.
If a property is missing, we get an undefined
. But it doesn't mean that we have that property on the object pointing to undefined
. It is more like, we are asking JS for the value (expression) and it is replying us that it is not defined, undefined
.
Assigning to a property
sherlock.age=65
Mutation
Suppose we have the following
let sherlock={
surname:'Holmes',
address:{
city:'London'
}
}
let john={
surname:'John',
address: sherlock.address
}
Now we want to change john
.
john.surname='Lennon'
john.address.city='Malibu'
But we observe we could see sherlock.address.city
has also changed to Malibu
from London
. This is because both sherlock.address
and john.address
pointed to the same Object.
So because of this, the mutation can be dangerous. It might unintentionally change the values at all the places where it is being referred.
In order to avoid mutation, we could have done the following:
- When mutating
john
,
john={
surname:'Lennon',
address:{ city: 'Malibu' }
}
2.
john.surname='Lennon'
john.address={ city:'Malibu' }
Is Mutation that Bad?
The mutation is not bad at all, but we should pay closer attention to it. The bliss with the mutation is, it helps us update or change a value realtime at multiple places. If think the other way, that is misery with it too.
Even though you declare an Object with const
it will not presentation mutation to the Object. It will only prevent the reassignments.
const x = {
name:'Shihab'
}
x.name = 'Shifa' // allowed
x.age = 22 // allowed
x = {} // not allowed
Prototype __proto__
let human={
teeth:32
}
let gwen={
age:19
}
console.log(gwen.teeth) // undefined
But we can access teeth
property of human
in gwen
by,
let gwen={
__proto__: human
}
Now,
console.log(gwen.teeth) // 32
With adding __proto__
we instruct JS, to continue searching for teeth
in __proto__
too.
Prototype Chain
The search for the values will continue until the base prototype
is reached. In JS the base prototype
is Object.__proto__
which is set to null
.
As you can see, so this is kind of a chain that is getting created when we as JS to look for a property on an Object. This is being referred to as prototype chain
.
let mammal={
brainy:true
}
let human={
__proto__:mammal,
teeth:32
}
let gwen={
__proto__:human,
age:19
}
console.log(gwen.brainy) // true
Shadowing
When an Object has the same property on it and as well as inside the __proto__
, the own shadows the value on __proto__
. This is called Shadowing.
Assignments
The property assignments directly happen on the Object and not on the __proto__
.
let human={
teeth:32
}
let gwen={
__proto__:human
}
On gwen.teeth=31
To check if the property belongs to an Object or its __proto__
, we have a method called hasOwnProperty
on Object.
ObjectName.hasOwnProperty(prop)
If the prop
is a property on ObjectName
, it will return true
if not false
.
Object prototype
When we create a new Object, there is a __proto__
that gets added by default. It is the prototype of the Object.
To terminate the prototype chain of any Object we can just assign null
to its __proto__
.
Polluting prototype
All the in-built methods and properties of Objects, Arrays and Strings are defined in the __proto__
of their base. In this way, these get shared among all the values, that are being created out of it.
But this practice of sharing is highly discouraged.
But the sharing of methods and properties via the prototype chain is the base of classes and all other features. But the direct usage of polluting prototype is not recommended.
proto vs. prototype
You might be wondering: what in the world is the prototype property?
The story around this is confusing. Before JavaScript added classes, it was common to write them as functions that produce objects, for example:
function Donut() {
return { shape: 'round' };
}
let donut = Donut();
You’d want all donuts to share a prototype with some shared methods. However, manually adding __proto__
to every object looks gross:
function Donut() {
return { shape: 'round' };
}
let donutProto = {
eat() {
console.log('Nom nom nom');
}
};
let donut1 = Donut();
donut1.__proto__ = donutProto;
let donut2 = Donut();
donut2.__proto__ = donutProto;
donut1.eat();
donut2.eat();
As a shortcut, adding .prototype
on the function itself and adding new
before your function calls would automatically attach the __proto__
:
function Donut() {
return { shape: 'round' };
}
Donut.prototype = {
eat() {
console.log('Nom nom nom');
}
};
let donut1 = new Donut(); // __proto__: Donut.prototype
let donut2 = new Donut(); // __proto__: Donut.prototype
donut1.eat();
donut2.eat();
Now this pattern has mostly fallen into obscurity, but you can still see prototype property on the built-in functions and even on classes. To conclude, a function’s prototype
specifies the __proto__
of the objects created by calling that function with a new
keyword.
Posted on August 10, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.