Een gemakkelijke inleiding tot Lexical Scoping in JavaScript

Lexicale scoping is een onderwerp dat veel programmeurs beangstigt. Een van de beste verklaringen van lexicale scoping is te vinden in Kyle Simpsons boek You Don't Know JS: Scope and Closures. Maar zelfs zijn verklaring ontbreekt omdat hij geen echt voorbeeld gebruikt.

Een van de beste echte voorbeelden van hoe lexicale scoping werkt en waarom het belangrijk is, is te vinden in het beroemde leerboek "The Structure and Interpretation of Computer Programmes" (SICP) van Harold Abelson en Gerald Jay Sussman. Hier is een link naar een pdf-versie van het boek: SICP.

SICP gebruikt Scheme, een dialect van Lisp, en wordt beschouwd als een van de beste inleidende informatica-teksten ooit geschreven. In dit artikel wil ik graag terugkomen op hun voorbeeld van lexicale scoping met JavaScript als programmeertaal.

Ons voorbeeld

Het voorbeeld dat Abelson en Sussman hebben gebruikt, is het berekenen van vierkantswortels met behulp van de methode van Newton. Newton's methode werkt door het bepalen van opeenvolgende benaderingen voor de vierkantswortel van een getal totdat de benadering binnen een tolerantielimiet komt om aanvaardbaar te zijn. Laten we een voorbeeld bekijken, zoals Abelson en Sussman doen in SICP.

Het voorbeeld dat ze gebruiken is het vinden van de vierkantswortel van 2. Je begint met een gok naar de vierkantswortel van 2, zeg 1. Je verbetert deze gok door het oorspronkelijke getal te delen door de gok en vervolgens het gemiddelde te nemen van dat quotiënt en de huidige gok naar kom met de volgende gok. U stopt wanneer u een acceptabel niveau van benadering bereikt. Abelson en Sussman gebruiken de waarde 0,001. Hier is een doorloop van de eerste paar stappen in de berekening:

Square root to find: 2First guess: 1Quotient: 2 / 1 = 2Average: (2+1) / 2 = 1.5Next guess: 1.5Quotient: 1.5 / 2 = 1.3333Average: (1.3333 + 1.5) / 2 = 1.4167Next guess: 1.4167Quotient: 1.4167 / 2 = 1.4118Average: (1.4167 + 1.4118) / 2 = 1.4142

En zo verder totdat de gok binnen onze benaderingslimiet valt, die voor dit algoritme 0,001 is.

Een JavaScript-functie voor de methode van Newton

Na deze demonstratie van de methode beschrijven de auteurs een algemene procedure om dit probleem op te lossen in Scheme. In plaats van u de Scheme-code te laten zien, zal ik deze in JavaScript opschrijven:

function sqrt_iter(guess, x) { if (isGoodEnough(guess, x)) { return guess; } else { return sqrt_iter(improve(guess, x), x); }}

Vervolgens moeten we verschillende andere functies uitwerken, waaronder isGoodEnough () en verbeteren (), samen met enkele andere helperfuncties. We beginnen met verbeteren (). Hier is de definitie:

function improve(guess, x) { return average(guess, (x / guess));}

Deze functie gebruikt een hulpfunctie gemiddelde (). Hier is die definitie:

function average(x, y) { return (x+y) / 2;}

Nu zijn we klaar om de functie isGoodEnough () te definiëren. Deze functie dient om te bepalen wanneer onze schatting dicht genoeg bij onze benaderingstolerantie (0,001) ligt. Hier is de definitie van isGoodEnough ():

function isGoodEnough(guess, x) { return (Math.abs(square(guess) - x)) < 0.001;}

Deze functie gebruikt een vierkante () functie, die eenvoudig te definiëren is:

function square(x) { return x * x;}

Nu hebben we alleen een functie nodig om dingen op gang te krijgen:

function sqrt(x) { return sqrt_iter(1.0, x);}

Deze functie gebruikt 1.0 als startschatting, wat meestal prima is.

Nu zijn we klaar om onze functies te testen om te zien of ze werken. We laden ze in een JS-shell en berekenen vervolgens een paar vierkantswortels:

> .load sqrt_iter.js> sqrt(3)1.7321428571428572> sqrt(9)3.00009155413138> sqrt(94 + 87)13.453624188555612> sqrt(144)12.000000012408687

