Notes from YDKJS - this & Object Prototypes

Table of contents

You can find some notes I wrote while reading this & Object Prototypes from the You Don’t Know JS Yet book series. I feel the same is true for this book as with Async & Performance: Good points, complex language. Additionally, although I appreciate the effort he puts in pointing out all the small details, I felt this time I wasn’t getting any practical knowledge until the end of the book (Chapter 6), which seems a bit too late.

Chapter 1: this Or That?

this is not a reference to the function’s lexical scope or to itself, but instead, is a binding that happens when the function gets called. What’s this?

Chapter 2: this All Makes Sense Now!

Finding the call-site

To figure out what this refers to in a function call, you have to find the call-site which is the place in code where the function was called. The best way to find the call-site is to add a breakpoint inside the function you’re interested in and open the developer tools. The last item on the stack is the function in question and the second last the call-site; you can search there to find out how the function was called. Call-site.

Types of binding

  • Default binding is used when none of the following rules apply. We talk about standalone function calls, and in this case, this refers to the global object. Default binding.

  • Implicit binding. You call an object’s function, so in this case, this points to the object: obj.foo() Implicit Binding. You lose implicit binding if you pass a reference of that object’s function to a variable, and then you call that variable (function): Implicitly lost.

    function foo() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      foo: foo,
    };
    var bar = obj.foo; // function reference/alias!
    var a = "oops, global"; // `a` also property on global object
    bar(); // "oops, global"
  • Explicit binding when we force this to a desired object when we call the function. You can do that with bind, apply, or call.

    Use bind to set this to an object and pass some arguments to the function. bind also creates and returns a new function you can store in a variable. apply and call let you do the same thing(set this and arguments), but instead of returning the function, they call it. The difference between apply and call is that apply gets the arguments (after the this parameter) as an array, but call accepts each argument separately.

    Bind has a .name property, for example, in bar = foo.bind(..), bar.name is equal to “bound foo.” Explicit binding.

  • new binding. Constructors in JavaScript are plain functions that “hijack” the use of new in front of them.

    When a function is invoked with new in front of it (aka constructor call), the following things happen:

    1. a brand new object is created.
    2. that object is [[Prototype]]-linked with constructor object.
    3. that object = this inside the constructor call.
    4. the constructor returns that newly created object—if you don’t return something else.

    For example:

    function person(name) {
      this.name = name;
    }
    
    var mark = new person("Mark");
    console.log(mark.name); // "Mark"

    This is the final way you can bind this inside a function call, and it’s called new binding. new binding

Order of precedence for binding rules

  • Default binding < Implicit < Explicit < new.
  • You can’t use new along with apply or call, only with bind.

Now, we can summarize the rules for determining this from a function call’s call-site, in their order of precedence. Ask these questions in this order, and stop when the first rule applies.

  • Is the function called with new (new binding)? If so, this is the newly constructed object.

    var bar = new foo()

  • Is the function called with call or apply (explicit binding), even hidden inside a bind hard binding? If so, this is the explicitly specified object.

    var bar = foo.call( obj2 )

  • Is the function called with a context (implicit binding), otherwise known as an owning or containing object? If so, this is that context object.

    var bar = obj1.foo()

  • Otherwise, default the this (default binding). If in strict mode, pick undefined, otherwise pick the global object.

    var bar = foo()

That’s it. That’s all it takes to understand the rules of this binding for normal function calls. Well… almost.

Everything in order, Determining this

Binding exceptions

  • If you pass null or undefined as a this binding parameter to call, apply, or bind, this will now refer to the global object. This is useful when you want to partially apply arguments with bind or spread arguments with apply (in ES6, the last is not relevant anymore):

    function foo(a, b) {
      console.log("a:" + a + ", b:" + b);
    }
    // currying with `bind(..)`
    var bar = foo.bind(null, 2);
    bar(3); // a:2, b:3
    
    // spreading out array as parameters
    foo.apply(null, [2, 3]); // a:2, b:3

    It’s better to pass something though—see Safer this—instead of null, because with default binding and a third-party library you can pollute the global object if that library uses somewhere this. Ignored this

  • Softening binding: Hard-binding greatly reduces the flexibility of a function, preventing manual this override with either the implicit binding or even subsequent explicit binding attempts. Shows a utility that implements soft binding. Softening binding.

