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)