Operator Precedence and Expressions

Understanding how JavaScript evaluates complex expressions

Introduction to Expressions

Expressions are JavaScript code fragments that produce a value. They are the building blocks of JavaScript statements and can be as simple as a single variable or as complex as a multi-operator calculation.

Types of Expressions

Understanding how these expressions are evaluated, especially when combined, requires knowledge of operator precedence and associativity.

graph LR A[Expressions] A --> B[Literals] A --> C[Variables] A --> D[Arithmetic] A --> E[String] A --> F[Logical] A --> G[Assignment] A --> H[Function Call] A --> I[Object/Array] D --> D1["1 + 2 * 3"] E --> E1["'Hello' + ' ' + name"] F --> F1["a && b || c"] G --> G1["x = y = z = 0"] H --> H1["Math.max(a, b, c)"] I --> I1["{key: value}"] I --> I2["[1, 2, 3]"]

Analogy: Expressions as Recipe Ingredients

Think of expressions like ingredients in a recipe. Just as a chef follows a specific order when combining ingredients (e.g., mixing dry ingredients before adding liquid), JavaScript follows specific rules (operator precedence) when evaluating expressions.

Simple ingredients (like salt or sugar) are like literal values or variables. More complex ingredients (like a sauce or marinade) are like compound expressions that are made up of multiple simpler ingredients combined in a specific way.

Operator Precedence

Operator precedence determines the order in which operators are evaluated in an expression. Operators with higher precedence are evaluated first.

Precedence Table

Here's a simplified precedence table, from highest precedence (evaluated first) to lowest:

Precedence Operator Type Associativity Example
19 Grouping n/a (expression)
18 Member access left-to-right object.property, array[index]
17 Function call left-to-right func(args)
16 New with arguments n/a new Constructor(args)
15 Postfix increment/decrement n/a x++, x--
14 Logical NOT, Unary operators right-to-left !x, +x, -x, ++x, --x, typeof, void, delete
13 Exponentiation right-to-left x ** y
12 Multiplication, Division, Remainder left-to-right x * y, x / y, x % y
11 Addition, Subtraction left-to-right x + y, x - y
10 Bitwise shift left-to-right x << y, x >> y, x >>> y
9 Relational left-to-right x < y, x <= y, x > y, x >= y, instanceof, in
8 Equality left-to-right x == y, x != y, x === y, x !== y
7 Bitwise AND left-to-right x & y
6 Bitwise XOR left-to-right x ^ y
5 Bitwise OR left-to-right x | y
4 Logical AND left-to-right x && y
3 Logical OR left-to-right x || y
3 Nullish coalescing left-to-right x ?? y
2 Conditional (ternary) right-to-left condition ? trueExpr : falseExpr
1 Assignment right-to-left x = y, x += y, x *= y, etc.
0 Comma left-to-right x, y

Examples of Precedence

// Addition vs. multiplication
console.log(2 + 3 * 4);       // 14 (not 20) because * has higher precedence than +
console.log((2 + 3) * 4);     // 20 with parentheses changing the order

// Comparison and logical operators
console.log(5 > 3 && 2 < 4);  // true (both comparisons are evaluated first, then &&)
console.log(5 > 3 && 2 > 4);  // false (5 > 3 is true, 2 > 4 is false, true && false is false)

// Assignment and comparison
let x = 5;
let y = 10;
console.log(x = y); // 10 (assignment happens, then the value of y is returned)
console.log(x == y); // true (after the previous assignment, x is now 10)

// Unary operators and arithmetic
console.log(-2 * 4);         // -8 (unary - applies to 2, then multiply)
console.log(-(2 * 4));       // -8 (same result, but explicit grouping)

// Multiple logical operators
console.log(true || false && false); // true (&& has higher precedence than ||)
console.log((true || false) && false); // false (with parentheses changing the order)

Real-World Example: Discount Calculation

Understanding operator precedence is crucial in financial calculations:

