DavidCastro

You Don't Know JS Yet: Objects & Classes

Deep Dive into Objects, Prototypes, 'this', and Classes in JavaScript.

#javascript
#objects
#prototypes
#this
#classes
#ydkjs

My next review covers the second edition book, “Objects & Classes,” from the “You Don’t Know JS Yet” series by Kyle Simpson. This edition replaces the classic “this & Object Prototypes,” diving deep into how JavaScript’s object system really works, from fundamental properties to the class keyword and beyond. Understanding this is key to truly understanding JS.

Chapter 1: Object Foundations

This introductory chapter firmly establishes the central role of objects in JavaScript, while immediately debunking the common myth that “everything in JS is an object.” While not all values are objects, objects are fundamental data structures – flexible key/value containers – and form the bedrock for understanding prototypes and the class system, key pillars of the language.

The chapter contrasts this new edition with the first (“this & Object Prototypes”), highlighting a shift in focus to reflect modern JavaScript practices where class syntax is prevalent, though still built upon prototypes.


Key takeaways on object fundamentals include:

  1. Creation: Objects are typically created using literal syntax ({ ... }), which is preferred over new Object().

    const myObj = {
      key: "value",
      anotherKey: 123,
    };
  2. Properties: Objects store key/value pairs (properties).

    • Definition: Defined as propertyName: value within literals. Values can be literals or computed expressions. “Lazy” evaluation requires wrapping expressions in functions.

    • Property Names: Usually treated as strings. Computed property names use [expression]. Symbols (Symbol()) provide unique keys.

      const dynamicProp = "dynamicKey";
      const mySymbol = Symbol("description");
      
      const objWithDynamicProps = {
        [dynamicProp]: "This key was computed",
        [mySymbol]: "Value for a symbol key",
        "key with spaces": true,
      };
    • Conciseness: ES6 introduced shorthands: concise properties ({ variable }) and concise methods (method() { ... }).

      const name = "Alice";
      const age = 30;
      
      const person = {
        name, // Concise property
        age,
        greet() {
          // Concise method
          console.log(`Hello, my name is ${this.name}`);
        },
      };
      person.greet(); // Output: Hello, my name is Alice
    • Spread (...): Used within object literals ({ ...sourceObj }) to perform a shallow copy of an object’s owned, enumerable properties.

      const source = { a: 1, b: 2 };
      const target = { b: 3, c: 4, ...source };
      // target is now { b: 2, c: 4, a: 1 } (source.b overwrites target.b)
      
      const shallowCopy = { ...source };
      // shallowCopy is { a: 1, b: 2 }
    • Deep Copy: Standardized deep cloning is available via structuredClone().

      const original = { a: 1, b: { c: 2 } };
      const deepCopy = structuredClone(original);
      deepCopy.b.c = 3;
      console.log(original.b.c); // 2 (original is unaffected)
  3. Access:

    • Dot Notation (.): Preferred method (myObj.prop).

    • Bracket Notation ([]): Required for keys with invalid identifier characters or computed keys (myObj["prop-name"], myObj[variable]).

      console.log(person.name); // Alice
      console.log(objWithDynamicProps["key with spaces"]); // true
    • Destructuring ({ prop } = obj): Declarative syntax to extract values, optionally renaming and providing defaults.

      const { name: personName, age: personAge = 25 } = person;
      console.log(personName); // Alice
      console.log(personAge); // 30 (default not used as age exists)
      
      const { nonExistentProp = "default" } = person;
      console.log(nonExistentProp); // default
    • Optional Chaining (?. / ?.[]): Safely access nested properties.

      const user = {
        id: 1,
        // address: { street: '123 Main St' } // address might be missing
      };
      const street = user.address?.street; // undefined (no error)
      console.log(street);
    • Boxing: Primitives are temporarily wrapped (“boxed”) when properties are accessed.

      const greeting = "Hello";
      console.log(greeting.length); // 5 (accessing length property on string primitive)
  4. Modification:

    • Assignment (=): Adds or updates property values.

      person.age = 31;
      person.city = "New York"; // Adds new property
    • Object.assign(target, ...sources): Copies properties into a target object.

      const updates = { age: 32, occupation: "Developer" };
      Object.assign(person, updates);
      // person is now { name: 'Alice', age: 32, greet: [Function: greet], city: 'New York', occupation: 'Developer' }
    • Deletion (delete): Removes a property.

      delete person.city;
      console.log(person.city); // undefined
  5. Inspection:

    • Existence: Object.hasOwn(obj, "prop") is preferred for checking owned properties. in checks the prototype chain too.

      console.log(Object.hasOwn(person, "name")); // true
      console.log(Object.hasOwn(person, "toString")); // false (inherited)
      console.log("toString" in person); // true (found on prototype chain)
    • Listing Contents: Various methods list keys, values, or entries.

      console.log(Object.keys(person)); // ['name', 'age', 'greet', 'occupation']
      console.log(Object.values(person)); // ['Alice', 32, [Function: greet], 'Developer']
      console.log(Object.entries(person)); // [['name', 'Alice'], ...]
  6. Usage: Objects often serve as temporary containers, frequently combined with destructuring.

    function processCoords({ x, y }) {
      // Object argument destructured
      const magnitude = Math.sqrt(x * x + y * y);
      const normalizedX = x / magnitude;
      // Return object used as temporary container
      return { magnitude, normalizedX };
    }
    
    const point = { x: 3, y: 4 };
    // Destructure the returned temporary object
    const { magnitude } = processCoords(point);
    console.log(magnitude); // 5

