Home > Blockchain >  What is the purpose of classScope in the EcmaScript specification?
What is the purpose of classScope in the EcmaScript specification?

Time:05-13

I was reading the steps in the specification (12th edition) that occur when a class declaration or expression is used, and saw that it starts off by creating a class scope at section 15.7.7 Runtime Semantics: ClassDefinitionEvaluation:

1. Let env be the LexicalEnvironment of the running execution context.
2. Let classScope be NewDeclarativeEnvironment(env).
...

I can then see that this algorithm in the spec then creates and sets the classBinding as a binding of the classScope envrionment record (at steps 3a, and 19a), but other than that, I can't seem to see what else it is being used for. If that's the case, it seems like creating a classScope would only be needed for class expressions (where the class name is available from only within the class and not in the scope its declared) and not class declarations. I'm confused as to why classScope is created for class declarations also (15.7.8 for class declarations runs the above algorithm 15.7.7) when the class name binding is added to the surrounding scope, perhaps it's something to do with when a class uses extends?

CodePudding user response:

Even setting aside modern features added after that milestone copy of the spec was published (1, 2), class declarations still needed class scope to handle the class binding correctly within the class.

You noted that a class declaration creates a binding for the class name in the scope containing the declaration, whereas a class expression doesn't:

class Example1 { }
console.log(typeof Example1); // "function"
const x = class Example2 { };
console.log(typeof Example2); // "undefined"

While that's true, the inner binding in the class scope is still created, and that's important for resolving the class binding correctly within it. The constructor and methods within the class shouldn't have to rely on that external binding for the class binding, not least because that external binding is mutable:

"use strict";
// Class declaration, creates binding in the current scope
class Example {
    method() {
        // Note this uses `Example`
        return new Example();
    }
}

// **BUT**, code can change that binding
const OldExample = Example;
Example = {};

const e1 = new OldExample();
// Should this fail because `Example` has been reassigned, but it's used by `method`?
const e2 = e1.method();
// No, it works just fine
console.log(e2 instanceof OldExample); // true

If method relied on the outer binding of Example, it would be using the wrong thing when it did new Example. But thanks to class scope, it doesn't rely on that binding, it relies on the inner binding for it within the class scope (which is immutable).

As I mentioned, class fields and methods make further use of the class scope, but it was needed even before they were added.

One tricky bit is this sequence you pointed out in a comment on my previous incorrect answer:

  • 5.a. Set the running execution context's LexicalEnvironment to classScope.
  • 5.b. Let superclassRef be the result of evaluating ClassHeritage.
  • 5.c. Set the running execution context's LexicalEnvironment to env.

Why set the running execution context's LexicalEnvironment to classScope just to evaluate ClassHeritage?

Bergi came up with the answer to that, and it's the same answer as for method above: So that the class's binding is properly resolved within ClassHeritage. His wonderfully-succinct example uses a proposal (static methods) that hadn't landed in that spec yet, but it still makes the point:

// shows the class
(class X extends (class Y { static logX() { console.log(X); } }) { }).logX();

That shows a class expression for class X that extends class Y, where class Y is defined within the ClassHeritage syntax production — and refers to X! (Is this a good idea? Probably not. But there may be very edgy edge cases.)

Just for clarity, let's expand that a bit and stick to features in the spec you linked to:

const x = new class X extends (
    class Y {
        logX() {
            console.log(X);
        }
    }
) {
};
x.logX();               // shows the class
console.log(typeof X);  // undefined

So there we are, even class declarations have use for classScope, even before class fields and such were added.

  • Related