// E-commerce shopping cart calculations
function calculateTotal(cart, couponCode) {
  // Initialize variables
  let subtotal = 0;
  let discount = 0;
  let tax = 0;
  let shipping = 5.99;
  
  // Calculate subtotal
  for (const item of cart) {
    // Precedence: * has higher precedence than +
    subtotal += item.price * item.quantity; // Correct: multiply first, then add
  }
  
  // Apply coupon if valid
  if (couponCode === 'SAVE10') {
    // Precedence: * has higher precedence than =, and / has higher precedence than *
    discount = subtotal * 0.1; // 10% discount
  } else if (couponCode === 'SAVE20' && subtotal >= 100) {
    // Precedence: >= has higher precedence than &&, and * has higher precedence than =
    discount = subtotal * 0.2; // 20% discount for orders $100+
  }
  
  // Calculate tax (after discount)
  // Precedence: - has higher precedence than *, and * has higher precedence than =
  tax = (subtotal - discount) * 0.08; // 8% tax
  
  // Free shipping for orders over $50 after discount
  // Precedence: - has higher precedence than >=
  if (subtotal - discount >= 50) {
    shipping = 0;
  }
  
  // Calculate final total
  // Precedence matters for the entire calculation
  const total = subtotal - discount + tax + shipping;
  
  return {
    subtotal: subtotal.toFixed(2),
    discount: discount.toFixed(2),
    tax: tax.toFixed(2),
    shipping: shipping.toFixed(2),
    total: total.toFixed(2)
  };
}

// Test the function
const cart = [
  { name: 'T-shirt', price: 19.99, quantity: 2 },
  { name: 'Jeans', price: 49.99, quantity: 1 }
];

console.log(calculateTotal(cart, 'SAVE10'));
// If precedence wasn't handled correctly, the calculations would be wrong

Operator Associativity

Associativity determines the order in which operators of the same precedence are evaluated. There are two types:

Left-Associative Examples

// Subtraction (left-associative)
console.log(10 - 5 - 2);   // 3, equivalent to ((10 - 5) - 2)

// Division (left-associative)
console.log(20 / 5 / 2);   // 2, equivalent to ((20 / 5) / 2)

// Greater than (left-associative)
console.log(3 > 2 > 1);    // false! This is ((3 > 2) > 1), or (true > 1)
                          // true gets converted to 1, and 1 is not > 1

Right-Associative Examples

// Assignment (right-associative)
let a, b, c;
a = b = c = 5;    // Equivalent to: a = (b = (c = 5))
console.log(a, b, c); // 5 5 5

// Exponentiation (right-associative)
console.log(2 ** 3 ** 2);  // 512, equivalent to (2 ** (3 ** 2))
                          // 3 ** 2 = 9, then 2 ** 9 = 512
                          
// Multiple conditional operators (right-associative)
const value = 15;
const result = value < 10 ? "Small" : value < 20 ? "Medium" : "Large";
console.log(result); // "Medium", equivalent to:
// value < 10 ? "Small" : (value < 20 ? "Medium" : "Large")
graph TD A["Expression: a = b = c = 5"] B["Step 1: Evaluate c = 5"] C["Step 2: Evaluate b = (result of step 1)"] D["Step 3: Evaluate a = (result of step 2)"] A --> B B --> C C --> D

Analogy: Associativity as Assembly Line Direction

Think of associativity like the direction of an assembly line:

  • Left-associativity is like a standard assembly line that moves from left to right. Each station (operator) processes the product coming from its left and passes it to the right.
  • Right-associativity is like an assembly line running in reverse. Each station first waits for the station to its right to finish, then processes its result.

Imagine a car factory: in left-associative operations, you add the engine, then the doors, then the wheels (from left to right). In right-associative operations, you'd need to know what wheels are being used before you can choose doors, and know what doors before you can choose the engine (working from right to left).

Grouping with Parentheses

Parentheses () have the highest precedence and can be used to override the default operator precedence, making expressions more readable and ensuring they evaluate as intended.

Using Parentheses for Clarity

// Without parentheses (relies on precedence rules)
let result = 2 + 3 * 4;          // 14 (multiplication happens first)
let condition = a > b && c < d;   // && happens after comparisons
let assignment = x = y = z;       // Right-to-left association of assignments

// With parentheses (explicit order, more readable)
let explicitResult = 2 + (3 * 4);        // Same result, but clearer intent
let bettercondition = (a > b) && (c < d); // More explicit grouping
let clearAssignment = (x = (y = z));     // Shows the right-to-left nature

Changing Evaluation Order

// Changing arithmetic order
console.log(2 + 3 * 4);     // 14
console.log((2 + 3) * 4);   // 20

// Changing logical evaluation order
console.log(true || false && false);     // true (&& has higher precedence)
console.log((true || false) && false);   // false (forcing || to evaluate first)

