77

Since ES6 classes are just a syntactical sugar over JavaScript's existing prototype-based inheritance [1] it would (IMO) make sense to hoist it's definition:

var foo = new Foo(1, 2); //this works

function Foo(x, y) {
   this.x = x;
   this.y = y;
}

But the following won't work:

var foo = new Foo(1, 2); //ReferenceError

class Foo {
   constructor(x, y) {
      this.x = x;
      this.y = y;
   }
}

Why are ES6 classes not hoisted?

Petr Peller
  • 8,581
  • 10
  • 49
  • 66
  • 7
    ES6 classes *aren't* just syntactic sugar, although they're *mostly* syntactic sugar. – T.J. Crowder Feb 21 '16 at 14:56
  • 4
    Hoisting has been an almost endless source of misunderstanding and confusion. All of the new declaration constructs (`let`, `const`, `class`) added in ES6 are un-hoisted (well, they're *half-hoisted*). Barring a quote from Eich or similar, you're not going to get an answer that isn't effectively speculation. – T.J. Crowder Feb 21 '16 at 14:57
  • 1
    @mmm: MDN is edited by the community, and sometimes wrong. Not often, not nearly as often as, say, that other site, but sometimes. See [this answer](http://stackoverflow.com/a/31222689/157247) for how they're both hoisted and not hoisted. – T.J. Crowder Feb 21 '16 at 15:01
  • Ok, interesting - I've read Bergi's answer before and was wondering about mdn... @T.J.Crowder – baao Feb 21 '16 at 15:02
  • @T.J.Crowder So do you think the ES6 committee just decided that hoisting (as in ES6 functions) is wrong? – Petr Peller Feb 21 '16 at 15:19
  • 4
    @PetrPeller: I think they decided it was wrong for variables, constants, and classes, yes, and very likely because of issues such as the one Bergi mentioned above. I find the fact that *functions* are hoisted useful, but I don't know that they'd agree. Where it breaks down is when you have things that are both hoisted (function decls) and non-hoisted (adding properties to them or their `prototype` object). But normal functions, it's quite handy. – T.J. Crowder Feb 21 '16 at 15:26
  • 3
    One implication of this is that you can't put "module.exports = MyClass" at the top of the file, and then declare "class MyClass { ... }" later. This won't work. I find this unfortunate, because I like to put the "exports" at the top to make the API readily visible. – Duncan Jan 15 '19 at 22:29

5 Answers5

68

Why are ES6 classes not hoisted?

Actually they are hoisted (the variable binding is available in the whole scope) just like let and const are - they only are not initialised.

It would make sense to hoist its definition

No. It's never a good idea to use a class before its definition. Consider the example

var foo = new Bar(); // this appears to work
console.log(foo.x)   // but doesn't

function Bar(x) {
    this.x = x || Bar.defaultX;
}
Bar.defaultX = 0;

and compare it to

var foo = new Bar(); // ReferenceError
console.log(foo.x);

class Bar {
    constructor (x = Bar.defaultX) {
        this.x = x;
    }
}
Bar.defaultX = 0;

which throws an error as you would expect. This is a problem for static properties, prototype mixins, decorators and everything. Also it is quite important for subclassing, which broke entirely in ES5 when you used a class with its non-adjusted prototype, but now throws an error if an extended class is not yet initialised.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    To clarify in your first example, the issue is that `console.log(foo.x)` would yield `undefined` instead of `0` not that there would be a run time error. – Jorge Cabot Aug 04 '17 at 21:08
  • You should change that example to something more robust. In your `class` example, I can still insert my instantiation+console.log between the definition of the class and the `Bar.defaultX = 0` assignment, and the log would still print undefined :) Consider using a simple method instead of a static property, and calling that method in the log. Methods are defined inside the `class` definition, whereas they need a separated `prototype` assignment in ES5 version. That seems fully unambiguous this way. – Aurelien Ribon Jan 19 '19 at 13:41
  • @AurélienRibon One should consider the class definition statements as one unit. You wouldn't put unrelated instantiation code in the middle of it :-) The point is that some parts of a class definition, like creation of static property values, mixin or decorator calls, and superclass expressions cannot be hoisted. A method definition could have been made to hoist with a `class` it is declared in. – Bergi Jan 19 '19 at 13:45
  • I'm not sure I see the issue in the example given. If `Bar.defaultX = 0;` is placed outside the function or class then I would not expect it to have run. That is, I could also place `var foo = new Bar();` after the class or function declaration but before the default assignment and expect the same results except that this *would* be valid code in the case of the class. – BVernon Jan 19 '21 at 06:21
  • @BVernon Would you also not have expected it to run if it was `class Bar { static defaultX = 0 }` (like the public class fields proposal offers)? It's the same code, just desugared - e.g. transpilers would use this. – Bergi Jan 19 '21 at 08:14
  • Ugh, sorry. Not sure I said that correctly. I meant I would expect the code to compile and run in the case of the function, but that specific statement wouldn't run before you use the function. Put another way, I don't understand why hoisting would cause a problem in your 2nd code block. How is it different from the 1st where hoisting is done? Hope that makes sense. – BVernon Jan 19 '21 at 18:49
  • That is, if hoisting were allowed in your second code block, how would it introduce any issue not present in the 1st code block? – BVernon Jan 19 '21 at 18:50
  • @BVernon The issue would be that code with the mistake would not throw an exception but fail silently with logging `undefined`, just like the first snippet does. The failure mode of the uninitialised `class` variable is an improvement over the hoisted declaration, using ES6 gives you the advantage of easily locating the mistake. – Bergi Jan 19 '21 at 20:02
  • tbh I still don't understand the reasoning for this behavior after the answer given that a far more common use case for classes (at least more common than the error situation depicted) is extending a class in a separate file — because of the the hoisting behavior this file separation becomes impossible in some situations involving cyclic references. At that point it's basically back to stuffing everything into one file (or some other nasty work around). – Ian Apr 03 '22 at 20:07
  • @Ian It is possible to extend classes in separate files, even with circular references - you just would need to ensure that your modules are evaluated in the right order, so that the parent class is always initialised first. You cannot create a class inheriting from a prototype that doesn't exist yet. Granted, guaranteeing evaluation order in circular dependent modules is not easy… – Bergi Apr 04 '22 at 01:44
  • I understand the fact of the limitation I'm just angry-confused by how it could be that es modules were, as I understand it, specifically designed to handle cyclic references but users are still stuck handling evaluation order via arcane workarounds in certain circumstances. – Ian Apr 05 '22 at 01:17
  • As others have said, it seems Bergi is misunderstanding the question. IMO javascript's spec *should* be changed hoist the *definition* and not just the declaration of a class. The class declaration is entirely useless until its definition is attached to it. The extends clause would almost never cause any problems in this regard, and if it did, only then would you have to pay attention to the positioning of the class definition. – B T Dec 13 '22 at 19:44
  • @BT There is no misunderstanding. The question is why classes behave as they behave, and my answer gives the design rationale for that. The `extends` clause, static blocks/initialisers, decorators, and similar features implemented by hand: they all need to evaluate code and **do** cause problems if they were to be hoisted. The solution is simple: define your class before you use it. I don't see what's wrong with that, or why you would need the spec to change. – Bergi Dec 13 '22 at 20:41
  • Functions hoist not only the name but also the definition. This is quite useful. It could be useful for classes too if only they hoisted in the same way, but they don't. The fact that you don't see what's wrong with that is the misunderstanding. – B T Dec 13 '22 at 21:02
  • @BT Hoisting in general [is very useful](https://stackoverflow.com/a/52880419/1048572), but calling a function before it is defined is less useful than you might think. Do you have a concrete example? And hoisting the definition of the function is only possible because initialising a function object does not have any side effects, it is pure. This is not possible with classes (at least in general - and would pose a refactoring hazard if exceptions were made). – Bergi Dec 13 '22 at 21:11
  • @BT If you have a concrete problem where you would need a hoisted class definition and can't solve it, you might want to [ask a new question](https://stackoverflow.com/questions/ask) with the code, where I'm happy to help. If you really want to suggest changing the spec, you may want to [discuss this at es.discourse](https://es.discourse.group), but I predict you'll be shot down there, if you don't have a really convincing use case. – Bergi Dec 13 '22 at 21:14
  • There is no problem that can't be solved without class definition hoisting. It would be purely a quality of life improvement that would align behavior of major types of declarations in javascript. How are you not getting this? – B T Dec 14 '22 at 06:17
  • @BT Oh, I totally get that you think your life would be easier if you had this. But still, it is impossible to do this, if you did it nonetheless it would introduce hazards that would make life harder than a predicatable error. Notice that the behavior of declarations *is* aligned already: `class` works like `let` or `const`. You could similarly make the argument that hoisting an initialisation like `const five = 5` or `const { PI } = Math` would be a "qualitity of life improvement" - but it would still be a bad idea to execute code out of order. – Bergi Dec 14 '22 at 13:14
16

While non-hoisted classes (in the sense that they behave like let bindings) can be considered preferable as they lead to a safer usage (see Bergi's answer), the following explanation found on the 2ality blog seems to provide a slightly more fundamental reason for this implementation:

The reason for this limitation [non-hoisting] is that classes can have an extends clause whose value is an arbitrary expression. That expression must be evaluated in the proper “location”, its evaluation can’t be hoisted.

Oliver Sieweke
  • 1,812
  • 2
  • 14
  • 23
  • That is a good reason. However if there is no extends clause, then I think hoisting should be allowed (in the next version of the standard). It would be just so much more convenient for instance to have an export-statement which exports a class defined later in the same module. And it would be backwards compatible. – Panu Logic Apr 30 '23 at 20:32
9

In Javascript all declarations (var, let, const, function, function*, class) are hoisted but it should be declared in same scope.

As you told "ES6 classes are just a syntactical sugar over JavaScript's existing prototype-based inheritance"

So Let's understand what it is?

Here you declared a class which is in fact "special function".Let's assume that your function Foo() and class Foo both are in global scope.

class Foo {
   constructor(x, y) {
      this.x = x;
      this.y = y;
   }
}

Following is the compiled code of your class Foo.

var Foo = (function () {
    function Foo(x, y) {
        this.x = x;
        this.y = y;
    }
    return Foo;
}());

Internally your class is converted to function with the same name inside wrapper function(iife) and that wrapper function returns your function.

Because your function's(class) scope is changed. and you are trying to create object of function in global scope which is in reality not exist.

you get the function in variable Foo once compilation comes to that. so later you have function in var you can create object of that.

mihir hapaliya
  • 279
  • 2
  • 9
  • 1
    While it has a similar effect, this is not what happens. No IIFE is created to evaluate the class. https://www.ecma-international.org/ecma-262/8.0/#sec-runtime-semantics-classdefinitionevaluation – Felix Kling Sep 22 '17 at 04:46
  • if i am not wrong this a iife syntax (function () {}()). if you compile class this code will be generated.since it is assigned to var it is not invoked globally. instead its self invoked and returns value to Foo. – mihir hapaliya Sep 23 '17 at 11:38
  • 3
    If you use something like babel, yes. But environments that support classes natively won’t do that. – Felix Kling Sep 23 '17 at 14:18
  • class isn't hoisted.. see this page. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes – seunggabi Sep 17 '18 at 03:06
  • `let ... ` is not hoisted either – JohnRC Dec 05 '20 at 09:20
  • As pointed out by others, this answer is plainly incorrect and should be deleted. – Domino Aug 26 '21 at 19:14
3

Classes are not hoisted because, for example when a class extends an expression rather than a function, error occurs:

 class Dog extends Animal {}
 var Animal = function Animal() {
 this.move = function () {
 alert(defaultMove);
 }
 }
var defaultMove = "moving";
var dog = new Dog();
dog.move();

After hoisting this will become:

var Animal, defaultMove, dog;
class Dog extends Animal {}
Animal = function Animal() {
this.move = function () {
alert(defaultMove);
}
}
defaultMove = "moving";
dog = new Dog();
dog.move();

Such at the point where class Dog extends Animal is interpreted Animal is actually undefined and we get an error. We can easily fix that by moving the Animal expression before the declaration of Dog. Pls see this great article about the topic: https://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html

0
var foo = new Foo(1, 2); 

//may omit new keyword
var foo=Foo(1,2)

//use class instead of instance by
var fooClass=Foo().constructor
var foo=new fooClass(1,2)

function Foo(x,y){
return new class Foo {
   constructor(x, y) {
      this.x = x;
      this.y = y;
   }
}(x,y)
}