Mély merülés a hatókörláncokba és zárásokba

Hogyan működik a Scope lánc és a záróelemek a motorháztető alatt példákkal.

A JavaScript hatókörének és bezárásának megértése

Ha mélyre akar ásni és megszerezni a szükséges információkat, gondolkodjon úgy, mint egy újságíró. Tedd fel a hat fő kérdést: ki, mit, miért, hol, mikor és hogyan. Ha mindezekre egy adott témában tud válaszolni, akkor megszerezte a lényegét annak, amit tudnia kell.

Mielőtt eljutnánk a bezárásokhoz, meg kell értenünk a hatókört.

Először is, ha tudja, mi a [[hatókör]] (kettős zárójeles hatókör), akkor ez a cikk nem az Ön számára készült. Fejlettebb tudással rendelkezik, és tovább léphet.

A mi…

Mi a hatókör és miért számít?

A hatókör a függvény írásakor létrehozott kontextuskörnyezet (más néven lexikális környezet). Ez a kontextus meghatározza, hogy milyen egyéb adatokhoz férhet hozzá.

Másképp fogalmazva, a hatókör a hozzáférésről szól. Képes-e a függvény keresni egy változót végrehajtáshoz vagy manipulációhoz, mely változók láthatók?

Kétféle hatókör létezik: lokális és globális. A hatókör felbontása vagy annak meghatározása, hogy mely változók hová tartoznak, a legbelső kontextusból indul ki, és kifelé halad, amíg az azonosító megtalálható. Kezdjük kicsiben ...

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

A mikor, miért és hogyan… végrehajtási kontextus

A függvény meghívásakor új végrehajtási kontextust alkot. Mi a végrehajtási kontextus? Nos, ahogy kétféle hatókörrel rendelkezünk, kétféle végrehajtási kontextussal is rendelkezünk. Globális végrehajtási és függvény-végrehajtási környezet.

A globális kontextus mindig fut. Böngészőkörnyezet esetén csak akkor áll le, ha a böngésző be van zárva. Ha meghívunk egy függvényt, akkor a függvény végrehajtási környezetét a globális végrehajtási kontextus tetejére helyezzük. Ezért a terminológiát rakjuk össze .

A JavaScript egyetlen szálú nyelv, ami azt jelenti, hogy egyszerre csak egy dolgot tud megtenni. Ha meghívunk egy függvényt, az előző végrehajtási környezet szünetel. A meghívott függvény a tetején található, majd végrehajtásra kerül. Amikor ez befejeződik, leugrik a veremről, majd folytatódik a régebbi végrehajtási környezet. Ez a végrehajtás „vereme” az, ami nyomon követi az alkalmazásunk végrehajtásának helyzetét. Fontos az azonosítók felkutatása során is.

Tehát most kialakult egy végrehajtási kontextus, mi következik?

Minden végrehajtási környezethez tartozik egy változó objektum

Először egy aktiválási objektum jön létre (kóddal nem érhető el, mégis a háttérben működik). Ehhez a végrehajtási kontextushoz van társítva. Ez az objektum tartalmazza az összes deklarált változót , függvényt és paramétertaz adott kontextusban (annak hatóköre vagy akadálymentességi tartománya).

A függvény paraméterei implicit módon vannak meghatározva. Ezek a funkciók „lokálisak”. Ezek a deklarált változók „fel vannak emelve”, és a hatókör tetejére kerülnek.

Mielőtt továbblépnék, a zavart elkerülése végett - a globális végrehajtási kontextusban létrejön egy változó objektum , és ha ez egy függvény, akkor egy aktiválási objektum . Nagyjából azonosak.

Most, amikor ezt a függvényt meghívja, létrejön ezen objektumok „hatókörlánc”. Miért? A hatókör lánc egy olyan módszer, amely összekapcsolja vagy szisztematikus hozzáférést biztosít minden olyan változóhoz és egyéb funkcióhoz, amelyhez az aktuális végrehajtási kontextus (ebben az esetben a funkció) hozzáfér. A [[hatókör]] az a rejtett mechanizmus, amely összekapcsolja ezeket a változó objektumokat az azonosító keresése céljából. Ez a rejtett [[Hatókör]] a függvény tulajdonsága, amelyet deklarációkor hozunk létre, és nem meghívást.

A hatókörlánc-vonat élén, ha ez egy függvény, az Aktiválási objektum áll . Ennek az aktiválási objektumnak vannak saját deklarált változói, argumentumai és ez.

Ezután a hatókör láncolatán a következő objektum jelenik meg a tartalmi kontextusból. Ha ez egy globális változó, akkor ez egy változó objektum. Ha ez egy függvény, akkor egy aktiválási objektum . Ez addig történik, amíg el nem érjük a globális kontextust. Ezért láthatja, hogy a legbelső kontextustól kezdve a legkülsőig gondolkodunk, gondoljuk orosz fészkelő babákra.

Mi a különbség a deklarált és a nem bejelentett változó között? Ha az azonosító előtt egy var, let vagy const áll, akkor azt kifejezetten deklaráljuk, és memóriaterületet osztunk ki a változó használatához. Ha az azonosító nincs kifejezetten deklarálva, akkor implicit módon deklarálva van a globális hatókörben, amelyet hamarosan megvizsgálunk. E cikk alkalmazásában a var mellett maradok, különösebb ok nélkül.

Tudom, hogy a fentiek kissé technikai jellegűek voltak, és hogy őszinte legyek, amikor ezt írtam, csak magam tudtam meg a Változó és az Aktiválás objektumokat. Most, hogy megvan a mély merülés magyarázata, íme egy nagy szögű leírás ...

A hatókör lánc hasonló a prototípus lánchoz. Ha egy változó vagy tulajdonság nem található, addig folytatja a láncot, amíg meg nem találja, vagy hibát nem dob. A függvény létrehoz egy rejtett [[hatókör]] tulajdonságot. Ez a tulajdonság összeköti a legbelső hatóköröket a legkülső hatókörökkel. Ebben az esetben a szám hatóköre láncolva van a globális ablakobjektummal (amely tartalmazza a függvényszámot tartalmazó kontextust). Ez teszi lehetővé a motor számára, hogy a függvény számán kívülre nézzen, hogy megtalálja az firstNum és a SecondNum értékeket.

Vegyük például ugyanazt a függvényszámot, és változtassunk meg egyet:

// 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