First, you make an incorrect assumption: modern JavaScript is compiled. Engines like V8, SpiderMonkey, and Nitro compile JS source into the native machine code of the host platform.
Even in older engines, JavaScript isn't interpreted. They transform source code into bytecode, which the engine's virtual machine executes.
This is actually how things in Java and .NET languages work: When you "compile" your application, you're actually transforming source code into the platform's bytecode, Java bytecode and CIL respectively. Then at runtime, a JIT compiler compiles the bytecode into machine code.
Only very old and simplistic JS engines actually interpret the JavaScript source code, because interpretation is very slow.
So how does JS compilation work? In the first phase, the source text is transformed into an abstract syntax tree (AST), a data structure that represents your code in a format that machines can deal with. Conceptually, this is much like how HTML text is transformed into its DOM representation, which is what your code actually works with.
In order to generate an AST, the engine must deal with an input of raw bytes. This is typically done by a lexical analyzer. The lexer does not really read the file "line-by-line"; rather it reads byte-by-byte, using the rules of the language's syntax to convert the source text into tokens. The lexer then passes the stream of tokens to a parser, which is what actually builds the AST. The parser verifies that the tokens form a valid sequence.
You should now be able to see plainly why a syntax error prevents your code from working at all. If unexpected characters appear in your source text, the engine cannot generate a complete AST, and it cannot move on to the next phase.
Once an engine has an AST:
- An interpreter might simply begin executing the instructions directly from the AST. This is very slow.
- A JS VM implementation uses the AST to generate bytecode, then begins executing the bytecode.
- A compiler uses the AST to generate machine code, which the CPU executes.
So you should now be able to see that at minimum, JS execution happens in two phases.
However, the phases of execution really have no impact on why your example works. It works because of the rules that define how JavaScript programs are to be evaluated and executed. The rules could just as easily be written in a way such that your example would not work, with no impact on how the engine itself actually interprets/compiles source code.
Specifically, JavaScript has a feature commonly known as hoisting. In order to understand hoisting, you must understand the difference between a function declaration and a function expression.
Simply, a function declaration is when you declare a new function that will be called elsewhere:
function foo() {
}
A function expression is when you use the function
keyword in any place that expects an expression, such as variable assignment or in an argument:
var foo = function() { };
$.get('/something', function() { /* callback */ });
JavaScript mandates that function declarations (the first type) be assigned to variable names at the beginning of an execution context, regardless of where the declaration appears in source text (of the context). An execution context is roughly equatable to scope – in plain terms, the code inside a function, or the very top of your script if not inside a function.
This can lead to very curious behavior:
var foo = function() { console.log('bar'); };
function foo() { console.log('baz'); }
foo();
What would you expect to be logged to the console? If you simply read the code linearly, you might think baz
. However, it will actually log bar
, because the declaration of foo
is hoisted above the expression that assigns to foo
.
So to conclude:
- JS source code is never "read" line-by-line.
- JS source code is actually compiled (in the true sense of the word) in modern browsers.
- Engines compile code in multiple passes.
- The behavior is your example is a byproduct of the rules of the JavaScript language, not how it is compiled or interpreted.