Важни изводи от поредицата книги YDKJS на Кайл Симпсън — Обхват и затваряния

Глава 1 — Какво е обхват?

Включването на променливи в нашата програма поражда най-интересните въпроси, които сега ще разгледаме: къде живеят тези променливи? С други думи, къде се съхраняват? И най-важното, как нашата програма ги намира, когато има нужда от тях? Тези въпроси говорят за необходимостта от добре дефиниран набор от правила за съхраняване на променливи на някакво място и за намиране на тези променливи по-късно. Ще наречем този набор от правила: обхват. Но къде и как се задават тези правила за обхват?

Теория на компилатора

JavaScript попада в общата категория на „динамични“ или „интерпретирани“ езици, той всъщност е компилиран език. Той не се компилира достатъчно предварително, както много традиционно компилирани езици, нито резултатите от компилацията са преносими между различни разпределени системи. Но, въпреки това, двигателят на JavaScript изпълнява много от същите стъпки, макар и по по-сложни начини, отколкото обикновено знаем за всеки традиционен езиков компилатор.

В традиционен процес на компилиран език, част от изходния код, вашата програма, обикновено преминава през три стъпки, преди да бъде изпълнена, грубо наречена „компилация“:

  • Токенизиране/лексинг: Разбиване на низ от знаци на значими (за езика) части, наречени токени.
  • Парсиране: Вземане на поток (масив) от токени и превръщането му в дърво от вложени елементи, които колективно представляват граматичната структура на програмата. Това дърво се нарича „AST“ (абстрактно синтактично дърво) („astexplorer.net“).
  • Генериране на код: Процесът на вземане на AST и превръщането му в изпълним код. Тази част варира значително в зависимост от езика, платформата, към която е насочена, и т.н.

Механизмът на JavaScript е много по-сложен от тези три стъпки, както и повечето други езикови компилатори. Например, в процеса на анализиране и генериране на код има определени стъпки за оптимизиране на производителността на изпълнението, включително свиване на излишни елементи и т.н.

JS компилаторът ще вземе програмата var a = 2; и първо го компилирайте и след това бъдете готови да го изпълните, обикновено веднага.

  • Двигател: Отговаря за компилирането от началото до края и изпълнението на нашата JavaScript програма.
  • Компилатор: Един от приятелите на Engine; се справя с цялата мръсна работа по анализиране и генериране на код (вижте предишния раздел).
  • Обхват: Друг приятел на Engine; събира и поддържа списък за търсене на всички декларирани идентификатори (променливи) и налага строг набор от правила за това как те са достъпни за текущо изпълняващия се код.

Търсене на обхват

Когато Engine изпълни кода, който компилаторът е произвел за стъпка 2, той трябва да потърси променливата a, за да види дали е декларирана, и това търсене се консултира с Scope. Но видът на търсенето, което двигателят извършва, влияе върху резултата от търсенето. В нашия случай се казва, че двигателят ще извършва LHS търсене за променливата a. Другият вид търсене се нарича RHS.

Нека си представим горния обмен (който обработва този кодов фрагмент) като разговор. Разговорът ще протече горе-долу така:

Двигател: Хей Scope, имам RHS справка за foo. Чували ли сте за него?
Обхват: Защо да, чувал съм. Компилаторът го декларира само преди секунда. Това е функция. Ето го.
Двигател: Страхотно, благодаря! Добре, изпълнявам foo.
Engine: Хей, Scope, имам LHS препратка за a, чувал ли си за него?
Scope: Защо да, имам. Компилаторът го декларира като формален параметър на foo съвсем наскоро. Ето го.
Двигател: Полезен както винаги, Scope. Благодаря отново. Сега е време да присвоите 2 на a.
Двигател: Хей, Scope, съжалявам, че ви безпокоя отново. Имам нужда от RHS справка за console. Чували ли сте за него?
Обхват: Няма проблем, Engine, това правя по цял ден. Да, имам console. Вграден е. Ето го.
Двигател: Перфектен. Търсене на дневник(..). Добре, страхотно, това е функция.
Двигател: Йо, обхват. Можете ли да ми помогнете с RHS препратка към a. Мисля, че го помня, но искам да проверя отново.
Обхват: Прав си, Engine. Същата променлива не се е променила. Ето го.
Двигател: Готин. Предаване на стойността на a, която е 2, в log(..).

