Search code examples
javascripttypescripthoisting

How to get hoisting to work for extended classes?


I'm talking about:

class MyAction {
}
class MyActionEdit extends MyActionNew { // <-- error pointing here
}
class MyActionNew extends MyAction {
}

I'm getting the following error: Uncaught ReferenceError: Cannot access 'MyActionNew' before initialization.

I'm not creating an instance, before all js is loaded. So I don't understand, why hoisting is not working here. The problem is: I cannot change the order of the class definitions (they are loaded from various files and combined to one big script - 3 combined files in this example).

I've already read this and many other questions, but none did answer my question.

This here is working for example:

class MyAction {
}
let test: MyActionNew = new MyActionNew(); // <- Hoisting works here
class MyActionEdit extends MyAction {
}
class MyActionNew extends MyAction {
}

How can I deal with this? I don't know if this helps, but I'm using typescript to generate the javascript files. May be there is something?

Related: Typescript class: will class be hoisted when I use two classes?

I've read all question with the tag "hoisting" here on stackoverflow, but did not find an answer.


Solution

  • In the related link is an example with the Hero class. There are many answer here in stackoverflow, that are telling, that classes are hoisted and that this behaviour changed between ES5 and ES6. Take a look at this answer: Why are ES6 classes not hoisted?

    Yes but no. It is correct that the class name is hoisted. However, that it rather irrelevant to your situation.

    Javascript is executed in two steps:

    1. Parsing/compilation
    2. Runtime

    In the parsing step, the Javascript engine takes your code apart, reads it, tokenises it, and generally converts it into an executable form. In this step, it lays out general structures it's going to use during runtime, among them the names of things and what scope they belong to. So, the name MyActionEdit will be created in the correct scope. That is what hoisting is. Because in the second step, the runtime, when the code actually runs, that name will already exist, even if it comes later in the scope:

    console.log(foo);
    
    var foo = 'bar';

    This executes like:

    1. Parse var foo and create a scope with that name reserved.
    2. Runtime: execute console.log(foo), then execute foo = 'bar'.

    That's why this doesn't raise an error and why it logs undefined. This is what "hoisting" is.

    Now, classes are also defined in their entirety during parse time. The parse step creates the classes. If that parse step succeeds, then the classes are already "hoisted" and available at runtime.* But that is rather irrelevant, since the order of declarations during parse time is wrong, and Javascript cannot extend a class which hasn't been declared yet. It would have to ignore that first class declaration, skip to the next one, and then repeat the parsing process to declare the previously skipped declaration. That can lead to infinite loops and is generally extremely inefficient. Or if it could "hoist" classes first, in what order should they be hoisted?! How should the engine know to hoist the class declaration in the order they need to be in for the declarations to make logical sense?

    It's up to you to declare your classes in the correct order.

    * Except they're not accessible before their declaration because this hoisting behaviour has been deemed confusing and is explicitly being suppressed by the TDZ.