The chapter concludes by framing objects as collections of properties, setting the stage to explore their deeper mechanics in the subsequent chapters.



Chapter 2: How Objects Work

This chapter delves deeper than viewing objects merely as containers, exploring the underlying mechanisms – the Metaobject Protocol (MOP) – that govern their behavior and allow for customization.


1. Property Descriptors:

Every object property has an associated property descriptor, a metaobject defining its characteristics. You can retrieve it using Object.getOwnPropertyDescriptor() and define/modify properties precisely using Object.defineProperty() or Object.defineProperties().

const myObj = { a: 1 };
const descriptor = Object.getOwnPropertyDescriptor(myObj, "a");
// descriptor: { value: 1, writable: true, enumerable: true, configurable: true }

const anotherObj = {};
Object.defineProperty(anotherObj, "b", {
  value: 2,
  writable: false, // read-only
  enumerable: true,
  configurable: false, // cannot be deleted or reconfigured
});
console.log(anotherObj.b); // 2
anotherObj.b = 3; // Fails silently (or throws in strict mode)
console.log(anotherObj.b); // 2
  • value: The property’s data value.
  • writable: If false, the value cannot be changed with =.
  • enumerable: If false, the property won’t show up in for...in loops, Object.keys, Object.entries, spread (...), or Object.assign.
  • configurable: If false, the property cannot be deleted, and its descriptor attributes (except value if writable is true) cannot be changed. writable can be changed from true to false, but not back.

2. Accessor Properties (Getters/Setters):

Instead of a static value, properties can have getter (get) and/or setter (set) functions defined in their descriptor. These functions are invoked automatically on property access (obj.prop) or assignment (obj.prop = val).

const user = {
  _firstName: "John",
  _lastName: "Doe",

  get fullName() {
    console.log("Getting fullName");
    return `${this._firstName} ${this._lastName}`;
  },

  set fullName(value) {
    console.log(`Setting fullName to: ${value}`);
    const parts = value.split(" ");
    this._firstName = parts[0];
    this._lastName = parts[1];
  },
};

console.log(user.fullName); // Getting fullName -> John Doe
user.fullName = "Jane Smith"; // Setting fullName to: Jane Smith
console.log(user._firstName); // Jane

