9 Important Javascript Concepts

If you apply for a Frontend or Web Developer position, your JavaScript skills may be tested. There are many difficult JS concepts, such as hoisting, closure, data types, asynchronous, and others. Some of the most frequently asked JavaScript Concepts are as follows:

1) Mutability

JavaScript has seven primitive data types (string, number, bigint, boolean, undefined, symbol, and null). They are all immutable, which means that once a value is assigned, we cannot change it; all we can do is reassign it to a different value (different memory pointer). Some data types, such as Object and Function, are mutable, which means we can change the value in the same memory pointer.

let text = 'abcde'
text[1] = 'z'
console.log(text) //ans: abcde

Strings are immutable, which means they cannot be changed once assigned to a value. You can reassign it, however. Note that altering a value and reassigning it to another value are not the same thing.

const arr = [1,2,3]
arr.length = 0
console.log(arr) //ans: []

Assigning arr.length to 0 is equivalent to resetting or clearing the array, therefore the array is currently empty.

const arr = [1,2,3,4]
arr[100] = undefined
console.log(arr, arr.length) //ans: [ 1, 2, 3, 4, <96 empty items>, undefined ] 101

Because an array uses a contiguous memory space, JS will reserve the memory from index 0 to index 100 when we assign the index 100 to a value (including undefined). This means that the array's length is now 101.

2) Var and Hoisting

var variable = 10;
(() => {
variable2 = 100;
console.log(variable);
console.log(variable2);
variable = 20;
var variable2 = 50;
console.log(variable);
})();
console.log(variable);
var variable = 30
console.log(variable2);
//ans:
//10
//100
//20
//20
//ReferenceError: variable2 is not defined

var is a variable with a functional scope, whereas let and const are variables with a block scope. Only var can be hoisted, which means that variable declarations are always at the top. Due to the hoisting, you can assign, call, or use the variable before declaring it with the var keyword. Let and const, on the other hand, cannot be hoisted because they enable the TDZ (Temporal Dead Zone), which means the variable is unreachable before it is declared.

Variable2 is declared inside a function with the var keyword, which makes that variable only available within the function scopes. When something outside the function attempts to use or call that variable, a referenceError is thrown.

test() //not error
function test(){
console.log('test')
}
test2() //error
var test2 = () => console.log('test2')

Function declaration with function keyword can be hoisted. However, an arrow function cannot be hoisted, even if it is declared with var variable.

3) Accidental Global Variable

//Q6
function foo() {
  let a = b = 0;
  a++;
  return a;
}
foo();
typeof b; // => number
typeof a; // => undefined
console.log(a) //error: ReferenceError: a is not defined

var is a functional scoped variable and let is a block-scoped variable. Although it seems that a and bare declared using let in let a = b = 0, the reality is variable b is declared as a global variable and assigned to a window object. In another word, it is similar as

function foo() {
  window.b = 0
  let a = b;
  a++;
  return a;
}

4) Closure

//Q7
const length = 4;
const fns = [];
const fns2 = [];
for (var i = 0; i < length; i++) {
fns.push(() => console.log(i));
}
for (let i = 0; i < length; i++) {
fns2.push(() => console.log(i));
}
fns.forEach(fn => fn()); // => 4 4 4 4
fns2.forEach(fn => fn()); // => 0 1 2 3

Closure is the preservation of a variable environment after it has been altered or garbage collected. The distinction in the above question is in the variable declaration, where the first loop uses var and the second loop uses let. Because var is a functional scoped variable, when it is declared within a for loop block, it is treated as a global variable rather than an internal block variable. Let, on the other hand, is a block-scoped variable, similar to how variables are declared in other languages like Java and C++.

Closure occurs just in the let variable in this scenario. Each function added to the fns2 array remembers the current value of the variable, even if it is modified in the future. The fns, on the other hand, does not remember the present value of the variable; instead, it utilises the future or final value of the global variable.

5) Object

//Q8
var obj1 = {n: 1}
var obj2 = obj1
obj2.n = 2
console.log(obj1) //ans: {n: 2}
//Q9
function foo(obj){
 obj.n = 3
 obj.name = "test"
}
foo(obj2)
console.log(obj1) //ans: {n: 3, name: "test"}

As we know, the object variable only contains the pointer of memory location of that object. So here obj2 and obj1 point to the same object. This means that if we change any value in obj2, obj1 will also be affected because essentially it’s the same object. Similarly, when we assign an object as a parameter in a function, the argument passed only contains the object pointer. So, the function can modify the object directly without returning anything. This technique is called passed by reference

//Q10
var foo = {n: 1};
var bar = foo;
console.log(foo === bar) //true
foo.x = foo = {n: 2};
console.log(foo) //ans: {n: 2}
console.log(bar) //ans: {n: 1, x: {n: 2}}
console.log(foo === bar) //false

Because an object variable just stores a pointer to the object's memory location, when we declare var bar = foo, both foo and bar point to the same object.

In the following logic, foo = n:2 is executed first, where foo is assigned to a different object. As a result, foo contains a pointer to a separate object. Simultaneously, foo.x = foo is running, and foo still has the old address. as a result, the logic is comparable with

foo = {n: 2}
bar.x = foo
so bar.x = {n: 2}. Finally the value of foo is {n: 2} , while bar is {n:1, x: {n: 2 }}

