69

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,376
  • 9
  • 48
  • 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
  • @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

4 Answers4

63

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
  • 572,313
  • 128
  • 898
  • 1,281
  • 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
15

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,472
  • 1
  • 12
  • 22
7

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
  • 249
  • 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
  • 2
    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
1

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