Lexical binding

You can achieve lexical binding with arrow functions or with the self=this pattern. Lexical this.

Chapter 3: Objects

  • When you create a primitive string, number, or boolean, the engine coerces it to the object form; that’s why you can use methods like toString. Built-in Objects.

  • Property Descriptors:

    • Get the characteristics for an object property:
      Object.getOwnPropertyDescriptor(myObject, "a");
    • Define a new property:
      Object.defineProperty(myObject, "a", {
        value: 2,
        writable: true,
        configurable: true,
        enumerable: true,
      });
    • writable is the ability to change the value of a property
    • configurable is the ability to modify the property description with Object.defineProperty
    • enumerable, if you can use it in for-in loops. Property Descriptors.
  • Immutability for objects (tricks):

    • Object Constant

      var myObject = {};
      
      Object.defineProperty(myObject, "FAVORITE_NUMBER", {
        value: 42,
        writable: false,
        configurable: false,
      });
    • Prevent extensions

      var myObject = {
        a: 2,
      };
      
      Object.preventExtensions(myObject);
      
      myObject.b = 3;
      myObject.b; 
    • Seal

    Object.seal() = Object.preventExtensions() + configurable:false

    • Freeze + deep freeze.

      Object.freeze() = Object.seal() + writable:false. Deep freeze if you do the previous recursively for every nested property.

  • When you define a property to have either a getter, a setter, or both, its definition becomes an “accessor descriptor” (as opposed to a “data descriptor”).

    Two ways to define a getter:

    // 1.
    var myObject = {
      // define a getter for a
      get a() {
        return 2;
      },
    };
    
    // 2.
    Object.defineProperty(
      myObject, // target
      "b", // property name
      {
        
        
        get: function () {
          return this.a * 2;
        },
        enumerable: true, 
      }
    );
    
    myObject.a; // 2
    myObject.b; // 4

    Don’t forget to define a setter:

    var myObject = {
      // define a getter for a
      get a() {
        return this._a_;
      },
      // define a setter for a
      set a(val) {
        this._a_ = val * 2;
      },
    };
    myObject.a = 2;
    myObject.a; // 4

    Getters & Setters.

  • Regular objects do not have a built-in iterator as the arrays. Iteration

Chapter 4: Mixing (Up) “Class” Objects

  • Classes means copies, and JavaScript does not automatically create copies (as classes imply) between objects.

  • Why we call the parent constructor with .call(this, arguments) in a subclass (with the pre-ES6 class syntax):

    But if we said Vehicle.drive(), the this binding for that function call would be the Vehicle object instead of the Car object (see Chapter 2), which is not what we want. So, instead, we use .call(this) (Chapter 2) to ensure that drive() is executed in the context of the Car object.

    “Polymorphism” Revisited

  • Read TL;DR for more.