6) This

//Q11
const obj = {
  name: "test",
  prop: {
      name: "prop name",
      print: function(){console.log(this.name)},
  },
  print: function(){ console.log(this.name) }
  print2: () => console.log(this.name, this)
}
obj.print() //ans: test
obj.prop.print() //ans: prop name
obj.print2() //ans: undefined, window global object

The preceding example demonstrates how to use this keyword in an object. In a function execution, this refers to an object execution context. This scope, however, is only possible in a standard function declaration, not in the arrow function. The preceding example demonstrates explicit binding, in which, for example, object1.object2.object3.object4.print(), the print function will utilise the most recent object, object4, as the this context. If this object is not bound, it will revert to the root object, which is the window global object, as we see when we use obj.print2()

Nevertheless, you must also understand implicit binding, which occurs when the object context is already bound and the next function execution always uses that object as the this context. For example, func.bind(<object>) returns a new function that uses <object> as the new execution context.

7) Coercion

//Q12
console.log(1 +  "2" + "2"); //ans: 122
console.log(1 +  +"2" + "2"); //ans: 32
console.log(1 +  -"1" + "2"); //ans: 02
console.log(+"1" +  "1" + "2"); //ans: 112
console.log( "A" - "B" + "2"); //ans: NaN2
console.log( "A" - "B" + 2); //ans: NaN
“10,11” == [[[[10]],11]] //10,11 == 10,11, and: true
"[object Object]" == {name: "test"} //ans true

One of the most difficult JS questions is coercion. There are two general rules. The first rule is that if two operands are joined with the + operator, both operands are converted to strings first using the function toString() { [native code] } method and then concatenated. Meanwhile, other operators like -, *, or / will convert the operand to a number. If it cannot be forced into a number, it will return NaN.

It will be more difficult if the operand contains an object or array. The function toString() { [native code] } method on any object returns [object Object]. With an array, however, the function toString() { [native code] } method will return the underlying value separated by a comma.

Noted that: == means that coercion is allowed to happen, while === is not.

8) async

//Q13
console.log(1); 
new Promise(resolve => {
  console.log(2); 
  return setTimeout(() => {
    console.log(3)
    resolve()
  }, 0)
})
setTimeout(function(){console.log(4)}, 1000); 
setTimeout(function(){console.log(5)}, 0); 
console.log(6);
//ans: 1 2 6 3 5 4

You must understand how the event loop, macrotask queue, and microtask queue function in this case. You may read more about those principles in my earlier article here. In general, the asynchronous function will execute after all of the synchronous functions have completed.

//Q14
async function foo() {return 10}
console.log(foo()) //ans: Promise { 10 }

Once the function is declared with async. it always returns a Promise, no matter if the internal logic is synchronous or asynchronous.

//Q15
const delay = async (item) => new Promise(
resolve => setTimeout(() => {
console.log(item);
resolve(item)
}, Math.random() * 100)
)
console.log(1)
let arr = [3,4,5,6]
arr.forEach(async (item) => await delay(item))
console.log(2)

forEach function is always synchronous, no matter if each loop is synchronous or asynchronous. This means that each loop will not wait for the other. If you want to execute each loop in sequence and wait for each other, use for of instead.

9) Function

//Q16
if(function f(){}){ console.log(f) }
//error: ReferenceError: f is not defined

The if condition is fulfilled in the preceding example because the function declaration is regarded a true value. But, because they have distinct block scopes, the internal block cannot access the function declaration.

//Q17
function foo(){
return
{ name: 2 }
}
foo() //return undefined

Because of the automatic semicolon insertion (ASI), the return statement is terminated with a semicolon, and everything below it is skipped.

//Q18
function foo(a,b,a){return a+b}
console.log(foo(1,2,3)) //ans: 3+2 = 5
function foo2(a,b,c = a){return a+b+c}
console.log(foo2(1,2)) //ans = 1+2+1 = 4

function foo3(a = b, b){return a+b}
console.log(foo3(1,2)) //ans = 1+2 = 3
console.log(foo3(undefined,2)) //error

The first 3 executions are quite clear, but the last function execution throws an error because b is used before it’s declared, similar to this

let a = b
let b = 2
10) Prototype
//Q19
function Person() { }
Person.prototype.walk = function() {
  return this;
}
Person.run = function() {
  return this;
}
let user = new Person();
let walk = user.walk;
console.log(walk()); //window object
console.log(user.walk()); //user object
let run = Person.run;
console.log(run()); //window object
console.log(user.run()); //TypeError: user.run is not a function

A prototype is a variable object that is used to inherit features from its parent. When you declare a string variable, for example, it has a prototype that inherits from String.prototype. As a result, you can use string methods such as string.replace(), string.substring(), and so on within the string variable.

In the preceding example, we assign walk to the Person function prototype and run to the function object. It's two distinct Things. The method from the function prototype will be inherited by every object generated by the function using the new keyword, not the function object. But keep in mind that if we assign that function to a variable, such as let walk = user.walk, the function will forget about the user as this execution environment and will instead fall back to the window object.

I sincerely hope that the majority of you find the approach covered here to be helpful. Thank you for reading, and please feel free to leave any comments or questions in the comments section below.

Post a Comment

0 Comments