Understanding lexical scope is essential for mastering JavaScript, as it dictates how variables are accessed and resolved at compile time. This article explores the core principles of lexical scope, delving into the definition, how it shapes variable behavior, and its role in the JavaScript two-pass system. We'll also examine the creation of scopes through functions and blocks and explore key concepts like shadowing, global variables, strict mode, types of errors, undeclared vs. undefined variables, nested scopes, and the role of arguments and parameters in JavaScript functions.
Table of Content
- Lexical Scope Definition
- JavaScript's Two-Pass System
- Organizing Scopes with Functions and Blocks
- How JavaScript Creates Scopes
- Shadowing
- Global Variables and Strict Mode
- Types of Errors
- Undeclared vs. Undefined
- Nested Scopes
- Arguments and Parameters in JavaScript
- Conclusion
- Sources
Lexical Scope Definition
Lexical scope consists of two key components: lexical and scope. Lexical refers to the lexing phase of compilation, which we discussed in a previous article. But what exactly is scope?
Scope is the region where JavaScript searches for and accesses variables.
It answers two essential questions:
Key Questions
Lexical scope helps answer two key questions:
What are we looking for? Lexical scope is primarily about finding identifiers, such as variables or functions.
Where are we looking for them?
Lexical scope defines the location where identifiers are accessible in your code.
In essence, we're primarily searching for identifiers, such as variables and functions, within various scopes in your code. Variables play two roles:
Receiving the assignment of a value Variables in this role act as
targets
, where values are assigned, likex = 42
ormyFunc = function func(arguments) {}
.Retrieving a value from the variable Variables in this role serve as
sources
, where values are retrieved, likeconsole.log(y)
or function calls, such asask()
.
During the code's execution, JavaScript engine processes scopes, asking two fundamental questions:
- What is the position of this variable within the scope?
- To which scope does it belong?
JavaScript's Two-Pass System
Understanding how JavaScript handles lexical scope involves recognizing that JavaScript is a two-pass system:
process the code first: Javascript processing of lexical scopes and putting all of these identifiers, into their correct scope in the parsing stage where it goes through all of our code, produces this abstract syntax tree (
AST
) and produces a plan. This plan is executable code that is handed off to be executed by the other part of the JavaScript engine (Virtual Machine),execute it: JavaScript virtual machine, which is embedded inside of the same JavaScript engine. It interprets all that plan and use all that information to execute the code, and one of the things that it interprets is, whenever it enters a scope it creates all the identifiers according to what the plan told it to do and locates memory for them
Organizing Scopes with Functions and Blocks
In JavaScript, lexical scopes are created when the code encounters functions
and blocks
(curly braces {}
).
Functions:
When you declare afunction
, it creates a new scope. Functions can be nested within other functions, forming a hierarchy of lexical scopes.Blocks:
Blocks defined by curly braces{}
in constructs likeif
,for
, andwhile
statements also introduce new lexical scopes.
How JavaScript Creates Scopes
When JavaScript encounters variable or function declarations, it creates scopes and places those identifiers within the appropriate scope. Consider the following code:
var teacher = 'kyle';
function otherClass() {
var teacher = "saman";
console.log("Welcome!")
}
function ask() {
var question = "why";
console.log(question)
}
In this code, you can observe how JavaScript creates scopes and assigns identifiers:
- Variable
teacher
is assigned in the global scope. - Function
otherClass
creates a new scope, and a local variableteacher
is assigned in that scope. - The function
ask
also creates a scope, where the variablequestion
is defined.
In lexically scoped languages like Javascript, all the lexical scopes and identifiers, are determined at compile
time not in run time. It is used at run time but determined at compile time, this behaviour allows the engine to be much more effective in optimising because everything is known and it is fixed.
After all scopes are determined during the parsing stage, JavaScript knows the scope of each identifier. This knowledge helps optimize execution since everything is pre-determined at compile time.The decision about scopes are author's time decisions
Shadowing
When you have multiple variables with the same name at different scopes, it is called shadowing. This means the innermost variable with the same name takes precedence when accessing that identifier. For example, if you have a local variable named x
inside a function
, it shadows an outer variable x
.
Global Variables and Strict Mode
JavaScript provides a mechanism for dynamic global variables (auto globals) that are automatically created if you try to assign to a variable that has never been formally declared. This behaviour occurs when the variable is not declared in any accessible scope. However, auto globals are discouraged in favour of explicitly declared variables as they can lead to performance issues.
Strict mode in JavaScript enforces stricter error handling and helps catch potential issues early in development. It's not always enabled by default but is commonly used in modern development tools and transpilers. Inside class
and ES6 modules
, you don't need to explicitly declare "use strict"
; strict mode
is assumed.
Types of Errors
JavaScript can throw various types of errors:
-
TypeError:
Occurs when you perform illegal operations with variables, such as attempting to execute
undefined
ornull
, access properties onundefined
ornull
, or reassign aconst
variable.
TypeError
happens at runtime.
-
SyntaxError:
Happens when there are syntax errors in your code, such as extra parentheses or curly braces, improper dots, or illegal operations with
let
orconst
, like redefining a variable multiple times.
SyntaxError
occurs during the compile time.
-
ReferenceError: This error arises when you try to access a variable that doesn't exist, and JavaScript can't find it.
ReferenceError
includes situations like accessing undeclared variables or trying to access variables before they are declared.
ReferenceError
happens at runtime.
Undeclared vs. Undefined
Understanding the difference between undeclared
and undefined
is crucial in JavaScript:
Undefined: It signifies that a variable exists but currently has no assigned value. It might have never had a value or previously had a value but doesn't at the moment. It represents an empty state, where the value is missing.
Undeclared: An undeclared variable doesn't exist in any accessible scope. It's essentially unknown in your code, and trying to access it results in a
ReferenceError
.
Nested Scopes
JavaScript's lexical scopes can be envisioned as nested layers, similar to a multi-story building. JavaScript conducts a sequential search for variables, beginning from the current scope (the first floor) and progressively moving to outer scopes. This process mimics a linear search, akin to using an elevator to traverse floors within a building.
In this conceptual analogy:
- The first floor represents the current scope, where a reference is made.
- The top floor corresponds to the global scope. The search progresses one floor at a time, ensuring that variables are resolved in a structured and predictable manner. This methodology forms the foundation of JavaScript's lexical scope.
Arguments and Parameters in JavaScript
In JavaScript, functions play a crucial role in processing data and performing tasks. To understand how functions work, it's essential to grasp the concept of arguments and parameters, which are integral to how functions receive and process data.
Parameters: Receiving Data
Parameters are the placeholders or variables defined in a function's declaration. They act as targets for the data you pass to the function when you call it. Parameters specify what kind of data the function expects to work with and provide names for accessing that data within the function's scope.
Here's an example of a function with parameters:
function greet(name) {
console.log(`Hello, ${name}!`);
}
In this function, name is a parameter. It serves as a target for the data you pass when calling the greet function. For instance, if you call greet("Alice"), the parameter name will take on the value "Alice," and the function will greet Alice.
Arguments: Passing Data
Arguments are the actual data or values you provide when calling a function. These values are assigned to the function's parameters. Arguments are the source of data that the function works with.
Here's how you pass arguments to the greet function:
greet("Bob"); // "Bob" is the argument
greet("Carol"); // "Carol" is the argument
In these examples, "Bob" and "Carol" are the arguments that get assigned to the name parameter within the greet function.
Parameters Create Identifiers
It's important to note that when you declare parameters in a function, they formally create identifiers within the function's scope. These identifiers allow the function to access and work with the data you pass as arguments.
In the greet function example:
function greet(name) {
console.log(`Hello, ${name}!`);
}
The name parameter is a formal identifier within the function's scope, allowing you to use it to reference the argument you pass when calling the function.
Conclusion
By delving into these fundamental aspects of lexical scope, you'll gain a deeper understanding of JavaScript and how it manages variables, errors, and scope. This knowledge is invaluable for writing efficient, optimized code and solving complex programming challenges.
Sources
Kyle Simpson's "You Don't Know JS"
MDN Web Docs - The Mozilla Developer Network (MDN)
Top comments (0)