// Complex expressions made clearer
let a = 5, b = 10, c = 15;
let complex = a + b * c - a / b;            // 5 + (10 * 15) - (5 / 10) = 5 + 150 - 0.5 = 154.5
let explicitComplex = a + (b * c) - (a / b); // Same result, but clearer

Best Practices for Parentheses

Real-World Example: Form Validation

Parentheses are crucial for complex validation logic:

// Form validation function
function validateSignup(formData) {
  const errors = {};
  
  // Validate username (required, 3-20 characters, alphanumeric)
  if (!formData.username || 
      !(formData.username.length >= 3 && formData.username.length <= 20) || 
      !/^[a-zA-Z0-9_]+$/.test(formData.username)) {
    errors.username = "Username must be 3-20 characters and contain only letters, numbers, and underscores";
  }
  
  // Validate password (required, 8+ characters, has uppercase, lowercase, number)
  if (!formData.password || 
      !(formData.password.length >= 8) || 
      !(/[A-Z]/.test(formData.password) && 
        /[a-z]/.test(formData.password) && 
        /[0-9]/.test(formData.password))) {
    errors.password = "Password must be at least 8 characters with uppercase, lowercase, and number";
  }
  
  // Validate email (required, valid format)
  if (!formData.email || !/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(formData.email)) {
    errors.email = "Please enter a valid email address";
  }
  
  // Validate age (optional, but must be 13+ if provided)
  if (formData.age !== undefined && formData.age !== "" && 
      (isNaN(Number(formData.age)) || Number(formData.age) < 13)) {
    errors.age = "Age must be 13 or older";
  }
  
  // Validate terms acceptance (required)
  if (!formData.agreeToTerms) {
    errors.agreeToTerms = "You must agree to the terms and conditions";
  }
  
  // Return validation result
  return {
    isValid: Object.keys(errors).length === 0,
    errors
  };
}

// Test the validation
const formData = {
  username: "user123",
  password: "Password123",
  email: "user@example.com",
  age: "25",
  agreeToTerms: true
};

console.log(validateSignup(formData));

Expression Evaluation and Side Effects

When JavaScript evaluates expressions, it doesn't just produce values - it can also produce side effects like modifying variables, calling functions, or altering objects.

Assignment Expressions and Side Effects

// Assignment as an expression
let a = 5;
let b = (a = 10); // Assignment inside an expression
console.log(a);    // 10 (side effect: a was modified)
console.log(b);    // 10 (expression value)

// Compound assignment
let counter = 0;
let value = (counter += 5); // Compound assignment in expression
console.log(counter); // 5 (side effect: counter was modified)
console.log(value);   // 5 (expression value)

// Multiple assignments in expression
let x, y, z;
let result = (x = y = z = 42);
console.log(x, y, z); // 42 42 42 (side effect: all variables were modified)
console.log(result);  // 42 (expression value)

Increment/Decrement Side Effects

// Increment/decrement operators have side effects
let count = 1;

// Postfix
let afterValue = count++; // First return count (1), then increment
console.log(count);      // 2 (side effect: count was incremented)
console.log(afterValue); // 1 (expression value before increment)

// Prefix
count = 1;              // Reset count
let beforeValue = ++count; // First increment, then return new value
console.log(count);        // 2 (side effect: count was incremented)
console.log(beforeValue);  // 2 (expression value after increment)

Function Call Side Effects

// Function calls in expressions
let message = "";

function logMessage(msg) {
  console.log(msg);     // Side effect: console output
  message = msg;        // Side effect: modifies message variable
  return msg.length;    // Return value
}

// Function call in an expression
let length = logMessage("Hello, world!"); // Logs "Hello, world!"
console.log(message);  // "Hello, world!" (side effect)
console.log(length);   // 13 (expression value)

// Multiple function calls in expression
function increment() {
  counter++; // Side effect
  return counter;
}

let counter = 0;
let sum = increment() + increment() + increment();
console.log(counter); // 3 (side effect)
console.log(sum);     // 1 + 2 + 3 = 6 (expression value)

Real-World Example: Event Handling and State Updates

Side effects are common in event handlers and state updates:

// Simplified shopping cart management
class ShoppingCart {
  constructor() {
    this.items = [];
    this.total = 0;
    this.count = 0;
    this.listeners = [];
  }
  
  // Add an event listener
  onUpdate(callback) {
    this.listeners.push(callback);
    return this.listeners.length; // Return index for potential removal
  }
  
