Let js совместимость с браузерами
Написать данную заметку меня сподвигло прочтение статьи на Хабре «Var, let или const? Проблемы областей видимости переменных и ES6» и комментариев к ней, а также соответствующей части книги Закаса Н. «Understanding of ECMAScript 6». Исходя из прочитанного я вынес, что не всё так однозначно в оценке использования var или let. Авторы и комментаторы склоняются к тому, что при отсутствии необходимости поддержки старых версий браузеров имеет смысл полностью отказаться от использования var, а также использовать некоторые упрощенные конструкции, заместо старых, по умолчанию.
Про области видимости этих объявлений уже сказано достаточно, в том числе и в указанных выше материалах, поэтому я хотел бы заострить внимание только на некоторых неочевидных моментах.
Для начала хотелось бы рассмотреть выражения немедленно вызываемых функций (Immediately Invoked Function Expression, IIFE) в циклах.
или можно обойтись без них используя let:
Закас Н. утверждает, что оба подобных примера выдавая один и тот же результат при этом также и работают абсолютно одинаково:
что, впрочем, сам же, чуть далее, косвенно опровергает.
Дело в том, что каждая итерация цикла при использовании let создает отдельную локальную переменную i и при этом привязка в функциях отправленных в массив идет также по отдельным переменным (областям видимости) в каждой итерации.
В данном конкретном случае, результат действительно не отличается, но, что если мы немного усложним код?
Здесь, добавив ++i наш результат оказался вполне предсказуем, так как мы вызвали функцию со значениями i, актуальными на момент вызова ещё при проходах самого цикла, поэтому последующая операция ++i не повлияла на значение переданное функции в массиве, так как оно было передано в замыкание с помощью вызова внешней function(i) и оказалось связанно с полученным ею аргументом.
Теперь сравним с let-варинтом без IIFE
Результат, как видно, изменился, и, природа этого изменения, в том, что значение используемое в замыкании теперь связано не с областью видимости охватывающей функции, как до этого, а с областью относящейся к конкретному проходу цикла for. И на момент вызова функции через оператор Array.prototype.forEach() в этой области значение i уже было увеличено на 1 следующей за добавлением функции в массив инструкцией ++i.
Чтобы глубже понять суть происходящего, рассмотрим примеры с двумя массивами. И для начала, возьмём var, без IIFE:
Здесь всё пока очевидно — замыканий нет, т. е., аналогичной, но с замыканием, будет подобная запись:
В обоих примерах происходит следующее:
1. В начале последней итерации цикла i == 2, затем инкрементируется на 1 (++i), и в конце добавляется еще 1 от i++, В результате на конец всего цикла i == 4.
2. Поочередно вызываются функции находящиеся в массивах func1 и func2, и в каждой из них последовательно инкрементируется одна и та же переменная i, захваченная по ссылке ими обеими из внешней области видимости (глобальной в первом варианте и function test() во втором).
Добавим IIFE.
Первый вариант:Второй вариант:
При добавлении IIFE в первом случае мы вызвали зафиксированные значения i из области видимости function(i) (0 и 2, при первом и втором проходе цикла соответственно), и инкрементировали их на 1, каждая функция отдельно от другой, так как здесь нет захвата общей переменной, ввиду того, что значение i было переданно немедленно, как аргумент функции вернувшей замыкания, при соответствующих проходах цикла. Во втором случае общее связывание также отсутствует, но тут значение передается с одновременной инкременцией, поэтому на конец первого прохода i == 4, и, цикл дальше не пошёл. Но, обращаю внимание, на то, что в замыканиях связывания переменных всё также присутствуют как в первом так и втором вариантах. Например:
прим.: даже если обрамить цикл функцией, общими, связывания, естественно не станут, так как у каждой функции сейчас своя отдельная область видимости образованная вызовом вернувших их функций.
Теперь же рассмотрим инструкцию let, без IIFE соответственно.
А вот здесь, у нас образовались замыкания со связанными переменными цикла, и не отдельными, а общими.
В итоге мы имеем, что в первой области видимости, до вызова, значение i == 1, а во второй i == 3. Это значения, которые получила переменная i, до i++ в конце соответствующих итераций цикла, но после всех инструкций в блоке цикла.
Далее вызываются функции находящиеся в массиве func1 и они инкрементируют соответствующие переменные в обеих областях видимости и в результате в первой i == 2, а во второй i == 4.
Последующий вызов func2 инкрементирует те же переменные дальше и получает i == 3 и 5 соответственно.
Я специально поставил func2 и func1 внутри блока в таком порядке, чтобы была наглядней видна независимость от их расположения, и, чтобы подчеркнуть внимание читателя именно на факте захвата переменных цикла.
Напоследок приведу тривиальный пример направленный на закрепление понимания замыканий и области видимости let :
Что мы имеем в итоге
1. Задействование выражений немедленно вызываемых функций не является эквивалентным использованию итерируемых let переменных в функциях в циклах, и, в ряде случаев, приводит к различному результату.
2. Из-за того, что при использовании let объявления для итератора в каждой итерации создаётся отдельная локальная переменная, встаёт вопрос об утилизации ненужных данных сборщиком мусора. На этом пункте, признаться, я и хотел изначально заострить внимание, подозревая, что создание большого количества переменных в больших, соответственно, циклах будет тормозить работу компилятора, однако, при сортировке тестового массива с использованием только let объявлений переменных показало выигрыш по времени выполнения почти в два раза для массива в 100000 ячеек (по данной теме есть отдельная статья – см. ниже ссылку в комментарии – и там поясняется, что рассмотренный тест и его результаты имеют специфическую природу):
При этом время выполнения практически не зависело от наличия/отсутствия инструкций:
The let statement declares a block-scoped local variable, optionally initializing it to a value.
Try it
Syntax
Parameters
The names of the variable or variables to declare. Each must be a legal JavaScript identifier.
For each variable declared, you may optionally specify its initial value to any legal JavaScript expression.
Alternatively, the Destructuring Assignment syntax can also be used to declare variables.
Description
let allows you to declare variables that are limited to the scope of a block statement, or expression on which it is used, unlike the var keyword, which declares a variable globally, or locally to an entire function regardless of block scope. The other difference between var and let is that the latter is initialized to a value only when a parser evaluates it (see below).
Just like const the let does not create properties of the window object when declared globally (in the top-most scope).
An explanation of why the name "let" was chosen can be found here.
Note: Many issues with let variables can be avoided by declaring them at the top of the scope in which they are used (doing so may impact readability).
Examples
Scoping rules
Variables declared by let have their scope in the block for which they are declared, as well as in any contained sub-blocks. In this way, let works very much like var . The main difference is that the scope of a var variable is the entire enclosing function:
At the top level of programs and functions, let , unlike var , does not create a property on the global object. For example:
Emulating private members
In dealing with constructors it is possible to use the let bindings to share one or more private members without using closures:
The same privacy pattern with closures over local variables can be created with var , but those need a function scope (typically an IIFE in the module pattern) instead of just a block scope like in the example above.
Redeclarations
Redeclaring the same variable within the same function or block scope raises a SyntaxError .
You may encounter errors in switch statements because there is only one block.
However, it's important to point out that a block nested inside a case clause will create a new block scoped lexical environment, which will not produce the redeclaration errors shown above.
Temporal dead zone (TDZ)
let variables cannot be read/written until they have been declared. If no initial value is specified on declaration, the variable is initialized with a value of undefined . Accessing the variable before the declaration results in a ReferenceError .
Note: This differs from var variables, which will return a value of undefined if they are accessed before they are declared.
The variable is said to be in a "temporal dead zone" (TDZ) from the start of the block until the declaration has completed.
The term "temporal" is used because the zone depends on the order of execution (time) rather than the order in which the code is written (position). For example, the code below works because, even though the function that uses the let variable appears before the variable is declared, the function is called outside the TDZ.
The TDZ and typeof
Using the typeof operator for a let variable in its TDZ will throw a ReferenceError :
This differs from using typeof for undeclared variables, and variables that hold a value of undefined :
TDZ combined with lexical scoping
The following code results in a ReferenceError at the line shown:
The if block is evaluated because the outer var foo has a value. However due to lexical scoping this value is not available inside the block: the identifier foo inside the if block is the let foo . The expression (foo + 55) throws a ReferenceError because initialization of let foo has not completed — it is still in the temporal dead zone.
This phenomenon can be confusing in a situation like the following. The instruction let n of n.a is already inside the private scope of the for loop's block. So, the identifier n.a is resolved to the property ' a ' of the ' n ' object located in the first part of the instruction itself ( let n ).
This is still in the temporal dead zone as its declaration statement has not been reached and terminated.
Other situations
When used inside a block, let limits the variable's scope to that block. Note the difference between var , whose scope is inside the function where it is declared.
However, this combination of var and let declaration below is a SyntaxError due to var being hoisted to the top of the block. This results in an implicit re-declaration of the variable.
Шаблонными литералами называются строковые литералы, допускающие использование выражений внутри. С ними вы можете использовать многострочные литералы и строковую интерполяцию. В спецификациях до ES2015 они назывались "шаблонными строками".
Синтаксис
Описание
Шаблонные литералы заключены в обратные кавычки (` `) вместо двойных или одинарных. Они могут содержать подстановки, обозначаемые знаком доллара и фигурными скобками ( $ ). Выражения в подстановках и текст между ними передаются в функцию. По умолчанию функция просто объединяет все части в строку. Если перед строкой есть выражение (здесь это tag ), то шаблонная строка называется "теговым шаблоном". В этом случае, теговое выражение (обычно функция) вызывается с обработанным шаблонным литералом, который вы можете изменить перед выводом. Для экранирования обратной кавычки в шаблонных литералах указывается обратный слеш \.
Многострочные литералы
Символы новой строки являются частью шаблонных литералов. Используя обычные строки, вставка переноса потребовала бы следующего синтаксиса:
То же с использованием шаблонных литералов:
Интерполяция выражений
Для вставки выражений в обычные строки вам пришлось бы использовать следующий синтаксис:
Теперь, при помощи шаблонных литералов, вам доступен "синтаксический сахар", делающий подстановки вроде той более читабельными:
Вложенные шаблоны
Временами, вложить шаблон — это кратчайший и, возможно, более читабельный способ составить строку. Просто поместите внутрь шаблона с обратными кавычками ещё одни, обернув их в подстановку $ < >. Например, если выражение истинно, можно вернуть шаблонный литерал.
В ES2015 с шаблонными литералами без вложения:
В ES2015 с вложенными шаблонными литералами:
Теговые шаблоны
Расширенной формой шаблонных литералов являются теговые шаблоны. Они позволяют разбирать шаблонные литералы с помощью функции. Первый аргумент такой функции содержит массив строковых значений, а остальные содержат выражения из подстановок. В итоге, функция должна вернуть собранную строку (или что-либо совсем иное, как будет показано далее). Имя функции может быть любым.
Функция тега не обязана возвращать строку, как показано в примере ниже:
Сырые строки
Специальное свойство raw , доступное для первого аргумента тегового шаблона, позволяет получить строку в том виде, в каком она была введена, без экранирования.
Вдобавок, существует метод String.raw() , возвращающий точно такую же исходную строку, какую вернула бы функция шаблона по умолчанию и строковая конкатенация вместе.
Теговые шаблоны и экранирование символов
Поведение в ES2016
В ECMAScript 2016 теговые шаблоны следуют правилам экранирования следующих символов:
- символы Unicode, начинающиеся с "\u", например, \u00A9
- точки кода Unicode, начинающиеся с "\u<>", например, \u
- шестнадцатеричные представления символов, начинающиеся с "\x", например, \xA9
- восьмеричные представления символов, начинающиеся с "\", например, \251
Отсюда вытекает проблема теговых шаблонов: следуя грамматике ECMAScript, анализатор кода, найдя символ \ , будет искать корректное представление символа Unicode, но может не найти его вовсе. Пример ниже показывает это:
Поведение в ES2018
Теговые шаблоны должны позволять встраивать языки (например, DSLs или LaTeX), в которых широко используются многие другие экранирования. Предложение Редакция шаблонных литералов (уровень 4, одобренный к добавлению в стандарт ECMAScript 2018) устраняет синтаксические ограничения экранирования теговых шаблонов в ECMAScript.
Однако, некорректное экранирование символов по-прежнему нужно отображать в "приготовленном" отображении. Оно показывается в виде undefined в "приготовленном" массиве:
Заметьте, что ограничение на экранирование символов проявляется лишь в теговых шаблонах, и не проявляется в нетеговых шаблонных литералах:
Директива let объявляет переменную с блочной областью видимости с возможностью инициализировать её значением.
Синтаксис
Параметры
var1 , var2 , …, varN Имя переменной. Может использоваться любой допустимый идентификатор. value1 , value2 , …, valueN Значение переменной. Любое допустимое выражение.
Описание
Директива let позволяет объявить локальную переменную с областью видимости, ограниченной текущим блоком кода . В отличие от ключевого слова var , которое объявляет переменную глобально или локально во всей функции, независимо от области блока.
Объяснение, почему было выбрано название "let" можно найти здесь.
Правила области видимости
Областью видимости переменных, объявленных ключевым словом let , является блок, в котором они объявлены, и все его подблоки. В этом работа директива let схожа с работой директивы var . Основная разница заключается в том, что областью видимости переменной, объявленной директивой var , является вся функция, в которой она объявлена:
Чище код во вложенных функциях
let иногда делает код чище при использовании вложенных функций.
Пример выше будет выполнен как и ожидается, так как пять экземпляров внутренней функции (анонимной) будут ссылаться на пять разных экземпляров переменной i . Пример будет выполнен неверно, если заменить директиву let на var, или удалить переменную i из параметров вложенной функции и использовать внешнюю переменную i во внутренней функции.
На верхнем уровне скриптов и функций let, в отличии от var, не создаёт свойства на глобальном объекте . Например:
В выводе программы будет отображено слово "global_x" для this.x , но undefined для this.y .
Эмуляция приватных членов
При взаимодействии с конструкторами можно использовать выражение let чтобы открыть доступ к одному или нескольким приватным членам через использование замыканий:
Эта техника позволяет получить только "статичное" приватное состояние - в примере выше, все экземпляры полученные из конструктора SomeConstructor будут ссылаться на одну и ту же область видимости privateScope .
Временные мёртвые зоны и ошибки при использовании let
Повторное объявление той же переменной в том же блоке или функции приведёт к выбросу исключения SyntaxError.
В стандарте ECMAScript 2015 переменные, объявленные директивой let, переносятся в начало блока. Но если вы сошлётесь в блоке на переменную, до того как она объявлена директивой let, то это приведёт к выбросу исключения ReferenceError , потому что переменная находится во "временной мёртвой зоне" с начала блока и до места её объявления. (В отличии от переменной, объявленной через var , которая просто будет содержать значение undefined )
Вы можете столкнуться с ошибкой в операторах блока switch , так как он имеет только один подблок.
Использование let в циклах for
Вы можете использовать ключевое слово let для привязки переменных к локальной области видимости цикла for . Разница с использованием var в заголовке цикла for , заключается в том, что переменные объявленные var , будут видны во всей функции, в которой находится этот цикл.
Правила области видимости
В этом примере expr2 , expr3, statement заключены в неявный блок, который содержит блок локальных переменных, объявленных конструкцией let expr1 . Пример приведён выше.
Примеры
let vs var
Когда let используется внутри блока, то область видимости переменной ограничивается этим блоком. Напомним, что отличие заключается в том, что областью видимости переменных, объявленных директивой var, является вся функция, в которой они были объявлены.
let в циклах
Вы можете использовать ключевое слово let для привязки переменных к локальной области видимости цикла for , вместо того что бы использовать глобальные переменные (объявленные с помощью var ).
Нестандартизированные расширения let
let блок
Поддержка let блоков была убрана в Gecko 44 баг 1023609.
let блок предоставляет способ, ассоциировать значения с переменными внутри области видимости этого блока, без влияния на значения переменных с теми же именами вне этого блока.
Синтаксис
Описание
let блок предоставляет локальную область видимости для переменных. Работа его заключается в привязке нуля или более переменных к области видимости этого блока кода, другими словами, он является блоком операторов. Отметим, что область видимости переменных, объявленных директивой var , в блоке let , будет той же самой, что и если бы эти переменные были объявлены вне блока let , иными словами областью видимости таких переменных по-прежнему является функция. Скобки в блоке let являются обязательными. Опускание их приведёт к синтаксической ошибке.
Пример
Правила для этого блока кода аналогичны как и для любого другого блока кода в JavaScript. Он может содержать свои локальные переменные, объявленные let .
Правила области видимости
Областью видимости переменных, объявленных директивой let , в блоке let является сам блок и все подблоки в нем, если они не содержат объявлений переменных с теми же именами.
let выражения
Поддержка let выражений была убрана в Gecko 41 баг 1023609.
let выражение позволяет объявить переменные с областью видимости ограниченной одним выражением.
Синтаксис
Пример
Вы можете использовать let для объявления переменных, областью видимости которых является только одно выражение:
Правила области видимости
В данном let выражении:
expr оборачивается в неявный блок.
Спецификации
Поддержка браузерами
BCD tables only load in the browser
Found a problem with this page?
Last modified: 22 февр. 2022 г. , by MDN contributors
Your blueprint for a better internet.
Support
Our communities
Developers
Если у вас был опыт в написании JS приложений, то наверняка вы знакомы с его синтаксисом. Он весьма не обычен и имеет ряд причуд, но в целом обдуманный, понятный и имеющий множество сходств с другими языками.
ES6 добавляет несколько новых синтаксических форм с которыми нам предстоит познакомиться. В этой главе мы пробежимся по многим из них и узнаем, что же нового у нас в арсенале.
Блочная область видимости
Наверняка вам известно, что фундаментальной единицей видимости в JavaScript, является функция (function). Если вам необходимо реализовать блок со своей областью видимости, то наиболее подходящий способ заключается в использовании IIFE (выражение немедленно вызывающейся функции), например:
Оператор let
Однако, теперь мы можем реализовать области видимость в любом виде блока и называется это — блочной областью видимости (block scoping). Все, что нам необходимо для создания новой области видимости — пара, всем давно известных фигурных скобок < .. >. Вместо использования оператора var , который мы использовали для объявления переменных внутренней (или глобальной) области видимости, мы можем использовать let для объявления блочной:
Конечно для JS это необычно, но для разработчиков которые работали с другими языками, данный паттерн давно известен.
Это лучший способ создания блочной видимости с использованием < . >. Я рекомендую вам, помещать объявления let в самом начале блока и если у вас более одного объявления, использовать всего один оператор let .
Стилистически, я бы даже рекомендовал вам, делать объявление на одной строке с открывающимся < , чтобы явно подчеркнуть для какого блока предназначены эти переменные:
Сейчас это выглядит довольно странно и не совпадает с большинством рекомендаций из литературы по ES6, но у меня есть причины для моего безумия. (прим. перев.: узнаем чуть ниже).
Существует и другая предложенная форма оператора let , и называется она let-блок (let-block):
Это отличный пример, того, что я называю "явная блочная область видимости", в то время как let схожий с var — не явный, так как распространяется на весь блок области видимости.
К сожалению форма let (..) < .. >, не была принята в ES6. Быть может в последующих обновлениях ES6, мы увидим эту форму, но пока первый пример, это лучшее, что у нас есть.
Чтобы подчеркнуть неявную природу оператора let , давайте рассмотрим следующий пример:
А теперь небольшая викторинка, без повторного изучения кода, подскажите мне: какие переменные существуют в блоке if , а какие только в блоке цикла for ?
Ответ: блок if содержит переменные a и b блочной области видимости, а for — переменные i и j блочной области видимости.
Не удивляет ли вас то, что переменная i не была добавлена в область видимости блока if ?
В данном примере так же таится опасность в столь низком объявлении переменной c ;
Традиционный оператор var своего рода "прикрепляет" переменную к области видимости и инициализирует ее при входе в функцию еще до ее выполнения, в не зависимости от, того где она была объявлена. Данное явление известно как поднятием. Оператор let , так же прикрепляет переменную к области видимости до ее выполнения, но не инициализирует ее при входе в функцию, что в случае попытки обращения к такой переменной, раньше чем она будет объявлена/инициализирована, приведет к ошибке.
Обратите внимание: ReferenceError который будет вызван ранним обращением к переменной, до того как она была объявлена или инициализирована, технически называется — ошибка TDZ (temporal dead zone).
Хочу обратить ваше внимание на, то, что «не инициализирована», не означает, что ей необходимо явно присвоить какое либо значение. let b; — вполне достаточно. Предполагается, что переменная который не было присвоено значение во время объявления, будет инициализирована значением undefined , по умолчанию. По этому let b; тоже самое, что и let b = undefined; . Но повторюсь — до того как, выражение let b; не будет выполнено, использовать данную переменную, вы не можете.
Еще один неожиданный поворот заключается в поведении typeof с TDZ переменными:
Так как переменная а не была объявлена, то единственный безопасным способом проверить ее на существование будет использование оператора typeof . Но не тут-то было в случае с переменной b , которая бросит исключение, так как она была объявлена намного ниже с помощью оператора let .
Я не просто рекомендую, а настаиваю на том, чтобы все объявления с использованием нового оператора let , были в самом верху блока. Это избавит вас от головной боли рефакторинга, устранения необъяснимых ошибок, сделает код более понятным и предсказуемым.
Примечание: Больше об операторе let и блочной области видимости, мы поговорим в главе 3.
let + for
Еще одной значительной особенностью оператора let , является то, как он себя ведет с циклом for .
Рассмотрим пример:
Выражение let i в шапке for объявляет новую переменную не для всего цикла, а для каждой отдельно взятой итерации. То же самое верно и для замыкания внутри цикла. По факту, код ведет себя в точности так, как и ожидается.
Если вы попытаетесь вместо let i , использовать var i , то вы бы получили в результате выполнения данного пример 5, а не 3, так как в таком случае замыкание будет общим для всех итераций.
Немного добавив воды, можно получить следующий пример, который делает то же самое:
В данном примере мы явно создаем новую переменную j для каждой итерации. Я предпочитаю первый вариант, который, конечно же, менее явный, но не настолько, чтобы от него отказаться.
Оператор const
У нас имеется еще один оператор для рассмотрения, также относящийся к блочной области видимости — const .
Константа, это такая переменная которая после первичной инициализации доступна только для чтения.
Например:
Вы не можете поменять значение переменной, которой уже было присвоено значение во время объявления. При объявлении константы, вы должны явно указывать значение переменной. По умолчанию константа не получает значение undefined , следовательно если вам понадобится такое значение, то вам придется вручную его присвоить — const a = undefined; .
Константы имеют ограничения на присвоения, но нет никаких ограничений связанных со значение переменной. Например если константой является комплексное значение, то вы по-прежнему можете его модифицировать:
Таким образом, константа своего рода держит константную ссылку, но массив на который она указывает — по-прежнему динамичен.
Обратите внимание: Так как мы не можем удалять ссылки, присвоение объекта или массива константе, означает, что значение не сможет быть удалено сборщиком мусора до тех пор, пока не покинет лексическую область видимости. Конечно это может быть желательным поведением, но будьте осторожны.
Раньше мы использовали заглавные буквы в литералах которые предназначались переменным, тем самым намекая, что значения таких переменных менять не желательно. Сейчас же, мы можем использовать константы которые предостерегают нас от нежелательных изменений.
По слухам, так как использование константы, дает понять движку JS, что значение никогда не будет изменено, сделает данный оператор более оптимизированным решением нежели let или var .
Не смотря на наши фантазии по этому поводу, намного важней, чтобы вы понимали когда вам действительно необходимо использование константы. Не стоит использовать константы для простых переменных, иначе это может стать причиной недопонимание.
Блочная область видимости функций
Начиная с ES6, подразумевается, что функции которые были объявлены в блоке, буду иметь его область видимости (блочную). До ES6, спецификация умалчивала по этому поводу, в отличии от различных конкретных реализации. Тем не менее, сейчас спецификация соответствует реальности.
В данном примере, функция объявлена в блочной области видимости, следовательно она не доступна из вне. Стоит так же отметить интересный факт — в отличии от объявление let , с которым мы не можем работать до того, как такая переменная будет проиницилизированна, в данном случае ошибки TDZ не возникает.
Блочная область видимости функций может стать для вас проблемой, если вы до сих пор пишете код как в приведенном ниже примере, рассчитывая на старое поведение:
В пред-ES6 окружениях, результатом вызова функции foo() будет "2" — в независимости от значения переменной something , так как благодаря поднятию рабочей функцией будет только вторая.
Читайте также: