DEV Community

Cover image for JavaScript Fundamentals Part 1: Core Concepts & Syntax
Aber Paul
Aber Paul

Posted on

JavaScript Fundamentals Part 1: Core Concepts & Syntax

Table of Contents

  1. JavaScript History and Evolution.
  2. Core Syntax and Data Types.
  3. Variables, Functions, and Scope.
  4. Understanding this and Closures.
  5. Hoisting and Temporal Dead Zones.
  6. Primitive vs Reference Types.
  7. 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";
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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)
│       }
│   }
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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?


Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
*/
Enter fullscreen mode Exit fullscreen mode

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";
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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, and const.

  • 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)