Have you grasped the basics of JavaScript and now crave a deeper understanding of its inner workings and strengths? Then you have found the right place. We will explore how JavaScript works, how you can code in different programming paradigms, and delve into asynchronous programming in JavaScript.
JavaScript is a dynamically typed, interpreted, multi-paradigm, single-threaded, event-driven language. What does that mean? Read on and find out.
Dynamically typed
A language can be dynamically or statically typed. This refers to whether you as a developer need to specify the type you are working with. JavaScript is dynamically typed so you do not need to specify the type explicitly.
Consider this example:
Let age = 39; age = “40”;
Here we have a variable called age assigned with a value. Initially, it is declared with a value of type integer
and then updated with a value of type string
.
You see, it is flexible. JavaScript does not mind what type you use and you can easily change the variable type from one type to another.
Even though this is flexible and can make it easy and fast to work with, it can also lead to unexpected behaviors and errors.
Imagine you accidentally reassigned this age variable to a string, and now you want to increment the value by 1.
age = “40” + 1;
Now you will get the result 401, which is not what you expected.
Interpreted
Computers understand binary code in the format of ones and zeros. When you write a program in any language, those lines of code have to be translated into machine code.
There are compiled and interpreted languages. JavaScript is interpreted.
An analogy for how this works is how you translate from one language to another in real life. A compiled language works like translating a book. The author writes the book and then hands it over to a translator that translates the whole book and then the final copy gets released. This is how a compiled language works where the whole code base gets translated into machine code at once and then it is compiled. You can imagine that the translation is a bit slow but as soon as it is done the code works very fast and is ready to execute.
There are some scenarios where this won’t work very well where the translation process needs to be dynamic or more flexible. Let’s say you invite some friends from Italy who do not speak English and you don’t speak Italian. You could have a translator that sits in between and translates sentence by sentence so that you can communicate. This is how an interpreted or so-called scripting language works. The translation to machine code happens line by line while it’s running and it might be a bit slower but also more flexible. This makes JavaScript easy and fast for developers to write and deploy and easy to execute in different browsers and environments.
Even though JavaScript is by its core an interpreted language, engines like v8 used in Google Chrome have come up with a solution to make the language perform higher by JIT, just in time compilation. This means that the code is translated or compiled into optimized machine code right before execution.
Multi-paradigm
JavaScript can be used for procedural programming, functional programming, and object-oriented programming. This makes it a multi-paradigm language.
Procedural programming
Probably you know about procedural programming as it is the simplest way of writing code in my opinion where you solve a problem step by step.
Here you see a simple example of code that is written procedurally.
function filterOutOddNums(nums){
const arrWithoutOdds = [];
for(let i = 0; i < nums.length; i++){
if(nums[i] % 2 === 0)
{
arrWithoutOdds.push(nums[i]);
}
}
return arrWithoutOdds
}
Functional programming
This is another paradigm. In functional programming, you are not writing the code in a step-by-step manner, instead you describe what you want the code to do. This is called declarative
code, and makes it intuitive to write and read.
In the functional programming paradigm, a function must be pure
, so no side effects
, and the return value should only be dependent on its parameters.
Side effects
are when you work with a system outside of the function itself or when the function is dependent on something else than its parameters.
This makes the code more testable and safe from errors.
The code I showed before in the example of Procedural programming is impure
because the function depends on the variable “arrWithoutOdds” and not only the parameter nums.
The code is also imperative and not declarative.
Let’s make the code pure and declarative.
function filterOutOddNums(nums){
return nums.filter(num => num % 2 === 0);
}
This code is only dependent on the parameter nums and just by using the higher order function filter makes it declarative.
JavaScript has higher-order functions built in like filter, map, reduce, and sort. Higher-order functions are functions that take functions as parameters or return functions.
Another way to make this pure is by utilizing recursion.
function filterOutOddNums(nums) {
if (nums.length === 0) {
return [];
}
const [first, ...rest] = nums;
if (first % 2 === 0) {
return [first, ...filterOutOddNums(rest)];
} else {
return filterOutOddNums(rest);
}
}
The rest operator and recursion are good tools to write pure functions. if you do not know about recursion I suggest you take some time to learn what it is. Essentially it is when a function calls itself and is used for iterating where a a normal loop would be very messy to work with. In this case, we use it to make the function declarative and pure.
JavaScript supports closures. This is another good tool to have in your toolbox.
function append(arr) {
return function value(val) {
return [...arr, val];
}
}
Or use arrow functions.
const append = arr => val => […arr, val]
This is a function that returns a function. When a function is returned by another function the function that is returned has access to the outer functions variables and parameters. This is true for JavaScript in general where the outer scope is always reachable.
The variables and parameters are automatically stored in the heap when the function returns another function otherwise it’s stored in the call stack and disappears after the function is called. But because it’s in the heap memory if a closure is made the variable remains accessible.
Call stack and the heap.
Both the call stack and the heap are part of the RAM and are used when running a program to store data. The call stack is the code that will execute using a first-in-last-out order and as soon as an instruction is executed it’s popped off the stack.
The heap on the other side is mostly unordered data that persist. In other languages like C, you need to explicitly remove data from the heap but in JavaScript, there is a garbage collector that takes care of removing data from the heap when no longer referenced in the code.
Curring
Now we will curry up functions. This means that you create multiple nested functions with one parameter each instead of having multiple parameters in a function.
function manyParameters(a, b, c) {
return a + b + c;
}
Spicy it up with curry. This works with normal functions and shorthand functions.
function curried(a) {
return (b) => {
return (c ) => {
return a + b + c
}
}
}
This makes it more flexible as you can call the function in different places before the whole curried function is executed.
const IHaveArgument1 = curried(2)
const IHaveArgument2 = IHaveArgument1(3)
console.log(IHaveArgument2(4)) // expect 9
You can also create a compose function or a pipe function. The only difference is that compose functions read from left to right while pipe functions read from left to right, so I preferred pipes.
function pipe(...fns) {
return function (input) {
return fns.reduce((acc, fn) => fn(acc), input);
};
}
// Example usage:
function add2(x) {
return x + 2;
}
function double(x) {
return x * 2;
}
function subtract5(x) {
return x - 5;
}
const transform = pipe(add2, double, subtract5);
console.log(transform(10)); // Output: 17
You see now you can create multiple functions and an initial value and get a final calculated value.
Let’s reverse and capitalize a string using our pipe function.
function reverseString(str) {
return str.split('').reverse().join('');
}
function capitalizeString(str) {
return str.toUpperCase();
}
const reverseAndCapitalize = pipe(reverseString, capitalizeString);
const inputString = 'hello world';
const result = reverseAndCapitalize(inputString);
console.log(result); // Output: 'DLROW OLLEH'
Object Oriented programming
In JavaScript, you can also take an OOP approach, but it’s fundamentally different from Java or C#.
Create an object with curly brackets
const myObj = {};
Or with the new operator
const myObj = new Object();
Objects can have properties and methods.
const person = {
name: 'John',
age: 30,
greet: function() {
console.log(`Hello, my name is ${this.name} and I am
${this.age} years old.`);
}
};
Functions are also objects in JavaScript so this is also valid. functions can have properties and methods.
function Person(name, age) {
this.name = name;
this.age = age;
// Method to greet the person
this.greet = function() {
return `Hello, my name is ${this.name} and I am
${this.age} years old.`;
};
}
The parameters name and age here also work as constructor parameters to initiate the object.
Prototype chain
Each object in JavaScript has a prototype. This makes it possible for an object to inherit properties and methods from other objects. All objects inherit from the global object and all functions inherit from the global function object.
Here is a demonstration of the prototype chain.
Object1.prototype.object2.prototype.object3
Here object 3 has access to the properties and methods in object 2 but also further in the chain to object 1.
Practically it looks like this.
// Constructor function for creating a Person object
function Person(name, age) {
this.name = name;
this.age = age;
}
// Creating an instance of Person
const john = new Person('John', 30);
// Object to be used as a prototype
const personPrototype = {
// Method to greet the person
greet: function() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
};
// Assigning the object as the prototype of the Person constructor
Person.prototype = personPrototype;
// Calling the method
console.log(john.greet()); // Output: Hello, my name is John and I am 30 years old.
This works for objects and functions of course.
Since ES6 you can use classes and they are built the same way under the hood.
class Dog {
constructor(color, breed) {
this.color = color,
this.breed = breed
}
bark() {
console.log(“woof”)
}
walk () {
console.log(“walking”)
}
}
class Wolf extends Dog {
constructor(color) {
super(color)
}
walk() {
super.walk()
}
}
We create a class dog together with a constructor. Notice that the fields for the constructor properties are defined in the constructor and do not need to be stored in a variable outside of the constructor. You can see that you can create a method by declaring the name of the method, parentheses, and the block.
In the second class, we have a class Wolf and we inherit from Dog. By using super we can call methods from the parent, here we call the parent class constructor and the walk method.
You can also make fields and methods private by utilizing the # symbol.
class Example {
#privateField = 42;
getPrivateField() {
return this.#privateField; // Private field can be accessed from within the class
}
}
class Example {
#privateMethod() {
// Private method implementation
}
publicMethod() {
this.#privateMethod(); // Private method can be called from within the class
}
}
If you want to get the most out of OOP I suggest you use TypeScript, a superset of JavaScript that provides further functionality like protected and virtual fields and methods and abstract classes, etc.
What paradigm should I use?
All of the paradigms have their pros and cons and you can also use all three.
Procedural programming is more simple and often more efficient memory and speed-wise.
Functional programming is expressive and easy to read and test.
Object-oriented programming is easy to maintain and reuse because of encapsulation and inheritance which helps for scalability.
Concurrency in a single-threaded language
What is concurrency?
Concurrency is the concept of computers multitasking. Doing more than one set of instructions seamlessly. This does not necessarily mean that the instructions are handled at the exact same time but can switch between different tasks to make it feel like it is happening at the same time.
Imagine a kitchen in a restaurant. The kitchen gets a lot of orders of food to prepare. If you have two chefs, they can do two tasks at the same time. They work in parallel. However, one chef can still perform different tasks at the same time. He can put something in the oven and while it’s getting ready the chef can cut vegetables.
The CPU is the chef.
CPU stands for the central processing unit. It works as the computer's brain and it executes instructions. So when you create a program it is the CPU that will handle the logic, the set of instructions.
Cores and Parallelism
The CPU has cores and the cores can handle tasks or sets of instructions independently. Your computer’s CPU can have multiple cores but let’s say your computer has 2 cores. This means your computer can handle 2 sets of instructions at the exact same time. This is called parallelism. This is like having two chefs.
But in an application, there are a lot of sets of instructions to handle. This is when threads come into the picture.
Threads
Threads are tasks or a set of operations that need to be done. This could be, handling user inputs, fetching data from an API, or updating the DOM.
As I wrote before two cores can do one thing each in parallel but one core can handle multiple threads. The CPU can switch between different threads. The CPU can call an API and while waiting for the response execute another instruction.
This is like the chef putting something in the oven and then starting another task instead of waiting for the food in the oven to be ready.
Asynchronous programming
This means that you seamlessly handle multiple sets of instructions but you don’t do it in parallel but rather like the chef who puts something in the oven and then does another task while waiting for the food in the oven to be ready.
Callbacks
Before we get into the event loop, it is important to know what a callback is.
A callback is a function, that is passed in as an argument of another function.
function fnWithCallback (callback) {
// other logic
callback(...arguments)
return
}
The callback function can be invoked inside the function.
Event Loop in JavaScript
JavaScript with its only thread together with an event queue teaming up with the browser or the runtime environment makes it a powerful event-driven non-blocking language that can handle I/O efficiently.
JavaScript executes instructions in two main places: the call stack and the event queue.
Call Stack: JavaScript prioritizes tasks in the call stack, executing synchronous code first. Synchronous code means the code that runs line by line, waiting for each line to finish before moving on to the next.
Event Queue: Asynchronous tasks like network requests or waiting for user interactions are added to the event queue. These tasks don't block the execution of other code. Instead, the browser or the runtime environment handles them separately.
When the call stack is clear and there's nothing else to execute, JavaScript checks the event queue. If tasks are waiting, it takes them one by one and executes them. This happens in the background while JavaScript continues to handle other tasks.
Once an asynchronous task is finished, its callback function is called. JavaScript moves this callback from the event queue to the call stack and executes it.
This way, JavaScript can handle multiple tasks simultaneously without blocking the execution of other code, making it non-blocking and efficient.
Think about it, a lot of the code you write in JavaScript is event-driven with a callback. User interactions, network requests. In Node.js you also work with callbacks when doing network requests or working with the file system.
Here is an example:
console.log('Start');
// Asynchronous operation 1: Fetch data from an API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log('Data fetched:', data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
// Asynchronous operation 2: Event listener for DOMContentLoaded.
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM Content Loaded');
});
console.log('End');
Output:
Start
End
DOM Content Loaded
Data fetched: { /* fetched data */ }
So JavaScript is built to be an event-driven non-blocking language and great for handling asynchronous tasks and event-driven operations.
Block scope vs function scope
Now that you understand the event loop and asynchronous programming I want to show you a typical example of why var is not recommended to use and why block scope is safer. Const and let are blocked scoped while var is function scoped when declaring a variable.
See this example:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
I use var in the loop. If I would use let instead I would get the result 0, 1, 2, 3, 4 in the console after x seconds. This is because let is blocked scope. Every iteration where the number is updated is encapsulated into its scope.
But what happens now when we use var. Var is function scoped so in the example above will lead to that the variable i is updated 5 times in the same scope. The output will be 5, 5, 5, 5, 5.
Let’s dig deeper. We have two sets of instructions.
A synchronous operation to update the variable i five times through the loop. And an asynchronous operation with setTimeout and a callback to log out i after i seconds.
JavaScript will run the synchronous code first updating i 5 times. Now the variable i is 5 and stored in the heap. Now JavaScript starts to pop off the sets of operations from the queue. Once the asynchronous operation is done and the callback is pushed to the stack for execution the variable i is referenced from the heap and is already 5. That is why 5 is logged out five times.
This is because the var keyword makes the variable scoped to the function and not the block scope.
Summary
I hope that this post will help you get a deeper understanding of JavaScript and help you code and debug more smoothly. You have learned about how Javascript is interpreted and dynamically typed. How you can work with different paradigms, and how concurrency and the event loop work in JavaScript.
Happy coding!
Top comments (0)