Profundice en las cadenas y cierres de alcance

Cómo funcionan las cadenas y los cierres Scope bajo el capó con ejemplos.

Comprender el alcance y los cierres en JavaScript

Para profundizar y obtener la información que necesita, piense como un periodista. Haga las seis preguntas principales: quién, qué, por qué, dónde, cuándo y cómo. Si puede responder a todas estas preguntas sobre un tema en particular, habrá obtenido la esencia de lo que necesita saber.

Antes de llegar a los cierres, debemos comprender el alcance.

Primero, si sabe qué es [[scope]] (alcance de doble paréntesis), este artículo no es para usted. Tienes conocimientos más avanzados y puedes seguir adelante.

El qué…

¿Qué es el alcance y por qué es importante?

El alcance es el entorno de contexto (también conocido como entorno léxico) que se crea cuando se escribe una función. Este contexto define a qué otros datos tiene acceso.

Dicho de otra manera, el alcance tiene que ver con el acceso. ¿Tiene la función la capacidad de buscar una variable para su ejecución o manipulación, qué variables son visibles?

Hay dos tipos de alcance: local y global. La resolución del alcance, o encontrar qué variables pertenecen a dónde, comienza en el contexto más interno y avanza hacia afuera hasta que se encuentra el identificador. Empecemos con algo pequeño ...

var firstNum = 1;
function number() { var secondNum = 2; return firstNum + secondNum;}
number();

El cuándo, por qué y cómo ... contexto de ejecución

Cuando se invoca la función, forma un nuevo contexto de ejecución. ¿Qué es un contexto de ejecución? Bueno, así como tenemos dos tipos de alcance, tenemos dos tipos de contexto de ejecución. Son un contexto de ejecución global y un contexto de ejecución de funciones.

El contexto global siempre está funcionando. En el caso de un entorno de navegador, solo se detiene cuando se cierra el navegador. Cuando llamamos a una función, colocamos el contexto de ejecución de esa función encima del contexto de ejecución global. De ahí la terminología que los apilamos .

JavaScript es un lenguaje de un solo hilo, lo que significa que solo puede hacer una cosa a la vez. Cuando llamamos a una función, se pausa el contexto de ejecución anterior. La función llamada está en la parte superior y luego se ejecuta. Cuando eso termina, se saca de la pila y luego se reanuda el contexto de ejecución anterior. Esta 'pila' de ejecución es lo que realiza un seguimiento de la posición de ejecución en nuestra aplicación. También es importante para buscar identificadores.

Entonces, ahora tenemos un contexto de ejecución formado, ¿qué sigue?

Cada contexto de ejecución tiene un objeto variable asociado

Primero, se forma un Objeto de Activación (no accesible por código, pero opera en segundo plano). Está asociado con este contexto de ejecución. Este objeto contiene todas las variables , funciones y parámetros declaradospasado dentro de ese contexto (su alcance o rango de accesibilidad).

Los parámetros de una función se definen implícitamente. Son "locales" al alcance de esa función. Estas variables declaradas son “elevadas”, llevadas a la parte superior del ámbito al que pertenecen.

Antes de continuar, para evitar confusiones, en el contexto de ejecución global, se crea un objeto variable y, si es una función, es un objeto de activación . Son prácticamente idénticos.

Ahora, cuando se invoca esta función, se crea una "cadena de alcance" de estos objetos. ¿Por qué? La cadena de alcance es una forma de vincular o proporcionar un acceso sistemático a todas las variables y otras funciones a las que tiene acceso el contexto de ejecución actual (función en este caso). [[Alcance]] es el mecanismo oculto que vincula estos objetos variables para la búsqueda de identificadores. Este [[Scope]] oculto es una propiedad de la función, creada en la declaración, no en la invocación.

A la cabeza del tren de la cadena del osciloscopio, si es una función, está el Objeto de activación . Este objeto de activación tiene sus propias variables declaradas, argumentos y esto.

A continuación, en la cadena de alcance, está el siguiente objeto del contexto contenedor. Si es una variable global, es un objeto variable. Si es una función, es un Objeto de Activación . Esto sucede hasta que llegamos al contexto global. Es por eso que puede ver que comenzamos desde el contexto más interno hasta el más externo, piense en las muñecas de anidación rusas.

