1

As we know, traditionally JS has been lacking block scope. As we know JS has had only function scope up until recently.

In recent versions of JS though, we can have let and const variables which are visible only in the scope where they are defined.

But... deep down... how is this done/implemented really? Is there really now in the language a first-class notion of block scope in JS, or is the block scope thing just some simulation to make certain variables visible only in the block where they are defined?

I mean is block scope in recent JS versions a first-class thing, just like function scope is, or is block scope just some sort of simulation while we actually still have just the old good function scope?

5
  • 2
    What would it mean practically for it to be a first-class notion as opposed to it being simulated? Commented Oct 30, 2019 at 10:10
  • 2
    The language defines a syntax and behaviour for it. An engine may implement it any way it chooses, as long as it exhibits the defined characteristics. Compilers may compile it down to other primitives, as long as they show the defined characteristics. Commented Oct 30, 2019 at 10:12
  • @deceze Strictly speaking, that is true, OK, I agree. I meant more like... is block scope a first class thing, or just a syntax sugar of some sort on top of the old traditional function scope. I am sure people see what I mean. Commented Oct 30, 2019 at 10:24
  • 1
    Yeah, but… where's the distinction? The language specification treats it as first class behaviour. If the implementation behaves that way, where's the difference? Commented Oct 30, 2019 at 10:43
  • @deceze As I said, I agree... OK :) Commented Oct 30, 2019 at 11:54

1 Answer 1

3

But... deep down... how is this done/implemented really? Is there really now in the language a first-class notion of block scope in JS...?

Yes, there is. A new block¹ creates a new lexical environment in the same way that creating a function does (without, obviously, all the other aspects of creating a function). You can see that in the Evaluation section of blocks in the spec.

It's a first-class language construct.


¹ I originally wrote "...containing a let, const, or class declaration..." but the specification doesn't actually make that distinction, though I expect JavaScript engines do (since there's no need for a new environment if there are no lexically-declared bindings).


In a comment you've asked:

What about hoisting? I read that block scoped-variables are hoisted to the top of the block they are defined in... but then also... You get an error if you try to access a block-scoped variable before the line/statement where it is declared in its block? This sounds contradictory, no? If they are hoisted we would not be getting this error but we would be getting undefined. What is the truth here?

In my book I describe them as half-hoisted: The creation of the variable (more generally, the "binding") is hoisted to the top of the scope in which its declaration (let x or whatever) appears (the block, in this case), but the binding isn't initialized until the declaration is reached in the step-by-step execution of the code. The time between creation and initialization is called the Temporal Dead Zone. You can't use the binding (at all) within the TDZ.

This only applies to let, const, and class declarations. var is handled differently in two ways: 1. Obviously, var is hoisted to the top of the function (or global) scope, not just block scope. 2. Less obviously, var bindings are both created and initialized (with the value undefined) upon entry to the scope. They're fully hoisted. (The declaration of them is; any initializer on the var statement is actually an assignment, and done when that statement is reached in the step-by-step execution of the code.)

Here's an example:

function foo(n) {
    // `l1` and `l2` don't exist here at all here
    // `v` is created and initialized with `undefined` here, so we can happily
    // do this:
    console.log(`v = ${v}`);
    if (n > 10) {
        // `l1` and `l2` are created here but not initialized; if we tried to
        // use them here, we'd get an error; uncomment this line to see it:
        // console.log(`l1 = ${l1}`);
        console.log("n is > 10");
        var v = "a";  // `v` is assigned the value `"a"` here, replacing the
                      // value `undefined`
        let l1 = "b"; // `l1` is initialized with the value `"b"` here
        console.log(`l1 = ${l1}`);
        let l2;       // `l2` is initialized with the value `undefined `here
        console.log(`l2 = ${l2}`);
        l2 = "c";     // `l2` is assigned the value `"c"` here, replacing the
                      // value `undefined`
        console.log(`l2 = ${l2}`);
    }
}
foo(20);

Just for completeness, function declarations are also fully-hoisted, but even more so than var: The function is actually created and assigned to the binding upon entry to the scope (unlike var, which gets the value undefined).


In a comment you've observed:

Then... I don't see what's the difference between no hoisting and half-hoisting...

Good point, I didn't explain that. The difference relates to shadowing identifiers in the outer scope. Consider:

function foo() {
    let a = 1;
    if (/*...*/) {
        console.log(`a = ${a}`);
        let a = 2;
        // ...
    }
}

What should the log show?

Sorry, that was a trick question; the log doesn't show anything, because you get an error trying to use a there, because the inner a declaration shadows (hides) the outer a declaration, but the inner a isn't initialized yet, so you can't use it yet. It's in the TDZ.

It would have been possible to make the outer a accessible there, or to make the inner a accessible there with the value undefined (e.g., fully hoisting it like var, but just within the block), but both of those have problems the TDZ helps solve. (Specifically: Using the outer a would have been confusing for programmers [a means one thing at the beginning of the block but something else later?!] and would have meant JavaScript engines had to create new lexical environments all over the place, basically every let or const or class would introduce a new one. And pre-initializing with undefined is confusing for programmers, as var has shown us over the years...)

Sign up to request clarification or add additional context in comments.

9 Comments

more over, the block is an old language structure (-> labels and break).
@NinaScholz - Right. Blocks are as old as JS; block scope is new (to JS) in ES2015. (Well...mostly. The block on with statements had aspects of block scope.)
OK, thanks, that's the answer/confirmation I was looking for.
One more question, if I may... What about hoisting? I read that block scoped-variables are hoisted to the top of the block they are defined in... but then also... You get an error if you try to access a block-scoped variable before the line/statement where it is declared in its block? This sounds contradictory, no? If they are hoisted we would not be getting this error but we would be getting undefined. What is the truth here?
OK :) Thanks a lot once again.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.