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
- Literal Expressions: Direct values like
42,"Hello", ortrue - Variable Expressions: References to variables such as
xoruserName - Arithmetic Expressions: Mathematical operations like
x + yorprice * quantity - String Expressions: String concatenation like
"Hello, " + name - Logical Expressions: Boolean operations like
age >= 18 && hasID - Assignment Expressions: Variable assignments like
x = 5(which evaluates to5) - Function Call Expressions: Function invocations like
Math.max(a, b) - Object/Array Expressions: Creating objects/arrays like
{ name: "Alice" }or[1, 2, 3]
Understanding how these expressions are evaluated, especially when combined, requires knowledge of operator precedence and associativity.
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-to-right (left-associative): Operators are evaluated from left to right
- Right-to-left (right-associative): Operators are evaluated from right to left
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")
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
- Use parentheses when mixing different operators to make your code more readable
- Don't rely on others (or yourself in the future) to remember operator precedence
- Be especially careful with complex logical expressions and assignments
- When in doubt, add parentheses to clarify your intent
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
- Expressions are code fragments that produce values and are the building blocks of JavaScript statements
- Operator precedence determines the order in which operations are performed in complex expressions
- Associativity (left-to-right or right-to-left) resolves the evaluation order when operators have the same precedence
- Parentheses
()have the highest precedence and can be used to override the default evaluation order - Expressions can have side effects (modifying variables, calling functions with side effects) in addition to producing values
- Common precedence pitfalls include logical operator combinations, assignment vs. equality, and increment/decrement in expressions
- Best practices include using parentheses for clarity, breaking complex expressions into variables, avoiding side effects in complex expressions, and using functions for readability
- Refactoring complex expressions into simpler components makes code more maintainable and less error-prone