One of the fundamental elements of any modern programming language is the abstraction of variables. The variable may be described as a link between an identifier and a value. This high-level concept allows a program to store and operate upon the internal state. There are many different alternatives regarding the abstraction’s elements that define the ultimate implementation.
Today, I would like us to go through those critical details and examine which of their variants influenced the development of JavaScript, the programming language that is probably the most widely known in the world, to the biggest extent.
At the very beginning, we should state the obvious and notice that variables play a vital role in the JavaScript programming language. They are not only blended into the language via var
, let
, and const
keywords, but most notably, they immensely affect the core implementation. The concept of variables includes the type system, memory management with global and functional context, and those are critical elements of the language itself. In other words, the variables and their corresponding ideas are everywhere.
Types
The first attribute describing a variable is the type, which we define as a span of values entitled to be assigned. We distinguish two significant type systems. The first one is the static-type system mostly seen in compiled languages because the compilation phase is a great moment to conduct various static code analyses, type validation included.
The second approach is the dynamic-type system, which allows for more flexibility and is popular among interpreted languages. Both of these type systems have pros and cons, but the question is, where does JavaScript stand on this matter.
First of all, JavaScript does not introduce any type-enforcing variable declaration. The type is determined based on the assigned value, so we may use the identifier with any value we want. We are also entitled to reassign any identifier declared by the let
(or var
) syntax. The type of a variable plays an essential role in utilizing type-specific methods and various operators, which behave differently for different value types. For instance, the +
operator combines values when used with strings and adds them up when used with numbers.
Moreover, when we talk about complex values, like objects, arrays, and functions, for JavaScript, they are only different forms of the object
application. We may store various types of data inside those data structures. All of this proves that JavaScript is, in fact, a dynamically-typed language with great flexibility and a relatively shallow learning curve.
When we talk about types in JavaScript, it is worth mentioning the language that has been gaining popularity in recent years — TypeScript. This superset of the original language is a syntax that introduces type annotations and performs static-type checking during the compilation phase.
As web applications grew in size and complexity, the programmers noticed an increasing demand for a more consistent code, which led to the reduction of a large category of common errors. With that in mind, it seems that the static-type system and compilation process will be even more prevalent in the future of web development.
Memory Management
The second attribute regarding variables is memory management, which we divide into three main spaces: static, stack, and heap.
In terms of JavaScript, we can argue that the static variables are the ever-existing identifiers and APIs accessible in a browser environment. For instance, the window
variable exists even before our script begins its execution and is available throughout the process. That’s why we may regard it as static.
Every variable that is defined inside a function, whether a named parameter or explicit declaration in the function’s body, is a variable that resides on the stack. The main property of this memory is that when the function ends its execution, all the local variables are taken away (popped) from the stack and we cannot access them anymore.
The heap space is a location for complex values that vary in size. That is why all non-primitive values like objects and arrays are stored on the heap. The necessity emerges due to potentially huge data inside those structures, which may prove the stack to be inefficient.
Such a separation is also the main reason why some values are given by value and others by reference. All the immutable (primitive) values like numbers and strings are passed by value, and when we modify them inside a receiving function, we actually operate on a separate value. On the contrary, the mutable (complex) values like objects and arrays are passed by reference, and when we modify them inside other functions, we operate on the exact same memory location. Hence, in this situation, changing the received data means changing the original value as well.
Thanks to the straightforward literals, creating various object-based structures, like { }
and [ ]
, we utilize the implicit usage of the heap space. All complex values are created on the fly, and we do not need to care for memory allocation.
At the other end, there is the automatic freeing of memory. As the allocation itself, the deallocation process is implicit and does not require any attention from the programmer. The process that takes care of freeing the memory after it is no longer needed is called the garbage collector.
Two effective heuristics try to answer the question of whether the value is in use. One is counting the references that point to the specific location. The other is verifying its accessibility by checking bindings, starting from the main context of the program. The second approach potentially solves the problem of cycle references, where no variable is really accessible from the outside. However, knowing that the very problem is quite complicated, the programmer should remember not to duplicate references to massive data structures when unnecessary.
Scope and extent
The last two attributes concerning variables are the scope of the identifier accessibility and the extent of a variable lifetime.
The scope can be either static, which means it is based on a lexical closeness, or dynamic, based on an execution chain closeness.
The most popular approach in the programming world is definitely the static way, and JavaScript falls into this category as well. The code determines the visibility of different variables in the current function, and what matters is where the given function was defined, not where it was invoked.
From the variable perspective, its identifier is visible in the defining function’s whole body and completely hidden from the outside. Moreover, it is accessible in any function written inside the defining one, simply because the lexical rule stands.
This perspective allows introducing a handy concept in JavaScript called closure, or lexical closure, to be precise. Because a specific function sees the variables from the context of the enclosing one, we may access them even after the enclosing function is no longer executing. Those variables may be treated as a hidden state for the current function, thus allowing for the implementation of private data with public API.
The extent of variables is closely related to the scope due to automatic memory management with a garbage collector in place. There is no way to have an identifier without properly allocated memory because the language interpreter implicitly manages the stack and heap spaces.
When it comes to memory leaks, the only way to make that happen is to impede the garbage collector from freeing unused memory. As mentioned before, we may avoid leaks by keeping track of all references to the large data structures we create.
Conclusion
Variables are a vital part of the JavaScript language. Many designs and decisions play a crucial role in the implementation of this abstraction.
Understanding the concept behind dynamic-type system that allows for flexible data operations. Getting to know the internals of memory management, with its automatic allocation and usage of a garbage collector. Learning the static scope with its lexical identifier resolution. All those elements allow for a better language apprehension and thus helping design and implement reasonable complex solutions.