Table of Contents
- JavaScript History and Evolution.
- Core Syntax and Data Types.
- Variables, Functions, and Scope.
-
Understanding
this
and Closures. - Hoisting and Temporal Dead Zones.
- Primitive vs Reference Types.
- What's Next.
JavaScript History and Evolution
The Birth of JavaScript (1995)
JavaScript was created by Brendan Eich at Netscape Communications in just 10 days in May 1995. Originally named "Mocha," then "LiveScript," it was finally renamed JavaScript to capitalize on Java's popularity, despite having no relation to Java.
Key Milestones
- 1995: JavaScript 1.0 released with Netscape Navigator 2.0.
- 1997: ECMAScript 1 standardized by ECMA International.
- 1999: ECMAScript 3 introduced regular expressions, try/catch.
- 2009: ECMAScript 5 added strict mode, JSON support.
- 2015: ECMAScript 6 (ES2015) brought classes, modules, arrow functions.
- 2016-Present: Annual ECMAScript releases with incremental improvements.
JavaScript Today
JavaScript has evolved from a simple scripting language to a full-stack development platform powering:
- Web browsers (client-side),
- Servers (Node.js),
- Mobile applications (React Native, Ionic),
- Desktop applications (Electron),
- IoT devices and embedded systems.
Core Syntax and Data Types
Basic Syntax Rules
// Comments
// Single line comment
/*
Multi-line comment
Can span multiple lines
*/
// Statements end with semicolons (optional but recommended)
let message = "Hello, World!";
// Case-sensitive
let userName = "Aber";
let username = "Mark"; // Different variable
// Unicode support
let π = 3.14159;
let 名前 = "JavaScript";
Data Types Overview
JavaScript has 8 data types: 7 primitive types and 1 non-primitive type.
Primitive Types
// 1. Number
let integer = 42;
let float = 3.14;
let scientific = 2.5e10; // 25,000,000,000
let infinity = Infinity;
let notANumber = NaN;
console.log(typeof 42); // "number"
// 2. String
let singleQuotes = 'Hello';
let doubleQuotes = "World";
let backticks = `Template literal with ${integer}`;
console.log(typeof "Hello"); // "string"
// 3. Boolean
let isTrue = true;
let isFalse = false;
console.log(typeof true); // "boolean"
// 4. Undefined
let undefinedVar;
let explicitUndefined = undefined;
console.log(typeof undefinedVar); // "undefined"
// 5. Null
let nullValue = null;
console.log(typeof null); // "object" (this is a known bug!)
// 6. Symbol (ES6+)
let symbol1 = Symbol('id');
let symbol2 = Symbol('id');
console.log(symbol1 === symbol2); // false (always unique)
// 7. BigInt (ES2020)
let bigInteger = 123456789012345678901234567890n;
let bigInt = BigInt(123456789012345678901234567890);
console.log(typeof bigInteger); // "bigint"
Non-Primitive Type
// Object (includes arrays, functions, dates, etc.)
let person = {
name: "Alice",
age: 30,
isStudent: false
};
let numbers = [1, 2, 3, 4, 5];
let currentDate = new Date();
console.log(typeof person); // "object"
console.log(typeof numbers); // "object"
console.log(typeof currentDate); // "object"
Input Validation and Type Conversion
Always validate user input before processing:
// Safe number validation
function validateNumber(input) {
if (input === null || input === undefined || input === "") {
throw new Error("Input cannot be empty");
}
const number = Number(input);
if (isNaN(number)) {
throw new Error("Input must be a valid number");
}
return number;
}
// Usage example
try {
const userInput = "42";
const validNumber = validateNumber(userInput);
console.log("Valid number:", validNumber);
} catch (error) {
console.error("Validation error:", error.message);
}
// Type conversion examples
let result1 = "5" + 3; // "53" (string concatenation)
let result2 = "5" - 3; // 2 (numeric subtraction)
let result3 = "5" * "2"; // 10 (numeric multiplication)
// Explicit conversion (preferred)
let stringToNumber = Number("42"); // 42
let numberToString = String(42); // "42"
let booleanValue = Boolean(""); // false (empty string is falsy)
// Checking for NaN (use Number.isNaN for accuracy)
console.log(isNaN("hello")); // true (converts to number first)
console.log(Number.isNaN(NaN)); // true (preferred method)
Variables, Functions, and Scope
Variable Declarations: Modern Best Practices
Important: In modern JavaScript (ES6+), avoid var
in favor of let
and const
// const (preferred when value won't change)
const PI = 3.14159;
const users = []; // Array can still be modified, reference can't
// let (when value needs to change)
let counter = 0;
let userName = "Guest";
// var (legacy - avoid in modern code)
// var has function scope and hoisting issues
When to use each:
-
const
: Default choice - use for values that won't be reassigned -
let
: Use when you need to reassign the variable -
var
: Avoid unless working with legacy code
// Multiple declarations
let a = 1, b = 2, c = 3;
// Destructuring assignment (modern approach)
let [first, second] = [10, 20];
let {name, age} = {name: "Alice", age: 25};
// Array destructuring with rest
let [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
Function Declarations and Expressions
// Function Declaration (hoisted - available before declaration)
function greet(name) {
return `Hello, ${name}!`;
}
// Function Expression (not hoisted)
const farewell = function(name) {
return `Goodbye, ${name}!`;
};
// Arrow Function (ES6+ - modern and concise)
const multiply = (a, b) => a * b;
// Arrow function with single parameter (parentheses optional)
const square = x => x * x;
// Arrow function with block body
const processUser = user => {
const processed = {
...user,
processed: true,
timestamp: Date.now()
};
return processed;
};
// Function with default parameters and validation
function createUser(name = "Anonymous", role = "user") {
if (typeof name !== 'string' || name.trim() === '') {
throw new Error("Name must be a non-empty string");
}
return { name: name.trim(), role };
}
// Rest parameters (gather arguments into array)
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum()); // 0 (handles empty case)
Scope Chain and Lexical Scoping
Understanding scope is important for avoiding bugs:
let globalVar = "I'm global";
function outerFunction(x) {
let outerVar = "I'm in outer function";
function innerFunction(y) {
let innerVar = "I'm in inner function";
// Scope chain: inner → outer → global
console.log(innerVar); // ✓ Available
console.log(outerVar); // ✓ Available (lexical scoping)
console.log(globalVar); // ✓ Available
console.log(x); // ✓ Available (parameter)
console.log(y); // ✓ Available (parameter)
}
return innerFunction;
}
const myFunction = outerFunction("outer param");
myFunction("inner param");
Block Scope vs Function Scope
Visual Representation of Scope:
Global Scope
├── function outerFunction() {
│ ├── Function Scope (can access global)
│ ├── if (condition) {
│ │ └── Block Scope (can access function + global)
│ │ }
│ └── for (let i = 0; i < 5; i++) {
│ └── Block Scope (new 'i' each iteration)
│ }
│ }
// Function scope (var) - legacy behavior
function functionScopeExample() {
console.log(functionScoped); // undefined (hoisted but not assigned)
if (true) {
var functionScoped = "I'm function scoped";
}
console.log(functionScoped); // "I'm function scoped" ✓ Available
}
// Block scope (let/const) - modern behavior
function blockScopeExample() {
// console.log(blockScoped); // ReferenceError: Cannot access before initialization
if (true) {
let blockScoped = "I'm block scoped";
const alsoBlockScoped = "Me too";
console.log(blockScoped); // ✓ Works here
}
// console.log(blockScoped); // ❌ ReferenceError: not defined
}
// Loop scope differences (common interview question)
console.log("=== Loop Scope Demo ===");
// Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 100); // Prints: 3, 3, 3
}
// Solution with let
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 100); // Prints: 0, 1, 2
}
Exercise: Scope Challenge (share your answer in the comment section below)
Try to predict the output before running:
let x = 1;
function scopeTest() {
console.log("1:", x); // What will this print?
if (true) {
let x = 2;
console.log("2:", x); // What will this print?
function inner() {
let x = 3;
console.log("3:", x); // What will this print?
}
inner();
console.log("4:", x); // What will this print?
}
console.log("5:", x); // What will this print?
}
scopeTest();
console.log("6:", x); // What will this print?
Understanding this
and Closures
The this
Keyword
The value of this
is determined by how a function is called, not where it's defined:
// Global context
console.log(this); // Window object (browser) or global object (Node.js)
// Object method
const person = {
name: "Alex",
age: 30,
// Regular function - 'this' refers to the object
greet: function() {
console.log(`Hello, I'm ${this.name}`); // "Hello, I'm Alex"
},
// Arrow function - 'this' comes from enclosing scope
greetArrow: () => {
console.log(`Hello, I'm ${this.name}`); // "Hello, I'm undefined"
},
// Method that returns a function
getIntroducer: function() {
// Arrow function preserves 'this' from enclosing method
return () => console.log(`Hi, I'm ${this.name}`);
}
};
person.greet(); // "Hello, I'm Alex"
person.greetArrow(); // "Hello, I'm undefined"
const introducer = person.getIntroducer();
introducer(); // "Hi, I'm Alex" (arrow function preserved 'this')
this
in Different Contexts
// 1. Function call (standalone)
function showThis() {
console.log(this); // undefined (strict mode) or Window (non-strict)
}
// 2. Constructor function
function Person(name) {
this.name = name; // 'this' refers to new object being created
this.introduce = function() {
console.log(`Hi, I'm ${this.name}`);
};
}
const alice = new Person("Alice");
alice.introduce(); // "Hi, I'm Alice"
// 3. Method call
const user = {
name: "Bob",
greet: function() { console.log(this.name); }
};
user.greet(); // "Bob"
// 4. Lost context (common pitfall)
const greetFunction = user.greet;
greetFunction(); // undefined (context lost)
// 5. Explicit binding with call, apply, bind
const mary = { name: "Mary" };
alice.introduce.call(mary); // "Hi, I'm Mary"
alice.introduce.apply(mary); // "Hi, I'm Mary" (same as call for this example)
const boundIntroduce = alice.introduce.bind(mary);
boundIntroduce(); // "Hi, I'm Mary"
Closures: Functions that "Remember"
A closure is formed when an inner function accesses variables from its outer function's scope:
// Basic closure example
function createCounter() {
let count = 0; // This variable is "closed over"
return function() {
return ++count; // Inner function remembers 'count'
};
}
const counter1 = createCounter();
const counter2 = createCounter(); // Independent closure
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (independent state)
console.log(counter1()); // 3
// Practical closure: Module pattern
const calculator = (function() {
let result = 0; // Private variable
return {
add(x) {
result += x;
return this; // Enable method chaining
},
multiply(x) {
result *= x;
return this;
},
getValue() {
return result;
},
reset() {
result = 0;
return this;
}
};
})(); // IIFE (Immediately Invoked Function Expression)
// Method chaining example
const finalResult = calculator
.add(5)
.multiply(3)
.add(2)
.getValue(); // 17
console.log("Final result:", finalResult);
// Function factory using closures
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
Advanced Closure Example: Private Variables
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
let transactionHistory = []; // Private array
// Validate amount helper (private function)
function validateAmount(amount) {
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('Amount must be a positive number');
}
}
// Return public API
return {
deposit(amount) {
validateAmount(amount);
balance += amount;
transactionHistory.push({
type: 'deposit',
amount,
balance,
date: new Date()
});
return balance;
},
withdraw(amount) {
validateAmount(amount);
if (amount > balance) {
throw new Error('Insufficient funds');
}
balance -= amount;
transactionHistory.push({
type: 'withdraw',
amount,
balance,
date: new Date()
});
return balance;
},
getBalance() {
return balance;
},
getHistory() {
// Return copy to prevent external modification
return [...transactionHistory];
}
};
}
// Usage
const myAccount = createBankAccount(1000);
console.log(myAccount.deposit(500)); // 1500
console.log(myAccount.withdraw(200)); // 1300
console.log(myAccount.getBalance()); // 1300
// balance and transactionHistory are not accessible from outside
// console.log(myAccount.balance); // undefined
Hoisting and Temporal Dead Zones
Variable Hoisting Explained
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during compilation:
// What you write:
console.log(hoistedVar); // undefined (not error!)
var hoistedVar = "I'm hoisted";
console.log(hoistedVar); // "I'm hoisted"
// What JavaScript effectively does:
var hoistedVar; // Declaration hoisted to top
console.log(hoistedVar); // undefined
hoistedVar = "I'm hoisted"; // Assignment stays in place
console.log(hoistedVar); // "I'm hoisted"
let and const: Temporal Dead Zone
function temporalDeadZoneDemo() {
console.log("Function starts");
// Temporal Dead Zone starts here for 'myLet' and 'myConst'
// These would throw ReferenceError:
// console.log(myLet);
// console.log(myConst);
// console.log(typeof myLet); // ReferenceError!
let myLet = "Now I'm alive";
const myConst = "Me too";
// TDZ ends here - variables are now accessible
console.log(myLet); // "Now I'm alive"
console.log(myConst); // "Me too"
}
// Visual representation:
/*
function scope {
// ↓ TDZ starts
// myLet and myConst exist but are uninitialized
// Any access throws ReferenceError
// ↓ TDZ ends
let myLet = "value";
const myConst = "value";
// Variables are now accessible
}
*/
Function Hoisting
// Function declarations are fully hoisted
console.log(hoistedFunction()); // "I work!" (called before declaration)
function hoistedFunction() {
return "I work!";
}
// Function expressions are NOT hoisted
console.log(typeof notHoisted); // "undefined"
// console.log(notHoisted()); // TypeError: notHoisted is not a function
var notHoisted = function() {
return "I don't work before declaration";
};
// let/const function expressions
// console.log(arrowFunc); // ReferenceError: Cannot access before initialization
const arrowFunc = () => "Arrow functions aren't hoisted";
// Class declarations are also in TDZ
// const instance = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = "MyClass";
}
}
Hoisting Quiz
Try to predict the output:
var x = 1;
function hoistingQuiz() {
console.log("x is:", x); // What will this print?
if (false) {
var x = 2; // This never executes, but var is still hoisted!
}
console.log("x is:", x); // What will this print?
}
hoistingQuiz();
// Answer: "x is: undefined", "x is: undefined"
// The var declaration is hoisted, shadowing the global x
Primitive vs Reference Types
Understanding the difference between primitive and reference types is crucial for avoiding bugs:
Primitive Types (Pass by Value)
// Primitives are copied when assigned
let a = 5;
let b = a; // b gets a COPY of a's value
a = 10;
console.log("a:", a); // 10
console.log("b:", b); // 5 (unchanged - independent copy)
// Function parameters with primitives
function modifyPrimitive(x) {
x = 100; // Only modifies the local copy
console.log("Inside function:", x); // 100
}
let num = 50;
modifyPrimitive(num);
console.log("Outside function:", num); // 50 (original unchanged)
// String immutability
let str = "Hello";
let modifiedStr = str.toUpperCase(); // Returns new string
console.log("Original:", str); // "Hello" (unchanged)
console.log("Modified:", modifiedStr); // "HELLO"
Reference Types (Pass by Reference)
// Objects are referenced, not copied
let obj1 = { name: "Alice", age: 30 };
let obj2 = obj1; // obj2 points to the SAME object
obj1.age = 31;
console.log("obj1:", obj1.age); // 31
console.log("obj2:", obj2.age); // 31 (both changed - same reference)
// Arrays are also reference types
let arr1 = [1, 2, 3];
let arr2 = arr1; // Same reference
arr1.push(4);
console.log("arr1:", arr1); // [1, 2, 3, 4]
console.log("arr2:", arr2); // [1, 2, 3, 4] (both changed)
// Function parameters with objects
function modifyObject(obj) {
obj.modified = true; // Modifies the original object
obj.count = 42;
}
let myObj = { name: "Test" };
modifyObject(myObj);
console.log("After function:", myObj);
// { name: "Test", modified: true, count: 42 }
Object Copying: Shallow vs Deep
let original = {
name: "Alice",
age: 30,
address: {
city: "New York",
country: "USA"
},
hobbies: ["reading", "coding"]
};
// 1. Shallow copy methods
let shallowCopy1 = { ...original }; // Spread operator
let shallowCopy2 = Object.assign({}, original); // Object.assign
// 2. Modifying nested objects affects shallow copies
original.address.city = "Los Angeles";
original.hobbies.push("gaming");
console.log("Original city:", original.address.city); // "Los Angeles"
console.log("Shallow copy city:", shallowCopy1.address.city); // "Los Angeles" ❌
console.log("Original hobbies:", original.hobbies); // ["reading", "coding", "gaming"]
console.log("Shallow copy hobbies:", shallowCopy1.hobbies); // ["reading", "coding", "gaming"] ❌
// 3. Deep copy (simple objects only - no functions, dates, etc.)
let deepCopy = JSON.parse(JSON.stringify(original));
original.address.city = "Chicago";
console.log("Original city:", original.address.city); // "Chicago"
console.log("Deep copy city:", deepCopy.address.city); // "Los Angeles" ✓
// 4. Better deep copy function (handles more cases)
function deepCopyObject(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepCopyObject(item));
}
const copiedObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copiedObj[key] = deepCopyObject(obj[key]);
}
}
return copiedObj;
}
Comparing Objects and Arrays
// Equality with references
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
let obj3 = obj1;
console.log(obj1 === obj2); // false (different objects)
console.log(obj1 === obj3); // true (same reference)
// Arrays comparison
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false (different arrays)
// Comparing array contents
function arraysEqual(a, b) {
return a.length === b.length && a.every((val, i) => val === b[i]);
}
console.log(arraysEqual(arr1, arr2)); // true (same contents)
What's Next
Congratulations! You have covered the essential fundamentals of JavaScript.
In Part 2 of this first series, we will explore:
- Hands-On Project: Build a comprehensive Personal Finance Calculator
- Common Pitfalls: Type coercion traps, context loss, and async callback issues
- Best Practices: Modern coding patterns and error handling
These fundamentals you've learned form the foundation for everything else in JavaScript. Make sure you are comfortable with:
Variable scoping and the difference between
var
,let
, andconst
.How
this
works in different contexts.Closures and lexical scoping.
The difference between primitive and reference types.
Hoisting and temporal dead zones.
Practice Exercise: Before moving to Part 2, try building a simple calculator using closures to maintain state, demonstrate different function types, and handle various data types properly.
Next: JavaScript Fundamentals Part 2: Projects, Pitfalls & Best Practices
Again, I hope by the end of this series, you would have gathered enough knowledge to build a killer project.
Drop a comment if you are ready for it, like and don't forget to follow me.
Top comments (0)