¿Cuál es la diferencia entre una variable declarada y una no declarada? Si el identificador está precedido por una var, let o const, se declara explícitamente y se asigna espacio de memoria para el uso de esa variable. Si el identificador no se declara explícitamente, se declara implícitamente en el alcance global que exploraremos en breve. Para los propósitos de este artículo, me quedo con var, sin ninguna razón en particular.

Lo sé, lo anterior fue un poco técnico y, para ser honesto, mientras escribía esto, solo me enteré de los objetos Variable y Activación. Ahora que tiene la explicación de la inmersión profunda, aquí hay una descripción de alto ángulo ...

La cadena de alcance es similar a la cadena de prototipos. Si no se encuentra una variable o propiedad, continúa hacia arriba en la cadena hasta que se encuentra o se lanza un error. La función crea una propiedad [[alcance]] oculta. Esta propiedad vincula los ámbitos más internos a los más externos. En este caso, la cadena de alcance de number está vinculada al objeto de ventana global (el contexto contenedor que contiene la función number). Esto es lo que permite que el motor busque fuera del número de función para encontrar firstNum y secondNum.

Por ejemplo, tomemos el mismo número de función y cambiemos una cosa:

// global scope - includes firstNum, secondNum, and the// function number
var firstNum = 1;
function number() { // local scope for number - only thirdNum is local to number() // because it was explicitly declared. secondNum is implicitly declared in the // the global scope.
secondNum = 2; var thirdNum = 3; return firstNum + secondNum; }// what do we have access to in the global scope?number(); // 3firstNum; // 1secondNum; // 2thirdNum; // Reference Error: thirdNum is not defined

When speaking of global scope, variable declarations, non-nested function declarations, and function expressions (still considered a variable definition) are considered in the scope of the global window object in the browser. So as we see above, the window object has a properties firstNum, secondNum, and number added to it. If we proceed along the scope chain looking for it, we keep looking until we reach the global context’s variable object. If it’s not in there, then we get the Reference Error.

In a new tab, type "about:blank" in the search bar. A blank page will open and hit cmd-option-i to open dev tools.
Type the code above and remember, shift-enter for a new line!
Now type "window" and explore all the properties on the window object.
Look closely and you will see the properties firstNum, secondNum, and number are all available on the window object.

When we try to access thirdNum outside of where it was declared, we get a Reference Error. The engine that compiles the code failed to find an identifier in the window global scope object.

ThirdNum is only available inside of the function where it was declared. It is encapsulated or private to function number

The question you may have is “Does the global scope has access to everything inside of number?” Again, scope only works from the inside out, the innermost context, local, to the outermost context, global.

Starting with local scope, we can say that data and variables that are wrapped in a function are only accessible to members of that function. The scope chain is what links firstNum to number().

When number() is invoked, the non-technical conversation goes like this…

Engine: “Number, I’m giving you a new execution context. Let me find what you need to run” Engine : “Ok, I see that thirdNum is explicitly declared. I’m setting space aside for you, go to the top of number’s function block and wait till I call you… Engine : “Number, I see secondNum, does he belong to you?” Number : “Nope.” Engine : “Ok, I see you’re linked to the global window object, let me look outside of you.” Engine : “Window, I have an identifier named secondNum, does he belong to you?” Window : “He didn’t declare himself explicitly in Number with a var, let, or

const, so I’ll take him and set space aside.”Engine: “Cool. Number, I see firstNum in your function block, does he belong to you?”Number: “Nope.”Engine: “Window, I see firstNum being used inside of Number, he needs him, does he belong to you?”Window: “Yes, he was declared.”Engine: “Everyone is accounted for, Now I’m assigning values to variables.”Engine: Number, I’m executing you, ready, go!”

That’s pretty much it for understanding scope, The key takeaways are:

  1. Identifier lookup works from the inside out and stops at the first match.
  2. There are two types of scope, global and local
  3. The scope chain is created at function invocation and is based on where variables and/or blocks of code are written (lexical environment). Are variables or functions nested?
  4. In JavaScript, if an identifier is not proceeded with a var, let, or const, it is implicitly declared in the global scope.
  5. Scope does not go 1 for 1 with a function, it goes 1 to 1 with function invocation. Execute a function 3 times, get 3 different scopes. Why? Because if the execution of a function is finished, it is popped off the execution stack and with it, its access to other variables via its scope chain. Thus, a new scope is created each time a function is executed. Closures work a little differently!

Let’s finish up with a more complex example before we move on to closures.

