JS Handbook: Functions are also an "object"
This article is the 3rd part of the JS Handbook Series, in which I'll be covering objects, functions and "this" keyword in depth. Only the topics that require deeper conceptual understanding will be covered
Understanding Objects
Let have quick walkthrough over the basic stuff of objects. It is a non-primitive, that stores properties as key:value pair
const character = { //declaring an object
//properties
name: "Luna the Brave",
class: "Mage",
level: 12,
health: 85,
mana: 120,
isAlive: true
}
//accessing obj properties using the dot operator
console.log(obj.name) //Luna the Brave
//accessing obj properties using the square brackets
console.log(obj["level"]) // 12
The core difference between using the two different ways to perform accessing props is -
dot operator "." - this cannot be used when the property is stored in a variable and you to access the key via the variable or the key name contains some symbols due to which it gets enclosed between quotes (makes it a string).
square brackets "[]" - just better than dot :), but dot provides easier access/ recommendations on IDE while trying to access an existing property.
let obj = {
name: "Don Joe",
"male-or-female": "M",
//when trying to create unique keys using symbols you gotta enclose them as a string.
}
let maleOrFemale = "male-or-female"
console.log(obj[maleOrFemale])// M
console.log(obj.maleOrFemale) //undefined cannot access a string
// To Check For a Key-Value Pair Exist Or Not Using "in" keyword
console.log("name" in obj) // true
console.log("blast" in obj) // fasle
// To DELETE a Property From Object Using "delete" keyword
delete obj.name
delete obj[maleOrFemale]
console.log(obj)// {} empty object
Note: As we know objects are non-primitives and are stored in heap memory, they are passed by reference when you try copying an object to another variable. it doesn't copy the structure rather the reference.
What does that mean? that when you make a change in the copy it is reflected in the original one too cause they share the same ref.
Object Methods
There are 2 types of Copies to prevent pass by reference
Shallow Copies: Copies the top-level properties, if nested properties exist, they share the same reference as that of the one from which it was copied. We use spread operator {...}
Deep Copies: Create a complete independent copy, if even nested properties exists its completely independent of the one from which it is copied. We use structureClone()
//objects are passed by reference until -
let game = {
name: "Red Dead Redemption II",
release: "October 26, 2018",
copiesSold: 79000000
}
let gameCopy = game; //pass by reference
gameCopy.name = "Grand Theft Auto V"
console.log(game)
/* {
name: "Grand Theft Auto V",
release: "October 26, 2018",
copiesSold: 79_000_000
}
*/
//Create Shallow Copy//
gameCopy = {...game} // shallow copy created
//Create Deep Copy//
const car = {
brand: "Toyota",
model: "Corolla",
engine: {
type: "Petrol",
horsepower: 150
}
};
const carDeepCopy = structureClone(car)
carShallowCopy.engine.type = "Diesel"
console.log(car)
/*
{
brand: "Toyota",
model: "Corolla",
engine: {
type: "Petrol",
horsepower: 150
}
}
*/
const carShallowCopy = {...car}
carShallowCopy.engine.type = "Diesel"
console.log(car)
/*
{
brand: "Toyota",
model: "Corolla",
engine: {
type: "Diesel",
horsepower: 150
}
}
*/
const carDeepCopy = structureClone(car)
carShallowCopy.engine.type = "Diesel"
console.log(car)
Object methods to store only keys, values or key-value pairs -
const character_protagonist = {
//properties
name: "Luna the Brave",
class: "Mage",
level: 12,
health: 85,
mana: 120,
isAlive: true
}
const pet = {
petName: "hana",
petType: "dog"
}
// Object.assign(target, ...sources) assining src objects to the target
const game = Object.assign({}, character_protagonist , pet)
console.log(game)
/*
{
name: "Luna the Brave",
class: "Mage",
level: 12,
health: 85,
mana: 120,
isAlive: true,
petName: "hana",
petType: "dog"
}
*/
// Methods to Store object properties
// store keys
const keyList = Object.keys(pet)
console.log(keyList) // ["petName", "petType"]
//store values
const keyList = Object.values(pet)
console.log(keyList) // ["hana", "dog"]
//store key-value pairs
const pairList = Object.entries(pet)
console.log(pet) // [["petName","hana"],["petType","dog"]]
console.log(Object.fromEntries(pairList))
/*{
petName: "hana",
petType: "dog"
}*/ //converting from arrays/entries back to object!
Two methods to Lock an Object -
freezemethod - complete freeze the structure itself along with its propertiessealmethod - freezes the structure but the properties are modifiable
let obj1 = {
prop1 : "data1",
prop2: "data2",
}
Object.freeze(obj1)
obj1.prop3 = "data3"
console.log(obj1)
/*{
prop1 : "data1",
prop2: "data2",
}*/
obj1 = {} // even re-initialising the structure wont make any effect.
let obj2 = {
prop1 : "data1",
prop2: "data2",
}
Object.seal(obj2)
obj2.prop3 = "data3"
console.log(obj2)
/*
{
prop1: "data1",
prop2: "data2",
prop3: "data3"
}
*/
iterating over each key-value pair with the help for..in loop
let obj = {
key1: "val1",
key2: "val2",
}
for(key in obj){
console.log(key); // key1 key2
}
Understanding Functions
In simple terms we can refer to function as a resuable block of code responsible for executing a specific task.
They help us modularise our code files, as the amount of tasks increase, seperation required for each task becomes necessary to improve its readability along with maintainability.
Keep one thing in mind, just like all the datatypes that can be passed into a function, in javascript you can also pass a function into the parameters, and also return a function as return value of a function. This is called Higher Order Function. 📈
Lets see the difference in implementation of fn declaration and fn expression
//Function Declaration//
function addNum(num1, num2){ //(num1,num2) -> parameters
return num1 + num2;
}
// [Function: addNum] -> function definition
console.log(addNum) //we are currently passing the function definition.
// to execute the function we have to invoke it! ensure all the parameters are provided
console.log(addNum(1)) //NaN since adding a number and undefined = NaN
console.log(addNum(1,2)) // 3
//Function Expression//
//assigning variable a function.
const multiply = function(num1, num2){
return num1 * num2;
}
console.log(multiply) // multiply stores the function definition.
console.log(multiply(2,2)) //multiply is invoked! Result: 4
Now from observing the code from function definition to invoking a function both the methods seem exactly the same, just a different way of naming them/declaring them. but but there is a catch that most of the people fumble if they aren't strong with datatypes and how execution context exactly works for functions and variables.
Although i have attached the links that will redirect you to my article of fundamentals that covers these two topics in depth seperately.
But just for a quick understanding if someone has less time to understand the concept -
So when you run the code, a memory phase whose job is to simply scan the code and accordingly hoist up all the declarations. the declared variables are hoisted just with their identifiers not the actual value, where as on the other hand declared function are hoisted along with their body content(the code that exist between the scope or you may say curly braces {})
After the memory phase has been completed an execution phase starts up whose job is to only update the declared variables values and execute the function by spinning up another context similar to the global one that we did during the memory phase but this time just for the particular function.
code snippet :
console.log(multiply(4,5)); //executes since the entire function was hoisted along with its body so we already do have the context for the entire function to be executed.
console.log(add(1,1)); // whereas on the other hand this will throw a reference error, saying that yes the variable add was hoisted but not with its assigned value during the memory phase, that value is in our case a fn.
function multiply(num1, num2){
return num1*num2;
}
var add = function(num1, num2){
return num1 + num2;
}
Hope the example was helpful enough to help you understand. But would still highly recon you all to visit my article on this topic, since i have covered there intensively which will surely clear all your doubts :)
A question that came to my mind how and when do we decide what to use is an architectural decision.
Function Declarations are fully hoisted, allowing for a 'top-down' file structure where high-level execution happens at the top and implementation details are kept at the bottom.
Function Expressions, specifically when declared with const, enforce strict predictability by preventing invocation before definition via the Temporal Dead Zone, and prevent accidental reassignment. Additionally, expressions are mandatory when passing anonymous functions as callbacks to higher-order functions
Arrow Functions => (modern syntax of functions with some quirks)
shorthand for a regular function, executes exactly like a regular function, unlike a regular function, arrows function have more than 1 way of returning response/value.
Explicity return - basically using the return keyword to return the response on invoking a fn, this is done when you use curly braches {} after the arrow.
Implicit return - returning the response without the use of a return keyword. This works when there is a single line of code to be executed, or else you have to default back to explicit conventiion of returning the result.
//for declaring arrow function we gotta use fn expression
const arrowFn = () => {/*...fn content*/}
const addTwo = (num1, num2) => {
return num1 + num2 //explicit return -> when curly braches used.
}
const multiply = (num1, num2) => num1*num2 //implicit return no need of writing a return function.
const multiply = (num1, num2) => (num1*num2) //similar to the above
const addThenMultiply = (num) => ( //❌ incorrect wouldnt work
num += num;
num *= num;
}
console.log(addTwo(2,5)) // 7
console.log(multiply(4,4)) // 15
So far we understood the basics of arrow functions now lets understand the quirks it has that differentiates it from a regular function.
Regular Function v/s Arrow Function
A regular function has its own arguments object which it can access, where as arrow function doesn't have its argument object(in browser but in node its inaccessible), now you must be wondering what is an argument object, people usually interchange arguments with parameters and vice versa for fucntion inputs.
Lets understand this with code snippet -
function args(){
console.log(arguments);
//original structure of arguments
/*
{
0: 10,
1: 20,
2: 30,
length: 3
}
*/
console.log("tyep: ", typeof arguments) //object
console.log("is array: ", Array.isArray(arguments)) //false
//converting it to an actual array
console.log(Array.from(arguments))// [10,20,30]
}
args(10,40,70);
//RESULT: Arguments(3) [10, 40, 70] (just devtool formatting)
//from the result we can observe we are getting output as an array, but keep in mind this is not an instance of Array.prototype but object.prototype
//lets see if the object is created for an arrow func
const arrowArgs = () => {
console.log(arguments) // Depends on the enviornement
//If browser -> Reference Error
//If Node -> empty object, cannot access the arguments object.
}
arrowArgs();
There is one more difference which ill be covering as I cover this in detail, as it will only then only it will make more sense.
this keyword (make or break in your interview)
The most confusing keyword to exist in the entire javascript, although it exist in almost every languages but the level of quirkyness it has in JS which is something special to JS and JS only.
Golden Rule: for regular functions, the value of this is not determined by where the function is written. It is entirely determined by how the function is invoked at runtime.
Implicity Binding:
When a function is invoked as a method of an object (using dot notation),
thispoints strictly to the object standing to the left of the dot.
const protagonist = {
name: "Itadori Yuji",
resolveScene: function() {
console.log(`${this.name} was responsible for the shibuya incident`);
}
};
protagonist.resolveScene(); // 'this' is bound to the 'user' object. Outputs: "Itadori was responsible for the shibuya incident"
Global Binding (Default):
If a standard function is invoked on its own (no dot notation),
thisdefaults to the Global Object (windowin the browser). Note: In "strict mode" ("use strict";),thiswill beundefinedto prevent global object pollution.
function sayHello() {
console.log(this);
}
sayHello(); // Outputs the Window object (or undefined in strict mode)
Explicit Binding (
call,apply,bind)call(thisArg, arg1, arg2): Immediately invokes the function with the specifiedthiscontext and comma-separated arguments.apply(thisArg, [argsArray]): Immediately invokes the function, but takes arguments as an array.bind(thisArg, arg1): Does not invoke the function. It returns a brand new function permanently bound to the specifiedthiscontext, to be executed later.
function cookDish(ingredients, dishName){
return this.name + `is the best chef in the world for cooking \({dishName}(main ingredients: \){ingredients})`
}
const sharmasKitchen = {name: "gordon ramsey"};
const punjabiDhaba = {name: "Navjot Singh"};
// now how do I pass the reference of this of any of the restaurants to the cookDish function? since it accepts inly 2 params
console.log(cookDish.call(sharmasKitchen, "Panner masala", "spices, paneer, butter")); //call accepts indv value as parameter input
console.log(cookDish.apply(punjabiDhaba, ["Panner masala","spices, paneer, butter"])); //apply accepts array as parameter input
console.log(cookDish.bind(sharmasKitchen, "idk ka masala", "diiikstra, chutney")) //returns the function definition to which the method is being applied
const cookFen = cookDish.bind(sharmasKitchen, "idk ka masala", "diiikstra, chutney")
console.log(cookFen());
So far we understood how does this keyword work. Now lets understand how does it make a difference among the bindings of this in the two types of function.
this bindings in arrow function v/s regular function, and how it responds with nested functions
const games = {
name: "RDR2",
playGame(){
// only the regular function has the access to this of the current object.
console.log(`${this.name} game is booting up to play`)
// nested regular function does not inherit the "this" resulting in undefined
function resetGame(){
console.log(`do you want to reset ${this.name} game?`)
}
resetGame()
//arrow function inside of a regular function still inherits the "this" from the current object
const changeSettings = () => {
console.log(`do you want to change the settings of ${this.name}?`)
const closeGame = () => {
console.log(`do you want to close ${this.name}?`)
}
closeGame()
//nested arrow function inherits the this bindings since its still under the main function
}
changeSettings();
},
resumeGame: () => {
console.log(`resume ${this.name}`)
}
}
games.playGame()
games.resumeGame()
Result:
RDR2 game is booting up to play
do you want to reset undefined game?
do you want to change the settings of RDR2?
do you want to close RDR2?
resume undefined
This above section is the gotchya section, where most of the people are tricked into believing that arrow functions do not have this bindings well they do inherit it from the functions if nested :)
Key points to remember:
nested regular function do not inherit
thisreferencenested arrow functions do inherit
thisreference
I will keep updating this article with better and precise explainations simplifying the concepts in a more intuitive way, so can bookmark this article for future references!
Thank you for your time! hope this help, a like and share would be really appreciated! :)
