10 JavaScript Features You Didn't Know Existed

10 JavaScript Features You Didn't Know Existed

You know that feeling when you discover a JavaScript feature that makes you think "Where has this been all my life?" Well, buckle up because I'm about to blow your mind with 10 JavaScript features that probably flew under your radar. These aren't your typical array methods or ES6 basics – these are the hidden gems that can genuinely level up your code.

1. The Nullish Coalescing Operator (??)

Forget everything you know about the logical OR operator when dealing with falsy values. The nullish coalescing operator (??) only checks for null and undefined, not other falsy values like 0, false, or empty strings.

const userSettings = {
  theme: '',
  notifications: false,
  volume: 0
};

// Old way (problematic)
const theme = userSettings.theme || 'dark';        // Returns 'dark' (wrong!)
const notifications = userSettings.notifications || true;  // Returns true (wrong!)
const volume = userSettings.volume || 50;         // Returns 50 (wrong!)

// New way (correct)
const themeNew = userSettings.theme ?? 'dark';        // Returns ''
const notificationsNew = userSettings.notifications ?? true;  // Returns false
const volumeNew = userSettings.volume ?? 50;         // Returns 0
Nullish coalescing operator preserves falsy values that aren't null or undefined

2. Optional Chaining with Dynamic Properties

You probably know about optional chaining (?.), but did you know you can use it with dynamic properties and method calls? This is a game-changer for dealing with complex nested objects.

const user = {
  profile: {
    social: {
      twitter: '@johndoe'
    }
  },
  getName: function() {
    return 'John Doe';
  }
};

// Dynamic property access
const platform = 'twitter';
const handle = user.profile?.social?.[platform]; // '@johndoe'

// Method chaining with optional calling
const name = user.getName?.(); // 'John Doe'
const invalidMethod = user.getAge?.(); // undefined (no error!)

// Complex nested optional chaining
const config = getConfig?.()?.api?.endpoints?.users?.[0]?.url;
Optional chaining works with dynamic properties, methods, and complex nested structures

3. The Intl Object: Your Internationalization Swiss Army Knife

JavaScript's built-in Intl object is criminally underused. It can format dates, numbers, currencies, and even relative time without any external libraries. Say goodbye to moment.js for basic formatting!

// Date and time formatting
const date = new Date();
const usFormat = new Intl.DateTimeFormat('en-US').format(date);
const ukFormat = new Intl.DateTimeFormat('en-GB').format(date);
const relativeTime = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

console.log(relativeTime.format(-1, 'day')); // "yesterday"
console.log(relativeTime.format(2, 'hour')); // "in 2 hours"

// Number and currency formatting
const number = 1234567.89;
const currency = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(number); // "$1,234,567.89"

// List formatting
const fruits = ['apple', 'banana', 'orange'];
const list = new Intl.ListFormat('en', { 
  style: 'long', 
  type: 'conjunction' 
}).format(fruits); // "apple, banana, and orange"
The Intl object handles internationalization without external dependencies

4. Temporal Dead Zone: Understanding Let and Const Hoisting

Here's something that trips up even experienced developers. Variables declared with let and const are hoisted, but they exist in a "temporal dead zone" until their declaration is reached.

JavaScript code on screen
Understanding JavaScript's execution context
Debugging JavaScript code
Debugging temporal dead zone issues

5. Proxy Objects: Meta-Programming Magic

Proxies let you intercept and customize operations on objects (like property access, assignment, function calls). Think of them as middleware for your objects. They're perfect for creating reactive systems or implementing validation.

// Create a reactive object that logs changes
const createReactive = (target) => {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`Getting ${prop}: ${obj[prop]}`);
      return obj[prop];
    },
    set(obj, prop, value) {
      console.log(`Setting ${prop} to ${value}`);
      obj[prop] = value;
      return true;
    },
    has(obj, prop) {
      console.log(`Checking if ${prop} exists`);
      return prop in obj;
    }
  });
};

const user = createReactive({ name: 'John', age: 30 });
user.name; // Logs: Getting name: John
user.email = 'john@example.com'; // Logs: Setting email to john@example.com
'age' in user; // Logs: Checking if age exists
Proxy objects enable meta-programming and reactive behavior patterns

6. WeakMap and WeakSet: Garbage Collection Friendly Collections

Unlike regular Map and Set, WeakMap and WeakSet don't prevent their contents from being garbage collected. This makes them perfect for metadata storage and avoiding memory leaks.

  • WeakMap keys must be objects and don't prevent garbage collection
  • Perfect for storing private data or metadata associated with objects
  • WeakSet only holds object references, not primitive values
  • Neither WeakMap nor WeakSet are enumerable for performance reasons

7. BigInt: Handling Really, Really Big Numbers

JavaScript's Number type can safely represent integers up to 2^53 - 1. But what if you need bigger numbers? Enter BigInt, which can represent integers of arbitrary precision.

