Глава 1
Как работят блок обвързванията

По традиция, начина по който декларациите на променливите работят е една трудна част от програмирането в JavaScript. В повечето С-базирани езици, променливите (или bindings) се създават на мястото, където се среща декларацията. В JavaScript, обаче, това не е така. Създаването на вашите променливи зависи всъщност от това, как сте ги декларирали и ECMAScript 6 предлага опции за по-лесен контрол на обхвата. Тази глава показва, защо класически var декларации могат да бъдат объркващи, въвежда блок-обвързвания в ECMAScript 6 и след това предлага някои добри практики за използването им.

Var декларации и Hoisting

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

  function getValue(condition) {
      if (condition) {
          var value = "blue";
          // друг код
          return value;
      } else {
    // стойността съществува тук със стойност undefined                  
          return null;
      }
   // стойността съществува тук със стойност undefined                     
  }
        

Ако не сте запознати с JavaScript, тогава може би очаквате променливата стойност да бъде създадена само ако състоянието се оцени да е истина. В действителност стойността на променливата се създава независимо от това. Зад кулисите, двигателя на JavaScript променя getValue функцията да изглежда така:

function getValue(condition) {
    var value;
    if (condition) {
        value = "blue";
        // друг код
        return value;
    } else {
        return null;
    }
}            
          

Обявяването на стойността е вдигнато ( hoisted) на върха, докато инициализацията остава на същото място. Това означава, че променливата стойност все още е достъпна от вътрешността на else клаузата. Ако се разглежда от там, променливата ще има стойност undefined, защото тя не е инициализирана.

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

Блокови декларации

Блок-ниво декларациите са тези, които декларират променливи, които са недостъпни извън дадения обхват на блока. Създаване на блок обхват:

  1. Вътре във функция
  2. Вътре във блок (посочен от { и } характери)

Блок определяне на обхвата се отнася за работата на много С-базирани езици и въвеждането на блокови декларации в ECMAScript 6 има за цел да допринесе за същата гъвкавост (и еднаквост) на JavaScript.

Let декларации

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

Ето един пример:

function getValue(condition) {
     if(condition) {
      let value = "blue";
     // друг код
        return value;
    } else {
         // стойността не съществува тук
         return null;
    }
    // стойността не съществува тук
}          
        

Тази версия на getValue функцията се държи много по-близо до това, което бихме очаквали в други С-базирани езици. Тъй като променливата стойност е обявена с използване на let вместо с var, декларацията не се издига до върха на дефиницията на функцията и променливата стойност е унищожена след изпълнение на if блока. Ако if състоянието се оцени на false, тогава стойността никога не се обявява или инициализира.

Не на повторното деклариране

Ако индентификатор вече е бил дефиниран в някакъв обхват и след това използваме идентификатора в let декларация вътре в този обхват, това причинява грешка и ще бъде изхвърлен. Например:

var count = 30;
// синтактична грешка
let count = 40;
       

В този пример count е обявен два пъти: един път с var и един път с let. И понеже let предефинира идентификатор, който вече съществува в същия обхват, let декларацията ще хвърли грешка. От друга страна няма да се хвърли грешка, ако let декларацията създава нова променлива със същото име, като променлива в своя съдържащ обхват, както е показано в следния код:

var count = 30;
// не хвърля грешка
if (condition) {
let count = 40;
// още код
}	
       

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

Constant декларации

Можете също така да дефинирате променливи в ECMAScript 6 със const синтаксис на декларацията. Променливи декларации използвайки const се считат за constants (константи), което означава, че веднъж определени техните стойности не могат да бъдат променяни. Поради тази причина, всяка const променлива трябва да се инициализира с декларация, както е показано в този пример:

 // Валидна константа
 const maxItems = 30;
 // Синтактична грешка: липсва инициализация
 const name;      	
       

Променливата maxItems се инициализира, така че const декларацията трябва да работи без проблем. Променливата name, обаче предизвиква синтактична грешка, ако се опитате да стартирате програма съдържаща такъв код, защото name не е инициализирана.

Constants срещу let декларации

Constants също, като let са блокови декларации. Това означава, че constants се унищожават след изпълнение и излизане от блока, в който са декларирани и декларациите не са hoisted, както е показано в този пример:

if (condition) {
    const maxItems = 5;
    // още код
}
// maxItems не е достъпна тук
       

В този код, константата maxItems е обявена в рамките на if изявление. След като изявлението приключи, maxItems се унищожава и не е достъпна извън този блок.

В друго сходство с let, const декларацията хвърля грешка, когато се прави с идентификатор на вече дефинирана променлива в същия обхват. Също няма значение, ако променливата е обявена използвайки var (за глобален или на функцията обхват) или let (за блок обхват). Например, да разгледаме този код:

var message = "Hello!";
let age = 25;
// Всеки от тях ще хвърли грешка
const message = "Goodbye!";
const age = 30;
       

Само двете const декларации ще бъдат валидни, но като се има в предвид предишните var и let декларации, в този случай няма да работят по предназначение.

Въпреки тези прилики, има една голяма разлика между let и const, която трябва да се помни. Всеки опит за присвояване на постоянна предварително дефинирана константа ще хвърли грешка и в двата - стриктен и не стриктен режим:

const maxItems = 5;
maxItems = 6;      
//хвърля грешка
       

По подобие на константите на други езици, на променливата maxItems не може да и бъде назначена нова стойност по-късно. Въпреки това, за разлика от константите на други езици, стойността притежавана от константа може да бъде модифицирана, ако е обект.

Деклариране на обеки с константи

Const декларацията предотвратява промяна на обвързването, а не на самата стойност. Това означава, че const декларациите на обекти не пречат на модификацията на тези обекти. Например:

 
const person = {  
    name: "Nicholas"
};
//работи
person.name = "Greg";
//хвърля грешка
person = {
    name: "Greg"
};	
       

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

Мъртва зона във времето

За разлика от var синтаксиса, let и const имат no hoisting характеристики. Променливи дефинирани с тях могат да бъдат достъпни едва след декларацията. Опитите да се използват, ще доведат до референтна грешка, дори ако се използват за обикновено безопасни операции, като typeof в това if изявление:

if (condition) { 
    console.log(typeof value);   // ReferenceError!
    let value = "blue";
}
       

Тука променливата стойност е дефинирана и инициализирана с let, но това твърдение никога не се изпълнява, тъй като предишния ред хвърля грешка. Проблема е, че стойността съществува в това, което JavaScript обществото нарича темпорална мъртва зона - temporal dead zone(TDZ). TDZ не е определена изрично в спецификациите на ECMAScript, но терминът често се използва, за да се опише поведението на non-hoisting на let и const. В този раздел има някои тънкости за разположение на декларациите в случаи на TDZ и въпреки, че показаните примери използват let, имайте в предвид, че същото се отнася и за const.

Когато една JavaScript машина минава през настоящ блок и намира променлива декларация, той или я изкачва (за var) или я поставя в TDZ (за let и const). Всеки опит за достъп до променливата в резултат на TDZ е грешка по време на работа. Тази променлива се отстранява само от TDZ и следователно е безопасно да се използва след изпълнението, следвайки променливата декларация.

Това е вярно, за всеки път, когато се опитате да използвате променлива декларация с let, преди да е била дефинирана. Както предишния пример демонстрира, това важи дори за нормално безопасни оператори, като typeof. Можете обаче да използвате typeof върху променлива извън блока, където тази променлива е обявена, макар че той може да не даде очакваните резултати. Помислете върху този код:

console.log(typeof value); // "undefined"

if (condition) {  
    let value = "blue";
}
       	

Променливата стойност не е в TDZ когато typeof операцията се изпълнява, защото това се случва извън блока, в който value е обявена. Това означава, че няма свързана стойност и typeof просто връща " undefined".

TDZ е просто един уникален аспект на блок-обвързванията. Друг уникален аспект е свързан с употребата им във вътрешността на циклите.

Блок обвързаност в цикли

Може би една област, в която повечето програмисти искат да блокират блок обхвата на променливите е в рамките на циклите, където брояча на променливата е предназначен да се използва само в рамките на цикъла. Например, не е необичайно да се види такъв JavaScript код:

 for (var i=0; i < 10; i++) { 
    process(items[i]);
}
// i е все още достъпна тук
console.log(i);           // 10
       	