  // Notify listeners of changes
  notify() {
    for (const listener of this.listeners) {
      listener({
        total: this.total,
        count: this.count,
        items: [...this.items]
      });
    }
  }
  
  // Add item to cart
  addItem(product, quantity = 1) {
    // Find if item already exists
    const existingItem = this.items.find(item => item.id === product.id);
    
    if (existingItem) {
      // Update existing item
      existingItem.quantity += quantity;
    } else {
      // Add new item
      this.items.push({
        ...product,
        quantity
      });
    }
    
    // Update cart totals (side effects)
    this.count += quantity;
    this.total += product.price * quantity;
    
    // Notify listeners (side effect)
    this.notify();
    
    // Return the cart for chaining
    return this;
  }
  
  // Remove item from cart
  removeItem(productId, quantity = 1) {
    const index = this.items.findIndex(item => item.id === productId);
    
    if (index === -1) {
      return this; // Item not found
    }
    
    const item = this.items[index];
    
    // If removing less than the quantity, just update
    if (quantity < item.quantity) {
      // Update item (side effect)
      item.quantity -= quantity;
      
      // Update cart totals (side effects)
      this.count -= quantity;
      this.total -= item.price * quantity;
    } else {
      // Remove the entire item (side effect)
      this.items.splice(index, 1);
      
      // Update cart totals (side effects)
      this.count -= item.quantity;
      this.total -= item.price * item.quantity;
    }
    
    // Notify listeners (side effect)
    this.notify();
    
    // Return the cart for chaining
    return this;
  }
  
  // Clear the cart
  clear() {
    // Reset everything (side effects)
    this.items = [];
    this.total = 0;
    this.count = 0;
    
    // Notify listeners (side effect)
    this.notify();
    
    // Return the cart for chaining
    return this;
  }
}

// Usage in a UI
const cart = new ShoppingCart();

// Add display elements
const totalElement = { textContent: "" };
const countElement = { textContent: "" };
const itemsElement = { innerHTML: "" };