В обобщение: Във фазата на изпълнение двигателят извършва два вида търсене на променливи — LHS и RHS. LHS справки възникват, когато се присвояват стойности на променливи, както във var a = 2; пример. RHS справки възникват при извличане на стойностите на променливи. Например, когато console.log(a) и, двигателят трябва да потърси стойността на a.

В: Проверете разбирането си досега. Уверете се, че играете ролята на Engine и провеждате „разговор“ с Scope:

  1. Идентифицирайте всички търсения на LHS (има 3!).
  2. Идентифицирайте всички RHS справки (има 4!).

Забележка: Точно както блок или функция е вложен в друг блок или функция, обхватите са вложени в други обхвати. Така че, ако дадена променлива не може да бъде намерена в непосредствения обхват, двигателят се консултира със следващия външен обхват, съдържащ, продължавайки, докато бъде намерена или докато бъде достигнат най-външният (известен още като глобален) обхват.

  • Неизпълнените RHS препратки водят до хвърляне на ReferenceErrors.
  • Неизпълнените LHS препратки водят до автоматично, имплицитно създадено глобално с това име (ако не е в строг режим) или ReferenceError (ако е в строг режим).

Глава 2 — Какво е лексикален обхват?

Има два основни модела на обхват, използвани в различни езици за програмиране - лексикален и динамичен. Лексикален обхват е това, което се използва в JavaScript. Лексикалният обхват (понякога известен като статичен обхват) е конвенция, използвана в много езици за програмиране, която задава обхвата (диапазон от функционалност) на променлива, така че тя да може да бъде извикан (препратен) от блока код, в който е дефиниран. Обхватът се определя, когато кодът се компилира.

  • Балон 1 обхваща глобалния обхват и има само един идентификатор в него: foo.
  • Балон 2 обхваща обхвата на foo, който включва трите идентификатора: a, bar и b.
  • Балон 3 обхваща обхвата на bar и включва само един идентификатор: c.

Мехурчетата на обхвата се определят от това къде са написани блоковете на обхвата, кой е вложен в другия и т.н.

Търсенето на обхват спира, след като открие първото съвпадение. Едно и също име на идентификатор може да бъде указано на множество слоеве на вложен обхват, което се нарича „засенчване“ (вътрешният идентификатор „засенчва“ външния идентификатор). Независимо от засенчването, търсенето на обхват винаги започва от най-вътрешния обхват, който се изпълнява по това време, проправя си път навън/нагоре до първото съвпадение и спира.

Процесът на търсене на лексикален обхват се прилага само за първокласни идентификатори, като a, b и c. Ако сте имали препратка към foo.bar.baz в част от кода, търсенето в лексикалния обхват ще се приложи за намиране на идентификатора foo, но след като открие тази променлива, правилата за достъп до свойствата на обекта поемат, за да разрешат съответно свойствата bar и baz .

Начини за измама на лексикалния обхват?

измамата на лексикалния обхват води до по-лоша производителност

eval: Функцията eval(..) в JavaScript приема низ като аргумент и третира съдържанието на низа, сякаш всъщност е бил авторски код в този момент от програмата. Той може да модифицира съществуващ лексикален обхват (по време на изпълнение) чрез оценка на низ от „код“, който има една или повече декларации в него.

Низът „var b = 3;“ се третира в точката на извикването eval(..) като код, който е бил там през цялото време. Тъй като случайно този код декларира нова променлива b, той променя съществуващия лексикален обхват на foo(..). Всъщност, както споменахме по-рано, този код всъщност създава променлива b вътре в foo(..), която засенчва b, деклариран във външния (глобален) обхват. Когато се появи извикването console.log(..), то намира както a, така и b в обхвата на foo(..) и никога не намира външния b. По този начин ние отпечатваме „1, 3“ вместо „1, 2“, както би било нормално.