В други езици, където блок-нивото за определяне на обхвата е по подразбиране, този пример ще работи по предназначение и само for цикълът ще има достъп до i променливата. В JavaScript обаче, променливата i все още е достъпна след завършения цикъл, тъй като var декларацията я издига. Използвайки let в следния код ще даде очакваното поведение:

 for (let i=0; i < 10; i++) {   
    process(items[i]);
}
// i не е достъпна тук - хвърля грешка
console.log(i);   		
       	

В този пример, променливата съществува само в рамките на for цикъла. След като цикълът е завършен, променливата се унищожава и вече не е достъпна другаде.

Функции в Loops

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

var funcs = [];
for (var i=0; i < 10; i++) {  
    funcs.push(function() { 
    console.log(i); 
    });
}
funcs.forEach(function(func) {
    func();     // извежда числото "10" десет пъти
});
       	

Може би очаквате този код да отпечата цифрите от 0 до 9, но той извежда числото 10, десет пъти подред. Това е, защото i се споделя между всяка итерация на цикъла, което означава, че всички функции създадени вътре в цикъла имат препратка към една и съща променлива. Променливата i има стойност 10 при завършването на цикъла и console.log(i) извиква тази стойност за разпечатване всеки път.

За да решат този проблем, програмистите използват immediately-invoked function expressions (IIFEs)(незабавно-изпълняващ се функционален израз) вътре в цикъла за итерациите, които да бъдат създадени за новото копие на променливата, както в този пример:

var funcs = [];
for (var i=0; i < 10; i++) {
    funcs.push((function(value) { 
        return function() {  
            console.log(value);
        }
    }(i)));
}
funcs.forEach(function(func) {
    func();     // изход 0, 1, 2, до 9
});	
       	

Тази версия използва IIFE вътре в цикъла. Променливата i се подава на IIFE, което създава свое собствено копие и го съхранява, като стойност. Тази стойност се използва от функцията за итерацията и така извикана всяка функция връща очакваната стойност, като цикъла брои от 0 до 9. За щастие, блоковото обвързване с let и const в ECMAScript 6 може да опрости този цикъл.

Let декларации в Loops

Декларацията let опростява цикъла, като ефективно имитира това, което IIFE прави в предишния пример. На всяка итерация, цикълът създава нова променлива и инициализира стойността на променливата със същото име от предната итерация. Това означава, че може да се пропусне напълно IIFE и да получим резултата, които очакваме:

var funcs = [];
for (let i=0; i < 10; i++) { 
    funcs.push(function() {   
        console.log(i);
    });
}
funcs.forEach(function(func) {
    func();     // изход 0, 1, 2, до 9
})	
       	

Този цикъл работи точно, както цикъла, който използва IIFE, но е може би малко по-чист. Декларацията let създава нова променлива i всеки път през цикъла, така че всяка функция създадена вътре в цикъла получава свое собствено копие на i. Всяко копие на i има стойност определена в началото на итерацията на цикъла, в която е била създадена. Същото важи и за for-in и for-of цикли, както е показано тук:

var funcs = [],    
    object = {   
        a: true, 
        b: true, 
        c: true
    };
for (let key in object) { 
    funcs.push(function() {
        console.log(key);
    });
}
funcs.forEach(function(func) {
    func();     // изход "a", "b", "c"
});
       	

В този пример, for-in цикъла показва същото поведение, както for цикъл. Всеки път през цикъла се създава ново key обвързване и така всяка функция има свое собствено копие на key променливата. Резултата е, че всяка функция извежда различна стойност. Ако беше използван var за деклариране на key, всички функции щяха да изкарат на изхода "c".

info
Важно е да се разбере, че поведението на let декларациите в циклите е специално дефинирано поведение в спецификацията и не е непременно свързано с non-hoisting характеристиката на let. Всъщност, първите реализации на let не са имали този проблем, тъй като е добавен по-късно в процеса.

Constant декларации в Loops

Спецификацията на ECMAScript 6 не забранява изрично const декларации в цикли. Въпреки това, съществуват различни поведения в зависимост от вида на цикъла, който използвате. За нормален for цикъл, може да използвате инициализация на const, но цикъла ще хвърли грешка, ако се опитате да промените стойността. На пример:

 var funcs = [];
