Home > Mobile >  JavaScript Super.prop in object literals
JavaScript Super.prop in object literals

Time:02-15

let proto = {
    whoami() { console.log('I am proto'); } 
};
let obj = { 
    whoami() { 
        Object.getPrototypeOf( obj ).whoami.call( this ); // super.whoami();
        console.log('I am obj'); 
    } 
};
Object.setPrototypeOf( obj, proto );

This code lets obj inherit the proto. I got confused about this code Object.getPrototypeOf( obj ).whoami.call( this ); which is equvalent to super.whoami(). Can anyone explain how this code works?

CodePudding user response:

You may want to first read about the following topics in my answer below before skipping to the explanation:

  • Prototypes
    • (Prototype-chain)
    • Property shadowing
  • Functions
    • (Pure and impure functions)
    • Closures
    • this context

Prototypes

Prototype-chain

In JavaScript, objects can have prototypes. An object inherits all the properties of its prototype. Since prototypes themselves are objects, they too can have prototypes. This is called the prototype-chain.

Accessing an object's property will cause JS to first try to find it on the object itself, and then on each of its prototypes consecutively.

Example

Let's define both a person and their prototype human:

const human = { isSelfAware: true };
const person = { name: "john" };
Object.setPrototypeOf(person, human);

If we try to access person.name, then JS will return the property of person because it has this property defined.

If we however try to access person.isSelfAware, JS will return the property of human because person doesn't have the property defined but its prototype does.

Property shadowing

If an object has a similarly named property as one of its prototypes, the prototype's property is shadowed, because JS will return the earliest defined property.

Shadowing is useful because objects can define different property values without affecting the prototype.

Example

const lightBulbProto = { serialNumber: 12345, isFunctional: true };
const oldLightBulb = { isFunctional: false };
const newLightBulb = {};
Object.setPrototypeOf(oldLightBulb , lightBulbProto);
Object.setPrototypeOf(newLightBulb, lightBulbProto);

By default, all light bulbs are functional. But now we defined an old light bulb to be non-functional (isFunctional: false).

This doesn't affect newLightBulb because we didn't define the functionality on the prototype, but on the specific object oldLightBulb.

Functions

First off: By "function" I only mean functions and function expressions function() {}. Method definitions are syntactic sugar for function expressions, so they are considered the same. Arrow function expressions () => {} are excluded.

Types of functions

JS tries to enclose your function's scope to the smallest possible. If your function is deterministic (same input results in same output) and self-enclosed (no side-effects), it is a pure function; otherwise it is an impure function.

Pure functions are usually easy to understand and debug, since all relevant code is contained in its definition.

const array = [];
function addToArray(o) { // Impure function
  array.push(o);
}

function add(a, b) { // Pure function
  return a   b;
}

Scope and closures

JS tries to bundle your function with the smallest possible scope. If your function uses variables outside of its own scope, a closure is created at function creation during runtime. This means, the surrounding scope is kept alive for as long as the dependent function is accessible.

Closures aren't inherently bad, but may be confusing or cause of a memory-leak.

Example

createGet initializes object in its scope, and then returns a getter for the object:

function createGet() {
  const object = {};
  return function() {
    return object;
  }
}

let get1 = createGet();
let get2 = createGet();

// They are independent!
get1().name = "get1";
get2().number = 2;

console.log(get1()); // { "name": "get1" }
console.log(get2()); // { "number": 2 }

// Release the closures!
get1 = undefined;
get2 = undefined;

The code looks as if each getter function returns the same object. This is not true, because every call of createGet initializes a different object each time. Because of this, each getter function refers to a different object, making each getter independent.

Since closures bundle a different scope at every creation, they may be a memory-leak. In our example, only after making the getters (and thus the closures) inaccessible (e.g. get1 = undefined), they may be garbage-collected.

Strict-mode

Apart from the default "sloppy mode", JS also features a strict mode.

this context

The current context is effectively the value of this in the current scope.

The context of a function is determined by how the function is called. A function may be called standalone (e.g. aFunc()) or as a method (e.g. obj.aMethod()).

If a function is called on its own, it inherits the surrounding context. If there is no surrounding function context, the surrounding context will either be globalThis in "sloppy mode", or be undefined in strict mode.

If the function is called as a method obj.aMethod(), this will refer to obj.

Additionally, the first argument of the functions Function.prototype.bind, Function.prototype.call and Function.prototype.apply can specify the context of the underlying function.

console.log("Global context:");
console.log("- Sloppy: this == globalThis? "   (this === globalThis));
(function() {
  "use strict";
  console.log("- Strict: this == undefined? "   (this === undefined));
})();

const compareThis = function(o) {
  return this === o;
};
const obj = { compareThis };

console.log("As function:");
console.log("- Default: this == obj? "   compareThis(obj));
console.log("- With call(obj): this == obj? "   compareThis.call(obj, obj));

console.log("As method:");
console.log("- Default: this == obj? "   obj.compareThis(obj));
console.log("- With call(undefined): this == obj? "   obj.compareThis.call(undefined, obj));
.as-console-wrapper{max-height:unset !important}

Finally: The explanation

First, two objects (obj and proto) are initialized. proto will be the prototype of obj.

Because both objects have the property whoami defined, the prototype's property will be shadowed. Since we still want to access the prototype's property, we have to access it directly. To do this, we first get the prototype of obj (Object.getPrototypeOf(obj), or "super"), and then access its property (.whoami, or "super.whoami").

Since whoami was originally called on obj, it would make sense to use obj as the context. For this reason, we call whoami.call(this) (with this equal to obj; effectively "super.whoami()") on the prototype's whoami method, because if we didn't the method would use the prototype as its context.

Because obj is part of the surrounding lexical scope instead of the scope of obj.whoami, whoami creates a closure because of it using the identifier obj in Object.getPrototypeOf(obj). Ideally we would get the prototype using this instead, to not have an unnecessary closure: Object.getPrototypeOf(this).

  • Related