Chapter 5: Prototypes

  • When you want to access an object property, if that property is not on the object, we search for it in the [[Prototype]] chain. If we still don’t find it there, you’ll get undefined [[Prototype]].

  • At the top of the prototype chain is the Object.prototype. Object.prototype.

  • Shadowing a property on an object, that already exists higher on the prototype chain, is not that straightforward if you use the assignment operator. See the 3 cases in Setting & Shadowing properties. You can’t shadow if the property is not writable or has a setter up in the look-up chain. In those cases, you’ll have to use the Object.defineProperty method. You should try to avoid shadowing, see chapter 6 for an alternative.

  • Object.create creates a new object that has the given object as its internal prototype. MDN. By internal prototype I mean the hidden [[Prototype]] you access with __proto__ or Object.getPrototypeOf(), not the MyClass.prototype that’s used with the new keyword. By the way, the equivalent of var a = new MyClass() with Object.create is var a = Object.create(MyClass.prototype)—not 100% the same, you still have to call the constructor function to initialize the object.

  • The .prototype property in an object will be used as the internal [[Prototype]] for the objects we create with the new operator. In the following example, the internal [[Prototype]] of object a is the object Foo.prototype:

    function Foo() {
      // ...
    }
    
    var a = new Foo();
    
    Object.getPrototypeOf(a) === Foo.prototype; // true
  • “Inheritance” implies a copy operation, and JavaScript doesn’t copy object properties (natively, by default). Instead, JS creates a link between two objects, where one object can essentially delegate property/function access to another object. “Delegation” (see Chapter 6) is a much more accurate term for JavaScript’s object-linking mechanism.

    What’s in a name.

  • In other words, in JavaScript, it’s most appropriate to say that a “constructor” is any function called with the new keyword in front of it. Functions aren’t constructors, but function calls are “constructor calls” if and only if new is used.

    Constructor or call.

  • The constructor property does not mean “constructed by.” It’s a prototype lookup. Don’t trust the constructor property.

    function Foo() {
      /* .. */
    }
    
    // If you don't replace the prototype object like this, the
    // constructor property will point to Foo.
    Foo.prototype = {
      /* .. */
    }; 
    
    var a1 = new Foo();
    a1.constructor === Foo; // false!
    a1.constructor === Object; // true!

    Constructor redux.

  • If you want to inherit, use Bar.prototype = Object.create(Foo.prototype). With Bar.prototype = Foo.prototype you say that Bar.prototype is the same object as Foo.prototype, and that means you change both when you start adding properties to any of them. In ES6 you can also use Object.setPrototypeOf(Bar.prototype, Foo.prototype). He also mentioned that you can use the __proto__ property for that, but it’s not recommended because it’s a legacy feature. Prototypal inheritance.

  • Don’t use instanceof to determine an object’s prototype, but instead use Foo.prototype.isPrototypeOf(a), or just b.isPrototypeOf(c). You can also ask for the prototype with Object.getPrototypeOf(a).

    You don’t want to use instanceof because it pretends to check if the object is related to the class (constructor function), but in reality, it checks if it’s related to/has the .prototype property.

    Inspecting class relationships

  • The strange .__proto__ (not standardized until ES6!) property “magically” retrieves the internal [[Prototype]] of an object as a reference, which is quite helpful if you want to directly inspect (or even traverse: .__proto__.__proto__) the chain.

    You can think of __proto__ as a getter/setter:

    Object.defineProperty(Object.prototype, "__proto__", {
      get: function () {
        return Object.getPrototypeOf(this);
      },
      set: function (o) {
        // setPrototypeOf(..) as of ES6
        Object.setPrototypeOf(this, o);
        return o;
      },
    });

    Inspecting class relationships

  • Object.create() can be used instead of the new operator in front of a function. This way you don’t have to deal with the .prototype, .constructor, and new.

    An object created with object.create(null) is not linked to any other object, so it can be used for data storage—without surprises from the prototype chain look-up.

    We don’t need classes to create meaningful relationships between two objects. The only thing we should really care about is objects linked together for delegation, and Object.create() gives us that linkage without all the class cruft.

    Creating links.