// Regular numbers hit precision limits
const bigNumber = 9007199254740992; // 2^53
console.log(bigNumber + 1); // 9007199254740992 (wrong!)
console.log(bigNumber + 2); // 9007199254740994 (wrong!)

// BigInt handles large numbers correctly
const bigIntNumber = 9007199254740992n; // Note the 'n' suffix
console.log(bigIntNumber + 1n); // 9007199254740993n (correct!)
console.log(bigIntNumber + 2n); // 9007199254740994n (correct!)

// You can also create BigInt from strings
const fromString = BigInt('123456789012345678901234567890');
console.log(fromString); // 123456789012345678901234567890n

// Converting between BigInt and Number
const regular = Number(bigIntNumber); // Be careful with precision loss
const bigInt = BigInt(42);
BigInt enables precise arithmetic with arbitrarily large integers

8. Private Class Fields: True Encapsulation

Finally! True private properties in JavaScript classes. No more underscore conventions or WeakMap hacks. Private fields start with # and are genuinely inaccessible from outside the class.

class BankAccount {
  // Private fields
  #balance = 0;
  #accountNumber;
  
  // Private method
  #validateAmount(amount) {
    return amount > 0 && typeof amount === 'number';
  }
  
  constructor(accountNumber, initialBalance = 0) {
    this.#accountNumber = accountNumber;
    if (this.#validateAmount(initialBalance)) {
      this.#balance = initialBalance;
    }
  }
  
  deposit(amount) {
    if (this.#validateAmount(amount)) {
      this.#balance += amount;
      return this.#balance;
    }
    throw new Error('Invalid amount');
  }
  
  getBalance() {
    return this.#balance; // Only way to access private field
  }
}

const account = new BankAccount('123456', 1000);
console.log(account.getBalance()); // 1000
account.deposit(500); // 1500
// account.#balance; // SyntaxError: Private field '#balance' must be declared in an enclosing class
Private class fields provide true encapsulation without workarounds

9. Dynamic Imports: Code Splitting Made Easy

Dynamic imports allow you to load modules conditionally and asynchronously. This is crucial for code splitting and performance optimization in modern web applications.

// Conditional module loading
async function loadFeature(featureName) {
  if (featureName === 'charts') {
    const chartModule = await import('./charts.js');
    return chartModule.createChart();
  } else if (featureName === 'auth') {
    const authModule = await import('./auth.js');
    return authModule.initAuth();
  }
}

// Load modules based on user interaction
button.addEventListener('click', async () => {
  const { heavyFunction } = await import('./heavy-module.js');
  heavyFunction();
});

// Dynamic import with error handling
async function loadUserPreferences() {
  try {
    const module = await import(`./themes/${userTheme}.js`);
    module.applyTheme();
  } catch (error) {
    console.error('Failed to load theme:', error);
    // Fallback to default theme
    const defaultTheme = await import('./themes/default.js');
    defaultTheme.applyTheme();
  }
}
Dynamic imports enable conditional module loading and code splitting

The beauty of these JavaScript features isn't just in their individual power, but in how they work together to create more robust, maintainable, and performant applications. Each one solves real problems that developers face every day.

JavaScript Developer Experience

10. Top-Level Await: Async at Module Level

Gone are the days of wrapping your entire module in an IIFE just to use await. Top-level await allows you to use await directly in module scope, making async initialization much cleaner.

// Before: Wrapping in IIFE
(async () => {
  const config = await fetch('/api/config').then(r => r.json());
  const module = await import(`./modules/${config.module}.js`);
  module.init(config);
})();

// After: Top-level await (so much cleaner!)
const config = await fetch('/api/config').then(r => r.json());
const module = await import(`./modules/${config.module}.js`);
module.init(config);

// Conditional async loading
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
  const devTools = await import('./dev-tools.js');
  devTools.init();
}

// Multiple async operations
const [userData, appConfig, translations] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/config').then(r => r.json()),
  import(`./i18n/${navigator.language}.js`)
]);

console.log('All data loaded, app ready!');
Top-level await eliminates the need for IIFE wrappers in async modules

Why These Features Matter

These aren't just cool party tricks – each feature addresses real pain points in JavaScript development. The nullish coalescing operator prevents bugs with falsy values. Private class fields provide true encapsulation. Dynamic imports improve performance through code splitting. Together, they represent the evolution of JavaScript into a more mature, powerful language.

The next time you're writing JavaScript, remember these hidden gems. Your code will be cleaner, your bugs fewer, and your fellow developers will wonder how you became so much more productive. Start small – pick one or two features that solve problems you face regularly, and gradually incorporate them into your workflow.

JavaScript continues to evolve, and staying current with these lesser-known features gives you a significant advantage. While everyone else is still using the old patterns, you'll be writing code that's more robust, maintainable, and expressive. That's the difference between knowing JavaScript and truly understanding it.

0 Comment

Share your thoughts

Your email address will not be published. Required fields are marked *