Переменные в JavaScript
Переменная - это «именованное хранилище» для данных, которое используется для хранения изменяющейся (или постоянной) информации.
Объявление (declared) переменной означает её поименование (тип в JavaScript определяется динамически). Если переменная объявлена без начального значения, то она еще не готова полностью к работе.
Инициализация (initialization) переменной означает, что переменная объявлена, ей присвоено начальное значение и она запущена в работу. Без присвоения начального значения переменная просто объявлена, а с начальным значением она еще и инициализирована.
Создание (объявление и инициализация) переменной в JavaScript с использованием ключевого слова "let"
1 2 |
let name; // объявление переменной name let nikeName = "Serg"; // инициализация переменной nikeName |
Ключевое слово let присоединяет объявление переменной к области видимости того блока (обычно пара { .. }), в котором оно содержится. Объявления, сделанные с помощью let, не поднимаются во всей области видимости блока, в котором они появляются. Такие объявления не "существуют" в блоке до оператора объявления, поэтому неправильное объявление (т.е. обращение к переменной до её объявления) вызовет ошибку:
1 2 3 4 |
{ console.log( bar ); // ReferenceError! let bar = 2; } |
Несколько переменных в одной строке:
1 |
let user = 'John', age = 25, message = 'Hello'; |
Такой способ может показаться короче, но для лучшей читаемости объявляйте каждую переменную на новой строке.
Многострочный вариант немного длиннее, но легче для чтения:
1 2 3 |
let user = 'John'; let age = 25; let message = 'Hello'; |
Или с одним let и запятыми:
1 2 3 |
let user = 'John', age = 25, message = 'Hello'; |
Или с одним let и запятыми в начале строк:
1 2 3 |
let user = 'John' , age = 25 , message = 'Hello'; |
Все эти варианты работают одинаково.
Объявления let и const определяют переменные, которые ограничены текущим контекстом выполнения (областью видимости).
Переменная, объявленная в заголовке цикла с помощью ключевого слова let, будет объявленной для каждой итерации (будет видима в каждой итерации цикла). Таким образом, при её изменении внутри цикла она будет инициализирована в каждой последующей итерации значением с окончания предыдущей итерации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Ожидаемый результат с использованием let let result = []; for (let i = 0; i < 5; i++) { result[i] = function() { console.log(i); }; } result[0](); // 0 (ожидаем 0) result[1](); // 1 (ожидаем 1) result[2](); // 2 (ожидаем 2) result[3](); // 3 (ожидаем 3) result[4](); // 4 (ожидаем 4) // var не может быть блочной или локальной внутри цикла: var result = []; for (var i = 0; i < 5; i++) { result[i] = function() { console.log(i); }; } result[0](); // 5 (ожидаем 0, но т.к. var - не локальная, то в момент вызова result[]() i = 5 ) result[1](); // 5 (ожидаем 1) result[2](); // 5 (ожидаем 2) result[3](); // 5 (ожидаем 3) result[4](); // 5 (ожидаем 4) |
Особенности использования let:
- let не позволяет обратиться к переменной до её объявления;
- let препятствует объявлению в одной области видимости переменных (и/или функций) с одинаковым именем.
- let является блочной или локальной (видна только внутри блока с областью видимости, ограниченной текущим блоком кода).
Возможные ошибки при использовании let (для кода ниже):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
console.log(niсkName); // вызовет ошибку "Cannot access 'niсkName' before initialization", т.к. // переменная ещё не определена (let не позволяет обратиться к переменной до её инициализации) let name; // объявление переменной name let niсkName = "Serg"; // сохранит строку 'Serg' в переменной nikeName // (объявление и инициализация переменной nikeName) let niсkName='Se'; // вызовет ошибку "Identifier 'niсkName' has already been declared", // т.к. переменная уже объявлена (let препятствует объявлению переменных с одинаковым именем) console.log(name); // undefined - не определена (ошибки не будет, т.к. переменная объявлена, // хотя и не инициализирована) console.log(nikeName); |
Создание (объявление и инициализация) переменной в JavaScript с использованием ключевого слова "var"
Ключевое слово var – почти то же самое, что и let. Оно объявляет переменную, но немного по-другому, «устаревшим» способом (см. также "Руководство по написанию JavaScript-кода от Airbnb")
1 |
var message = 'Hello'; |
Особенности использования var:
- var не препятствует объявлению переменных с одинаковым именем (берется или функция, или последнее значение переменной; пример - ниже).
- Область видимости переменных var ограничивается либо функцией, либо, если переменная глобальная, то скриптом. Такие переменные доступны за пределами блока.
- Если блок кода находится внутри функции, то var становится локальной переменной в этой функции.
Особенности использования дублированных именований переменных и функций, объявленные с использованием var:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
foob(); // 1 var foob = 5; // будет проигнорировано (функция - главнее) function foob() { console.log(1); } // ИЛИ var foob =8; var foob = 5; var foob = 1; console.log(foob); // 1 (берется последнее значение) |
var в заголовке цикла for не может быть блочной или локальной внутри цикла:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// var не может быть блочной или локальной внутри цикла: var result = []; for (var i = 0; i < 5; i++) { result[i] = function() { console.log(i); }; } result[0](); // 5 (ожидаем 0, но т.к. var - не локальная, то в момент вызова result[]() i = 5 ) result[1](); // 5 (ожидаем 1) result[2](); // 5 (ожидаем 2) result[3](); // 5 (ожидаем 3) result[4](); // 5 (ожидаем 4) // Ожидаемый результат с использованием let for (let i = 0; i < 5; i++) { result[i] = function() { console.log(i); }; } result[0](); // 0 (ожидаем 0) result[1](); // 1 (ожидаем 1) result[2](); // 2 (ожидаем 2) result[3](); // 3 (ожидаем 3) result[4](); // 4 (ожидаем 4) |
Константы в JavaScript
Для понимания... ES6, ES8, ES2017: что такое ECMAScript и чем это отличается от JavaScript
В дополнение к let ES6 представляет ключевое слово const, которое также создает переменную блочной области видимости, но чье значение фиксировано (константа). Любая попытка изменить это значение позже приведет к ошибке.
Особенности использования констант:
- Константа должна быть сразу инициализирована (а не только объявлена).
- Константа не может быть переопределена.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const userNameOne; // вызовет ошибку "Missing initializer in const declaration", // т.к. константа объявлена, но ей не присвоено значение (а она должна быть инициализирована) const userNameOne = "Hima"; const userNameOne = 'One'; // вызовет ошибку "Identifier 'userNameOne' has already been declared", // т.к. константа уже определена console.log(userNameOne); /* Если константой определяется объект, то содержимое объекта может изменяться (переопределяться) программно */ const user = { name: "Alex", age: 45 }; console.log(user); // {name: "Alex", age: 45} user.name = "Olga"; console.log(user); // {name: "Olga", age: 45} |
Имена переменных и констант в JavaScript
Ограничения, касающиеся имён переменных JavaScript:
- Имя переменной должно содержать только буквы, цифры или символы $ и _.
- Первый символ не должен быть цифрой.
- Регистр букв имеет значение (apple и AppLE – это две разные переменные).
- Разрешен любой язык, включая кириллицу или даже иероглифы (но не рекомендуется).
- Существует список зарезервированных слов, которые нельзя использовать в качестве имён переменных, т.к. они используются самим языком (например: let, class, return, function).
Если имя содержит несколько слов, обычно используется CamelCase («ВерблюжийРегистр»), то есть слова следуют одно за другим, где каждое следующее слово начинается с заглавной буквы.
Примеры допустимых имён JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 |
let userName; let test123; let userName = 35; // стиль CamelCase, предпочтительный вариант var UserName; // стиль CamelCase, допустимо, но не принято, т.к. с Заглавных букв называют классы let user_name; var username = 40; // допустимо, но нечитабельно let $name = "Alex"; // знак $ используется в JQuery и некоторых фреймворках (нецелесообразно) var _name = "Olga"; // знак _ используется в особых случаях (нецелесообразно) var 1name; // недопустимо, имя начинается с цифры let my-name; // недопустимо, дефис '-' не разрешён в имени |
Общепринятые правила именования переменных:
- Используйте легко читаемые имена, такие как userName или shoppingCart.
- Избегайте использования аббревиатур или коротких имён, таких как a, b, c, за исключением тех случаев, когда вы точно знаете, что так нужно.
- Делайте имена максимально описательными и лаконичными. Примеры плохих имён: data и value. Такие имена ничего не говорят. Их можно использовать только в том случае, если из контекста кода очевидно, какие данные хранит переменная.
- Договоритесь с вашей командой об используемых терминах. Если посетитель сайта называется «user», тогда мы должны называть связанные с ним переменные currentUser или newUser, а не, к примеру, currentVisitor или newManInTown.
Обычно буквы в верхнем регистре для поименования констант или переменных используются в том случае, когда значение известно до выполнения скрипта и записывается непосредственно в код, например:
1 |
const BIRTHDAY = '18.04.1982'; // используют заглавные буквы |
Если переменная вычисляется во время выполнения скрипта, то используют нижний регистр:
1 |
const age = someCode(BIRTHDAY); |
Поднятие переменных (hoisting)
Области видимости (как функции, так и блока) работают по правилу:
- любая переменная, объявленная в области видимости, присоединяется к этой области видимости.
Можно предположить, что весь код, который вы видите в программе на JavaScript, интерпретируется строка за строкой. Несмотря на то, что по сути это правда, есть одна часть этого предположения, которая может привести к некорректному пониманию вашей программы.
На самом деле движок JavaScript скомпилирует ваш код до того, как начнет интерпретировать его. Частью фазы компиляции является нахождение и ассоциация всех объявлений с их соответствующими областями видимости (подробнее здесь).
Поэтому целесообразно рассматривать, что все объявления как переменных, так и функций, обрабатываются в первую очередь, до того как будет выполнена любая часть кода.
Например:
1 2 3 |
a = 2; var a; console.log(a); // 2 |
Но если мы немного изменим код, то получим иной результат:
1 2 3 4 5 6 7 |
console.log(a); // undefined var a = 2; // приведенная выше запись при работе движка аналогична коду: var a; console.log(a); // undefined (переменная а не определена) a = 2; |
Когда мы видим var a = 2;, то думаем о нем как об одном операторе. Но JavaScript на самом деле обрабатывает его как два оператора:
- var a; - первый оператор (объявление) обрабатывается во время фазы компиляции;
- a = 2; - второй оператор (присваивание) остается на своем месте в фазе исполнения.
Получается, что объявления переменной или функции "переезжают" с того места, где они появились в коде в начало кода. Это явление имеет название "поднятие (hoisting)".
ВАЖНО! Каждое поднятие соотносится с областью видимости.
Пример с функцией:
1 2 3 4 5 6 |
foo(); // функция может быть вызвана, т.к. её объявление поднято (hoisting) function foo() { console.log(a); // undefined (т.к. а = 2 присвоено ниже; var a поднимается наверх foo(..), а не наверх всей программы) var a = 2; } |
Объявление функции foo (которое в этом случае включает в себя соответствующее значение как актуальную функцию) поднимается, благодаря чему ее вызов в первой строке может быть выполнен.
Поднимаются только сами объявления (сначала - функции, затем - переменные), тогда как любые присваивания или другая логика выполнения остается на месте. Если поднятие намеренно используется, чтобы перестроить логику выполнения вашего кода, то это может вызвать хаос.
ВАЖНО! Объявления функций поднимаются, а функциональные выражения — нет.
1 2 3 4 5 |
foo(); // ReferenceError, а TypeError var foo = function bar() { // ... }; |
Идентификатор переменной foo поднимается и присоединяется к включающей его области видимости (глобальной) этой программы, поэтому foo() не вызовет ошибки ReferenceError. Но в foo пока еще нет значения (как если бы это было объявление обычной функции вместо выражения). Поэтому, foo() пытается вызвать значение undefined, которое является неправильной операцией с вызовом ошибки TypeError.
Также помните, что даже если это именованное функциональное выражение, идентификатор имени недоступен в окружающей области видимости:
1 2 3 4 5 6 |
foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... }; |
Этот код более точно интерпретируется (с учетом поднятия) как:
1 2 3 4 5 6 7 8 9 |
var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... } |
Сначала поднимаются функции, а затем уже переменные. Например:
1 2 3 4 5 6 7 8 9 10 11 |
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); }; |
1 выводится вместо 2, так как код интерпретируется движком так:
1 2 3 4 5 6 7 8 9 |
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); }; |
Внимание! var foo является дублем объявления и поэтому игнорируется) даже несмотря на то, что идет до объявления function foo(), так как объявления функций поднимаются до обычных переменных.
В то время как множественные/дублирующие объявления var фактически игнорируются, последующие объявления функции перекрывают предыдущие.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
foo(); // 3 function foo() { // первое объявление функции foo() console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { // второе объявление функции foo() (перекрывает первое и выводит "3") console.log( 3 ); } |