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)
.