This chapter explores advanced JavaScript features including asynchronous programming patterns, modules, metaprogramming with Proxy and Reflect, and typed arrays for binary data.
JavaScript is single-threaded but non-blocking. Asynchronous operations (like network requests or timers) run in the background and execute callbacks when complete. The event loop manages this concurrency model.
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 1000);
console.log("End");
// Output: Start, End, Timeout callback
We cover asynchronous programming in depth in Chapter 8: Asynchronous JavaScript.
ES6 modules let you split code into separate files. Use export to expose values and import to consume them:
// 📁 math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator { /* ... */ }
// 📁 app.js
import { PI, add } from "./math.js";
console.log(add(PI, 2)); // 5.14159
// 📁 utils.js
export default function formatDate(date) {
return date.toISOString();
}
// 📁 app.js
import formatDate from "./utils.js";
Load modules on demand with import():
// Dynamic import returns a promise
button.addEventListener("click", async () => {
const module = await import("./heavy-module.js");
module.run();
});
<script type="module"> in HTML to load module scripts. Module code runs after the DOM is ready.
A Proxy wraps an object and lets you intercept fundamental operations (getting, setting, deleting properties, etc.) via handler traps:
const target = { message: "Hello" };
const handler = {
get(obj, prop) {
if (prop === "message") {
return obj[prop] + " (intercepted)";
}
return obj[prop];
},
set(obj, prop, value) {
console.log(`Setting ${prop} to ${value}`);
obj[prop] = value;
return true; // Indicate success
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // "Hello (intercepted)"
proxy.newProp = 42; // Logs: "Setting newProp to 42"
Reflect provides methods that correspond to each Proxy trap. Use Reflect inside proxy handlers for default behavior:
const handler = {
get(obj, prop, receiver) {
console.log(`Getting ${prop}`);
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
console.log(`Setting ${prop} = ${value}`);
return Reflect.set(obj, prop, value, receiver);
}
};
const proxy = new Proxy({}, handler);
proxy.name = "Alice"; // Logs: Setting name = Alice
console.log(proxy.name); // Logs: Getting name → "Alice"
Typed arrays provide views over raw binary data buffers. Each typed array interprets the buffer as a sequence of a specific numeric type:
// Create a typed array with 4 entries (16 bytes total for Int32Array)
const int32 = new Int32Array(4);
int32[0] = 42;
int32[1] = 100;
int32[2] = 255;
int32[3] = 1024;
console.log(int32); // Int32Array [42, 100, 255, 1024]
// Float64Array (8 bytes per element)
const float64 = new Float64Array(3);
float64[0] = 3.14;
float64[1] = 2.718;
float64[2] = 1.618;
console.log(float64); // Float64Array [3.14, 2.718, 1.618]
ArrayBuffer is a fixed-length raw binary buffer. DataView provides a flexible interface to read/write different types at any byte offset:
const buffer = new ArrayBuffer(16); // 16 bytes
const view = new DataView(buffer);
view.setInt32(0, 12345); // Write 32-bit integer at byte 0
view.setFloat64(4, 3.1415); // Write 64-bit float at byte 4
view.setUint8(12, 255); // Write unsigned byte at byte 12
console.log(view.getInt32(0)); // 12345
console.log(view.getFloat64(4)); // 3.1415
console.log(view.getUint8(12)); // 255
// Byte order (endianness)
console.log(view.getInt32(0, true)); // Little-endian read
TypeError with a descriptive message if validation fails. Create a sample object and test both valid and invalid assignments.