Lesson 05 / 21
Naming and Scope Rules
Naming rules and reserved words, the resolution of lexical scope, shadowing, hoisting, and the temporal dead zone.
Contents
The previous lesson determined where a name is seen. This lesson answers two more questions: if the same name is declared in more than one scope, which one applies, and what happens if a name is read before its declaration line?
Both answers come from a single principle: JavaScript uses lexical scope. Which binding a name corresponds to is determined by looking at where the code is written; it does not matter who calls whom at runtime. This principle, defined in the Programming Foundations course’s Scope and Lifetime lesson, is the source of every behavior below.
Naming Rules
A name starts with a letter, an underscore, or a dollar sign; digits may follow after
that. Names are case-sensitive: threshold and Threshold are two different names.
const measurementCount = 4; const MEASUREMENT_THRESHOLD = 20; const $name = "A1"; const _temp = true; console.log(measurementCount, MEASUREMENT_THRESHOLD, $name, _temp); console.log("threshold" === "Threshold");
4 20 A1 true false
The grammar also accepts letters outside the Latin alphabet, but every name in this course is written with ASCII characters. The reason is practical: characters that look visually similar but correspond to different code points produce errors that are hard to tell apart. The code point concept defined in the How Computers Work course’s Character Encodings lesson applies directly here: two names are the same only if their sequences of code points are the same.
Words reserved by the language cannot be used as names:
const class = 1;
SyntaxError: Unexpected token 'class'
This too is a parsing error; the file does not run at all. The list of reserved words
is written in the standard and includes language constructs like class, const,
return, typeof, and new.
As a naming convention, this course writes variable and function names starting with a
lowercase letter and continuing with capitalized words (measurementCount); threshold
and setting values that will not be recalculated are written in uppercase
(MEASUREMENT_THRESHOLD). The convention is not the language’s rule; it is chosen for
consistency.
Lexical Scope
When a name is read, the scope it is currently in is checked first. If the name is not
there, the enclosing scope is checked, then that scope’s surroundings; if it is not
found even in the outermost scope, a ReferenceError is thrown. This chain is derived
from the code’s written structure.
const threshold = 20; function outerScope() { const threshold = 25; function innerScope() { return threshold; } return innerScope(); } function caller() { const threshold = 30; return outerScope(); } console.log(outerScope()); console.log(caller()); console.log(threshold);
25 25 20
The second line is decisive. Inside the caller function, the name threshold is
bound to 30, and outerScope is called from there. The result is still 25:
innerScope looks for the name not in the caller’s scope but in the enclosing scope of
where it is itself written. The call chain does not change the scope chain.
The alternative model — looking for a name in the caller’s scope — is called dynamic scoping and is found in some shell languages. JavaScript does not use this model. The distinction has a practical consequence: which names a function can see can be determined by whoever reads it, purely by looking at the source.
Shadowing
Declaring the same name in an inner scope as in an outer one is called shadowing. In
the example above, the threshold inside outerScope shadows the outermost
threshold name. The outer binding does not disappear; it only becomes unreachable in
that region — the last line printing 20 shows this.
Shadowing is useful when used deliberately: carrying a different value of the same concept in a narrow region is more readable than inventing a new name. When done by accident, it becomes hard to explain why the outer value was not used. The rule is simple: if shadowing is happening, it should be deliberate.
Hoisting
Declarations are processed at the start of the scope, not on the line where they are written in the source. This behavior is called hoisting. The three declaration kinds are not hoisted the same way, and this is exactly where the difference shows up.
A name declared with var is created at the start of the scope and initialized with
undefined. It can be read up until the declaration line; when read, it gives
undefined:
console.log(typeof varName); var varName = 20; console.log(varName);
undefined 20
A name declared with let or const is also created at the start of the scope, but it
is not initialized. Touching the name in the region between its creation and its
declaration line throws an error:
try { console.log(letName); } catch (error) { console.log(error.name + ": " + error.message); } let letName = 25; console.log(letName);
ReferenceError: Cannot access 'letName' before initialization 25
This region is called the temporal dead zone. The message needs to be read
carefully: it does not say “name not found,” it says “cannot access before
initialization.” The name exists in the scope; it just has not been bound to a value
yet. Compared with the previous lesson’s esik is not defined message — that name had
never been declared at all, this one has been declared but not initialized.
The dead zone, combined with shadowing, produces a result that looks counterintuitive:
const threshold = 20; function buggy() { try { console.log(threshold); const threshold = 25; } catch (error) { console.log(error.name + ": " + error.message); } } buggy();
ReferenceError: Cannot access 'threshold' before initialization
The outer threshold name, even though it is bound to 20, could not be read. The
reason is the definition of lexical scope: because threshold is declared inside the
function body, every reference to threshold in that body points to the local
binding. The local binding has not been initialized yet. There is no way to reach the
outer binding; shadowing also applies before the declaration line.
Function Declarations
Function declarations are hoisted together with their bodies, not just their names. This is why they can be called before their declaration line:
console.log(average([21.5, 19.75, 23, 18.25])); function average(array) { let total = 0; for (const value of array) total += value; return total / array.length; }
20.625
If a function is assigned to a variable as an expression, what gets hoisted is only the
variable declaration; the function itself comes into being on the assignment line. If it
is assigned with const, the dead-zone rule applies:
try { console.log(average([1, 2])); } catch (error) { console.log(error.name + ": " + error.message); } const average = (array) => array.reduce((total, value) => total + value, 0) / array.length; console.log(average([1, 2]));
ReferenceError: Cannot access 'average' before initialization 1.5
The behavioral difference between the two definition forms will be covered in detail in the Function Declaration and Expression lesson; here they were compared only from the hoisting angle.
Applying It to the Measurement Script
Scope rules directly determine the measurement script’s structure:
const THRESHOLD = 20; function thresholdReport(temperatures) { const exceedances = []; for (const value of temperatures) { if (value > THRESHOLD) { const difference = value - THRESHOLD; exceedances.push(difference); } } return exceedances; } console.log(thresholdReport([21.5, 19.75, 23, 18.25])); console.log(typeof exceedances);
[ 1.5, 3 ] undefined
THRESHOLD is in the outermost scope and is seen from inside the function through the
lexical scope chain. exceedances belongs to the function, difference belongs to the
if block; neither is accessible from outside. Writing the last line with typeof was
required — reading it directly would have thrown a ReferenceError.
The structure applies three rules together: every name is declared in the narrowest scope it is needed in, no shadowing is done, declarations come before use. The third is not required, thanks to hoisting, but it eliminates every dead-zone error entirely.
Summary
- Names are case-sensitive; reserved words cannot be used as names, and this is caught at the parsing stage.
- JavaScript uses lexical scope: a name is searched for in the enclosing scopes of where the code is written; the call chain does not affect this search.
- Declaring the same name in an inner scope shadows the outer binding; shadowing also applies before the declaration line.
varis initialized withundefinedat the start of the scope;letandconstare not initialized and stay in the temporal dead zone until their declaration line.- Function declarations are hoisted together with their bodies; function expressions assigned to a variable are not.
Next Step
The structure of the language — how code is run, how names are declared and resolved — has been established. Next comes the values these names are bound to. The next topic covers JavaScript’s value model: how many primitive types there are, which representation the number type uses, and why it carries a limit for integers. Its first lesson introduces the primitive types one by one.
To keep your progress and take notes, Log in
My notes
Log in to take notes.