DEV Community

loading...
Cover image for this and super in JavaScript

this and super in JavaScript

anonyco profile image Jack Giffin Updated on ・7 min read

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 !!! //
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

this

this as that happens for function f in 3 and only 3 circumstance in JavaScript as of 2021:

  1. Property access: that.callWithThat(), that["callWithThat"](), and that[0]() (or any index)
  2. Function methods: f.call(that), f.bind(that), f.apply(that), and Reflect.apply(f, that)
  3. Constructors: new f and Reflect.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
Enter fullscreen mode Exit fullscreen mode

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!`
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The same goes with arrays:

var arr = ["ArrayName", function() {
    console.log( this[0] );
}];
arr[1](); // logs "ArrayName"
Enter fullscreen mode Exit fullscreen mode

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 !!! //
Enter fullscreen mode Exit fullscreen mode

There are many problems with the simplistic demonstration above:

  1. A __call_method property is left on the proto object.
  2. The methods don't work when proto is null or a primitive
  3. The methods don't have the correct function names
  4. The bind method returns an object with a prototype
  5. The apply method doesn't work when the arguments list is null

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) 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) 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 !!! //
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

classes 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"`
Enter fullscreen mode Exit fullscreen mode

The real power of classes comes into play with extending 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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`
};
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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`
}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

new.target is important because, without it, userland functions are unable to determine whether they are supposed to initialize an object.

That's all. I hope this article helped expand your knowledge of JavaScript's inner workings.

Discussion (0)

pic
Editor guide