Prototyping in JavaScript
Before one can understand this
and super
, one must understand prototypes. Below is a demystification of how Object.*
methods work in terms of __proto__
.
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Object.setPrototypeOf = function(object, proto) {
object.__proto__ = proto;
return object;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Object.getPrototypeOf = function(object) {
return object.__proto__;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Object.create = function(proto, props) {
var _object = {__proto__: proto};
if (props) Object.defineProperties(_object, props);
return _object;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Prototypes work like this: accessing object.property
searches object
for "property"
. If object
does not have "property"
, then object.__proto__
is searched. Then, object.__proto__.__proto__
is searched. This keeps going until __proto__
is null
:
console.log(Object.prototype.__proto__); // logs `null`
var ap = Array.prototype.__proto__;
console.log(ap === Object.prototype); // logs `true`
Below is a demystification of how property lookups are performed. Getting object.property
will exhibit the same behavior as propertyLookup(object, "property")
all the time.
function propertyLookup(_o, prop) {
var obj = _o;
do {
var desc=Object.getOwnPropertyDescriptor(obj, prop);
if (desc) {
if (desc.get) return desc.get.call(_o);
return desc.value; // handles all other cases
}
} while (obj = obj.__proto__);
return undefined; // unneccecary because this is default
}
function propertyAssign(_o, prop, _value) {
var obj = _o;
do {
var desc=Object.getOwnPropertyDescriptor(obj, prop);
if (desc) {
if (desc.set) {
desc.set.call(_o, _value);
return _value;
}
if (desc.get) return _value; // no way to handle
if (!desc.writable) return _value;//won't handle
if (obj === _o) { // keep property description
desc.value = _value;
Object.defineProperty(obj, prop, desc);
return _value;
}
break; // handles all other cases
}
} while (obj = obj.__proto__);
Object.defineProperty(obj, prop, {
value: _value,
writable: true,
enumerable: true,
configurable: true
});
return _value;
}
this
this
as that
happens for function f
in 3 and only 3 circumstance in JavaScript as of 2021:
- Property access:
that.callWithThat()
,that["callWithThat"]()
, andthat[0]()
(or any index) - Function methods:
f.call(that)
,f.bind(that)
,f.apply(that)
, andReflect.apply(f, that)
- Constructors:
new f
andReflect.construct(f, [], that)
that.callWithThat()
Whenever you access a property you proceed to call, the object you accessed becomes the this
of the function you called. Observe:
function method() {
console.log( this.name );
}
var inner = {
name: "inner",
handle: method
};
var outer = {
name: "outer",
handle: method,
inner: inner
};
inner.handle(); // logs "inner"
outer.handle(); // logs "outer"
outer.inner.handle(); // logs "inner"
var handle = outer.handle; // or var { handle } = outer;
handle(); // throws an error because `this` is undefined
Note that the prototype of the function does not matter one bit:
var utils = {
print: function() {
console.log( this.value );
}
};
utils.print.value = "Hello!"
utils.print(); // logs `undefined`
utils.value = "World!";
utils.print(); // logs `World!`
However, you can do some interesting trickery using named functions:
var moldable = {
setProto: function protoMethod() {
Object.setPrototypeOf(this, protoMethod);
},
printFoob: function() {
console.log(this.foob);
}
};
moldable.setProto.foob = 10;
moldable.printFoob(); // logs undefined
moldable.setProto();
moldable.printFoob(); // logs `10`
You can also make a non-deterministic object (albeit a very very slow object) via getters:
var options = [
{value: 50},
{value: "dinosaur"},
{value: true},
{value: 1e+99}
];
var nondet = {
get status() {
Object.setPrototypeOf(this, options[
Math.random() * options.length | 0
]);
return "OK";
}
};
console.log(nondet.value); // logs `undefined`
console.log(nondet.status); // logs `OK`
console.log(nondet.value); // logs something random
console.log(nondet.status); // logs `OK`
console.log(nondet.value); // logs something random
The same goes with arrays:
var arr = ["ArrayName", function() {
console.log( this[0] );
}];
arr[1](); // logs "ArrayName"
2. Function methods
Function methods manually set the this
property. Below is the simplest possible demystification of the function methods:
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.call = function(proto, ...args) {
proto.__call_method = this; // `this` is a function
return proto.__call_method(...args);
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.bind = function(proto, ...args) {
var that = this; // `this` is a function
return function() {
proto.__call_method = that;
return proto.__call_method(...args, ...arguments);
};
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.apply = function(proto, argsList) {
proto.__call_method = this; // `this` is a function
return proto.__call_method(...argsList);
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
There are many problems with the simplistic demonstration above:
- A
__call_method
property is left on theproto
object. - The methods don't work when proto is
null
or a primitive - The methods don't have the correct function names
- The
bind
method returns an object with a prototype - The
apply
method doesn't work when the arguments list isnull
For the sake of completeness, below is a standard-compliant demystification of how the function methods work:
// sliceFrom has very similar behavior to Array.prototype.slice
function sliceFrom(array, start) {
var result = [];
for (var k=0, i=start, len=array.length; i < len; k++, i++)
result[k] = array[i];
return result;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.call = function call(that) {
if (that == null) return this(...sliceFrom(arguments, 1));
var proto=typeof that == "object" ? that : that.__proto__;
var uuid = Symbol();
proto[uuid] = this;
var returnValue = proto[uuid](...sliceFrom(arguments, 1));
delete proto[uuid];
return returnValue;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.bind = function bind(that) {
var f = this, superArgs = sliceFrom(arguments, 1);
if (that == null) return function() {
f(...superArgs, ...arguments);
};
var proto=typeof that == "object" ? that : that.__proto__;
var uuid = Symbol();
proto[uuid] = this;
function Binded() {
return proto[uuid](...superArgs, ...arguments);
};
Binded.prototype = undefined;
return Binded;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Function.prototype.apply = function apply(that, _args) {
var args = _args == null ? [] : _args;
if (that == null) return this(...args);
var proto=typeof that == "object" ? that : that.__proto__;
var uuid = Symbol();
proto[uuid] = this;
var returnValue = proto[uuid](...args);
delete proto[uuid];
return returnValue;
};
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
The only two discrepancies from the standard behavior of function methods is the introduction of a symbol upon the proto
, which can be revealed via Object.getOwnPropertySymbols
and the fact that the prototype is still in
the function returned from Function.prototype.bind
.
3. new
constructors
new f
sets the value of this
to be Object.create(f.prototype)
. This is true for functions and classes alike.
function constructor() {
console.log(this.__proto__ === constructor.prototype);
console.log(Object.getOwnPropertyNames(this).length);
}
new constructor(); // logs `true` and `0`
When f
is a plain old function (not a class), it can return an object, which will become the new value of the new f
. Observe:
var refObject = {name: "John"};
function construct() {
return refObject;
}
console.log(new construct().name) // logs `"john"`
console.log(new construct === new construct); // logs `true`
class
es are mostly just sugar syntax for plain old constructors. Below is how one would mix the two together.
function Value(initial) {
this.value = initial;
}
Value.prototype.get = function() {
return this.value;
};
Value.prototype.set = function(newvalue) {
this.value = newvalue;
};
class UnsetError extends Error {} // special error type
class MaybeValue extends Value {
constructor(value, state) {
super( value );
this.isset = !!state;
}
get() {
if (!this.isset)
throw new UnsetError("MaybeValue not ready");
return super.get();
}
set(newvalue) {
this.isset = true;
super.set( newvalue );
}
}
var val = new MaybeValue;
try {
console.log( val.get() ); // throws error
} catch(e) {
if (!(e instanceof UnsetError)) throw e; //propagate
val.set("example value"); // initialize the value
}
console.log( val.get() ); // logs `"example value"`
The real power of class
es comes into play with extend
ing native constructors:
class MyArray extends Array {
constructor() {
super("hello", ...arguments);
}
set(index, value) {
this[index] = value;
}
}
var arr = new MyArray;
arr.set(1, 10);
arr.set(2, 20);
console.log( arr.length ); // logs 3, just like a native array
console.log( arr[0] ); // logs `"hello"`
arr.length = 0;
console.log( arr[2] ); // logs `undefined` like a native array
In the above example, we extended the Array
constructor, and our MyClass
behaved exactly as a native array would. There is no pretty way to achieve this same behavior with old constructors. The solution below is how it would be done, and it's pretty ugly and slow because you have to emulate the behavior of length
with a getter and setter.
var lengthSymbol = Symbol("length");
var arrayDescriptor = {
get: function() {
var max = this[lengthSymbol];
for (var key in this)
if (max < key && Math.floor(key) === +key)
if (this.hasOwnProperty(key))
max = +key;
return max;
},
set: function(_val) {
var value = +_val;
if (value < 0 || Math.floor(value) !== value)
throw RangeError("Invalid array length");
this[lengthSymbol] = value;
for (var key in this)
if (value <= key && Math.floor(key) === +key)
delete this[key];
}
};
function extendArray(proto) {
Object.setPrototypeOf(proto, Array.prototype);
proto[lengthSymbol] = 0;
Object.defineProperty(proto, "length", arrayDescriptor);
}
function MyArray() {
this[0] = "hello";
for (var k=1, i=0, len=arguments.length; i < len; k++, i++)
this[k] = arguments[i];
this[lengthSymbol] = k;
}
extendArray( MyArray.prototype );
MyArray.prototype.set = function(index, value) {
this[index] = value;
};
var arr = new MyArray;
arr.set(1, 10);
arr.set(2, 20);
console.log( arr.length ); // logs 3, just like a native array
console.log( arr[0] ); // logs `"hello"`
arr.length = 0;
console.log( arr[2] ); // logs `undefined` like a native array
super
super
means exactly the same thing as this.__proto__
except that super
is a keyword so JavaScript won't execute (it raises a SyntaxError
) when super
is put in the wrong place.
var object = {
__proto__: {
value: [1, 2, 3],
},
value: ["a", "b", "c"],
printThisValue: function() {
console.log(this.value);
},
printSuperValue() {//same as printSuperValue: function()
console.log(super.value);
},
printThisProtoValue: function() {
console.log(this.__proto__.value);
}
};
object.printThisValue(); // logs ["a", "b", "c"]
object.printSuperValue(); // logs [1, 2, 3]
object.printThisProtoValue(); // [1, 2, 3], same as super
new.target
v.s. this
They both serve different purposes, but they are both local to the function being called. new.target
is either undefined
or a callable function whose .prototype
property was used to create this
:
function printTarget() {
// Both new.target and this are undefined here
console.log(new.target);
};
new function c() {
console.log(new.target === c); // logs `true`
var p = new.target.prototype; // c.prototype
console.log(p === this.__proto__); // logs `true`
printTarget(); // logs `undefined`
};
However, without new
, new.target
is undefined
everywhere:
(function() {
console.log(this); // logs `[1,2,3]`
console.log(new.target); // logs `undefined`
}).call([1,2,3])
new.target
, just like this
, is still visible in local arrow functions, as these arrow functions allow this
and new.target
to pass through.
new function c(){
this.hello = 427;
(() => {
console.log(this.hello);
console.log(new.target === c);
})(); // logs `427` and `true`
}
new.target
has the same lifespan as this
and persists within arrow expressions beyond the scope of the original instantiation. The value of this
and new.target
are frozen in time at the location where the arrow function is created.
(new function c(){
this.hello = 427;
return msg => {
console.log(msg);
console.log(this.hello);
console.log(new.target === c);
};
})("Foob"); // logs `"Foob"`, `427`, and `true`
new.target
is important because, without it, userland functions are unable to determine whether they are supposed to initialize an object.
Correctly understanding __proto__
The very first snippet at the top of this post exhibits wrongful behavior in the following two circumstances:
- when the object doesn't inherit from
Object.prototype
, for example,Object.create(null)
- When
__proto__
is shadowed (e.x.Object.defineProperty({}, "__proto__", {value: "test"})
would not change the internal proto and further attempt to read the object's__proto__
would yield"test"
)
To see this in action, observe the following code:
var n = new Number( 47 );
console.log( Object.keys( n ) ); // logs []
console.log( JSON.stringify(n) ); // logs "4.7"
console.log( n.toExponential() ); // logs "4.7e+1"
// This is equivalent to n.__proto__ = n.__proto__
n.__proto__ = Number.prototype;
console.log( Object.keys( n ) ); // logs []
console.log( JSON.stringify(n) ); // logs "4.7"
console.log( n.toExponential() ); // logs "4.7e+1"
n.__proto__ = null;
console.log( Object.keys( n ) ); // logs []
console.log( JSON.stringify(n) ); // Uncaught TypeError: can't convert n to number
console.log( n.toExponential() ); // Uncaught TypeError: n.toExponential is not a function
// Because n no longer inherits from Object.prototype, __proto__ is now an ordinary property
n.__proto__ = Number.prototype;
console.log( Object.keys( n ) ); // logs ["__proto__"]
console.log( JSON.stringify(n) ); // Uncaught TypeError: can't convert n to number
console.log( n.toExponential() ); // Uncaught TypeError: n.toExponential is not a function
It errors because __proto__
is actually a non-special ordinary property on Object.prototype
. Thus, the correct way to describe __proto__
is to write the getters and setters for Object.prototype.__proto__
in terms of Object.getPrototypeOf
and Object.setPrototypeOf
.
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
Object.defineProperty(Object.prototype, "__proto__", {
configurable: true,
enumerable: false,
get: function() {
return Object.getPrototypeOf( this );
},
set: function(newProto) {
Object.setPrototypeOf( this, newProto );
}
});
// !!! DO NOT INSERT THIS INTO YOUR CODE !!! //
The above code will always exhibit the proper behavior of __proto__
. See this for yourself by running Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")
in the console and observing the native getter and setter methods defined there.
That's all. I hope this article helped expand your knowledge of JavaScript's inner workings.
Top comments (2)
maybe you forgot to return the result of
this()
inif (that == null) this(...);
Yes I did forget to return that. I have now fixed it. Thank you for pointing this problem out.