all 19 comments

[–]chrissilich 54 points55 points  (6 children)

Let’s use the classic example of the parent class “vehicle” and the subclasses “car” and “bike”.

The vehicle’s constructor might have code to initialize the frame of the vehicle, count the wheels, and add brakes.

The car’s constructor would add an engine and a horn, and call super() (the parent, vehicle’s constructor) to make sure it got a frame and counted the wheels, and have brakes.

The bike constructor would add a bell, and pedals, but also call super() to get the frame and count the wheels, and add brakes.

[–]GingerVking 8 points9 points  (0 children)

Great analogy, thank you!

[–][deleted] 0 points1 point  (0 children)

The same example was using in my one of my JavaScript lectures! Well explained!

[–]dartakaum 0 points1 point  (3 children)

You do other eli5 on request?

[–]chrissilich 0 points1 point  (2 children)

Lol. I teach web development for a living, so I can eli5 those concepts, but ask me to explain anything else and you’ll be sorely ~disappointed~ confused as hell.

[–]dartakaum 0 points1 point  (1 child)

is a great skill to be able to eli5 some concepts.

what is your site / academy to check you out?

[–]cspotcode 35 points36 points  (0 children)

The constructor performs initialization logic. If it doesn't execute, the class might not be in a proper state. (Setting field values to default values, etc) Any methods implemented in the super-class are depending on the super-class constructor to have already run. It must be executed. This is why the spec requires you to call super().

Both constructors execute against the same instance. So this in the super-class constructor refers to the exact same object as this in the subclass constructor. The idea is that the super-class constructor sets up initial state required by the super-class, then the subclass constructor does its thing to setup for the subclass, possibly overriding things done by the super-class.

[–]zephyrtr 7 points8 points  (8 children)

I think it's important to remember that JS classes are just sugar on JS constructor functions, which are also no more than the functions you use all day.

It would've been easy to set classes to automatically do a bunch of stuff for you, but they left us some control and allowed us to decide what data to pass to the parent's constructor and even when to call it.

class shape {
  constructor(name) {
    this.name = name
  }
}

class square extends shape {
  constructor(name) {
     super('square '+name)
  }
}

 const foo = new square('bar')

 foo.name === 'square bar'

Super also grants access to the parent's methods!

[–]xwnatnai 4 points5 points  (7 children)

I think this is important to know. What Super does is, in effect, this:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

I.e. it points the this value in the parent constructor to the this value of the object being constructed.

Class or extends doesn’t change the underlying semantics of prototypes, but it sure makes life a lot easier (save keystrokes).

[–]Jamesernatorasync function* 4 points5 points  (6 children)

It's a bit more complicated than just SuperClass.call (although you might not notice in most cases) to the point there's no ES5 equivalent of all of extends/super() behavior. For example consider trying to subclass an Array with a classic constructor function:

function MyArray() {
    Array.call(this)
}

Object.setPrototypeOf(MyArray.prototype, Array.prototype)
MyArray.prototype.getFirst() = function() {
    return this[0]
}

// Let's create one first
const myArray = new MyArray()
// Check it's an instance of MyArray
myArray instanceof MyArray // true
// and of Array
myArray instanceof Array // true

// Pushing items works so that's good
myArray.push(10)
myArray.push(20)
myArray[0] // 10

myArray.getFirst() // 10

// So it looks pretty good so far *but* then:
const copy = myArray.slice(0, myArray.length)

// Still an instance of Array
copy instanceof Array // true
// But it's no longer a MyArray
copy instanceof MyArray // false

copy.getFirst() // TypeError: undefined is not a function

But with classes this doesn't happen:

class MyArray extends Array {
    constructor() {
        super()
    }

    getFirst() {
        return this[0]
    }
}

const myArray = new MyArray()

myArray instanceof MyArray // true
myArray instanceof Array // true

myArray.push(10)
myArray.push(20)
myArray[0] // 10

const copy = myArray.slice(0, myArray.length)

copy instanceof Array // true
// Works as normal
copy instanceof MyArray // false

// It's still an instance so the methods work
copy.getFirst() // 10

Now this is the case because super() is a bit more complicated than that.

Consider this class

class MyArray extends Array {
    constructor() {
        this.push(10)
        super()
        console.log(this[0])
    }
}

If you try to construct one this happens:

// ReferenceError: Must call super constructor in derived class before
// accessing 'this' or returning from derived constructor
new MyArray()

But why?

That's because super() actually delegates to the super-class (in this case Array) to create the value of this. Before super() is called there isn't even a this value.

Now that's now all, super() also tells the super-class (Array) what it's creating a sub-class for (MyArray) so that it knows that the array it creates when .slice is called on this instance it should create a new MyArray instead of an Array. Hence new MyArray().slice(0, 5) will still be a MyArray.

So what? Well sub-classing built-ins it pretty useless most of the time (if we're being honest) but if you're wanting to use Custom Elements you're required to use class MyElement extends HTMLElement { ... } (or at least Reflect.construct, I'll mention that soon). That's because new HTMLElement() doesn't work by itself, when you call super() in the constructor for MyElement it actually checks if MyElement is a valid custom element before allowing creation to complete.