3. Object Sub-Types (Arrays & Functions):

  • Arrays: Specialized objects for numerically indexed collections, created with []. They have an auto-updating length property (accessing it is not expensive). Avoid creating “empty slots” by assigning beyond the current length + 1, as they behave inconsistently with some array methods.

    const list = [1, 2];
    list[5] = 6;
    console.log(list); // [ 1, 2, <3 empty items>, 6 ]
    console.log(list.length); // 6
    console.log(list[3]); // undefined (but it's an empty slot)
  • Functions: Are also objects (“first-class objects”) and can have properties. They have built-in name and length (number of declared parameters before defaults or rest params) properties. Avoid adding custom properties directly; use Map or WeakMap to associate metadata with functions instead.

    function greet(name, msg = "Hello") {
      /* ... */
    }
    console.log(greet.name); // "greet"
    console.log(greet.length); // 1 (only 'name' counts)
    
    const functionMetadata = new Map();
    functionMetadata.set(greet, { description: "Greets a user" });

4. Object Characteristics (Extensibility, Sealed, Frozen):

  • Extensible: Controls if new properties can be added. Default is true. Object.preventExtensions(obj) makes it non-extensible.

    const obj = { a: 1 };
    Object.preventExtensions(obj);
    obj.b = 2; // Fails silently (or throws in strict mode)
    console.log(obj.b); // undefined
  • Sealed: An object that is non-extensible and all its existing properties are non-configurable. You can still change the values of existing properties (if they are writable), but you cannot add new properties, remove existing properties, or change the configurability of existing properties. Use Object.seal(obj).

    const sealedObj = { a: 1, b: 2 };
    Object.seal(sealedObj);
    
    sealedObj.a = 10; // Allowed
    console.log(sealedObj.a); // 10
    
    sealedObj.c = 3; // Fails silently (or throws in strict mode)
    console.log(sealedObj.c); // undefined
    
    delete sealedObj.b; // Fails silently (or throws in strict mode)
    console.log(sealedObj.b); // 2
    
    // Trying to change configurability will also fail
    // Object.defineProperty(sealedObj, 'a', { configurable: true }); // Throws Error
  • Frozen: An object that is sealed and all its data properties are non-writable. This is the strictest level of immutability. You cannot add new properties, remove existing properties, change the configurability of existing properties, or change the values of existing data properties. If a property is an accessor (getter/setter), the getter will still work, but setting the value via the setter may throw an error if the setter attempts to modify the object. Use Object.freeze(obj).

    const frozenObj = { a: 1, b: 2 };
    Object.freeze(frozenObj);
    
    frozenObj.a = 10; // Fails silently (or throws in strict mode)
    console.log(frozenObj.a); // 1
    
    frozenObj.c = 3; // Fails silently (or throws in strict mode)
    console.log(frozenObj.c); // undefined
    
    delete frozenObj.b; // Fails silently (or throws in strict mode)
    console.log(frozenObj.b); // 2
    
    // Trying to change writability or configurability will also fail
    // Object.defineProperty(frozenObj, 'a', { writable: true }); // Throws Error
    // Object.defineProperty(frozenObj, 'a', { configurable: true }); // Throws Error

Key Takeaways:

  • preventExtensions: Cannot add new properties.
  • seal: Cannot add new properties, cannot remove existing properties, cannot change configurability of existing properties. Can change values of existing writable properties.
  • freeze: Cannot add new properties, cannot remove existing properties, cannot change configurability of existing properties, cannot change values of existing data properties.

5. The [[Prototype]] Chain:

This is a crucial, internal linkage ([[Prototype]]) connecting an object to another object. When a property is accessed on an object and not found, the lookup delegates up this chain. This is how JavaScript implements inheritance (or more accurately, delegation).

  • By default, objects created with {} or new Object() are linked to the built-in Object.prototype.
  • Methods like toString() and hasOwnProperty() are typically found on Object.prototype and accessed via this delegation.
  • Object.hasOwn(obj, prop) (ES2022) is the preferred, safer static method over the inherited obj.hasOwnProperty(prop).
const myObj = { a: 1 };
console.log(myObj.a); // 1 (Own property)
console.log(myObj.toString()); // "[object Object]" (Inherited from Object.prototype)
console.log(Object.hasOwn(myObj, "a")); // true
console.log(Object.hasOwn(myObj, "toString")); // false

6. Creating and Managing Prototypes:

  • Object.create(protoObj): Creates a new object explicitly linked to protoObj.
  • { __proto__: protoObj, ... }: Literal syntax using the special __proto__ property (standardized but use with caution, see Appendix B warnings).
  • Object.create(null): Creates an object with no prototype linkage, often useful for “dictionary” objects to avoid inheriting from Object.prototype.