Chapter 6: Behavior Delegation

  • The example in Delegation Theory.

    var Task = {
      setID: function (ID) {
        this.id = ID;
      },
      outputID: function () {
        console.log(this.id);
      },
    };
    
    // make `XYZ` delegate to `Task`
    var XYZ = Object.create(Task);
    
    XYZ.prepareTask = function (ID, Label) {
      this.setID(ID);
      this.label = Label;
    };
    
    XYZ.outputTaskDetails = function () {
      this.outputID();
      console.log(this.label);
    };
    
    // ABC = Object.create( Task );
    // ABC ... = ...

    Differences between object-oriented (OO) and objects-linked-to-other-objects (OLOO) styles.

    1. You want the state to be on the delegators (XYZ, ABC) not on the delegate (Task).

    2. You avoid naming things the same in delegators and delegates because:

      having those name collisions creates awkward/brittle syntax to disambiguate references (see Chapter 4), and we want to avoid that if we can.

      In class design, you do that because of polymorphism.

    3. The general utility methods that exist on Task are available to us while interacting with XYZ because XYZ can delegate to Task.

    Behavior delegation means: let some object (XYZ) provide a delegation (to Task) for property or method references if not found on the object (XYZ). Think of objects as peers instead of the typical parent/child relationship.

    Delegation Theory.

  • OO vs OLOO example

    Object-oriented:

    function Foo(who) {
      this.me = who;
    }
    Foo.prototype.identify = function () {
      return "I am " + this.me;
    };
    
    function Bar(who) {
      Foo.call(this, who);
    }
    Bar.prototype = Object.create(Foo.prototype);
    
    Bar.prototype.speak = function () {
      alert("Hello, " + this.identify() + ".");
    };
    
    var b1 = new Bar("b1");
    var b2 = new Bar("b2");
    
    b1.speak();
    b2.speak();

    the same functionality in OLOO:

    var Foo = {
      init: function (who) {
        this.me = who;
      },
      identify: function () {
        return "I am " + this.me;
      },
    };
    
    var Bar = Object.create(Foo);
    
    Bar.speak = function () {
      alert("Hello, " + this.identify() + ".");
    };
    
    var b1 = Object.create(Bar);
    b1.init("b1");
    var b2 = Object.create(Bar);
    b2.init("b2");
    
    b1.speak();
    b2.speak();

    Don’t forget to check the images that show OLOO is a simpler design than OO in JavaScript.

    Mental models compared.

  • Gives 3 practical examples with UI elements—widgets and buttons—one with pre-ES6 classes, one with ES6 class, and one with OLOO.

    With OLOO we choose different names for the methods we want to “polymorph”—which is not the best in my opinion (naming is hard), despite what the writer says that they are more descriptive. At least, we don’t have to call the constructor and methods—we want to polymorph—with .call.

    With OLOO we use two calls to construct and initialize the objects:

    var btn1 = Object.create(Button);
    btn1.setup(125, 30, "Hello");

    compared to the one with classes: var btn1 = new Button(125, 30, "Hello"). This can be thought of as an advantage because it’s a separation of concerns (creation, initialization).

    Widget “Classes”

  • He gives another example where he implements an authentication and a login controller. He starts with pre-ES6 classes where both controllers inherit from a Controller class—AuthController and LoginController—but the AuthController also holds a reference to a LoginController that instantiates in the constructor (composition pattern). He later creates with OLOO a LoginController and an Authcontroller that delegates to the first. This is a simpler design because there’s no common Controller class, and the method names are more descriptive because there’s no polymorphism. Simpler Design

  • A supposed advantage of the ES6 class syntax is the omission of the function keyword in class methods. He argues that you can do the same thing inside objects with OLOO using ES6 syntax. Nicer Syntax.

Appendix A: ES6 class

Starts with a recap on the problems the class design has in JavaScript.

Advantages of the ES6 Class syntax over pre-ES6 class syntax.

  • No .prototype methods all over the code.
  • extends instead of .prototype = Object.create to show inheritance.
  • super in constructors and inside methods (polymorphism) instead of .call(this, arg, arg)
  • If I understand this, ES6 classes don’t allow you to define properties outside the constructor; you can define only methods because those properties would be shared state between the “instances”.
  • With extends, you can inherit from built-in objects, like arrays, which was difficult with pre-ES6 class syntax.

Problems still exist, though, in ES6 Class syntax Class gotchas

  • They are not real classes, just linked objects with prototype. If you replace, maybe by accident, a method on the parent class, at runtime, all the “instances” will be affected—he shows an example.
  • Accidental shadowing is still a problem.
  • I don’t mention the super gotchas, and as a result, the Static > Dynamic? stuff because I’m not sure if they are practical.

Notes from the classes chapter

  • Differences between ES6 class and prototype class syntax:

  • The new keyword is required in ES6 classes.

  • ES6 classes are not hoisted at the top, which means you can’t use them in the code before you define them.

  • With a simple extends keyword you can create a delegation link between objects, or if you prefer create prototype inheritance. extends and super

  • If you omit the constructor in a subclass, you get the default constructor:

      constructor(...args) {
        super(...args);
      }

    You can’t reference this in a subclass until you call super(args) because the parent constructor creates the this. The opposite is true for pre-ES6 class syntax—the subclass constructor creates the this and then you call the parent constructor with Parent.call(this, arg).

    Subclass Constructor.

  • A cool feature of the ES6 class syntax is that you can extend the native classes (Array, Error, etc.) Extending Natives.

  • Use new.target to access the constructor of the newly created object (inside the constructors). new.target

  • Use the static keyword to create static methods, but be careful because they are not available on the prototype chain. That means you can’t access them with obj1.staticProperty, only with MyObject.staticProperty. static

Other things to read

Popular

Previous/Next