with: Другата неодобрена (и вече отхвърлена!) функция в JavaScript, която мами лексикалния обхват, е с ключовата дума. Той по същество създава изцяло нов лексикален обхват (отново по време на изпълнение), като третира препратка към обект като обхват и свойствата на този обект като обхватни идентификатори.

Механизмът на JavaScript има редица оптимизации на производителността, които извършва по време на фазата на компилиране. Някои от тях се свеждат до възможността по същество да се анализира статично кода, докато той лексира, и да се определи предварително къде са всички декларации на променливи и функции, така че да са необходими по-малко усилия за разрешаване на идентификатори по време на изпълнение. Но ако машината намери eval(..) или в рамките на кода, тя по същество трябва да приеме, че цялата му информираност за местоположението на идентификатора може да е невалидна, защото не може да знае по време на лексиране точно какъв код можете да предадете на eval(..), за да промените лексикалния обхват или съдържанието на обекта, към който можете да преминете, за да създадете нов лексикален обхват, който да бъде консултиран. С други думи, в песимистичния смисъл, повечето от тези оптимизации, които би направил, са безсмислени, ако eval(..) или with присъстват, така че той просто не извършва оптимизациите изобщо.

Глава 3 — Функция v/s Block Scope?

JavaScript има функционален обхват. Това означава, че всяка функция, която декларирате, създава балон за себе си, но никакви други структури не създават свои собствени мехурчета за обхват. Както ще видим след малко, това не е съвсем вярно.

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

  • В този фрагмент балончето за обхват за foo(..) включва идентификатори a, b, c и bar. Няма значение къде в обхвата се появява декларация, независимо дали променливата или функцията принадлежи към балончето на обхвата.
  • bar(..) има собствено балонче за обхват. Същото прави и глобалният обхват, който има само един идентификатор, прикрепен към него: foo.
  • Тъй като a, b, c и bar всички принадлежат на обхвата на foo(..), те не са достъпни извън foo(..). Това означава, че следният код ще доведе до грешки ReferenceError, тъй като идентификаторите не са достъпни за глобалния обхват:
bar(); // fails 
console.log( a, b, c ); // all 3 fail

Забележка: Можем да „скрием“ променливи и функции, като ги включим в обхвата на функция.

Но защо да се крием?

  • Те са склонни да произтичат от принципа на дизайна на софтуера Принцип на най-малка привилегия, понякога наричан също най-малък авторитет или най-малко излагане. Този принцип гласи, че при проектирането на софтуер, като например API за модул/обект, трябва да изложите само това, което е минимално необходимо, и да „скриете“ всичко останало.
  • Друго предимство на „скриването“ на променливи и функции вътре в обхвата е да се избегне нежелан сблъсък между два различни идентификатора с едно и също име, но различни предназначения

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

Функции като обхвати

Видяхме, че можем да вземем всеки фрагмент от код и да обвием функция около него, и това ефективно „скрива“ всяка затворена променлива или функционални декларации от външния обхват във вътрешния обхват на тази функция.

Горният фрагмент създава няколко проблема:

  • Той декларира наименувана функция foo(), което означава, че самото име на идентификатора foo „замърсява“ обхващащия обхват (глобален, в този случай).
  • Трябва изрично да извикаме функцията по име (foo()), така че обвитият код действително да се изпълни.

Всяко решение на горния проблем? IIFE