// хвърля грешка след една итерация
for (const i=0; i < 10; i++) { 
    funcs.push(function() {   
        console.log(i);
    });
}		
       	

В този код променливата i е декларирана, като константа. Първата итерация на цикъла, когато i е 0, се изпълнява успешно. Грешка се хвърля, когато i++ се изпълни, защото това е опит да се промени константата. Като такава, може да използвате const за деклариране на променлива в инициализиране на цикъл, само ако не модифицирате тази променлива.

От друга страна, когато се използва в for-in или в for-of цикъл, const променливата се държи също, като let променливата. Така че, кода по-долу не трябва да доведе до грешка:

var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };
// не причинява грешка
for(const key in object) {
    funcs.push(function() {
        console.log(key);
    });
}
funcs.forEach(function(func) {
    func();     // изход "a", "b", "c"
});		
       	

Този код функционира почти по същия начин, както във втория пример в раздела “Let декларации в Loops ”. Единствената разлика е, че стойността на key не може да бъде променена в рамките на цикъла. Циклите for-in и for-of работят с const, защото инициализацията на цикъла създава ново обвързване за всяка итерация през цикъла, а не се опитва да променя съществуващата стойност (както в случая с предишния пример, използвайки for вместо for-in).

Глобални блок обвързвания

Друг начин, по който let и const са различни от var е тяхното поведение в глобалния обхват. Когато var се използва в глобалния обхват, тя създава нова глобална променлива, която е собственост на глобалния обект ( window в браузърите). Това означава, че може случайно да се презапише съществуващо глобално използване на var, както:

// в браузъра
var RegExp = "Hello!";
console.log(window.RegExp);     
 // "Hello!"
var ncz = "Hi!";
console.log(window.ncz);        
// "Hi!"   		
       	

Въпреки, че RegExp глобално се определя в window, не е безопасно да бъде заменен от var декларация. Този пример декларира нова глобална променлива RegExp, като презаписва оригинала. По същия начин, ncz се дефинира, като глобална променлива и веднага се определя, като свойство на window. Това е начина, по който JavaScript винаги е работил.

Ако вместо това използвате let или const в глобалния обхват, новото обвързване е създадено в глобалния обхват, но свойството не е добавено в глобалния обект. Това също означава, че не може да се замени една глобална променлива използвайки let или const, може само да и се направи сянка. Ето един пример:

// в браузъра
let RegExp = "Hello!";
console.log(RegExp);                    // "Hello"
console.log(window.RegExp === RegExp);  // false
const ncz = "Hi!";
console.log(ncz);                       // "Hi!"
console.log("ncz" in window);           // fales
       	

Тука, новата let декларация за RegExp създава обвързване със сянката на глобалния RegExp. Това означава, че window.RegExp и RegExp не са едни и същи, така че няма прекъсване на глобалния обхват. Също така, const декларацията за ncz създава обвързване но не създава свойство на глобалния обект. Тази възможност прави използването на let и const много по-безопасно в глобалния обхват, когато не искате да създадете свойства на глобалния обект.

info
Вие все още може да използвате var в глобалния обхват, ако имате код, който трябва да е на разположение на глобалния обект. Това е често срещано в браузъра, където искате да получите достъп до кода в цялата рамка или windows.

Най-добри практики за групово обвързване

Докато ECMAScript 6 е в процес на развитие, там е широко разпространено убеждението, че трябва да се използва let по подразбиране вместо var за деклариране на променливи. За много JavaScript програмисти, let се държи точно така, както са очаквали спрямо var и така пряката им подмяна има логически смисъл. В този случай, трябва да използвате const за променливите, за които е необходима защита от модификация.

Обаче, тъй като все повече програмисти мигрират към ECMAScript 6, един алтернативен подход придоби популярност: използването на const по подразбиране е само, когато let знае за стойността, която трябва да се промени. Обосновката е, че повечето променливи не трябва да променят своята стойност след инициализацията, защото промените с неочаквани стойности са източник на грешки. Тази идея има значително количество привърженици и е обект на проучване за приемане в ECMAScript 6.