const parent = { greet: "Hello" };
const child = Object.create(parent);
child.name = "Child";

console.log(child.greet); // "Hello" (Delegated to parent)

const dict = Object.create(null);
dict.key = "value";
console.log(dict.toString); // undefined (No Object.prototype linkage)

7. [[Prototype]] vs .prototype Distinction:

This is a common point of confusion:

  • [[Prototype]]: The internal link an object has to the object it inherits/delegates from.
  • .prototype: A public property on constructor functions (like Object, Array, MyClass). When you use new MyClass(), the newly created object’s internal [[Prototype]] link is set to the object referenced by MyClass.prototype.
function Dog(name) {
  this.name = name;
}
// Dog.prototype is an object that will become the [[Prototype]]
// of objects created via 'new Dog()'
Dog.prototype.bark = function () {
  console.log("Woof!");
};

const spot = new Dog("Spot");

// spot's internal [[Prototype]] === Dog.prototype
spot.bark(); // Woof! (Delegated to Dog.prototype)


Chapter 3: Classy Objects

This chapter transitions from the underlying object mechanics to the class-oriented design pattern in JavaScript, focusing primarily on the modern class syntax introduced in ES6. While acknowledging the older “prototypal class” patterns, the chapter emphasizes that class is the current standard, offering much more than just syntactic sugar over prototypes, although it still leverages the [[Prototype]] mechanism internally.


1. When to Use Classes:

Class-orientation is suitable when:

  • You can classify entities using “is-a” relationships (e.g., a Car is a Vehicle).
  • You need multiple instances of a defined type (class).
  • You want to leverage inheritance to share and override characteristics between related classes (subclassing).
  • You benefit from encapsulation (hiding internal details) and composition (building objects from other objects). If you only need a single instance of a complex object, other patterns (like modules or plain objects) might be simpler.

2. Basic class Syntax:

Classes can be defined using declarations or expressions. They contain methods defined without the function keyword. All code inside a class body runs in strict mode.

// Class Declaration
class Point2d {
  // Constructor: Called when 'new Point2d()' is used
  constructor(x, y) {
    console.log("Point2d instance created");
    // 'this' refers to the new instance being created
    this.x = x; // Instance property (member)
    this.y = y;
  }

  // Method: Defined on Point2d.prototype
  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

// Creating an instance
const p1 = new Point2d(5, 10);
console.log(p1.toString()); // Output: (5, 10)

// Class Expression (named)
const Vec = class Vector {
  /* ... */
};
// Class Expression (anonymous)
const Anon = class {
  /* ... */
};
  • The constructor is a special method for initialization. If omitted, a default empty one is provided.
  • Class constructors must be called with new.
  • Methods defined in the class body (like toString) are placed on the ClassName.prototype object and shared by all instances via [[Prototype]] delegation.

3. Instance Members (this and Public Fields):

  • Inside the constructor and methods, this refers to the current instance. Properties added via this.prop = value become instance members.

  • Public Fields: A declarative syntax allows defining instance members directly in the class body. These are effectively initialized at the start of the constructor (or more accurately, just after super() in subclasses).

    class Rectangle {
      // Public fields with initializers
      width = 0;
      height = 0;
      color = "blue";
      calculatedArea = this.width * this.height; // Can reference other fields
    
      constructor(width, height) {
        this.width = width;
        this.height = height;
        this.calculatedArea = this.width * this.height; // Re-calculate if needed
      }
    
      getArea() {
        return this.width * this.height;
      }
    }
    const r1 = new Rectangle(10, 5);
    console.log(r1.width); // 10
    console.log(r1.color); // blue
    console.log(r1.calculatedArea); // 50 (after constructor runs)
  • Anti-Pattern Warning: Avoid defining methods using arrow functions assigned to public fields (myMethod = () => { ... }). This creates a new function per instance instead of a shared method on the prototype, defeating a key benefit of classes and potentially causing performance/memory overhead. Use regular method syntax and understand this binding (covered later).

