简体   繁体   中英

JavaScript: Understanding closures and hoisting

I understand that function declarations are hoisted to the top of their scope. This allows us to use those functions before they're actually declared in JavaScript:

sayHello(); // It works!

function sayHello() {
  console.log('Hello');
}

I also understand that closures enable functions to retain references to variables declared within the same scope:

function outerFxn() {
  let num = 0;

  function innerFxn() {
    console.log(num);
    num++;
  }

  return innerFxn;
}

const logNum = outerFxn();
logNum(); // 0
logNum(); // 1
logNum(); // 2

So far so good. But here's some weirdness that I'm hoping someone can explain exactly what's going on...

Scenario 1: understandable closure

function zero(cb) {
  return setTimeout(cb, 0);
}

function test1() {
  let txt = 'this is a test message';

  function log() {
    console.log(txt);
  }

  zero(log);
}

In the example above, the log function retains a reference to the scope in which it was created, holding on the the txt variable. Then, when executed later on in the setTimeout , it successfully log's the txt variable's value. Great. Then there's this...

Scenario 2: what is happening?

function zero(cb) {
  return setTimeout(cb, 0);
}

function test1() {
  function log() {
    console.log(txt);
  }

  let txt = 'this is a test message';

  zero(log);
}

I've moved the log function declaration to the top of the scope (it would have been hoisted there anyway, right?), then I'm declaring the txt variable below it. This all still works and I'm not sure why. How is log retaining a reference to the txt variable when let 's and const 's aren't hoisted up? Are closure scopes analyzed as-a-whole ? I could use a bit of clarity on what the JavaScript engine is doing step by step here. Thank you SO land!

It's part of the scope after you leave the test1 function. It doesn't matter if it's being used with var , let or const at that point. Since the whole body has been evaluated, the variable exists on the scope.

Had you tried to use log before the let declaration is evaluated, you'd gotten an error.

Edit : Technically, the variables declared with let and const are in scope, but they are unitialized which results in an error if you try to access them. It's only until you get to the declarations that they are initialized and you can access them. So they are always in scope, just not available until declarations are evaluated.

"Are closure scopes analyzed as-a-whole?" - yes. Closures retain the scope as it is at the moment you (lexically) leave it. In your example, txt does exist when you reach the closing } in test1 , so it's in the scope, and log has no problems accessing it.

Note "lexically" above: bindings are done prior to runtime, when only thing that matters is your block structure. So even this would work, although it shouldn't from the "dynamic" point of view:

function test1() {
    function log() {
        console.log(txt);
    }

    zero(log);
    let txt = 'this is a test message';
}

In Scenario 2 you are doing:

  1. let txt = 'this is a test message' which means that txt will be part of the scope of test1() .
  2. At the same time you are declaring log() which will have access to the scope of its parent test1() .

So what happens on run-time? test1() will be evaluated and as such log() will have access to the scope of test1() . This means that txt will be available for log() to use immediately.

Tip: debug it, put some break points and see what happens.

Edit: You can also consider that within log() , txt is not defined and as such its value should be undefined... right? The fact that console.log(txt) works outputs this is a test message is due to the above explanation on scoping. It's always good practice to declare your variables at the top of the function scope, and your functions at the bottom of the scope since they will be evaluated first anyways. Consider the human factor in this situation, best practice can also mean: for you/anyone to understand what the code does just by reading it.

this is a timing/execution order thing. Think about it like

function test1(){
    var context = { }; 

    function log(){
        if(context.hasOwnProperty("txt")){
            console.log(context.txt); 
        }else{
            throw new Error("there is no value 'txt' declared in this context");
        }
    }

    context.txt = 'this is a test message';
    log();
}

Same in your code with the not hoisted variable txt . At the time, log is executed, let txt will have been declared in the proper function context. It is available even if it ain't hoisted. The function log doesn't store a reference to the variable itself, but to the whole surrounding execution context, and this context stores the variables.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM