Is hoisting really necessary in javascript to enable mutual recursion?
Asked Answered
E

3

6

In an online course, Kyle Simpson says the following code demonstrates the necessity of hoisting in javascript, because without hoisting "one of the functions would always be declared too late."

a(1)  // 39

function a(foo){
  if (foo > 20) return foo
    return b(foo+2)
}

function b(foo){
  return c(foo) + 1
}

function c(foo){
  return a(foo*2)
}

But this works just fine.

var a = function(foo){
  if (foo > 20) return foo
    return b(foo+2)
}

var b = function(foo){
  return c(foo) + 1
}

var c = function(foo){
  return a(foo*2)
}

a(1) // 39

So what's the story? Convenience and placement of invocation aside, are there any situations that require hoisting?

Elk answered 25/4, 2016 at 5:19 Comment(7)
second one will give error if a(1) is called before function definitions.Shamblin
Yes, but that's trivially true of any function and its invocation (as I note in the question). Simpson's claim seems to be that the mutually recursive structure of the functions requires hoisting, but that doesn't seem to be the case.Elk
In the first one, if you give a(1) before function definition, it won't give the same error.Shamblin
Thanks for taking the time to comment, but you're missing the point of the question.Elk
hoisting lets you put functions below instead of above the executing code.Mortonmortuary
I like this answer: "It's not a feature to use or not use, it's just a property of the environment to take into account" programmersGirasol
In the second one if you move a(1) invocation to the top you will get a TypeError: a is not a function and this is very normal since now a, b and c are function expressions and they won't get hoisted to the top. But be careful don't repeat the test in the same browser window that you did a test with the first snippet where a, b and c were function definitions and they are kept in memory of course. Open up a new browser session..Adolphus
V
7

The claim I've made about a non-hoisted JS being unable to support mutual recursion is just conjecture for illustration purposes. It's designed to help understand the need for the language to know about variables available in the scope(s). It's not a prescription for exact language behavior.

A language feature like hoisting -- actually hoisting doesn't exist, it's just a metaphor for variables being declared in scope environments ahead of time during compilation, before execution -- is such a fundamental characteristic that it can't easily be reasoned about when separated from the rest of the language's characteristics.

Morever, it is impossible to fully test this hypothesis in just JS. The snippet in the OP only deals with part of the equation, which is that it uses function expressions instead of function declarations to avoid function hoisting.

The language I was using to compare to for illustration is C, which for example requires function signatures to be declared in .h header files so that the compiler knows what a function looks like even if it hasn't "seen" it yet. Without it, the compiler chokes. That's a sort of manual hoisting in a sense. C does it for type checking, but one can imagine this sort of requirement existing for other reasons than that.


Another way of thinking about this is whether JS is a compiled language where everything has been discovered before it executes, or whether it is interpreted top-down in a single pass.

If JS were top-down interpreted, and it got to the definition of an a() function that referenced a b() inside it that it hadn't seen yet, that could be a problem. If that call expression was handled non-lazy, the engine couldn't figure out at that moment what the b() call would be about, because b() hadn't been processed yet. Some languages are lazy and some are non-lazy.

As is, JS is compiled first before execution, so the engine has discovered all the functions (aka "hoisting") before running any of them. JS also treats expressions as lazy, so together that explains why mutual recursion works fine.

But if JS had no hoisting and/or was not lazy, one can imagine the JS engine would be unable to handle mutual recursion because the circular reference between a() and b() would in fact mean that one of the two was always declared "too late".

That's really all I meant in the book.

Valdemar answered 25/4, 2016 at 13:38 Comment(3)
That clears up my question. Thanks for the answer, and the course, which is fantastic.Elk
Could you elaborate on "JS also treats expressions as lazy". Isn't JS eager evaluated?Girasol
What I meant is that the value of a in a() is not resolved until the moment it's run.Valdemar
I
4

Convenience and placement of invocation aside, there are not any situations that require hoisting.

Just make sure to declare all the functions before using them.

Note: In some browser, function a(){} creates a function with name a while var a = function(){} does not (considered anonymous function). The function name is used when debugging. You could also do var b = function a(){}.

Intimate answered 25/4, 2016 at 5:40 Comment(0)
D
-1

The second block of code works fine because you are invoking a(1) after all the functions are initialized. Try the following block:

var a = function(foo){
  if (foo > 20) return foo
    return b(foo+2)
}

var b = function(foo){
  return c(foo) + 1
}

a(1);

var c = function(foo){
  return a(foo*2)
}

This will give an error Uncaught TypeError: c is not a function because function assigned to c is not hoisted. This is the reason why you need hoisting.

Because if you declare functions as in your first block of code, all the functions will be hoisted and you can invoke a anywhere in the code. This is not true in the other cases.

Deglutinate answered 25/4, 2016 at 5:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.