Renan Roggia
I consider myself a tech problem solver.
Compiled: Typically, the whole source code is transformed at once, and those resulting instructions are saved as output (usually in a file) that can later be executed.
Interpretation performs a similar task to compilation, in that it transforms your program into machine-understandable instructions. But the processing model is different. Unlike a program being compiled all at once, with interpretation the source code is transformed line by line; each line or statement is executed before immediately proceeding to processing the next line of the source code.
In classic compiler theory, a program is processed by a compiler in three basic stages:
Tokenizing/Lexing: breaking up a string of characters into meaningful (to the language) chunks, called tokens. For instance, consider the program: var a = 2;
. This program would likely be broken up into the following tokens: var
, a
, =
, 2
, and ;
. Whitespace may or may not be persisted as a token, depending on whether it's meaningful or not.
(The difference between tokenizing and lexing is subtle and academic, but it centers on whether or not these tokens are identified in a stateless or stateful way. Put simply, if the tokenizer were to invoke stateful parsing rules to figure out whether a
should be considered a distinct token or just part of another token, that would be lexing.)
Parsing: taking a stream (array) of tokens and turning it into a tree of nested elements, which collectively represent the grammatical structure of the program. This is called an Abstract Syntax Tree (AST).
For example, the tree for var a = 2;
might start with a top-level node called VariableDeclaration
, with a child node called Identifier
(whose value is a
), and another child called AssignmentExpression
which itself has a child called NumericLiteral
(whose value is 2
).
Code Generation: taking an AST and turning it into executable code. This part varies greatly depending on the language, the platform it's targeting, and other factors.
The JS engine takes the just described AST for var a = 2;
and turns it into a set of machine instructions to actually create a variable called a
(including reserving memory, etc.), and then store a value into a
.
While the JS specification does not require "compilation" explicitly, it requires behavior that is essentially only practical with a compile-then-execute approach.
There are three program characteristics you can observe to prove this to yourself: syntax errors, early errors, and hoisting.
Other than declarations, all occurrences of variables/identifiers in a program serve in one of two "roles": either they're the target of an assignment or they're the source of a value.
However, assignment targets and sources don't always literally appear on the left or right of an =
, so it's probably clearer to think in terms of target / source rather than left / right.)
A function
declaration is a special case of a target reference. You can think of it sort of like var getStudentName = function(studentID)
, but that's not exactly accurate. An identifier getStudentName
is declared (at compile time), but the = function(studentID)
part is also handled at compilation; the association between getStudentName
and the function is automatically set up at the beginning of the scope rather than waiting for an =
assignment statement to be executed.
If you place a variable declaration inside a function, the compiler handles this declaration as it's parsing the function, and associates that declaration with the function's scope. If a variable is block-scope declared (let
/ const
), then it's associated with the nearest enclosing { .. }
block, rather than its enclosing function (as with var
).
a variable must be resolved as coming from one of the scopes that are lexically available to it; otherwise the variable is said to be "undeclared" (which usually results in an error!). If the variable is not declared in the current scope, the next outer/enclosing scope will be consulted. This process of stepping out one level of scope nesting continues until either a matching variable declaration can be found, or the global scope is reached and there's nowhere else to go.
compilation creates a map of all the lexical scopes that lays out what the program will need while it executes. You can think of this plan as inserted code for use at runtime, which defines all the scopes (aka, "lexical environments") and registers all the identifiers (variables) for each scope.
The term "lexical" refers to the first stage of compilation (lexing/parsing).
References (non-declarations) to variables/identifiers are allowed if there's a matching declaration either in the current scope, or any scope above/outside the current scope, but not with declarations from lower/nested scopes.
We can conceptualize the process of determining these non-declaration marble colors during runtime as a lookup.
Scopes can be lexically nested to any arbitrary depth as the program defines.
Each scope automatically has all its identifiers registered at the start of the scope being executed (variable hoisting)
If the variable is a source, an unresolved identifier lookup is considered an undeclared (unknown, missing) variable, which always results in a ReferenceError
being thrown. Also, if the variable is a target, and the code at that moment is running in strict-mode, the variable is considered undeclared and similarly throws a ReferenceError
.
The error message for an undeclared variable condition, in most JS environments, will look like, "Reference Error: XYZ is not defined." The phrase "not defined" seems almost identical to the word "undefined," as far as the English language goes. But these two are very different in JS, and this error message unfortunately creates a persistent confusion.
"Not defined" really means "not declared"—or, rather, "undeclared," as in a variable that has no matching formal declaration in any lexically available scope. By contrast, "undefined" really means a variable was found (declared), but the variable otherwise has no other value in it at the moment, so it defaults to the undefined
value.
The connections between scopes that are nested within other scopes is called the scope chain, which determines the path along which variables can be accessed. The chain is directed, meaning the lookup moves upward/outward only.
Consider a reference to a variable that isn't declared in any lexically available scopes in the current file—see Get Started, Chapter 1, which asserts that each file is its own separate program from the perspective of JS compilation. If no declaration is found, that's not necessarily an error. Another file (program) in the runtime may indeed declare that variable in the shared global scope.
So the ultimate determination of whether the variable was ever appropriately declared in some accessible bucket may need to be deferred to the runtime.
Where having different lexical scope buckets starts to matter more is when you have two or more variables, each in different scopes, with the same lexical names. A single scope cannot have two or more variables with the same name; such multiple references would be assumed as just one variable.
When you choose to shadow a variable from an outer scope, one direct impact is that from that scope inward/downward (through any nested scopes) it's now impossible for any marble to be colored as the shadowed variable.
It's lexically impossible to reference the global studentName
anywhere inside of the printStudent(..)
function (or from any nested scopes).
It is possible to access a global variable from a scope where that variable has been shadowed, but not through a typical lexical identifier reference.
No. Mutating the contents of the object value via a reference copy is not the same thing as lexically accessing the variable itself. We still can't reassign the BLUE(2) special
parameter.
Not all combinations of declaration shadowing are allowed. let
can shadow var
, but var
cannot shadow let
Summary: let
(in an inner scope) can always shadow an outer scope's var
. var
(in an inner scope) can only shadow an outer scope's let
if there is a function boundary in between.
Such a function
declaration will create an identifier in the enclosing scope.
function askQuestion() {
// ..
}
The same is true for the variable askQuestion
being created. But since it's a function
expression—a function definition used as value instead of a standalone declaration—the function itself will not "hoist".
A function without a name identifier is referred to as an "anonymous function expression."
Anonymous function expressions clearly have no name identifier that affects either scope.
var askQuestion = function(){
// ..
};
One major difference between function
declarations and function
expressions is what happens to the name identifier of the function.
We know askQuestion
ends up in the outer scope.
ofTheTeacher
is declared as an identifier inside the function itself
Not only is ofTheTeacher
declared inside the function rather than outside, but it's also defined as read-only
var askQuestion = function ofTheTeacher() {
console.log(ofTheTeacher);
};
askQuestion();
// function ofTheTeacher()...
console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined
Arrow functions are lexically anonymous, meaning they have no directly related identifier that references the function.
Other than being anonymous (and having no declarative form), =>
arrow functions have the same lexical scope rules as function
functions do.
When a function (declaration or expression) is defined, a new scope is created. The positioning of scopes nested inside one another creates a natural scope hierarchy throughout the program, called the scope chain. The scope chain controls variable access, directionally oriented upward and outward.
Each new scope offers a clean slate, a space to hold its own set of variables. When a variable name is repeated at different levels of the scope chain, shadowing occurs, which prevents access to the outer variable from that point inward.
So how exactly do all those separate files get stitched together in a single runtime context by the JS engine?
First, if you're directly using ES modules (not transpiling them into some other module-bundle format), these files are loaded individually by the JS environment. Each module then import
s references to whichever other modules it needs to access. The separate module files cooperate with each other exclusively through these shared imports, without needing any shared outer scope.
Second, if you're using a bundler in your build process, all the files are typically concatenated together before delivery to the browser and JS engine, which then only processes one big file. Even with all the pieces of the application co-located in a single file, some mechanism is necessary for each piece to register a name to be referred to by other pieces, as well as some facility for that access to occur.
In some build setups, the entire contents of the file are wrapped in a single enclosing scope, such as a wrapper function, universal module
And finally, the third way: whether a bundler tool is used for an application, or whether the (non-ES module) files are simply loaded in the browser individually (via tags or other dynamic JS resource loading), if there is no single surrounding scope encompassing all these pieces, the global scope is the only way for them to cooperate with each other
Most developers agree that the global scope shouldn't just be a dumping ground for every variable in your application.
But it's also undeniable that the global scope is an important glue for practically every JS application.
Different JS environments handle the scopes of your programs, especially the global scope, differently.
the global object (commonly, window
in the browser)
A simple way to avoid this gotcha with global declarations: always use var
for globals. Reserve let
and const
for block scopes
One surprising behavior in the global scope you may encounter with browser-based JS applications: a DOM element with an id
attribute automatically creates a global variable that references it.
We used var
for our declaration, which does not shadow the pre-defined name
global property. That means, effectively, the var
declaration is ignored, since there's already a global scope object property of that name. As we discussed earlier, had we used let name
, we would have shadowed window.name
with a separate global name
variable.
Web Workers are a web platform extension on top of browser-JS behavior, which allows a JS file to run in a completely separate thread (operating system wise) from the thread that's running the main JS program.
Since there is no DOM access, the window
alias for the global scope doesn't exist.
In a Web Worker, the global object reference is typically made using self
The take-away is that Developer Tools, while optimized to be convenient and useful for a variety of developer activities, are not suitable environments to determine or verify explicit and nuanced behaviors of an actual JS program context.
One of the most obvious impacts of using ESM is how it changes the behavior of the observably top-level scope in a file.
Despite being declared at the top level of the (module) file, in the outermost obvious scope, studentName
and hello
are not global variables. Instead, they are module-wide, or if you prefer, "module-global."
It's just that global variables don't get created by declaring variables in the top-level scope of a module.
The module's top-level scope is descended from the global scope, almost as if the entire contents of the module were wrapped in a function. Thus, all variables that exist in the global scope (whether they're on the global object or not!) are available as lexical identifiers from inside the module's scope.
The practical effect is that the top level of your Node programs is never actually the global scope, the way it is when loading a non-module file in the browser.
As noted earlier, Node defines a number of "globals" like require()
, but they're not actually identifiers in the global scope (nor properties of the global object). They're injected in the scope of every module, essentially a bit like the parameters listed in the Module(..)
function declaration.
The only way to do so is to add properties to another of Node's automatically provided "globals," which is ironically called global
.
As of ES2020, JS has finally defined a standardized reference to the global scope object, called globalThis
. So, subject to the recency of the JS engines your code runs in, you can use globalThis
in place of any of those other approaches.
Recall Chapter 1 points out that all identifiers are registered to their respective scopes during compile time. Moreover, every identifier is created at the beginning of the scope it belongs to, every time that scope is entered.
The term most commonly used for a variable being visible from the beginning of its enclosing scope, even though its declaration may appear further down in the scope, is called hoisting.
The answer is a special characteristic of formal function
declarations, called function hoisting. When a function
declaration's name identifier is registered at the top of its scope, it's additionally auto-initialized to that function's reference.
One key detail is that both function hoisting and var
-flavored variable hoisting attach their name identifiers to the nearest enclosing function scope (or, if none, the global scope), not a block scope.
Declarations with let
and const
still hoist. But these two declaration forms attach to their enclosing block rather than just an enclosing function as with var
and function
declarations.
Function hoisting only applies to formal function
declarations
In addition to being hoisted, variables declared with var
are also automatically initialized to undefined
at the beginning of their scope—again, the nearest enclosing function, or the global. Once initialized, they're available to be used (assigned to, retrieved from, etc.) throughout the whole scope.
A function
declaration is hoisted and initialized to its function value (again, called function hoisting). A var
variable is also hoisted, and then auto-initialized to undefined
. Any subsequent function
expression assignments to that variable don't happen until that assignment is processed during runtime execution.
There's two necessary parts to the explanation:
undefined
from the top of the scope.Rather than hoisting being a concrete execution step the JS engine performs, it's more useful to think of hoisting as a visualization of various actions JS takes in setting up the program before execution.
The typical assertion of what hoisting means: lifting—like lifting a heavy weight upward—any identifiers all the way to the top of a scope
I assert that hoisting should be used to refer to the compile-time operation of generating runtime instructions for the automatic registration of a variable at the beginning of its scope, each time that scope is entered.
A repeated var
declaration of the same identifier name in a scope is effectively a do-nothing operation.
It's not just that two declarations involving let
will throw this error. If either declaration uses let
, the other can be either let
or var
, and the error will still occur
In other words, the only way to "re-declare" a variable is to use var
for all (two or more) of its declarations.
So if const
declarations cannot be re-assigned, and const
declarations always require assignments, then we have a clear technical reason why const
must disallow any "re-declarations": any const
"re-declaration" would also necessarily be a const
re-assignment, which can't be allowed!
All the rules of scope (including "re-declaration" of let
-created variables) are applied per scope instance. In other words, each time a scope is entered during execution, everything resets.
Each loop iteration is its own new scope instance, and within each scope instance, value
is only being declared once. So there's no attempted "re-declaration," and thus no error.
Is value
being "re-declared" here, especially since we know var
allows it? No. Because var
is not treated as a block-scoping declaration (see Chapter 6), it attaches itself to the global scope
The straightforward answer is: const
can't be used with the classic for
-loop form because of the required re-assignment.
With var
declarations, the variable is "hoisted" to the top of its scope. But it's also automatically initialized to the undefined
value, so that the variable can be used throughout the entire scope.
However, let
and const
declarations are not quite the same in this respect.
the only way to do so is with an assignment attached to a declaration statement. An assignment by itself is insufficient!
The term coined by TC39 to refer to this period of time from the entering of a scope to where the auto-initialization of the variable occurs is: Temporal Dead Zone (TDZ).
The TDZ is the time window where a variable exists but is still uninitialized, and therefore cannot be accessed in any way. Only the execution of the instructions left by Compiler at the point of the original declaration can do that initialization. After that moment, the TDZ is done, and the variable is free to be used for the rest of the scope.
"temporal" in TDZ does indeed refer to time not position in code
The actual difference is that let
/const
declarations do not automatically initialize at the beginning of the scope, the way var
does. The debate then is if the auto-initialization is part of hoisting, or not? I think auto-registration of a variable at the top of the scope (i.e., what I call "hoisting") and auto-initialization at the top of the scope (to undefined
) are distinct operations and shouldn't be lumped together under the single term "hoisting."
So to summarize, TDZ errors occur because let
/const
declarations do hoist their declarations to the top of their scopes, but unlike var
, they defer the auto-initialization of their variables until the moment in the code's sequencing where the original declaration appeared. This window of time (hint: temporal), whatever its length, is the TDZ.
always put your let
and const
declarations at the top of any scope. Shrink the TDZ window to zero (or near zero) length, and then it'll be moot.
Principle of Least Privilege expresses a defensive posture to software architecture: components of the system should be designed to function with least privilege, least access, least exposure. If each piece is connected with minimum-necessary capabilities, the overall system is stronger from a security standpoint, because a compromise or failure of one piece has a minimized impact on the rest of the system.
Naming Collisions: if you use a common and useful variable/function name in two different parts of the program, but the identifier comes from one shared scope (like the global scope), then name collision occurs, and it's very likely that bugs will occur as one part uses the variable/function in a way the other part doesn't expect.
Unexpected Behavior: if you expose variables/functions whose usage is otherwise private to a piece of the program, it allows other developers to use them in ways you didn't intend, which can violate expected behavior and cause bugs.
Unintended Dependency: if you expose variables/functions unnecessarily, it invites other developers to use and depend on those otherwise private pieces.
Declare variables in as small and deeply nested of scopes as possible, rather than placing everything in the global (or even outer function) scope.
That means we can name every single occurrence of such a function expression the exact same name, and never have any collision. More appropriately, we can name each occurrence semantically based on whatever it is we're trying to hide, and not worry that whatever name we choose is going to collide with any other function
expression scope in the program.
we're defining a function
expression that's then immediately invoked. This common pattern has a (very creative!) name: Immediately Invoked Function Expression (IIFE).
Unlike earlier with hideTheCache()
, where the outer surrounding (..)
were noted as being an optional stylistic choice, for a standalone IIFE they're required; they distinguish the function
as an expression, not a statement. For consistency, however, always surround an IIFE function
with ( .. )
.
So, if the code you need to wrap a scope around has return
, this
, break
, or continue
in it, an IIFE is probably not the best approach. In that case, you might look to create the scope with a block instead of a function.
In general, any { .. }
curly-brace pair which is a statement will act as a block, but not necessarily as a scope.
A block only becomes a scope if necessary, to contain its block-scoped declarations (i.e., let
or const
).
Not all { .. }
curly-brace pairs create blocks (and thus are eligible to become scopes):
{ .. }
curly-brace pairs to delimit their key-value lists, but such object values are not scopes.class
uses { .. }
curly-braces around its body definition, but this is not a block or scope.function
uses { .. }
around its body, but this is not technically a block—it's a single statement for the function body. It is, however, a (function) scope.{ .. }
curly-brace pair on a switch
statement (around the set of case
clauses) does not define a block/scope.In most languages that support block scoping, an explicit block scope is an extremely common pattern for creating a narrow slice of scope for one or a few variables. So following the POLE principle, we should embrace this pattern more widespread in JS as well; use (explicit) block scoping to narrow the exposure of identifiers to the minimum practical.
If you find yourself placing a let
declaration in the middle of a scope, first think, "Oh, no! TDZ alert!" If this let
declaration isn't needed in the first half of that block, you should use an inner explicit block scope to further narrow its exposure!
But the benefits of the POLE principle are best achieved when you adopt the mindset of minimizing scope exposure by default, as a habit. If you follow the principle consistently even in the small cases, it will serve you more as your programs grow.
var
and let
That variable is used across the entire function (except the final return
statement). Any variable that is needed across all (or even most) of a function should be declared so that such usage is obvious. var
var
should be reserved for use in the top-level scope of a function.
Why not just use let
in that same location? Because var
is visually distinct from let
and therefore signals clearly, "this variable is function-scoped."
Using let
in the top-level scope, especially if not in the first few lines of a function, and when all the other declarations in blocks use let
, does not visually draw attention to the difference with the function-scoped declaration.
As long as your programs are going to need both function-scoped and block-scoped variables, the most sensible and readable approach is to use both var
and let
together, each for their own best purpose.
let
?"What is the most minimal scope exposure that's sufficient for this variable?" Once that is answered, you'll know if a variable belongs in a block scope or the function scope.
Declaration belongs in a block scope, use let
. If it belongs in the function scope, use var
(again, just my opinion).
Placing the var
declaration for tmp
inside the if
statement signals to the reader of the code that tmp
belongs to that block. Even though JS doesn't enforce that scoping, the semantic signal still has benefit for the reader of your code.
The err
variable declared by the catch
clause is block-scoped to that block. This catch
clause block can hold other block-scoped declarations via let
. But a var
declaration inside this block still attaches to the outer function/global scope.
So what about function
declarations that appear directly inside blocks? As a feature, this is called "FiB."
The JS specification says that function
declarations inside of blocks are block-scoped, so the answer should be (1).
As far as I'm concerned, the only practical answer to avoiding the vagaries of FiB is to simply avoid FiB entirely. In other words, never place a function
declaration directly inside any block.
Always place function
declarations anywhere in the top-level scope of a function (or in the global scope).
It's important to notice that here I'm placing a function
expression, not a declaration, inside the if
statement. That's perfectly fine and valid, for function
expressions to appear inside blocks. Our discussion about FiB is about avoiding function
declarations in blocks.
FiB is not worth it, and should be avoided.
Closure builds on this approach: for variables we need to use over time, instead of placing them in larger outer scopes, we can encapsulate (more narrowly scope) them but still preserve access from inside functions, for broader use. Functions remember these referenced scoped variables via closure.
Closure is a behavior of functions and only functions.
For closure to be observed, a function must be invoked, and specifically it must be invoked in a different branch of the scope chain from where it was originally defined.
While greetStudent(..)
does receive a single argument as the parameter named greeting
, it also makes reference to both students
and studentID
, identifiers which come from the enclosing scope of lookupStudent(..)
. Each of those references from the inner function to the variable in an outer scope is called a closure.
In academic terms, each instance of greetStudent(..)
closes over the outer variables students
and studentID
.
Closure allows greetStudent(..)
to continue to access those outer variables even after the outer scope is finished
It's just important not to skip over the fact that even tiny arrow functions can get in on the closure party.
closure is associated with an instance of a function, rather than its single lexical definition.
every time the outer adder(..)
function runs, a new inner addTo(..)
function instance is created, and for each new instance, a new closure.
Closure is actually a live link, preserving access to the full variable itself. We're not limited to merely reading a value;
the closed-over variable can be updated (re-assigned) as well!
But greeting()
is closed over the variable studentName
, not its value. The classic illustration of this mistake is defining functions inside a loop
Each of the three functions in the keeps
array do have individual closures, but they're all closed over that same shared i
variable.
Of course, a single variable can only ever hold one value at any given moment. So if you want to preserve multiple values, you need a different variable for each.
Each function is now closed over a separate (new) variable from each iteration, even though all of them are named j
. And each j
gets a copy of the value of i
at that point in the loop iteration; that j
never gets re-assigned.
In fact, global scope variables essentially cannot be (observably) closed over, because they're always accessible from everywhere. No function can ever be invoked in any part of the scope chain that is not a descendant of the global scope.
All function invocations can access global variables, regardless of whether closure is supported by the language or not. Global variables don't need to be closed over.
Variables that are merely present but never accessed don't result in closure:
Closure is observed when a function uses variable(s) from outer scope(s) even while running in a scope where those variable(s) wouldn't be accessible.
The key parts of this definition are:
Since closure is inherently tied to a function instance, its closure over a variable lasts as long as there is still a reference to that function.
Closure can unexpectedly prevent the GC of a variable that you're otherwise done with, which leads to run-away memory usage over time.
Conceptually, closure is per variable rather than per scope.
Closure must be per scope, implementation wise, and then an optional optimization trims down the scope to only what was closed over (a similar outcome as per variable closure).
In cases where a variable holds a large value (like an object or array) and that variable is present in a closure scope, if you don't need that value anymore and don't want that memory held, it's safer (memory usage) to manually discard the value rather than relying on closure optimization/GC.
Closure is the link-association that connects that function to the scope/variables outside of itself, no matter where that function goes.
Closure instead describes the magic of keeping alive a function instance, along with its whole scope environment and chain, for as long as there's at least one reference to that function instance floating around in any other part of the program.
The goal of encapsulation is the bundling or co-location of information (data) and behavior (functions) that together serve a common purpose.
The recent trend in modern front-end programming to organize applications around Component architecture pushes encapsulation even further.
It's easier to build and maintain software when we know where things are, with clear and obvious boundaries and connection points.
A module is a collection of related data and functions (often referred to as methods in this context), characterized by a division between hidden private details and public accessible details, usually called the "public AP
Utils
here is a useful collection of utilities, yet they're all state-independent functions. Gathering functionality together is generally good practice, but that doesn't make this a module.
Even if you bundle data and stateful functions together, if you're not limiting the visibility of any of it, then you're stopping short of the POLE aspect of encapsulation; it's not particularly helpful to label that a module.
classic module," which was originally referred to as the "revealing module" when it first emerged in the early 2000s.
The use of an IIFE implies that our program only ever needs a single central instance of the module, commonly referred to as a "singleton."
So to clarify what makes something a classic module:
In some older legacy code, you may run across references to just a bare exports
, but for code clarity you should always fully qualify that reference with the module.
prefix.
CommonJS modules behave as singleton instances, similar to the IIFE module definition style presented before. No matter how many times you require(..)
the same module, you just get additional references to the single shared module instance.
One notable difference is that ESM files are assumed to be strict-mode, without needing a "use strict"
pragma at the top. There's no way to define an ESM as non-strict-mode.
Even though export
appears before the function
keyword here, this form is still a function
declaration that also happens to be exported.
This is a so-called "default export," which has different semantics from other exports. In essence, a "default export" is a shorthand for consumers of the module when they import
, giving them a terser syntax when they only need this single default API member.
A named import can also be renamed with the as
keyword:
As is likely obvious, the *
imports everything exported to the API, default and named, and stores it all under the single namespace identifier as specified.
Here, studentID
is a considered a "simple" parameter, so it does behave as a member of the BLUE(2) function scope. But if we change it to be a non-simple parameter, that's no longer technically the case. Parameter forms considered non-simple include parameters with default values, rest parameters (using ...
), and destructured parameters.
My advice to avoid getting bitten by these weird nuances:
The name identifier of a function expression is in its own implied scope, nested between the outer enclosing scope and the main inner function scope.
As you contemplate naming your functions, consider:
Every function in your program has a purpose. If it doesn't have a purpose, take it out, because you're just wasting space. If it does have a purpose, there is a name for that purpose.
These are referred to as inferred names. Inferred names are fine, but they don't really address the full concern I'm discussing.
The vast majority of all function
expressions, especially anonymous ones, are used as callback arguments; none of these get a name. So relying on name inference is incomplete, at best.
Any assignment of a function
expression that's not a simple assignment will also fail name inferencing
Leaving off the lexical name from your callback makes it harder to reliably self-reference the function. You could declare a variable in an enclosing scope that references the function, but this variable is controlled by that enclosing scope—it could be re-assigned, etc.—so it's not as reliable as the function having its own internal self-reference.
There's just no reasonable argument to be made that omitting the name keepOnlyOdds
from the first callback more effectively communicates to the reader the purpose of this callback. You saved 13 characters, but lost important readability information
If you can't figure out a good name, you likely don't understand the function and its purpose yet. The function is perhaps poorly designed, or it does too many things, and should be re-worked. Once you have a well-designed, single-purpose function, its proper name should become evident.
Don't use them as a general replacement for regular functions. They're more concise, yes, but that brevity comes at the cost of omitting key visual delimiters that help our brains quickly parse out what we're reading. And, to the point of this discussion, they're anonymous, which makes them worse for readability from that angle as well.
Briefly: arrow functions don't define a this
identifier keyword at all. If you use a this
inside an arrow function, it behaves exactly as any other variable reference, which is that the scope chain is consulted to find a function scope (non-arrow function) where it is defined, and to use that one.
So, in the rare cases you need lexical this, use an arrow function. It's the best tool for that job. But just be aware that in doing so, you're accepting the downsides of an anonymous function.
The !
, +
, ~
, and several other unary operators (operators with one operand) can all be placed in front of function
to turn it into an expression. Then the final ()
call is valid, which makes it an IIFE.
The reason I prefer to take advantage of function hoisting is that it puts the executable code in any scope at the top, and any further declarations (functions) below.
I think function hoisting makes code more readable through a flowing, progressive reading order, from top to bottom.
While that kind of inverted ordering was helpful for function hoisting, here I think it usually makes code harder to reason about.
const
-antly ConfusedThe only time I ever use const
is when I'm assigning an already-immutable value (like 42
or "Hello, friends!"
), and when it's clearly a "constant" in the sense of being a named placeholder for a literal value, for semantic purposes. That's what const
is best used for. That's pretty rare in my code, though.
In this context, "calling back" makes a lot of sense. The JS engine is resuming our suspended program by calling back in at a specific location. OK, so a callback is asynchronous.
There's nothing to call back into per se, because the program hasn't paused or exited. We're passing a function (reference) from one part of the program to another part of the program, and then it's immediately invoked.
There's other established terms that might match what we're doing—passing in a function (reference) so that another part of the program can invoke it on our behalf. You might think of this as Dependency Injection (DI) or Inversion of Control (IoC).
Notably, Martin Fowler cites IoC as the difference between a framework and a library: with a library, you call its functions; with a framework, it calls your functions.