5

I've tried searching for an explanation on this exact scenario but couldn't find anything. I have a module that looks like the following (simplified):

export default function(name) {
  return map[name];
}

const map = {
  'a': 'b',
  'b': 'c'
};

Clearly the const definition above is hoisted but it should be undefined when being used in the function. I'm trying to find an exact explanation for why this function works when imported and used. Shouldn't a reference error be thrown? The only thing I can think of is that the entire module is loaded prior to when this function is called, but how exactly does that happen or where is this specific behavior explained? Any information would be really appreciated.

stevenelberger
  • 1,368
  • 1
  • 10
  • 19
  • 2
    Unless you're actually calling the function before `const map` runs, you _aren't_ using it before it is declared. – loganfsmyth Jan 29 '18 at 19:21
  • @loganfsmyth so you're saying when the function is imported the entire module is loaded from top to bottom which makes the `const map` available by the time the function is actually used? If so, where is that written? – stevenelberger Jan 29 '18 at 19:46
  • When talking about hoisting and declarations, it easier to think of "before" and "after" to refer to the *time* a piece of code is executed, not the location it is positioned in the file. – Felix Kling Jan 30 '18 at 06:21

2 Answers2

7

JS uses lexical scoping, meaning that a scope is defined as part of the nested structure of the code. Anything referencing the name of a variable anywhere in that scope can attempt to access the value. What the actual value is depends on when the value is initialized, which is a runtime property of the code.

When people talk about hoisting in JS generally it is not super clear which of these things they are describing. In JS, because scopes are lexically-based, the existence of a given variable is hoisted, in that it always exists within that scope.

The actual initialization of a variable in a scope depends on the way the variable is declared.

  • function foo(){} declarations have their initialization hoisted, so the variable foo will always be available in the body of the function, even if the declaration comes later. e.g.

    foo();
    function foo(){}
    

    is perfectly valid valid.

  • var foo = 4; hoists initialization, but the value is initialized to undefined until the declaration has executed, e.g.

    foo; // undefined
    var foo = 4;
    foo; // 4
    
  • let/const performs initialization only when executed, and attempts to access uninitialized variables will throw an exception.

    foo; // would throw because it is not initialized yet
    let foo = 4;
    foo; // 4
    

So taking those together, the behavior of variables is only determined at runtime. For your code example, it depends on when the function is called. Since let foo = 4; only initialized foo when it runs, if the function were called first, it would fail, and if the function were called after, it would work.

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

log(); // would throw because foo has not been initialized
let foo = 4;
log(); // would log 4

This behavior of delayed initialization for let and const is commonly referred to as the "temporal dead zone".

At the end of the day, as long as the variable has been initialized and has the value you care about by the time you access it, your code is fine. Whether you have access to the variable in the first place only depends on the nesting of your code.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • Thank you for the thorough response. I suppose I'm trying to understand exactly why the `const map` object was initialized by the time that function is called. Perhaps it's right to say that when you import the default from a module the entire module is loaded prior to any usage of said module? – stevenelberger Jan 29 '18 at 20:24
  • 1
    Right, there is no importing a single function, you import a module or you don't. What you list in your `import` statement is just what you're asking for access to. You can also just `import "foo";`, the names you list don't affect what is loaded and executed. – loganfsmyth Jan 29 '18 at 20:57
5

The call can only be made (outside of this module) after the whole module you provided is executed (which means that map is already initialized by the time the function is called)

This observation is based on the fact that when you import a module, all of the code inside it will be executed. To call the function, one would have to import the module you provided.

See Does ES6 module importing execute the code inside the imported file? for when code executes on import.

Leo Lei
  • 1,534
  • 17
  • 14