    class BadPattern {
      value = 10;
      // Avoid this: Creates a function per instance
      getValueArrow = () => this.value;
    
      // Prefer this: Method shared on prototype
      getValueMethod() {
        return this.value;
      }
    }
    const bp = new BadPattern();
    console.log(Object.hasOwn(bp, "getValueArrow")); // true (Oops!)
    console.log(Object.hasOwn(bp, "getValueMethod")); // false (Good!)

4. Class Inheritance (extends and super):

  • The extends keyword creates a subclass that inherits from a base class.

  • Subclasses inherit methods and properties (via the [[Prototype]] chain).

  • Methods and fields can be overridden in the subclass.

  • super.method() calls the overridden method from the parent class.

  • super() must be called within the subclass constructor before accessing this. It calls the parent class’s constructor. Field initializers in the subclass run after super().

    class Square extends Rectangle {
      constructor(size) {
        console.log("Square constructor start");
        // Must call parent constructor before 'this'
        super(size, size); // Calls Rectangle's constructor
        console.log("Square constructor after super");
        this.color = "red"; // Overrides inherited field
      }
    
      // Overrides Rectangle's getArea
      getArea() {
        console.log("Using Square's getArea");
        return this.width * this.width;
      }
    
      getParentArea() {
        // Call parent's getArea method
        return super.getArea();
      }
    }
    const sq = new Square(7); // Logs constructor messages
    console.log(sq.getArea()); // Using Square's getArea -> 49
    console.log(sq.getParentArea()); // 49 (super.getArea uses this.width/height from sq)
    console.log(sq.color); // red
  • instanceof checks if an object exists anywhere in a class’s prototype chain. obj.constructor === ClassName checks for direct instantiation.

  • Inheritance is sharing via [[Prototype]], not copying properties down.

5. Static Properties and Methods (static):

  • static defines properties/methods directly on the constructor function itself, not on the prototype or instances. Useful for utility functions or constants related to the class.

  • this inside static methods refers to the class (constructor function).

  • Static members are inherited by subclasses (via [[Prototype]] linkage of constructors) and can be overridden using static and accessed via super. in the subclass.

  • Static initialization blocks (static { ... }) (ES2022) allow more complex static property setup.

    class Circle {
      // Static property
      static PI = 3.14159;
    
      // Static method
      static calculateArea(radius) {
        // 'this' here refers to the Circle class
        return this.PI * radius * radius;
      }
    
      // Static block
      static {
        console.log("Circle class static block initialized.");
        this.defaultRadius = 1;
      }
    
      radius;
      constructor(radius) {
        this.radius = radius;
      }
    }
    console.log(Circle.PI); // 3.14159
    console.log(Circle.calculateArea(10)); // 314.159
    console.log(Circle.defaultRadius); // 1

6. Private Class Members (#):

  • ES2022 introduced private instance fields (#field), methods (#method()), static fields (static #staticField), and static methods (static #staticMethod()) using the # prefix.

  • Motivation: Encapsulation, Principle of Least Privilege (hiding implementation details).

  • Syntax: # is part of the name; only accessible within the class body where defined.

  • Limitation: Privates are not inherited or accessible by subclasses. This is a major difference from protected visibility in other languages and can complicate subclassing. JS currently lacks protected.

  • Brand Check: Use #privateIdentifier in objectInstance to safely check if an object possesses a specific private member without causing an error.

  • Exfiltration: Private members/methods can still be leaked (e.g., by assigning this.#privateMethod to an external variable or passing it as a callback).

  • Static Private Gotcha: When calling an inherited public static method from a subclass, if that method tries to access a private static member using this.#privateStatic, it fails. Workaround: Use ClassName.#privateStatic inside the static method instead of this.#privateStatic.

    class Counter {
      #count = 0; // Private instance field
      static #maxCount = 10; // Private static field
    
      increment() {
        if (this.#count < Counter.#maxCount) {
          this.#count++;
          this.#log(); // Call private instance method
        }
      }
    
      #log() {
        // Private instance method
        console.log(`Current count: ${this.#count}`);
      }
    
      getCount() {
        return this.#count;
      }
    
      // Ergonomic brand check example
      static isCounter(obj) {
        return #count in obj;
      }
    }
    const c = new Counter();
    c.increment(); // Current count: 1
    // console.log(c.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class
    console.log(Counter.isCounter(c)); // true
    console.log(Counter.isCounter({})); // false


Chapter 4: This Works

This chapter tackles the often-misunderstood this keyword in JavaScript, emphasizing its dynamic nature. Unlike lexical scope, this is not determined at author-time but rather at runtime, based entirely on how a function is called (the call-site). Understanding this is crucial, as a single this-aware function (any function using the this keyword) can behave differently depending on its invocation context.


Key Concepts:

  1. Dynamic Context: this provides a dynamic context mechanism, contrasting with the static context of lexical scope (closures). It allows a function to operate on different context objects during different invocations.

  2. The Burden of this: Using this requires authors and readers to analyze every call-site to understand a function’s behavior, adding complexity. Use it intentionally when its dynamic power outweighs this cost.

  3. The Four Binding Rules (in order of precedence): JS determines this for a regular function call based on these rules:

    • 1. New Binding (new): When a function is called with new, a new object is created, [[Prototype]]-linked, and set as the this for the call. The function usually returns this new object implicitly.

      function ConstructorFn(val) {
        this.a = val;
        // implicitly returns 'this'
      }
      const obj1 = new ConstructorFn(1);
      console.log(obj1.a); // 1
    • 2. Explicit Binding (call, apply): These methods invoke a function, explicitly setting the this context to the first argument passed to call/apply.

      function printA() {
        console.log(this.a);
      }
      const obj2 = { a: 2 };
      const obj3 = { a: 3 };
      printA.call(obj2); // 2 ('this' is obj2)
      printA.call(obj3); // 3 ('this' is obj3)
    • 3. Implicit Binding (Context Object): When a function is called as a method via a containing object reference (obj.method()), that object (obj) becomes the this for the call.

      const obj4 = {
        a: 4,
        printA: printA, // Reference to the function above
      };
      obj4.printA(); // 4 ('this' is obj4)
    • 4. Default Binding: If none of the above rules apply (e.g., a plain func() invocation):

      • In strict mode (most common, recommended): this is undefined. Accessing properties on it (this.prop) throws a TypeError.
      • In non-strict mode: this defaults to the global object (globalThis, e.g., window). This can lead to accidental global variable creation and is generally discouraged.
      "use strict";
      printA(); // TypeError: Cannot read properties of undefined (reading 'a')
      
      // --- In non-strict mode ---
      // printA(); // Logs globalThis.a (likely undefined, or pollutes global scope)
  4. Arrow Functions (=>) and Lexical this:

    • Arrow functions are irregular: they do not have their own this binding.
    • Inside an arrow function, this behaves like a normal lexical variable, inheriting its value from the nearest enclosing regular function scope.
    • call, apply, and bind have no effect on the this value of an arrow function. They also cannot be used with new.
    • This “lexical this” behavior is the primary reason arrow functions were introduced, providing a cleaner alternative to var self = this or var context = this hacks.
    const obj5 = {
      a: 5,
      regularFn: function () {
        // 'this' here is obj5 when called like obj5.regularFn()
        setTimeout(() => {
          // Arrow function inherits 'this' from regularFn's scope
          console.log(this.a); // Logs 5, because 'this' is obj5
        }, 10);
      },
      brokenFn: function () {
        setTimeout(function () {
          // Regular function gets default binding (undefined in strict mode)
          // console.log(this.a); // Throws TypeError
        }, 10);
      },
    };
    obj5.regularFn();
  5. The Callback Problem: When passing a this-aware method as a callback (e.g., event handlers), the original this context is often lost because the calling library/framework invokes the function using a different rule (often explicit binding to something else, or default binding).

    class Handler {
      message = "Clicked!";
      handleClick() {
        console.log(this.message); // Relies on 'this' being the Handler instance
      }
      setupListener(button) {
        // Problem: When button is clicked, 'this' inside handleClick
        // will likely NOT be the Handler instance.
        // button.addEventListener('click', this.handleClick); // 'this' will be wrong
    
        // Solution 1: Arrow function wrapper (Lexical this)
        button.addEventListener("click", (evt) => this.handleClick(evt));
    
        // Solution 2: bind()
        // button.addEventListener('click', this.handleClick.bind(this));
      }
    }
  6. Hard Binding (bind): func.bind(thisContext) creates a new function that is permanently bound to thisContext. Its this cannot be overridden by implicit or explicit binding (though new binding can still override it, surprisingly).

  7. Critique of Over-Binding: The chapter strongly cautions against patterns like defining class methods as arrow function fields (myMethod = () => {...}) or using .bind(this) in constructors for all methods just to avoid dealing with dynamic this.

    • Costs: Creates functions per instance (memory/performance overhead), defeats prototype sharing, adds complexity.
    • Argument: If fixed context is always needed, the closure pattern (using lexical scope without this) is often simpler, more appropriate, and more performant than forcing class methods to behave like closures. Use this when its dynamic nature is beneficial.
    // Author argues this is often better than forcing 'this' with => or bind
    function createPoint(px, py) {
      let x = px; // Lexical variables, not 'this'
      let y = py;
      return {
        getDoubleX() {
          return x * 2;
        }, // Closure keeps access to x
        toString() {
          return `(${x}, ${y})`;
        }, // Closure keeps access to x, y
      };
    }
    const p_alt = createPoint(3, 4);
    const getX_alt = p_alt.getDoubleX;
    console.log(getX_alt()); // 6 (Works simply via closure)
  8. Irregular Invocations:

    • Indirect Calls: Using comma operator ((1, obj.method)(...)) or other tricks can cause a function reference to lose its implicit context, resulting in default binding.
    • Tagged Templates: tagFn`...` results in default binding for tagFn. obj.tagFn`...` results in implicit binding ( this is obj) for tagFn.

Conclusion:

this is a powerful but complex feature providing dynamic context. Understanding the four binding rules and the lexical behavior of arrow functions is essential. Use this deliberately, leveraging its dynamic capabilities when needed, but consider alternatives like closure when fixed context is the primary goal, especially within class structures. Be aware of the costs and complexities involved.



Chapter 5: Delegation

This chapter presents delegation as a powerful, alternative design pattern inherent in JavaScript’s prototypal nature, contrasting it with traditional class-based inheritance. While acknowledging its non-mainstream status, Kyle argues that understanding delegation deepens one’s grasp of JS’s core [[Prototype]] and this mechanisms, even if primarily using class.


1. Deconstructing class and new:

The chapter starts by separating object creation from initialization. new handles creation (step 1) and prototype linking (step 2), while the constructor only handles initialization (step 3).

2. Factory Functions:

Objects can be created and initialized using simple factory functions without class or new.

// Example Prototype Object
const PointActions = {
  init(x, y) {
    this.x = x;
    this.y = y;
  },
  toString() {
    return `(${this.x}, ${this.y})`;
  },
};

// Factory Function using Object.create + init
function createPoint(x, y) {
  // Step 1 & 2: Create object linked to PointActions
  const instance = Object.create(PointActions);
  // Step 3: Initialize
  instance.init(x, y);
  // Step 4: Return
  return instance;
}

const p1 = createPoint(3, 4);
console.log(p1.toString()); // (3, 4)
  • Object.create(protoObj) performs the creation and [[Prototype]] linking steps.
  • An init() method (or similar) handles initialization, often called immediately after creation.
  • Using new with such factory functions is wasteful as it creates an extra, unused object.
  • A generic make(ObjType, ...args) helper can simplify the create-then-initialize pattern.

3. Delegation Pattern Explained:

Instead of hierarchical classes, delegation focuses on peer objects cooperating to accomplish tasks. Behavior isn’t copied down; it’s accessed via delegation through the [[Prototype]] chain or explicit calls, sharing context via dynamic this.

  • Explicit Delegation: Using call() or apply() to invoke a method from one object in the context of another.
  • Implicit Delegation: Using [[Prototype]] linkage so that method calls on an object naturally delegate up the chain if the method isn’t found locally.
const Coordinates = {
  setXY(x, y) {
    this.x = x;
    this.y = y;
  },
};
const Inspect = {
  toString() {
    return `(${this.x}, ${this.y})`;
  },
};

// point uses explicit delegation
const point = {};
Coordinates.setXY.call(point, 3, 4); // Explicitly delegate setXY to run with 'point' as 'this'
console.log(Inspect.toString.call(point)); // (3, 4) - Explicitly delegate toString

// anotherPoint uses implicit delegation for setXY
const anotherPoint = Object.create(Coordinates); // [[Prototype]] linked to Coordinates
anotherPoint.setXY(5, 6); // Implicitly delegates to Coordinates.setXY with 'anotherPoint' as 'this'
console.log(Inspect.toString.call(anotherPoint)); // (5, 6) - Still needs explicit for Inspect

This approach achieves virtual composition – objects behave as if composed together at runtime through this context sharing, without needing a predefined class structure. This aligns with the OLOO (Objects Linked to Other Objects) philosophy.


4. Composing Peer Objects Example:

A more complex example involving Canvas, Coordinates, and ControlPoint objects demonstrates intricate interactions:

  • Objects remain distinct peers (Canvas, Coordinates, ControlPoint).
  • ControlPoint links to Coordinates (__proto__ or Object.create).
  • Methods on one object (ControlPoint.render) explicitly delegate (Canvas.renderScene.call(this)) to methods on another (Canvas), sharing the ControlPoint instance as this.
  • Methods called via this (this.draw() inside Canvas.renderScene) resolve dynamically based on the current this context (ControlPoint in this case), finding the method on ControlPoint.

This dynamic interplay allows flexible composition and behavior sharing without rigid inheritance hierarchies.


5. Why Use this for Delegation?

The alternative to implicit this context sharing is explicitly passing the context object as an argument to every relevant function.

// Explicit Context Passing (Alternative, often more verbose)
var CoordinatesExplicit = {
  setXY(entity, x, y) {
    // 'entity' is the explicit context, instead of 'this'
    entity.x = x;
    entity.y = y;
    // entity.render(); // Assuming entity has render
  },
};
var pointExplicit = { x: 0, y: 0 };
CoordinatesExplicit.setXY(pointExplicit, 3, 4); // Pass context explicitly

While feasible, this explicit passing clutters function signatures and call-sites. The author argues that leveraging dynamic this for delegation, despite its learning curve, can lead to cleaner, less verbose code by implicitly managing the shared context.


Conclusion:

Delegation, using [[Prototype]] linkage and dynamic this, offers a powerful, JS-native alternative to class-based patterns. It emphasizes composing behavior through cooperating peer objects rather than rigid inheritance. While less common, understanding this pattern provides valuable insight into JavaScript’s core object model and enables flexible, dynamic code structures.



Conclusion: Mastering JavaScript’s Object Model

This journey through “YDKJS Yet: Objects & Classes” (2nd Edition) provides a comprehensive deep dive into the heart of JavaScript’s object system. Starting with the fundamental building blocks of objects and their properties, we explored the crucial underlying mechanics like property descriptors and the Metaobject Protocol (MOP) that dictate their behavior.


The book masterfully unravels the often-confusing this keyword, replacing guesswork with a clear set of binding rules determined by function call-sites, while also clarifying the distinct “lexical this” behavior of arrow functions. We then saw how the [[Prototype]] chain serves as the true engine for behavior delegation (often called inheritance) in JavaScript.


Building on this foundation, the review covered the modern class syntax, examining its features like constructor, extends, super, public/private fields (#), and static members. Crucially, the book consistently links this syntax back to the underlying prototypal mechanisms, ensuring we understand how it works, not just that it works.


Finally, Kyle Simpson challenges conventional thinking by presenting delegation (OLOO - Objects Linked to Other Objects) as a powerful, native pattern. This perspective leverages prototypes and dynamic this directly, offering a compelling alternative to traditional class hierarchies and showcasing the flexibility inherent in JavaScript’s core design.



#JavaScript #Objects #Prototypes #This #Classes #YDKJS #BookReview