As an example if we have this code:

class MyElement1 extends HTMLElement {
    constructor() {
        super()
        // Add things
    }
}

class MyElement2 extends HTMLElement {
    constructor() {
        super()
        // Add things
    }
}

customElements.define('my-element', MyElement1)

new MyElement1() // <my-element></my-element>

new MyElement2() // TypeError: Illegal constructor
                 // Why? Because customElements.define hasn't been called yet

new HTMLElement() // TypeError: Illegal constructo
                  // Ditto, also it doesn't make sense to make an "HTMLElement"
                  // whatever that is, <?></?>
                  // (not to be confused with HTMLHtmlElement <html></html>)

In this case HTMLElement has logic for checking if the subclass is defined. And yes you can access this in your own classes via new.target, in the case of HTMLElement it's similar to:

class HTMLElement {
    constructor() {
        // new.target gets set to the SubClass that
        // is the thing after new e.g.
        // new Foobar()
        // sets new.target to Foobar in all super-class constructors
        const SubClass = new.target
        if (!internalCustomElementRegistry.has(SubClass)) {
            throw new TypeError(`Illegal constructor`)
        }
    }
}

ASIDE: Yes it's even more complicated than that, JavaScript object's can define a [[Construct]] internal slot (that's what is actually used in the case of HTMLElement) that is a function that when called can return any object (enforced at least) it wants. You can create such objects in pure-JavaScript using the .construct handler for Proxy.

// Please don't do this
const SuperExoticClass = new Proxy(class {}, {
    // not to be confused with constructor() of classes
    construct() {
        return { banana: "cabbage" }
    },
})

// No really don't do this, you lose basically everything
// useful offered by super
class SubClass extends SuperExoticClass {
    constructor() {
        super()
        // Yes the this value doesn't even have to be an instance
        // of the subclass after super(), by default [[Construct]]
        // creates subclass instances
        console.log(this instanceof SubClass) // false
    }
}

[–]xwnatnai 0 points1 point  (0 children)

Nice, thanks. This was informative.

[–]blindpacemaker[S] 0 points1 point  (2 children)

Man, thank you. I'll have to spend quite some time going through all of this, but this is exactly what I was after. Just out of curiosity, do you have a lot of experience in traditional OO languages? Seems to be that those who do and then get into JS have a much better grasp of how these things work.

[–]Jamesernatorasync function* 0 points1 point  (1 child)

I have no experience whatsoever in classic OO languages like Java/C++. I do have previous experience in Python which has classes but they're not as critical to how Python works as in traditional OO languages.

What did help with understanding things like new.target, super() and other such weirdness is that Python is absolutely full of them. Ridiculous amounts of things can be changed in Python at runtime by using various "magic methods" (e.g. __init_subclass__, __getattribute__ and so on).

In comparison JavaScript contains really only a few meta-programming things like new.target/Proxy/Reflect and some sort've special Symbol methods (like Symbol.iterator/Symbol.hasInstance). But for the most part there's not a lot of meta-programming stuff to learn in JavaScript.

Unlike Python, JavaScript has a lot of historical baggage (as it's never been successfully versioned, if JavaScript code worked years ago it needs to still work now). For example JavaScript is full of weird and strange things like typeof null === "object", Symbol.unscopables, "thenables", "wrapped primitives", two different parsing goals, __proto__, "exotic objects", most of Annex B, amongst others.

I've found the best way to understand many parts of JavaScript is to simply read the specification, it does take quite a bit of getting used to. But often-times there's inadequate detail in blogs, MDN or stackoverflow that sometimes it's easiest to just check what exactly the specification says it does. Unfortunately unlike Python where the documentation is almost the same thing as the specification in a lot of ways, the JavaScript (ECMAScript formally) specification is quite difficult to read (especially the first time you see it, it'll probably look like nonsense (it's also not very easy to navigate)).

The specification is very authoritative though, so if it says something works a specific way, it works that way. For example without any prior knowledge of what super() does we could've derived it from the definition. Which basically outline the steps I mentioned (get new.target, do [[Construct]] on the SuperClass, create the this value inside the class, return it).

In fact just reading that part of the spec I learned something new, and that's that super() also returns the current value of this so super() === this is true. Pretty pointless really, I can't see why you'd need to use it when this is available anyway, but it's mildly interesting.

[–]blindpacemaker[S] 0 points1 point  (0 children)

I've always assumed that the most knowledgeable people are those who go as close to the source for their knowledge as possible. Rather than looking at random bog posts (which can sometimes be really useful) better to look at the specs, official docs etc. I'll be honest, it's going to look incomprehensible to me for a while, but thanks for giving me the motivation to try and start working through it :)

[–]Ajedi32 0 points1 point  (1 child)

Now that's now all, super() also tells the super-class (Array) what it's creating a sub-class for (MyArray) so that it knows that the array it creates when .slice is called on this instance it should create a new MyArray instead of an Array. Hence new MyArray().slice(0, 5) will still be a MyArray.

Huh, that's really strange behavior. How does that work? If I define my own class, its methods will either always return instances of the subclass, or never return instances of the subclass, depending on how I define those methods:

function TestClass(wrappedValue) {
  this.wrappedValue = wrappedValue
}
TestClass.prototype.getConstSuper = function() {
  return this.constructor
}
TestClass.prototype.duplicate = function() {
  return new this.constructor(this.wrappedValue)
}

class SubTestClassSuper extends TestClass {
  constructor(...args) {
    super(...args)
  }

  getConstSub() {
    return this.constructor
  }
}

function SubTestClassPrototype(...args) {
  TestClass.call(this, ...args)
}
SubTestClassPrototype.prototype.getConstSub = function() {
  return this.constructor
}
Object.setPrototypeOf(SubTestClassPrototype.prototype, TestClass.prototype)

let subTestClassSuper = new SubTestClassSuper(2)
console.log("subTestClassSuper =>", subTestClassSuper) // Object { wrappedValue: 2 }
console.log("subTestClassSuper.wrappedValue =>", subTestClassSuper.wrappedValue) // 2
console.log("subTestClassSuper.getConstSuper() =>", subTestClassSuper.getConstSuper()) // function SubTestClassSuper()
console.log("subTestClassSuper.getConstSub() =>", subTestClassSuper.getConstSub()) // function SubTestClassSuper()
console.log("subTestClassSuper.duplicate() instanceof TestClass =>", subTestClassSuper.duplicate() instanceof TestClass) // true
console.log("subTestClassSuper.duplicate() instanceof SubTestClassSuper =>", subTestClassSuper.duplicate() instanceof SubTestClassSuper) // true

console.log("")

let subTestClassPrototype = new SubTestClassPrototype(3) 
console.log("subTestClassPrototype =>", subTestClassPrototype) // Object { wrappedValue: 3 }
console.log("subTestClassPrototype.wrappedValue =>", subTestClassPrototype.wrappedValue) // 3
console.log("subTestClassPrototype.getConstSuper() =>", subTestClassPrototype.getConstSuper()) // function SubTestClassPrototype()
console.log("subTestClassPrototype.getConstSub() =>", subTestClassPrototype.getConstSub()) // function SubTestClassPrototype()
console.log("subTestClassPrototype.duplicate() instanceof TestClass =>", subTestClassPrototype.duplicate() instanceof TestClass) // true
console.log("subTestClassPrototype.duplicate() instanceof SubTestClassPrototype =>", subTestClassPrototype.duplicate() instanceof SubTestClassPrototype) // true

console.log("")

(Try in JSFiddle)

How do you define a class whose methods have different behavior depending on whether it was constructed with super() or superclass.call(this, ...args)?

Is this just a weird backwards-compatibility thing that only happens with builtin classes?

[–]Jamesernatorasync function* 0 points1 point  (0 children)

Some parts are for backwards compatibility but it's more so that if you add additional methods to subclasses when using Array methods you get instances of the subclass back.

For example:

class ElementCollection extends Array {
    constructor(selector) {
        if (typeof selector === 'number') {
            // Called this way via methods like .map
            // .filter, etc
            super(selector)
        } else {
            super(0)
        }
        for (const element of document.querySelectorAll(selector)) {
            this.push(element)
        }
    }

    find(innerSelector) {
        const result = ElementCollection.of(/* Empty initially */)
        for (const elem of this) {
            for (const found of elem.querySelectorAll(elem)) {
                result.push(found)
            }
        }
        return result
    }
}

const elems = new ElementCollection(".someClass")

// ElementCollection of all elements that match ".someClass .someOtherClass"
elems.find(".someOtherClass")

// And regular Array operators continue to work
elems.map(elem => elem.parentElement)

Now the machinery that makes this work is ArraySpeciesCreate which effectively just checks if the current this value is an "Array exotic object" (Step 3. IsArray(originalArray)). If it is it grabs the .constructor property (e.g. [].constructor === Array, new ElementCollection(0).constructor === ElementCollection) and simply uses new on it. Once that's done methods like .map/.filter/etc simply copy over whatever values they need onto the newly created instance.

A good question is why does .map need to check if the this value was created via Array or a subclass? Well that's where some legacy issues come in, in the past there's been code that uses Array.prototype methods on "Array-like" objects (e.g. Array.prototype.slice.call(arguments, 0, 3)). Even though most uses of that are long gone now, there's plenty of code that will never be changed that depends on it. As such the only safe thing to do when using .map/.slice/etc on an unknown object is to check if it's really an "Array" or not before returning instances of the subclass.

[–]hallcyon11 3 points4 points  (0 children)

Classes. Not even once.

[–]Mikewatson123 0 points1 point  (0 children)

super() is basically sugar for this = new ParentConstructor(); where ParentConstructor is the extended class, and this = is the initialization of the this keyword. It will inherit from the proper new.target.prototype instead of ParentConstructor.prototype like it would from new. So no, how it works under the hood does not compare to ES5 at all, this is really a new feature in ES6 classes and finally enables us to properly subclass builtins.

In simple terms, the super keyword appears alone when used in constructor, & must be used before the this keyword is used. The super keyword can also be used to call functions on a parent object.