// Set up event listener for cart updates
cart.onUpdate(cartState => {
  // Update UI (side effects)
  totalElement.textContent = `$${cartState.total.toFixed(2)}`;
  countElement.textContent = cartState.count;
  
  // Update items list (side effect)
  itemsElement.innerHTML = cartState.items.map(item => `
    
  • ${item.name} x ${item.quantity} - $${(item.price * item.quantity).toFixed(2)}
  • `).join(''); // Log for debugging (side effect) console.log('Cart updated:', cartState); }); // Add items to cart (expressions with side effects) const addToCartButton = (productId) => { // Find product (simplified) const product = { id: productId, name: `Product ${productId}`, price: 9.99 + productId // Just for demo }; // Add to cart (expression with side effects) cart.addItem(product); }; // Test with some products addToCartButton(1); addToCartButton(2); addToCartButton(1); // Add another of product 1

    Analogy: Side Effects as Ripples in a Pond

    Think of expressions with side effects like throwing stones in a pond. The primary purpose might be to see the splash (the expression value), but the ripples that spread outward are the side effects—changes to the surrounding environment that continue after the initial action.

    Just as ripples can affect distant shores in ways you might not anticipate, side effects in expressions can propagate through your program and affect parts you didn't expect to change.

    Operator Precedence Pitfalls

    Even experienced developers can get tripped up by operator precedence. Here are some common pitfalls to avoid:

    Logical Operator Precedence

    // Pitfall: && has higher precedence than ||
    // This condition looks for a definedValue OR (nullCheck AND fallback)
    const result = definedValue || nullCheck && fallback;
    
    // It might be clearer with parentheses
    const clearer = definedValue || (nullCheck && fallback);
    
    // But maybe you actually meant
    const different = (definedValue || nullCheck) && fallback;
    
    // Example with real values
    let name = "";
    let defaultName = "Guest";
    let allowEmpty = false;
    
    // This might not do what you expect
    let displayName = name || allowEmpty && defaultName;
    console.log(displayName); // "" (empty string, not "Guest")
    
    // Because it evaluates as:
    displayName = name || (allowEmpty && defaultName);
    // allowEmpty is false, so (allowEmpty && defaultName) is false
    // name || false gives "" (empty string)
    
    // Maybe you wanted:
    displayName = (name || allowEmpty) && defaultName;
    // This would always display defaultName if allowEmpty is false

    Assignment vs. Equality

    // Pitfall: Using = (assignment) instead of === (comparison)
    let x = 5;
    
    // This is an assignment, not a comparison
    if (x = 10) {
      console.log("This will always run because x = 10 returns 10, which is truthy");
    }
    
    console.log(x); // 10 (x has been changed)
    
    // What you probably meant was:
    x = 5; // Reset x
    if (x === 10) {
      console.log("This won't run because x is 5, not 10");
    }

    Ternary Operator Nesting

    // Pitfall: Nested ternary operators can be confusing
    const value = 75;
    
    // What does this do?
    const grade = value >= 90 ? "A" : value >= 80 ? "B" : value >= 70 ? "C" : value >= 60 ? "D" : "F";
    
    // It's right-associative, so equivalent to:
    const clearGrade = value >= 90 ? "A" : (value >= 80 ? "B" : (value >= 70 ? "C" : (value >= 60 ? "D" : "F")));
    
    console.log(grade); // "C" for value = 75
    
    // Better to use if/else for complex conditions
    function getGrade(value) {
      if (value >= 90) return "A";
      if (value >= 80) return "B";
      if (value >= 70) return "C";
      if (value >= 60) return "D";
      return "F";
    }

    Increment/Decrement in Expressions

    // Pitfall: Unexpected behavior with increment/decrement in expressions
    let i = 5;
    let j = 10;
    
    // What does this evaluate to?
    let result = i++ + ++j;
    
    // It's equivalent to:
    // (i++) + (++j)
    // 5 + 11 = 16
    
    console.log(result); // 16
    console.log(i);      // 6 (incremented after its value was used)
    console.log(j);      // 11 (incremented before its value was used)
    
    // Clearer code would separate these operations:
    i = 5;
    j = 10;
    j++;
    let betterResult = i + j;
    i++;
    console.log(betterResult); // 15

    Real-World Example: Bug in Condition Logic

    Precedence issues often lead to bugs in real-world code:

    // Authorization check in a web application
    function canAccessResource(user, resource) {
      // Buggy condition
      return user && user.isActive || user.isAdmin && resource.isPublic;
    }
    
    // Test cases
    const users = {
      regular: { id: 1, isActive: true, isAdmin: false },
      inactive: { id: 2, isActive: false, isAdmin: false },
      admin: { id: 3, isActive: false, isAdmin: true }
    };
    
    const resources = {
      public: { id: 101, isPublic: true },
      private: { id: 102, isPublic: false }
    };
    
    // Let's test the function
    console.log("Regular + Public:", canAccessResource(users.regular, resources.public));     // true
    console.log("Regular + Private:", canAccessResource(users.regular, resources.private));   // true (Bug!)
    console.log("Inactive + Public:", canAccessResource(users.inactive, resources.public));   // false
    console.log("Inactive + Private:", canAccessResource(users.inactive, resources.private)); // false
    console.log("Admin + Public:", canAccessResource(users.admin, resources.public));         // true
    console.log("Admin + Private:", canAccessResource(users.admin, resources.private));       // false
    
    // The function has a bug due to operator precedence
    // The current logic is equivalent to:
    function buggyLogic(user, resource) {
      return (user && user.isActive) || (user.isAdmin && resource.isPublic);
    }
    
    // What was probably intended:
    function fixedLogic(user, resource) {
      return user && (user.isActive || user.isAdmin) && (user.isAdmin || resource.isPublic);
    }
    
    // Test the fixed function
    console.log("FIXED - Regular + Public:", fixedLogic(users.regular, resources.public));     // true
    console.log("FIXED - Regular + Private:", fixedLogic(users.regular, resources.private));   // false (Fixed!)
    console.log("FIXED - Inactive + Public:", fixedLogic(users.inactive, resources.public));   // false
    console.log("FIXED - Inactive + Private:", fixedLogic(users.inactive, resources.private)); // false
    console.log("FIXED - Admin + Public:", fixedLogic(users.admin, resources.public));         // true
    console.log("FIXED - Admin + Private:", fixedLogic(users.admin, resources.private));       // true (Admins now access private resources)

    Best Practices for Complex Expressions

    When working with complex expressions, there are several best practices that can help you write more readable and maintainable code:

    Use Parentheses for Clarity

    // Without parentheses
    const result = a && b || c && d;
    
    // With parentheses for clarity
    const betterResult = (a && b) || (c && d);

    Break Complex Expressions into Variables

    // Complex expression
    const allowed = user && user.roles && (user.roles.includes('admin') || user.roles.includes('editor')) && (resource.isPublic || resource.ownerId === user.id);
    
    // Broken down into readable variables
    const userHasValidRoles = user?.roles?.includes('admin') || user?.roles?.includes('editor');
    const userCanAccessResource = resource.isPublic || resource.ownerId === user.id;
    const allowed = user && user.roles && userHasValidRoles && userCanAccessResource;

    Avoid Side Effects in Complex Expressions

    // Hard to reason about
    const result = getValue() + (counter++) * updateValue(x += 10);
    
    // Better approach
    const newX = x + 10;
    const updatedValue = updateValue(newX);
    x = newX; // Side effect separated
    
    counter++; // Side effect separated
    const result = getValue() + counter * updatedValue;

    Use Comments for Complex Logic

    // Complex validation
    const isValid = value && value.length >= minLength && 
                   (type === 'text' || (type === 'number' && !isNaN(Number(value)))) &&
                   !disallowedChars.test(value);
    
    // With explanatory comments
    const isValid = 
      value && // Must exist
      value.length >= minLength && // Must meet minimum length
      (
        type === 'text' || // Text can contain any characters
        (type === 'number' && !isNaN(Number(value))) // Numbers must be numeric
      ) &&
      !disallowedChars.test(value); // Must not contain disallowed characters

    Consider Using Functions for Readability

    // Complex calculation
    const total = subtotal - (subtotal * discount / 100) + 
                 (subtotal - (subtotal * discount / 100)) * taxRate / 100 +
                 (subtotal < freeShippingThreshold ? shippingCost : 0);
    
    // Using functions for clarity
    function calculateDiscountedPrice(price, discountPercent) {
      return price - (price * discountPercent / 100);
    }
    
    function calculateTax(price, taxRate) {
      return price * taxRate / 100;
    }
    
    function calculateShipping(subtotal, threshold, shippingCost) {
      return subtotal < threshold ? shippingCost : 0;
    }
    
    // Much clearer calculation
    const discountedPrice = calculateDiscountedPrice(subtotal, discount);
    const tax = calculateTax(discountedPrice, taxRate);
    const shipping = calculateShipping(subtotal, freeShippingThreshold, shippingCost);
    const total = discountedPrice + tax + shipping;

    Real-World Example: Query Builder

    In complex scenarios like building database queries, breaking down expressions helps significantly:

    // Query builder for a simple ORM
    class QueryBuilder {
      constructor(tableName) {
        this.table = tableName;
        this.selectFields = ['*'];
        this.whereConditions = [];
        this.orderByFields = [];
        this.limitValue = null;
        this.offsetValue = null;
        this.joinClauses = [];
      }
      
      select(fields) {
        if (fields && Array.isArray(fields) && fields.length > 0) {
          this.selectFields = fields;
        }
        return this;
      }
      
      where(field, operator, value) {
        this.whereConditions.push({ field, operator, value });
        return this;
      }
      
      andWhere(field, operator, value) {
        return this.where(field, operator, value);
      }
      
      orWhere(field, operator, value) {
        if (this.whereConditions.length === 0) {
          return this.where(field, operator, value);
        }
        
        this.whereConditions.push({ type: 'OR', field, operator, value });
        return this;
      }
      
      join(table, leftField, operator, rightField) {
        this.joinClauses.push({
          type: 'INNER',
          table,
          leftField,
          operator,
          rightField
        });
        return this;
      }
      
      leftJoin(table, leftField, operator, rightField) {
        this.joinClauses.push({
          type: 'LEFT',
          table,
          leftField,
          operator,
          rightField
        });
        return this;
      }
      
      orderBy(field, direction = 'ASC') {
        this.orderByFields.push({ field, direction });
        return this;
      }
      
      limit(value) {
        this.limitValue = value;
        return this;
      }
      
      offset(value) {
        this.offsetValue = value;
        return this;
      }
      
      buildQuery() {
        // Start with the SELECT clause
        let query = `SELECT ${this.selectFields.join(', ')} FROM ${this.table}`;
        
        // Add JOIN clauses
        if (this.joinClauses.length > 0) {
          const joinStatements = this.joinClauses.map(join => {
            return `${join.type} JOIN ${join.table} ON ${join.leftField} ${join.operator} ${join.rightField}`;
          });
          
          query += ' ' + joinStatements.join(' ');
        }
        
        // Add WHERE conditions
        if (this.whereConditions.length > 0) {
          // First condition is always preceded by WHERE
          const firstCondition = this.whereConditions[0];
          query += ` WHERE ${firstCondition.field} ${firstCondition.operator} ${this.formatValue(firstCondition.value)}`;
          
          // For subsequent conditions
          for (let i = 1; i < this.whereConditions.length; i++) {
            const condition = this.whereConditions[i];
            const connector = condition.type === 'OR' ? 'OR' : 'AND';
            query += ` ${connector} ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`;
          }
        }
        
        // Add ORDER BY clause
        if (this.orderByFields.length > 0) {
          const orderStatements = this.orderByFields.map(order => {
            return `${order.field} ${order.direction}`;
          });
          
          query += ` ORDER BY ${orderStatements.join(', ')}`;
        }
        
        // Add LIMIT and OFFSET
        if (this.limitValue !== null) {
          query += ` LIMIT ${this.limitValue}`;
          
          if (this.offsetValue !== null) {
            query += ` OFFSET ${this.offsetValue}`;
          }
        }
        
        return query;
      }
      
      formatValue(value) {
        if (value === null) {
          return 'NULL';
        } else if (typeof value === 'string') {
          return `'${value.replace(/'/g, "''")}'`;
        } else {
          return value;
        }
      }
    }
    
    // Usage example
    const query = new QueryBuilder('users')
      .select(['id', 'name', 'email'])
      .where('status', '=', 'active')
      .andWhere('created_at', '>=', '2023-01-01')
      .leftJoin('orders', 'users.id', '=', 'orders.user_id')
      .orderBy('created_at', 'DESC')
      .limit(10)
      .offset(20)
      .buildQuery();
    
    console.log(query);
    // This creates a much cleaner expression than having one complex string concatenation

    Practical Exercise

    Let's practice what we've learned with some exercises on operator precedence and expressions.

    Exercise 1: Predict the Output

    For each of the following expressions, try to predict the output before testing:

    // Expression 1
    let a = 5, b = 10, c = 15;
    let result1 = a + b * c;
    console.log(result1); // Prediction: 155 (5 + (10 * 15))
    
    // Expression 2
    let x = 20, y = 10;
    let result2 = x / y + y / x * 2;
    console.log(result2); // Prediction: 3 ((20/10) + ((10/20) * 2))
    
    // Expression 3
    let m = true, n = false, p = true;
    let result3 = m && n || p;
    console.log(result3); // Prediction: true ((true && false) || true)
    
    // Expression 4
    let count = 0;
    let result4 = count++ + ++count + count++;
    console.log(result4); // Prediction: 3 (0 + 2 + 2)
    console.log(count);   // Prediction: 3
    
    // Expression 5
    let str1 = "hello", str2 = "world";
    let result5 = !str1 || !!str2;
    console.log(result5); // Prediction: true ((!str1) is false, (!!str2) is true, false || true)

    Exercise 2: Fix the Bugs

    Identify and fix the bugs in the following code caused by operator precedence or associativity issues:

    // Bug 1: Unexpected calculation result
    function calculateDiscount(price, discountPercent, maxDiscount) {
      return price - price * discountPercent / 100 > maxDiscount ? maxDiscount : price * discountPercent / 100;
    }
    
    // Fixed version
    function calculateDiscountFixed(price, discountPercent, maxDiscount) {
      const discount = price * discountPercent / 100;
      return discount > maxDiscount ? maxDiscount : discount;
    }
    
    // Bug 2: Authentication logic flaw
    function canAccess(user, resource) {
      return user && user.isActive || user.role === 'admin' && resource.isPublic;
    }
    
    // Fixed version
    function canAccessFixed(user, resource) {
      return user && (user.isActive || user.role === 'admin') && 
             (user.role === 'admin' || resource.isPublic);
    }
    
    // Bug 3: Unexpected loop behavior
    function processItems(items) {
      let results = [];
      for (let i = 0; i < items.length; i++)
        if (items[i] > 10)
          results.push(items[i]);
          console.log("Processing item:", items[i]); // This will not work as expected!
      
      return results;
    }
    
    // Fixed version
    function processItemsFixed(items) {
      let results = [];
      for (let i = 0; i < items.length; i++) {
        if (items[i] > 10) {
          results.push(items[i]);
          console.log("Processing item:", items[i]);
        }
      }
      
      return results;
    }

    Exercise 3: Refactor Complex Expressions

    Refactor this complex pricing function to be more readable:

    // Original complex function
    function calculateOrderTotal(items, couponCode, shippingMethod, userType) {
      // This is intentionally complex to demonstrate refactoring
      let total = 0;
      
      // Add up all item prices
      for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity * 
                 (items[i].onSale ? 0.8 : 1) * 
                 (userType === 'premium' && items[i].premiumDiscount ? 0.9 : 1);
      }
      
      // Apply coupon if valid
      if (couponCode === 'SAVE10' && total > 50 || couponCode === 'SAVE20' && total > 100 || couponCode === 'FREESHIP' && shippingMethod !== 'express') {
        if (couponCode === 'SAVE10') total *= 0.9;
        else if (couponCode === 'SAVE20') total *= 0.8;
        else total = total; // No change for FREESHIP (it affects shipping)
      }
      
      // Add shipping costs
      if (shippingMethod === 'standard' && total < 100 && couponCode !== 'FREESHIP' || shippingMethod === 'express' && couponCode !== 'FREESHIP' || shippingMethod === 'nextDay') {
        if (shippingMethod === 'standard') total += 5.99;
        else if (shippingMethod === 'express') total += 12.99;
        else total += 24.99; // Next day shipping
      }
      
      // Add tax
      total += total * (userType === 'business' ? 0.05 : 0.08);
      
      return parseFloat(total.toFixed(2));
    }
    
    // Refactored version
    function calculateOrderTotalRefactored(items, couponCode, shippingMethod, userType) {
      // Calculate item subtotal
      const subtotal = calculateSubtotal(items, userType);
      
      // Apply coupon discount
      const discountedTotal = applyCouponDiscount(subtotal, couponCode);
      
      // Add shipping costs
      const totalWithShipping = addShippingCost(discountedTotal, shippingMethod, couponCode);
      
      // Add tax
      const finalTotal = addTax(totalWithShipping, userType);
      
      // Return formatted total
      return parseFloat(finalTotal.toFixed(2));
    }
    
    // Helper functions
    function calculateSubtotal(items, userType) {
      return items.reduce((total, item) => {
        // Calculate item price
        let itemPrice = item.price;
        
        // Apply sale discount if applicable
        if (item.onSale) {
          itemPrice *= 0.8; // 20% off for on-sale items
        }
        
        // Apply premium discount if applicable
        if (userType === 'premium' && item.premiumDiscount) {
          itemPrice *= 0.9; // Additional 10% off for premium users
        }
        
        // Multiply by quantity and add to total
        return total + (itemPrice * item.quantity);
      }, 0);
    }
    
    function applyCouponDiscount(total, couponCode) {
      // Check for percentage-off coupons
      if (couponCode === 'SAVE10' && total > 50) {
        return total * 0.9; // 10% off for orders over $50
      } else if (couponCode === 'SAVE20' && total > 100) {
        return total * 0.8; // 20% off for orders over $100
      }
      
      // No percentage discount
      return total;
    }
    
    function addShippingCost(total, shippingMethod, couponCode) {
      // Free shipping conditions
      const hasFreeShipping = 
        (couponCode === 'FREESHIP' && shippingMethod !== 'nextDay') || // Free shipping coupon (except next day)
        (total >= 100 && shippingMethod === 'standard'); // Free standard shipping over $100
      
      if (hasFreeShipping) {
        return total;
      }
      
      // Apply shipping cost based on method
      switch (shippingMethod) {
        case 'standard':
          return total + 5.99;
        case 'express':
          return total + 12.99;
        case 'nextDay':
          return total + 24.99;
        default:
          return total;
      }
    }
    
    function addTax(total, userType) {
      // Apply appropriate tax rate
      const taxRate = userType === 'business' ? 0.05 : 0.08; // 5% for business, 8% for others
      return total * (1 + taxRate);
    }
    
    // Test both functions with the same input
    const testItems = [
      { price: 29.99, quantity: 2, onSale: true, premiumDiscount: false },
      { price: 49.99, quantity: 1, onSale: false, premiumDiscount: true }
    ];
    
    console.log("Original function result:", calculateOrderTotal(testItems, 'SAVE10', 'standard', 'premium'));
    console.log("Refactored function result:", calculateOrderTotalRefactored(testItems, 'SAVE10', 'standard', 'premium'));
    // Both should produce the same result, but the refactored version is much easier to understand and maintain

    Key Takeaways

    Additional Resources