Лесно въведение в лексикалния обхват в JavaScript

Лексикалният обхват е тема, която плаши много програмисти. Едно от най-добрите обяснения на лексикалния обхват може да бъде намерено в книгата на Кайл Симпсън You Don't Know JS: Scope and Closures. Дори обяснението му обаче липсва, защото той не използва реален пример.

Един от най-добрите реални примери за това как работи лексикалният обхват и защо е важен, може да бъде намерен в известния учебник „Структурата и интерпретация на компютърни програми“ (SICP) от Харолд Абелсън и Джералд Джей Сусман. Ето линк към PDF версия на книгата: SICP.

SICP използва Scheme, диалект на Lisp и се счита за един от най-добрите въвеждащи текстове по компютърни науки, писани някога. В тази статия бих искал да прегледам техния пример за лексикално обхват, използвайки JavaScript като език за програмиране.

Нашият пример

Примерът, използван от Abelson и Sussman, е изчисляване на квадратни корени по метода на Нютон. Методът на Нютон работи чрез определяне на последователни апроксимации за квадратния корен на число, докато приближението не попадне в границите на допустимо отклонение за приемливост. Нека да разгледаме пример, както правят Abelson и Sussman в SICP.

Примерът, който използват, е намирането на корен квадратен от 2. Започвате като правите предположение при корен квадратен от 2, да речем 1. Подобрявате това предположение, като разделяте оригиналното число на предположението и след това осреднявате този коефициент и текущото предположение на измислете следващото предположение. Спирате, когато достигнете приемливо ниво на сближаване. Abelson и Sussman използват стойността 0,001. Ето преход от първите няколко стъпки в изчислението:

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

И така нататък, докато предположението не попадне в нашата граница на приближение, която за този алгоритъм е 0,001.

Функция на JavaScript за метода на Нютон

След тази демонстрация на метода авторите описват обща процедура за решаване на този проблем в Схема. Вместо да ви покажа кода на схемата, ще го напиша в JavaScript:

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

След това трябва да доработим няколко други функции, включително isGoodEnough () и подобрение (), заедно с някои други помощни функции. Ще започнем с подобряване (). Ето дефиницията:

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

Тази функция използва помощна функция средно (). Ето това определение:

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

Сега сме готови да дефинираме функцията isGoodEnough (). Тази функция служи за определяне кога нашето предположение е достатъчно близо до нашия толеранс на приближение (0,001). Ето дефиницията на isGoodEnough ():

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

Тази функция използва квадратна () функция, която е лесна за дефиниране:

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

Сега всичко, от което се нуждаем, е функция за стартиране на нещата:

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

Тази функция използва 1.0 като начално предположение, което обикновено е добре.

Сега сме готови да тестваме нашите функции, за да видим дали работят. Зареждаме ги в JS черупка и след това изчисляваме няколко квадратни корена:

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

Изглежда, че функциите работят добре. Тук обаче се крие по-добра идея. Всички тези функции са написани независимо, въпреки че са предназначени да работят заедно помежду си. Вероятно няма да използваме функцията isGoodEnough () с друг набор от функции или самостоятелно. Също така, единствената функция, която има значение за потребителя, е функцията sqrt (), тъй като това е тази, която се извиква, за да намери квадратен корен.

Блок обхватът скрива помощни функции

Решението тук е да се използва обхват на блокове, за да се дефинират всички необходими помощни функции в рамките на блока на функцията sqrt (). Ще премахнем квадрат () и средно () от дефиницията, тъй като тези функции могат да бъдат полезни в други дефиниции на функции и не са толкова ограничени за използване в алгоритъм, който реализира метода на Нютон. Ето дефиницията на функцията sqrt () сега с другите помощни функции, дефинирани в обхвата на 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);}

Сега можем да заредим тази програма в нашата черупка и да изчислим някои квадратни корени:

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

Забележете, че не можете да извикате нито една от помощните функции извън функцията sqrt ():

> 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

Тъй като дефинициите на тези функции (improve () и isGoodEnough ()) са преместени в обхвата на sqrt (), те не могат да бъдат достъпни на по-високо ниво. Разбира се, можете да преместите някоя от помощните дефиниции на функции извън функцията sqrt (), за да имате достъп до тях в световен мащаб, както направихме със средното () и квадратното ().

Ние значително подобрихме внедряването на метода на Нютон, но има още едно нещо, което можем да направим, за да подобрим нашата функция sqrt (), като я опростим още повече, като се възползваме от лексикалния обхват.

Подобряване на яснотата с лексикален обхват

Концепцията зад лексикалния обхват е, че когато променлива е свързана със среда, други процедури (функции), които са дефинирани в тази среда, имат достъп до стойността на тази променлива. Това означава, че във функцията sqrt () параметърът x е обвързан с тази функция, което означава, че всяка друга функция, дефинирана в тялото на sqrt (), може да има достъп до x.

Знаейки това, можем още повече да опростим дефиницията на sqrt (), като премахнем всички препратки към x в дефинициите на функции, тъй като x вече е безплатна променлива и достъпна за всички тях. Ето нашата нова дефиниция на 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);}

Единствените препратки към параметър x са в изчисления, където е необходима стойността на x. Нека да заредим тази нова дефиниция в черупката и да я тестваме:

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

Lexical scoping and block structure are important features of JavaScript and allow us to construct programs that are easier to understand and manage. This is especially important when we begin to construct larger, more complex programs.