3

Background

I have a functions that is responsible for generating a random number and making it available.

"use strict";

module.exports = function(args) {
    let {
        min,
        max,
    } = args;

    let currNumber = genRandom(min, max);

    const genRandom = (min, max) => Math.floor(Math.random() * max) + min;

    const getNumber = () => currNumber;    

    return Object.freeze({
        getNumber
    });
};

Problem

For a reason I don't understand, when I run this code with Node.js 7.8, I get the error that genRandom is not defined.

But if I change the code from:

let currNumber = genRandom(min, max);

const genRandom = (min, max) => Math.floor(Math.random() * max) + min;

to:

const genRandom = (min, max) => Math.floor(Math.random() * max) + min;

let currNumber = genRandom(min, max);

then it works!

I don't understand why this happens. I thought that const and let were hoisted just like var, but this leads me to believe I am wrong.

Question

Can someone explain me this behavior ?

Flame_Phoenix
  • 16,489
  • 37
  • 131
  • 266
  • 3
    one thing that `const` or `let` difference from `var` is that the variable is not accessible before declaration. – tsh Apr 08 '17 at 09:28
  • So, they are not hoisted to the top of the function, right ? – Flame_Phoenix Apr 08 '17 at 09:29
  • @tsh you might want to post that as an answer. – Philipp Apr 08 '17 at 09:29
  • 1
    FYI, using `var` would cause a problem too. The variable may exist because its definition is hoisted, but it does not have a value until the line of code that assigns it a value runs in the place you put it. Now, if you used `function genRandom() {}` to define your local function, then the whole function definition would be hoisted. So, this has nothing to do with `let`, `const` or ES6. Assignments to any kind of variable are not hoisted. – jfriend00 Apr 08 '17 at 09:34
  • @jfriend00: It would cause *a* problem, but a different one. :-) – T.J. Crowder Apr 08 '17 at 09:35
  • @T.J.Crowder - OK, fine - a different error. But, it wouldn't work even with `var` so this issue is really just a misunderstanding what is hoisted. I never understand why people define functions in this type of context with `const fn = ...` instead of `function fn() {}`. – jfriend00 Apr 08 '17 at 09:37
  • @jfriend00: Absolutely. On defining functions as constants: It has the advantage of making the identifier read-only, and some people (I'm not one of them) like *all* the code evaluated top-down rather than having hoisting... – T.J. Crowder Apr 08 '17 at 09:37
  • @T.J.Crowder - I guess I find the world easier to comprehend if a function is available in a scope rather than only in a piece of a scope. If I want to narrow the availability of a function, I can always create a smaller scope. And, the only thing it could conflict with in that scope is my own code so I've never found a reason to worry about that. Call me old school, but I don't find `const genRandom = (min, max) => ...` to be nearly as expressive, easy to read or understand as `function getRandom(min, max) {}` either. I think people are misusing arrow functions as the cool new toy. – jfriend00 Apr 08 '17 at 09:41
  • @jfriend00: :-) Well, they are the cool new toy. (Well, they were. Now `async`/`await` is the cool new toy...) – T.J. Crowder Apr 08 '17 at 09:44
  • @jfriend00: Letting the exception (e.g., rejection) propagate isn't a bad thing, provided it is handled at some appropriate stage. But people have always tended to ignore error conditions. Recent changes will encourage people to handle them (but I bet many will do it in the wrong place, either too early or too late): Browsers are starting to report unhandled rejections as errors in the console, and soon NodeJS will terminate on them, exactly like an unhandled exception. – T.J. Crowder Apr 08 '17 at 09:50

2 Answers2

8

I thought that const and let were hoisted just like var, but this leads me to believe I am wrong.

Not really, let and const are indeed hoisted, or as I like to call it, half-hoisted. There are two big differences between var foo and let foo: Scope and initialization. You already know about the scope difference. The second one is that with var foo, both the declaration and the initialization (with undefined) of foo are hoisted. With let, only the declaration of foo is hoisted, not the initialization. foo is only initialized when the step-by-step execution of the code reaches the let foo statement. You can't use (read from or write to) an un-initialized identifier. This time during which you can't use the identifier is called the Temporal Dead Zone (TDZ).

Even with var, the initialization that's hoisted is the initialization with undefined, not the value on the right-hand side of the =:

console.log(typeof foo); // "undefined"
foo();                   // TypeError: foo is not a function
var foo = () => {};

The change you've made, moving the declaration of getRandom up before the first use of it, is the correct thing to do. (Or use a function declaration, since the declaration as a whole [including the creation of the function] is hoisted.)

Let's look at this half-hoisting thing:

let foo = "outer";
function x()
{
    console.log("...");
    let foo = "inner";
    console.log(foo);
}
x();

(let and const have block scope, but I'm using a function because I'll be constrasting with var in a moment.)

Within x, that inner foo can't be used until the let foo line. But, you can't access the outer foo above it; this fails:

let foo = "outer";
function x()
{
    console.log(foo);    // ReferenceError: `foo` is not defined
    let foo = "inner";
    console.log(foo);
}
x();

That's the half-hoisting: The declaration of the inner foo is hoisted, but the variable isn't initialized until the let foo statement. That means you can't use foo at all (not even from a containing scope) above the let foo line. The inner foo shadows the outer foo throughout the function, but you can't use it until it's initialized. This is covered in Let and Const Declarations in the spec.

This is in constrast with var:

var foo = "outer";
function x()
{
    console.log(foo);    // undefined
    var foo = "inner";
    console.log(foo);    // "inner"
}
x();

That runs just fine because both the declaration and the initialization of foo (with undefined) are hoisted to the top of the function. (After hoisting, the var foo = "inner"; line becomes a simple assignment statement.) So the inner foo shadows the outer foo throughout, and is also accessible throughout, initially with its default value (undefined) and the later with "inner" (once that's been assigned to it).

Since the TDZ is temporal (related to time), not spatial (related to space or location within the scope), you can use an identifier created by let or const (or class) above its declaration, just not before its declaration. This fails because getNumber tries to access theNumber before it's initialized, while it's still in the TDZ:

const getNumber = () => theNumber;

console.log(getNumber()); // ReferenceError

let theNumber = 42;

This works because getNumber accesses theNumber after it's initialized:

const getNumber = () => theNumber;

let theNumber = 42;

console.log(getNumber()); // 42
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1

One thing that const or let difference from var is that the variable is not accessible before declaration.

Just checkout the specification: http://www.ecma-international.org/ecma-262/6.0/#sec-let-and-const-declarations

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

Syntax

LexicalDeclaration[In, Yield] :
    LetOrConst BindingList[?In, ?Yield] ;

LetOrConst :
    let
    const

BindingList[In, Yield] :
    LexicalBinding[?In, ?Yield]
    BindingList[?In, ?Yield] , LexicalBinding[?In, ?Yield]

LexicalBinding[In, Yield] :
    BindingIdentifier[?Yield] Initializer[?In, ?Yield]opt
    BindingPattern[?Yield] Initializer[?In, ?Yield]

Community
  • 1
  • 1
tsh
  • 4,263
  • 5
  • 28
  • 47