This content originally appeared on DEV Community and was authored by Ghassen Faidi

Names give meaning to our world, our feelings, and so our programs. and that's where our story shall begin.
Context is one of the most foundational concepts in programming and in JavaScript, and it is a building block that leads us to powerful ideas like lexical scope and closures
It's worth noting from the start that in many programming languages, they use the term "environment" instead of context. it's the same thing, and both terms work well.
To understand context, we need to take a step back and talk about values, the ones we deal with in programs: integers, strings, etc.
At first, of course, values alone are useless. We need to combine them together to create expressions, but at the end of the day, all expressions will evaluate to a value. What does "evaluate" mean?
It means we take an expression and, step by step, turn it into a value. We can think of a value as an expression that cannot be evaluated any further. For example, imagine you type 1 + 2 into your console and press enter. The interpreter (the thing that runs your code) will print 3. If you type 3 and press enter, it will print 3 again: it cannot evaluate it any further. It's already a value. (Some programming languages call this an atom.)
Let's see more examples. An expression like 3 + 5 will evaluate to 8, and "hello " + "world" will evaluate to "hello world". Every operator (or function) has a set of rules. the so-called evaluation rules, or the semantics of the language.
When we understand the evaluation rules, we understand how things work. JavaScript's documentation (ECMAScript specs) contains those evaluation rules, and those who implement the interpreter make sure it meets those requirements.
Now, expressions are pretty good. We use them all day. But as programmers, we figured it's probably a good idea to give names to expressions. It's much easier! it's basically an "abstraction". Instead of thinking about the value 3.1415, I'll just think about the name we give to that value: PI. (That's why we say math is abstract, lots of symbols!)
When we give a name to a value, we use the term binding. That is, we say we bind x to the value 5, or we say x is bound to 5.
But wait a minute. Before, we just had expressions floating in the sea of bits and bytes, but now, giving names creates new challenges. What if I see "x" in my code... how does JavaScript know what value it's associated with? We need a place to store the name and the value associated with it. We need some sort of memory (a data structure) where we can store those bindings—that is, we store each name and the value it maps to.
We usually don't use the term "name" that much and we prefer other terms like "variable", but the term "name" is very abstract and it will be made clear why that matters a bit later.
Anyway, imagine you are a interpreter. You see an expression like x + 1. Well, you know the value 1, it's clear, but who or what the heck is x? that's why we need some sort of memory! the best that comes to mind is some sort of a hash map (i.e., a dictionary, something that has key-value pairs)
{
"x": 5
}
That's good enough for our mental model! Now whenever the code sees x, it can look at this memory and figure out what it evaluates to (that is, 5).
We call this data structure a context.
Well, the thing is, by creating this data structure, we made our language much more sophisticated. We have cool functionalities (also known as "constructs") like assignment!
Assignment is simple. It's basically a function that binds a variable name to a value:
let hobby = "reading"
This will bind hobby to the value "reading".
One question that comes to mind: why do we use the term "bind"? Isn't it just assignment? Why all this jargon?
The reason is that binding is more general than assignment. Binding means associating a name to a value.
For instance, an import statement:
import m from "./my-module";
import { log, factorial } from "./math.js"
Now, the name (or identifier) m is bound to the module object (or whatever the module exports). We can say log is bound to a function, but we don't say it's an assignment, because it isn't an assignment. We also have function bindings (i.e. function declaration), and we even have this binding (i.e., what value this is bound to when we call a function).
In short, assignment is a form of binding, but not all bindings are assignments.
Anyway, enough discussion about terminology.
We talked about contexts being some sort of memory that keeps track of bindings. In JavaScript, we have a global context, which is accessible from anywhere.
let x = "friend"
function hello(x) {
console.log("hello", x)
}
hello(x)
after executing the first two lines, the global environment would look like this:
{
"x": "friend",
"hello": <function object>
}
functions are values, we can think of a function as a special object.
now the interesting part:
hello(x)
the interpreter sees this line and it feels anxious, "hello", what on earth is that? we have also two parenthesis, certainly it's a function call, and there is an "x" there, well, let's start evaluating.
it looks "hello" in the context and it finds it,
<function object>(x)
good, but I can't call the function with "x", we need to evaluate it first, we look it up in the global context, and evaluate it.
<function object>("friend")
now the function is called. and "hello friend" is printed.
But Soon we discovered the need for separate contexts for different places. For instance, inside a function body, you wouldn't want the variables defined there to be accessible outside. We don't want to clutter the global context with irrelevant variable names!
And so, we ended up with the concept of scope.
It's basically a set of rules each programming language defines, so that you as a programmer memorize them (and your compiler knows them by heart) so that you understand the code you read and can write code that makes sense. Basically, when the interpreter sees x + 1, scoping rules tell the compiler where to look to find the value of x (i.e., what x evaluates to)—that is, what context to look in and where not to look.
Takeaway: scope is simply a consequence of having contexts, which is a consequence of us programmers liking to give names.
And well, it's human nature to give names. Names make us express ourselves and give meaning to expressions :)
And so, really, scoping rules can get complicated, and every language can differ a bit, and that's okay! It's a human effort, and we can make mistakes, and it can get really complicated to the point that JavaScript has 3 or more ways to bind a variable!
Those terms are fundamental to understanding the essence of JavaScript. Closures and lexical scopes require articles of their own, but let's take one concept: lexical scoping.
Lexical scoping means a function body gets evaluated (we go through the lines of code and run them line by line) in the context where it was defined, not the context where it was called.
let x = 10
function test() {
console.log(x)
}
function main(y) {
console.log(y)
let x = 20
test()
}
main("hello")
hello
10
Let's trace through and think about context.
let x = 10
That line is a let binding. It binds x (or simply, "assigns x") to the value 10.
function test() {...}
This is a function binding. It binds test to the function object (again, functions are values too!).
Right now, the environment looks like this:
global context: {
"x": 10
"test": <function object>
}
One might wonder, what about the function body? there is a difference between a function binding (declaration) and a function call. We only evaluate the body when it's a function call.
function main() {...}
global context: {
"x": 10
"test": <function object>
"main": <function object>
}
Now, finally:
main("hello")
The scoping rules say, whenever you call a function, a brand new context for that function will be created! It's empty at first:
main's context: {}
The first thing we do is bind the parameters to the argument values. (As I said, the term "bind" is super useful: it can be used anywhere you want to associate a name to a value.)
In our case, we bind the parameter y to the argument value "hello". (Some people created this distinction for clarity, Parameters are the names we define in the function signature; arguments are the actual values we pass to it.)
main's context: {
"y": "hello"
}
The next line we want to evaluate is:
console.log(y)
Okay, it's a function call, and the function expects an argument. But the JavaScript interpreter pauses for a moment. Again? Who is y? We can't call the function until we know y's value.
But rest assured, we already have a place where we stored the value: the function's context!
console.log(y) evaluates to console.log("hello") and finally prints to the console.
let x = 20
This line binds x to the value 20. But what x? Well, that's the wrong question. We don't care about that. We just have a value we want to give a name to. The right question is where: where do we want to store this binding? the answer is, the current environment!
global context: {
"x": 10
"test": <function object>
"main: <function object>
}
main's context: {
"y": "hello"
"x": 20
}
Now, the final line!
test()
It's a function call. As you can see, it takes no arguments. The scoping rules say we should create a new context where we store the bindings defined in that function:
test's context: {}
It has only one line:
console.log(x)
Okay, we need to call the log function, but AGAIN, sadly we can't do it yet. we need to evaluate x. But where?
test's context: {}
We don't find it in the function's context... where do we look next?
Now, the concept of chaining contexts appears. It says, if you don't find the binding you are looking for in the local function's context, look it up in the context where you defined the function. That's called lexical scoping.
lexical scoping: you always look up for variable names in the context where the function was defined , and not context where it was called.
We defined the function in the global scope, so we look there:
global context: {
"x": 10
"test": <function object>
"main": <function object>
}
We find it, and we grab the value from there:
console.log(10)
Now we can call the log function and print 10 on the screen.
that's it for this example.
By understanding the concept of context, many things start to fall into place, and it gives meaning to many of JavaScript's quirks, like the difference between let and var, hoisting, closures, and much more.
It all started by someone who thought it's a good idea to give names to things in programs.
They should call it—let me see—the White Way of Delight. Isn't that a nice imaginative name? Anne
This content originally appeared on DEV Community and was authored by Ghassen Faidi
Ghassen Faidi | Sciencx (2025-10-28T21:58:50+00:00) Before You Learn Closures, Understand Context. Retrieved from https://www.scien.cx/2025/10/28/before-you-learn-closures-understand-context/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.