Обърнете внимание, че операторът на функцията за обвиване започва с (функция… за разлика от просто функция…. Въпреки че това може да изглежда като незначителен детайл, всъщност е голяма промяна. Вместо да третира функцията Декларация на функция срещу израз

Най-лесният начин за разграничаване на декларация от израз е позицията на думата функция в израза (не просто ред, а отделен израз). Ако функцията е първото нещо в оператора, тогава това е декларация на функция. В противен случай това е израз на функция.

Анонимен функционален израз:

setTimeout(function () {
 console.log(“I waited 1 second!”);
}, 1000);

Върху него няма идентификатор на име. Функционалните изрази могат да бъдат анонимни, но декларациите на функциите не могат да пропуснат името - това би било незаконна JS граматика.

Недостатъци на анонимните функционални изрази

  • Анонимните функции нямат полезно име за показване в проследяванията на стека, което може да затрудни отстраняването на грешки.
  • Без име, ако функцията трябва да препраща към себе си, за рекурсия и т.н., отхвърлените аргументи. за съжаление се изисква препратка към извиквания. Друг пример за нужда от самопрепратка е, когато функция за манипулиране на събития иска да се откачи, след като се задейства.
  • Анонимните функции пропускат име, което често е полезно за осигуряване на по-четлив/разбираем код. Описателно име помага за самодокументиране на въпросния код.

Вградените функционални изрази са мощни и полезни – въпросът за анонимни срещу именувани не намалява това. Предоставянето на име за вашия функционален израз доста ефективно адресира всички тези недостатъци, но няма осезаеми недостатъци. Най-добрата практика е винаги да наименувате вашите функционални изрази:

setTimeout(function timeoutHandler() { // ← Look, I have a name!
 console.log(“I waited 1 second!”);
}, 1000);

IIFE — незабавно извикан израз на функция.

Разбира се, IIFE не се нуждаят от имена, непременно - най-често срещаната форма на IIFE е да се използва израз на анонимна функция. Въпреки че определено е по-рядко, наименуването на IIFE има всички гореспоменати предимства пред изразите на анонимни функции, така че е добра практика да се приеме. Има 2 начина за писане:

Обхват на блока

Докато функциите са най-често срещаната единица за обхват, има и други начини за дефиниране на обхват. Вижте фрагмента по-долу:

Ние използваме променлива bar само в контекста на израза if, така че има някакъв смисъл да я декларираме в блока if. Въпреки това, когато декларираме променливи, не е от значение, когато използваме var, защото те винаги ще принадлежат към обхващащия обхват. Този фрагмент е по същество фалшив блоков обхват по стилистични причини и разчита на самоприлагане, за да не се използва случайно bar на друго място в този обхват.

Има начин да създадете блоков обхват, който няма да замърси глобалния обхват: let & const. catch също създава блоков обхват в оператора try-catch.

Въпреки това, декларациите, направени с let, няма да се вдигнат в целия обхват на блока, в който се появяват. Такива декларации няма да „съществуват“ в блока до изявлението за декларация.

{ 
 console.log(bar); // Reference Error! 
 let bar = 2; 
} // more on it in next chapter

Друга причина блоковият обхват да е полезен е свързан със затваряния и събиране на боклук за възстановяване на паметта.

Глава 4 — Повдигане?

Има изкушение да мислите, че целият код, който виждате в JavaScript програма, се интерпретира ред по ред, отгоре надолу в ред, докато програмата се изпълнява. Въпреки че това е до голяма степен вярно, има една част от това предположение, която може да доведе до неправилно мислене за вашата програма.

Вижте кода по-долу,

a = 2;
var a;
console.log( a );

Какво очаквате да бъде отпечатано в оператора console.log(..)? Много разработчици биха очаквали undefined, тъй като операторът var a идва след a = 2; и би изглеждало естествено да се приеме, че променливата е предефинирана и по този начин е присвоена стойността по подразбиране undefined. Изходът обаче ще бъде 2.

Помислете за друга част от кода:

console.log( a );
var a = 2;

Може да се изкушите да предположите, че тъй като предишният фрагмент показва някакво поведение, което не изглежда отгоре-надолу, може би в този фрагмент 2 също ще бъде отпечатано. Други може да си помислят, че тъй като променливата a се използва преди да бъде декларирана, това трябва да доведе до хвърляне на ReferenceError. За съжаление и двете предположения са неверни. undefined е изходът

И така, какво става тук? Изглежда, че имаме въпрос за кокошката и яйцето. Кое е първо, декларацията („яйцето“) или заданието („пилето“)?

Припомнете си, че машината всъщност ще компилира вашия JavaScript код, преди да го интерпретира. Част от фазата на компилиране беше да се намерят и свържат всички декларации с техните подходящи обхвати.

Така че, най-добрият начин да мислите за нещата е, че всички декларации, както променливи, така и функции, се обработват първо, преди която и да е част от вашия код да бъде изпълнена.

Забележка: Повдигат се само самите декларации, докато всички присвоявания или друга изпълнима логика остават на място. Ако hoisting трябваше да пренареди изпълнимата логика на нашия код, това може да предизвика хаос.

И двете декларации на функции и декларации на променливи се повдигат. Но фина подробност (която може да се появи в кода с множество „дублирани“ декларации) е, че функциите се издигат първо, а след това променливите. Помислете за фрагмента по-долу:

Глава 5 — Затваряне на обхвата?

Затварянето е навсякъде около вас в JavaScript, просто трябва да го разпознаете и прегърнете.

Затварянията се случват в резултат на писане на код, който разчита на лексикалния обхват. Те просто се случват. Дори не е нужно умишлено да създавате затваряния, за да се възползвате от тях. Затварянията се създават и използват вместо вас в целия ви код. Това, което ви липсва, е правилният умствен контекст, за да разпознаете, прегърнете и използвате затварянията за собствената си воля. Моментът на просветление трябва да бъде: о, затварянията вече се случват в целия ми код, сега най-накрая мога да ги видя. Разбирането на затварянията е като когато Нео вижда Матрицата за първи път.

Затварянето е, когато дадена функция е в състояние да запомни и има достъп до своя лексикален обхват, дори когато тази функция се изпълнява извън своя лексикален обхват.

Функцията bar() има достъп до лексикален обхват до вътрешния обхват на foo(). Но след това вземаме bar(), самата функция, и я предаваме като стойност. В този случай връщаме самия функционален обект, който bar препраща. След като изпълним foo(), ние присвояваме върнатата от него стойност (нашата вътрешна функция bar()) на променлива, наречена baz, и след това всъщност извикваме baz(), което, разбира се, извиква нашата вътрешна функция bar(), само чрез препратка към различен идентификатор. bar() е изпълнено със сигурност. Но в този случай той се изпълнява извън декларирания си лексикален обхват. След като foo() се изпълни, обикновено бихме очаквали, че целият вътрешен обхват на foo() ще изчезне, защото знаем, че двигателят използва събирач на боклук, който се появява и освобождава памет, след като вече не се използва. Тъй като изглежда, че съдържанието на foo() вече не се използва, би изглеждало естествено то да се счита за изчезнало. Но „магията“ на затварянията не позволява това да се случи. Този вътрешен обхват всъщност все още се използва и следователно не изчезва. Кой го използва? Самата функция bar(). По силата на мястото, където е деклариран, bar() има затваряне на лексикален обхват над този вътрешен обхват на foo(), което поддържа този обхват жив за bar() за справка по всяко време по-късно. bar() все още има препратка към този обхват и тази препратка се нарича closure. И така, няколко микросекунди по-късно, когато променливата baz се извика (извиквайки вътрешната функция, която първоначално обозначихме bar), тя надлежно има достъп до лексикалния обхват по време на автора, така че може да има достъп до променливата a точно както бихме очаквали. Функцията се извиква доста извън своя лексикален обхват по време на автора. Затварянето позволява на функцията да продължи да осъществява достъп до лексикалния обхват, в който е била дефинирана по време на автора.

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

Eg:

Реалният работещ пример, който виждаме всеки ден:

Взимаме вътрешна функция (наречена timer) и я предаваме на setTime out(..). Но таймерът има затваряне на обхват над обхвата на wait(..), като наистина запазва и използва препратка към променливото съобщение.

По същество винаги, когато и където и да третирате функции (които имат достъп до съответните си лексикални обхвати) като първокласни стойности и ги предавате наоколо, вероятно ще видите тези функции да упражняват затваряне. Независимо дали сте таймери, манипулатори на събития, заявки на Ajax, съобщения между прозорци, уеб работници или други асинхронни (или синхронни!) задачи, когато подадете функция за обратно извикване, пригответе се за затваряне!

Нека видим още няколко примера:

а?

Първо, нека обясним откъде идва 6. Крайното условие на цикъла е, когато i не е ‹=5. Първият път, когато това е случаят, е когато i е 6. Така че изходът отразява крайната стойност на i след края на цикъла. Това всъщност изглежда очевидно на втори поглед. Всички обратни извиквания на функцията за изчакване работят добре след завършване на цикъла. Всъщност, тъй като таймерите вървят, дори ако беше setTimeout(.., 0) на всяка итерация, всички тези обратни извиквания на функции пак ще се изпълняват стриктно след завършването на цикъла и по този начин ще отпечатват 6 всеки път.

Това, което липсва е, че се опитваме да внушим, че всяка итерация на цикъла „улавя“ собствено копие на i, по време на итерацията. Но начинът, по който работи обхватът, всичките пет от тези функции, въпреки че са дефинирани отделно във всяка итерация на цикъл, са затворени върху един и същ споделен глобален обхват, който всъщност има само един i в него.

Как да поправя това?

В глава 3 научихме, че IIFE създава обхват, като декларира функция и незабавно я изпълнява.

това работи ли Не… Но защо?

Сега очевидно имаме повече лексикален обхват. Всяко обратно извикване на функция за изчакване наистина се затваря над собствения си обхват на итерация, създаден съответно от всеки IIFE. Не е достатъчно да имате обхват, който да затворите, ако този обхват е празен. Вгледай се по-внимателно. Нашият IIFE е просто празен обхват, който не прави нищо. Има нужда от нещо в него, което да ни бъде полезно. Нуждае се от собствена променлива с копие на стойността i при всяка итерация.

Използвахме IIFE, за да създадем нов обхват на итерация. И така, ние всъщност се нуждаехме от блоков обхват на итерация и чухме нещо за let.

Модули

Има и други кодови модели, които използват силата на затваряне, но на повърхността не изглеждат свързани с обратни извиквания. Нека разгледаме най-мощния от тях: модулът.

Тъй като този код стои в момента, няма видимо затваряне. Просто имаме някои частни променливи с данни нещо и друго, както и няколко вътрешни функции doSomething() и doAnother(), които и двете имат лексикален обхват (и следователно затваряне!) над вътрешния обхват на foo().

Но сега помислете:

Това е моделът в JavaScript, който наричаме модул. Най-често срещаният начин за внедряване на модела на модула често се нарича разкриващ модул и това е вариантът, който представяме тук. Нека разгледаме някои неща за този код.

  • Първо, CoolModule() е просто функция, но тя трябва да бъде извикана, за да има създаден екземпляр на модул. Без изпълнението на външната функция, създаването на вътрешния обхват и затварянията не биха се случили.
  • Второ, функцията CoolModule() връща обект, обозначен със синтаксиса на обектния литерал { ключ: стойност, … }. Обектът, който връщаме, има препратки към нашите вътрешни функции, но не и към нашите вътрешни променливи с данни. Ние ги пазим скрити и поверителни. Уместно е да мислим за тази върната стойност на обект като по същество за публичен API за нашия модул. Тази върната стойност на обекта в крайна сметка се присвоява на външната променлива foo и след това можем да получим достъп до тези методи на свойства в API, като foo.doSomething().
  • Функциите doSomething() и doAnother() имат затваряне върху вътрешния обхват на екземпляра на модула (постигнат чрез действително извикване на CoolModule()). Когато транспортираме тези функции извън лексикалния обхват, чрез препратки към свойства на обекта, който връщаме, вече сме задали условие, при което може да се наблюдава и упражнява затваряне.

Изисквания за модела на модула, който трябва да се упражнява:

  1. Трябва да има външна обхващаща функция и тя трябва да бъде извикана поне веднъж (всеки път създава нов екземпляр на модула).
  2. Обграждащата функция трябва да върне обратно поне една вътрешна функция, така че тази вътрешна функция да има затваряне над частния обхват и да има достъп и/или да променя това частно състояние.

Преобразуване на модул в IIFE, така че да няма нужда да създавате нов екземпляр на модул всеки път:

ES6 добавя първокласна синтаксисна поддръжка за концепцията за модули. Когато се зарежда чрез модулната система, ES6 третира файла като отделен модул. Всеки модул може както да импортира други модули или конкретни членове на API, така и да експортира свои собствени публични членове на API. Модулите ES6 нямат „inline“ формат, те трябва да бъдат дефинирани в отделни файлове (по един на модул). Браузърите/двигателите имат „зареждане на модули“ по подразбиране (което може да се замени, но това е далеч отвъд нашата дискусия тук), което синхронно зарежда файл на модул, когато се импортира.

Обмисли:

Приложение A — Динамичен обхват

Лексикалният обхват е набор от правила за това как машината може да търси променлива и къде ще я намери. Ключовата характеристика на лексикалния обхват е, че той се дефинира по време на автора, когато кодът е написан (ако приемем, че не мамите с eval() или with). Динамичният обхват изглежда предполага и има основателна причина, че има модел, при който обхватът може да се определя динамично по време на изпълнение, а не статично по време на автора.

Лексическият обхват предполага, че RHS препратката към a във foo() ще бъде разрешена до глобалната променлива a, което ще доведе до извеждане на стойност 2.

Динамичният обхват, напротив, не се занимава с това как и къде се декларират функциите и обхватите, а по-скоро откъде се извикват. С други думи, веригата на обхвата се основава на стека на повикванията, а не на влагането на обхвати в кода.

Така че, ако JavaScript имаше динамичен обхват, когато се изпълни foo(), теоретично кодът по-долу ще доведе до 3 като изход.

Как е възможно това? Защото, когато foo() не може да разреши препратката към променливата за a, вместо да засили вложената (лексикална) верига на обхвата, той върви нагоре по стека на повикванията, за да намери откъде е извикан foo(). Тъй като foo() е извикан от bar(), той проверява променливите в обхвата за bar() и намира a там със стойност 3. Странно? Вероятно си мислите така в момента. Но това е само защото вероятно сте работили само върху (или поне задълбочено обмисляли) код, който е с лексикален обхват. Така че динамичният обхват изглежда чужд. Ако някога сте писали код само на език с динамичен обхват, това би изглеждало естествено, а лексикалния обхват щеше да е странно. За да бъде ясно, JavaScript всъщност няма динамичен обхват. Има лексикален обхват. Ясно и просто. Но този механизъм е нещо като динамичен обхват.

Приложение Б — Обхват на блока за полифилинг

Какво ще стане, ако искаме да използваме блоков обхват в среди преди ES6?

{
 let a = 2;
 console.log(a); // 2
}
console.log( a ); // ReferenceError

По принцип алтернатива за преди es6?

try { throw 2 } catch (a) {
 console.log(a); // 2
}
console.log(a); // ReferenceError
// Ugly code

Забележка: Google поддържа проект, наречен Traceur, който има за задача да транспилира функциите на ES6 в преди ES6 (предимно ES5, но не всички!) за обща употреба.

Това е всичко, хора за днес, много скоро ще пиша друг блог за следващата книга от поредицата

Искате още по-кратка версия на това? Вижте по-долу:



Отворен за отзиви и коментари, ако съм пропуснал някоя концепция, разгледана в тази книга.

🙏 Кредит:



Първоначално публикувано на адрес https://alok722.hashnode.dev.