a = 1;var b = 2;
function outer(z) { b = 3; c = 4; var d = 5; e = 6;
function inner() { var e = 0; d = 2 * d; return d; } return inner(); var e;}outer(1);
  1. Before we run anything, hoisting is started at the outside, global level. Therefore we start with a declaration for a variableb, and a function declaration for function object outer. At this point nothing is assigned, we only have these two keys set up in the global scope variable object.
  2. Next, we start at a = 1. This is an assignment, or a “write to” statement, yet there is no formal declaration for it. So what happens in the global scope, and if not in “strict mode”, is that a will be implicitly declared as belonging to the global scope variable object.
  3. We move to the next line and look up identifier b, through hoisting it was accounted for and now we can assign a value, 2, to it.

So far we have…

Global Scope

4. Since we built the function object outer, at hoisting time, we then jump to execution, outer(1);

5. Remember that upon function invocation, an execution context is first created. With that we create an Activation Object. It contains data and variables local to that context. We also form the scope chain.

6. The parameter z is implicitly declared for this function and is assigned 1.

A quick side note: at this time, the function’s execution context creates its “this” binding. It also creates an arguments array, which is an array of parameters passed, in this case, z. This is beyond the scope of this article, so allow me to glance over it.

7. Now we look for explicit variable declarations in function outer. We have d, and var e is declared after the function inner.

8. Here’s some hidden magic, at this time a hidden [[scope]] property for function outer links its scope chain of variable objects. In this case, it works like a Linked List with a parent type property connecting the function outer Activation Object to the global execution context’s Variable Object. You can see here that scope extends from the inside out to form this “linking”. This is the reference that allows us to proceed up the scope chain for lookups.

Scope for Function outer

9. We step inside of outer and start at b = 3. Is b declared? Nope. So JavaScript uses the hidden [[scope]] property attached to function outer to move up the scope chain to find a “b”. It finds it in the global scope object and, since we are in the body of function outer, we assign b the value 3.

Global Scope again

10. Next line, c = 4. Since this is a write to identifier c, was c explicitly declared in function outer? No, and therefore it is not found by lookup in outer’s Activation Object. So it moves up the scope chain and looks in the global scope Variable Object. It is not there. Because this is a write to/ assignment operation, the global scope will handle it and place it on its Variable Object.

Global Scope Variable Object

11. d = 5. Yes, it is here so we assign it 5.

Scope for function outer

12. e = 6. Remember that straggler, var e? It was still declared in the body of outer and so we already had a place for it — so we assign it 6. If it wasn’t declared like c, we would move up the scope chain for a lookup. Since it is a write and not a read operation and not in ‘strict mode’, it would have been placed in the global’s scope.

13. We get to invoking function inner. We start all over like we did with function outer: hoisting, set up an Activation Object, and create a hidden [[scope]] property. In this case, the containing context is function outer, and outer “points” to the global scope.

Scope for function inner

14. Now with e and in general, variables that are given the same name work like this. Since identifier lookup starts from the innermost scope to the outermost scope, lookup stops at the first finding of that identifier. In the body of inner, we see var e= 0, done, stop, go no further. The e in the body of function outer is “inaccessible”. The term that is commonly used is “shadowing” e in function inner “shadows” or obscures the e in function outer.

15. Next line is d = 2 * d. Before we assign a value to d on the left, we have to evaluate the expression on the right, 2 * d. Since d is not local in scope to inner, we move up the scope chain to find a variable for d and whether it has a value associated with it. We find it in the outer scope in function outer and it is there that the value is changed.

Scope for function outer

The important thing here is that inner is manipulating data in its outer scope!

16. Function inner returns a value d, 10.

17. Function outer returns the value of function inner.

18. Result is 10.

19. Once function outer has completely finished executing, garbage collection takes place. Garbage collection is the freeing up of resources that are longer needed. It starts at the global scope and works as far as it has “reachability”.

The global scope in this example has no handle to function outer or function inner, so whoosh, gone. This is important when we get to closures, because there, we need data and some variables to stick around even after a function has finished running.

Finally, let’s get some Closure!

How shall we define a closure?

Let’s start with a few definitions, all correct, some more in depth, but that get to the same point.

1. Closures are functions that have access to variables from another function's scope. This is accomplished by creating a function inside another function.
2. A Closure is a function that returns another function.
3. A Closure is an implicit, permanent link between a function and its scope chain.

Why Closures?

Without being able to leverage scope chain rules, async operations would be impossible. Because there is no guarantee that data will still be around to use later. JavaScript only has function scope as its encapsulation mechanism.