De functies lijken goed te werken. Er ligt hier echter een beter idee op de loer. Deze functies zijn allemaal onafhankelijk geschreven, ook al zijn ze bedoeld om in combinatie met elkaar te werken. We gaan de functie isGoodEnough () waarschijnlijk niet gebruiken met een andere set functies of op zichzelf. De enige functie die voor de gebruiker van belang is, is de functie sqrt (), aangezien die wordt aangeroepen om een ​​vierkantswortel te vinden.

Block Scoping verbergt Helper-functies

De oplossing hier is om block scoping te gebruiken om alle noodzakelijke helperfuncties binnen het blok van de sqrt () functie te definiëren. We gaan square () en average () uit de definitie verwijderen, aangezien die functies nuttig kunnen zijn in andere functiedefinities en niet zo beperkt zijn tot gebruik in een algoritme dat de methode van Newton implementeert. Hier is de definitie van de functie sqrt () nu met de andere hulpfuncties gedefinieerd binnen het bereik van sqrt ():

function sqrt(x) { function improve(guess, x) { return average(guess, (x / guess)); } function isGoodEnough(guess, x) { return (Math.abs(square(guess) - x)) > 0.001; } function sqrt_iter(guess, x) { if (isGoodEnough(guess, x)) { return guess; } else { return sqrt_iter(improve(guess, x), x); } } return sqrt_iter(1.0, x);}

We kunnen dit programma nu in onze shell laden en enkele vierkantswortels berekenen:

> .load sqrt_iter.js> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> sqrt(3.14159)1.772581833543688> sqrt(144)12.000000012408687

Merk op dat u geen van de helperfuncties van buiten de functie sqrt () kunt aanroepen:

> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> improve(1,2)ReferenceError: improve is not defined> isGoodEnough(1.414, 2)ReferenceError: isGoodEnough is not defined

Aangezien de definities van deze functies (verbeteren () en isGoodEnough ()) binnen het bereik van sqrt () zijn verplaatst, zijn ze niet toegankelijk op een hoger niveau. Natuurlijk kunt u alle definities van de helperfuncties buiten de functie sqrt () verplaatsen om er globaal toegang toe te hebben, zoals we deden met average () en square ().

We hebben onze implementatie van Newton's Method aanzienlijk verbeterd, maar er is nog een ding dat we kunnen doen om onze sqrt () -functie te verbeteren door deze nog meer te vereenvoudigen door gebruik te maken van de lexicale scope.

Duidelijkheid verbeteren met Lexical Scope

Het concept achter lexicale scope is dat wanneer een variabele is gebonden aan een omgeving, andere procedures (functies) die in die omgeving zijn gedefinieerd, toegang hebben tot de waarde van die variabele. Dit betekent dat in de functie sqrt () de parameter x is gebonden aan die functie, wat betekent dat elke andere functie die is gedefinieerd in de hoofdtekst van sqrt () toegang heeft tot x.

Dit wetende, kunnen we de definitie van sqrt () nog meer vereenvoudigen door alle verwijzingen naar x in functiedefinities te verwijderen, aangezien x nu een gratis variabele is en voor iedereen toegankelijk is. Hier is onze nieuwe definitie van sqrt ():

function sqrt(x) { function isGoodEnough(guess) { return (Math.abs(square(guess) - x)) > 0.001; } function improve(guess) { return average(guess, (x / guess)); } function sqrt_iter(guess) { if (isGoodEnough(guess)) { return guess; } else { return sqrt_iter(improve(guess)); } } return sqrt_iter(1.0);}

De enige verwijzingen naar parameter x zijn in berekeningen waarbij de waarde van x nodig is. Laten we deze nieuwe definitie in de shell laden en testen:

> .load sqrt_iter.js> sqrt(9)3.00009155413138> sqrt(2)1.4142156862745097> sqrt(123+37)12.649110680047308> sqrt(144)12.000000012408687

Lexicale scoping en blokstructuur zijn belangrijke kenmerken van JavaScript en stellen ons in staat om programma's te maken die gemakkelijker te begrijpen en te beheren zijn. Dit is vooral belangrijk wanneer we beginnen met het bouwen van grotere, complexere programma's.