Closures are the best form of privacy for functions and variables. This is evident in the use of many module patterns. A module pattern returns an object to expose a public API. It also keeps other methods and variables private. Closures are used in event handling and callbacks.

An example of a module …

var Toaster = (function(){ var setting = 0; var temperature; var low = 100; var med = 200; var high = 300; // public var turnOn = function(){ return heatSetting(); }; var adjustSetting = function(setting){ if(setting 3 && setting  6 && setting <= 10){ temperature = high;
}return temperature; }; // private var heatSetting = function(adjustSetting){ var thermostat = adjustSetting; return thermostat; }; return{ turnOn:turnOn, adjustSetting:adjustSetting };})();
Toaster.adjustSetting(5);Toaster.adjustSetting(8);

The module Toaster has private locals and a public interface and is written as an Immediately Invoked Function Expression (IIFE). We create a function, immediately invoke it, and grab the return value.

Another small example:

function firstName(first){ function fullName(last){ console.log(first + " " + last); } return fullName;}var name = firstName("Mister");name("Smith") // Mister Smithname("Jones"); //Mister Jones

The inner function fullName( ) is accessing the variable, first, in its outer scope, firstName( ). Even after the inner function, fullName, has returned, it still has access to that variable. How is this possible? The inner function’s scope chain includes the scope of its outer scope.

When a function is called, an execution context and a scope chain are created. Also the function get’s a hidden [[Scope]] property. The Activation Object for the function is initialized and placed in the chain. Then the outer function’s activation object is placed in the chain. In this case, finally the global Variable Object.

In this example, fullName is defined. A [[Scope]] property is created. The containing function’s activation object is added to fullName’s scope chain. It is also added to the global variable object. This reference to an outer function’s activation object enables access to all of the containing scopes variables. It does not get garbage collected.

This is most important. The activation object of the outer function, firstName(), cannot be destroyed once it is finished executing, because the reference still exists in fullName’s scope chain. After firstName( )

execution completes, its scope chain for that execution context is destroyed. But the activation object will remain in memory until fullName( ) is destroyed. We can do that by setting its reference to null.

The keen observer will note that we return a reference to fullName, not the return value of fullName( )!

This is what we mean by an implicit, permanent link between and function and it’s scope chain.

A closure always gets the last value from the containing function because the reference to the variable object is stored.

For instance …

var myFunctions= [];function createMyFunction(i) { return function() { console.log("My value: " + i); }; }for (var i = 0; i < 10; i++) {myFunctions[i] = createMyFunction(i);myFunctions[i]();}
My value: 0 My value: 1 My value: 2 My value: 3 My value: 4 My value: 5 My value: 6 My value: 7 My value: 8 My value: 9

If we go back to our original scope example and change one thing:

a = 1;var b = 2;
function outer(z) { b = 3; c = 4; var d = 5; e = 6;
function inner() { var e = 0; d = 2 * d; return d; } return inner; // we remove the call operator, now we are returning a reference to function inner. var e;}myG = outer(1); // store a reference to function inner in the global scope (the return value of outer)myG(); // when we execute myG, inner's [[Scope]] property is copied to recreate the scope chain, // and that gives it access to the scopes that contain function inner, outter then global. We got inner and inner's got outter.

Here are a few more examples:

function make_calculator() { var n = 0; // this calculator stores a single number n return { add: function(a) { n += a; return n; }, multiply: function(a) { n *= a; return n; } };}
first_calculator = make_calculator();second_calculator = make_calculator();
first_calculator.add(3); // returns 3second_calculator.add(400); // returns 400
first_calculator.multiply(11); // returns 33second_calculator.multiply(10); // returns 4000

Suppose we wanted to execute an array of functions:

function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { result.push(function number(i) { var item = 'item' + list[i]; console.log(item + ' ' + list[i])} ); } return result;}buildList([1,2,3,4,5]);
function testList() { var fnlist = buildList([1,2,3,4,5]); for (var i = 0; i < fnlist.length; i++) { fnlist[i](i); // another IIFE with i passed as a parameter!! } } testList();

I hope that this explanation of scope and closures helps. Play around with the patterns you see here, experiment. Actually writing this article was difficult — I gained a far deeper understanding than I had when I started.

Resources

YDKJS

Dmitry Soshnikov, Javascript:Core

ECMA 262.3

StackOverflow

Nick Zakas