Область действия, или набор правил, определяющих, где находятся ваши переменные, является одним из самых основных понятий любого языка программирования. На самом деле это настолько фундаментально, что легко забыть, какими тонкими могут быть правила!
Точное понимание того, как движок JavaScript «думает» о области видимости, не даст вам написать типичные ошибки, которые может вызвать подъем, подготовит вас к тому, чтобы обернуть голову вокруг замыканий, и сделает вас намного ближе к тому, чтобы никогда больше не писать ошибок.
… Ну, в любом случае, это поможет вам понять подъемы и затворы.
В этой статье мы рассмотрим:
- основы областей применения в JavaScript
- как интерпретатор решает, какие переменные принадлежат к какой области видимости
- как на самом деле работает подъем
- как ключевые слова ES6
let
иlet
изменить игру
Давайте погрузимся в.
Если вы хотите узнать больше о ES6 и о том, как использовать синтаксис и функции для улучшения и упрощения кода JavaScript, почему бы не попробовать эти два курса:
Лексическая сфера
Если вы написали хотя бы строку JavaScript раньше, вы будете знать, что где вы определяете свои переменные, определяет, где вы можете их использовать . Тот факт, что видимость переменной зависит от структуры вашего исходного кода, называется лексической областью действия.
Существует три способа создания области видимости в JavaScript:
- Создать функцию , Переменные, объявленные внутри функций, видны только внутри этой функции, в том числе во вложенных функциях.
- Объявите переменные с помощью
let
илиconst
внутри блока кода . Такие объявления видны только внутри блока. - Создать блок
catch
. Верьте или нет, это на самом деле создает новую область!
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
«use strict»;
var mr_global = «Mr Global»;
function foo () {
var mrs_local = «Mrs Local»;
console.log(«I can see » + mr_global + » and » + mrs_local + «.»);
function bar () {
console.log(«I can also see » + mr_global + » and » + mrs_local + «.»);
}
}
foo();
try {
console.log(«But /I/ can’t see » + mrs_local + «.»);
} catch (err) {
console.log(«You just got a » + err + «.»);
}
{
let foo = «foo»;
const bar = «bar»;
console.log(«I can use » + foo + bar + » in its block…»);
}
try {
console.log(«But not outside of it.»);
} catch (err) {
console.log(«You just got another » + err + «.»);
}
// Throws ReferenceError!
console.log(«Note that » + err + » doesn’t exist outside of ‘catch’!»)
|
Приведенный выше фрагмент демонстрирует все три механизма видимости. Вы можете запустить его в Node или Firefox, но Chrome пока не очень хорошо работает с let
.
Мы поговорим о каждом из них в изящных деталях. Давайте начнем с подробного рассмотрения того, как JavaScript определяет, какие переменные принадлежат к какой области видимости.
Процесс компиляции: взгляд с высоты птичьего полета
Когда вы запускаете фрагмент JavaScript, две вещи заставляют его работать.
- Во-первых, ваш источник компилируется.
- Затем скомпилированный код выполняется.
В течение шаг компиляции , движок JavaScript:
- принимает к сведению все ваши имена переменных
- регистрирует их в соответствующем объеме
- резервирует место для своих ценностей
Только во время выполнения движок JavaScript фактически устанавливает значение ссылок на переменные, равное их значениям присваивания. До тех пор они не undefined
.
Шаг 1: Компиляция
01
02
03
04
05
06
07
08
09
10
|
// I can use first_name anywhere in this program
var first_name = «Peleke»;
function popup (first_name) {
// I can only use last_name inside of this function
var last_name = «Sengstacke»;
alert(first_name + ‘ ‘ + last_name);
}
popup(first_name);
|
Давайте рассмотрим, что делает компилятор.
Сначала он читает строку var first_name = "Peleke"
. Затем он определяет, в какую область сохранить переменную. Поскольку мы находимся на верхнем уровне сценария, он понимает, что находится в глобальной области видимости . Затем он сохраняет переменную first_name
в глобальной области видимости и инициализирует ее значение undefined
.
Во-вторых, компилятор читает строку с function popup (first_name)
. Поскольку ключевое слово function является первым в строке, оно создает новую область видимости для функции, регистрирует определение функции в глобальной области видимости и заглядывает внутрь, чтобы найти объявления переменных.
Конечно же, компилятор найдет один. Так как в первой строке нашей функции есть var last_name = "Sengstacke"
, компилятор сохраняет переменную last_name
в контекст popup
— не для глобальной области видимости — и устанавливает его значение undefined
.
Поскольку внутри функции больше нет объявлений переменных, компилятор возвращается в глобальную область видимости. И поскольку там больше нет объявлений переменных, этот этап завершен.
Обратите внимание, что мы еще ничего не запустили . Задача компилятора на этом этапе — просто убедиться, что он знает имя каждого; это не волнует, что они делают.
На данный момент наша программа знает, что:
- В глобальной области видимости есть переменная
first_name
. - В глобальной области видимости есть функция
popup
. - В области
popup
last_name
есть переменная с именемlast_name
. - Значения
first_name
иlast_name
неundefined
.
И не важно, что мы присваиваем эти переменные значения в другом месте нашего кода. Двигатель позаботится об этом во время исполнения .
Шаг 2: Выполнение
На следующем шаге движок снова читает наш код, но на этот раз выполняет его.
Сначала он читает строку, var first_name = "Peleke"
. Для этого движок ищет переменную с именем first_name
. Поскольку компилятор уже зарегистрировал переменную с таким именем, движок находит ее и устанавливает ее значение "Peleke"
.
Далее он читает строку function popup (first_name)
. Поскольку мы здесь не выполняем функцию, движок не заинтересован и пропускает ее.
Наконец, он читает popup(first_name)
строку popup(first_name)
. Так как мы выполняем функцию здесь, движок:
- ищет значение
popup
- ищет значение
first_name
- выполняет
popup
как функцию, передавая значениеfirst_name
в качестве параметра
Когда он выполняет popup
, он проходит тот же процесс, но на этот раз внутри popup
функции. Это:
- ищет переменную с именем
last_name
- устанавливает значение
last_name
равным"Sengstacke"
- ищет
alert
, выполняя его как функцию с параметром
Оказывается, под капотом происходит гораздо больше, чем мы могли подумать!
Теперь, когда вы понимаете, как JavaScript читает и выполняет код, который вы пишете, мы готовы заняться чем-то более близким к дому: как работает подъем.
Подъем под микроскопом
Давайте начнем с некоторого кода.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
bar();
function bar () {
if (!foo) {
alert(foo + «? This is strange…»);
}
var foo = «bar»;
}
broken();
var broken = function () {
alert(«This alert won’t show up!»);
}
|
Если вы запустите этот код, вы заметите три вещи:
- Вы можете обратиться к
foo
прежде чем назначить ему, но его значение неundefined
. - Вы можете вызвать
broken
до того, как определите его, но вы получитеTypeError
. - Вы можете вызвать
bar
прежде, чем определите его, и он будет работать как нужно.
Под подъемом подразумевается тот факт, что JavaScript делает все объявленные нами имена переменных доступными повсюду в их областях — в том числе и до того, как мы присваиваем им.
В этом фрагменте есть три случая, о которых вам нужно знать в своем собственном коде, поэтому мы рассмотрим каждый из них по одному.
Подъемные объявления переменных
Помните, когда компилятор JavaScript читает строку вроде var foo = "bar"
, это:
- регистрирует имя
foo
в ближайшей области видимости - устанавливает значение
foo
неопределенным
Причина, по которой мы можем использовать foo
прежде чем присвоить ему, заключается в том, что, когда движок ищет переменную с этим именем, она существует. Вот почему он не генерирует ReferenceError
.
Вместо этого он получает значение undefined
и пытается использовать его для выполнения того, о чем вы его просили. Обычно это ошибка.
Имея это в виду, мы можем представить, что то, что JavaScript видит в нашей функциональной bar
выглядит примерно так:
1
2
3
4
5
6
7
8
|
function bar () {
var foo;
if (!foo) {
// !undefined is true, so alert
alert(foo + «? This is strange…»);
}
foo = «bar»;
}
|
Это первое правило подъема , если хотите: переменные доступны во всей их области видимости , но имеют значение undefined
пока ваш код не назначит их.
Распространенная идиома JavaScript состоит в том, чтобы писать все ваши объявления var
в верхней части области видимости , а не там, где вы впервые их используете. Перефразируя Дуга Крокфорда, это помогает вашему коду читать больше, как он работает .
Когда вы думаете об этом, это имеет смысл. Понятно, почему bar
ведет себя так, как когда мы пишем наш код так, как его читает JavaScript, не так ли? Так почему бы просто не писать так все время?
Подъемные выражения функций
Тот факт, что мы получили TypeError
когда мы пытались выполнить broken
до того, как мы его определили, является частным случаем первого правила подъема.
Мы определили переменную с именем broken
, которую компилятор регистрирует в глобальной области видимости и устанавливает значение undefined
. Когда мы пытаемся запустить его, движок ищет значение broken
, обнаруживает, что оно не undefined
, и пытается выполнить undefined
как функцию.
Очевидно, undefined
не является функцией — вот почему мы получаем TypeError
!
Объявления подъемных функций
Наконец, напомним, что мы смогли вызвать bar
до того, как определили его. Это связано со вторым правилом поднятия : когда компилятор JavaScript находит объявление функции, он делает доступным и ее имя, и определение в верхней части области видимости. Переписываем наш код еще раз:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
function bar () {
if (!foo) {
alert(foo + «? This is strange…»);
}
var foo = «bar»;
}
var broken;
bar();
broken();
broken = function () {
alert(«This alert won’t show up!»);
}
|
Опять же, это имеет гораздо больше смысла, когда вы пишете, как читает JavaScript, не так ли?
Для просмотра:
- Имена как объявлений переменных, так и выражений функций доступны во всей их области видимости, но их значения не
undefined
до назначения. - Имена и определения объявлений функций доступны во всей их области, даже до их определений.
Теперь давайте взглянем на два новых инструмента, которые работают немного по-другому: let
и const
.
let
, const
const
, и временная мертвая зона
В отличие от объявлений var
, переменные, объявленные с помощью let
и const
, не поднимаются компилятором.
По крайней мере, не совсем так.
Помните, как мы могли вызывать broken
, но получили TypeError
потому что мы пытались выполнить undefined
? Если бы мы определили broken
с помощью let
, вместо этого мы получили бы ReferenceError
:
1
2
3
4
5
6
|
«use strict»;
// You have to «use strict» to try this in Node
broken();
let broken = function () {
alert(«This alert won’t show up!»);
}
|
Когда компилятор JavaScript регистрирует переменные в своих областях при первом проходе, он обрабатывает let
и const
иначе, чем var
.
Когда он находит объявление var
, мы регистрируем имя переменной в ее области и немедленно инициализируем ее значение undefined
.
Однако с помощью let
компилятор регистрирует переменную в своей области видимости, но не инициализировать его значение undefined
. Вместо этого он оставляет переменную неинициализированной до тех пор , пока механизм не выполнит ваш оператор присваивания. При обращении к значению неинициализированной переменной выдается ошибка ReferenceError
, которая объясняет, почему приведенный выше фрагмент кода выдает его при запуске.
Пространство между началом вершины области действия объявления let
и оператора присваивания называется временной мертвой зоной . Название происходит от того факта, что, хотя движок знает о переменной с именем foo
в верхней части области видимости bar
, переменная «мертва», потому что у нее нет значения.
… Кроме того, потому что это убьет вашу программу, если вы попытаетесь использовать ее рано.
Ключевое слово const
работает так же, как let
, с двумя ключевыми отличиями:
- Вы должны присвоить значение при объявлении с помощью
const
. - Вы не можете переназначить значения переменной, объявленной с помощью
const
.
Это гарантирует, что const
всегда будет иметь значение, которое вы изначально присвоили ему.
1
2
3
4
5
6
|
// This is legal
const React = require(‘react’);
// This is totally not legal
const crypto;
crypto = require(‘crypto’);
|
Блок Сфера
let
и const
отличаются от var
другим способом: размером их областей.
Когда вы объявляете переменную с помощью var
, она видна как можно выше в цепочке областей видимости — обычно в верхней части объявления ближайшей функции или в глобальной области видимости, если вы объявляете ее на верхнем уровне.
Однако когда вы объявляете переменную с помощью let
или const
, она видна настолько локально, насколько это возможно — только в пределах ближайшего блока.
Блок — это фрагмент кода, выделенный фигурными скобками, как вы видите с блоками if
/ else
, for
циклов и в явно «заблокированных» кусках кода, как в этом фрагменте.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
«use strict»;
{
let foo = «foo»;
if (foo) {
const bar = «bar»;
var foobar = foo + bar;
console.log(«I can see » + bar + » in this bloc.»);
}
try {
console.log(«I can see » + foo + » in this block, but not » + bar + «.»);
} catch (err) {
console.log(«You got a » + err + «.»);
}
}
try {
console.log( foo + bar );
} catch (err) {
console.log( «You just got a » + err + «.»);
}
console.log( foobar );
|
Если вы объявляете переменную с помощью const
или let
внутри блока, она видна только внутри блока и только после того, как вы ее присвоили.
Однако переменная, объявленная с помощью var
, видна как можно дальше — в этом случае, в глобальной области видимости.
Если вы заинтересованы в мельчайших подробностях let
и const
, посмотрите, что доктор Раушмайер говорит о них в « Изучении ES6: переменные и области видимости» , и посмотрите на них документацию MDN .
Лексический this
& Arrow Функции
На первый взгляд, this
не имеет ничего общего с областью видимости. И, на самом деле, JavaScript не решает смысл this
соответствии с правилами области, о которых мы говорили здесь.
По крайней мере, не обычно. Общеизвестно, что JavaScript не разрешает значение this
ключевого слова в зависимости от того, где вы его использовали:
01
02
03
04
05
06
07
08
09
10
11
|
var foo = {
name: ‘Foo’,
languages: [‘Spanish’, ‘French’, ‘Italian’],
speak : function speak () {
this.languages.forEach(function(language) {
console.log(this.name + » speaks » + language + «.»);
})
}
};
foo.speak();
|
Большинство из нас ожидают, что this
будет означать foo
внутри цикла forEach
, потому что это то, что он имел в виду прямо за его пределами. Другими словами, мы ожидаем, что JavaScript разрешит смысл this
лексического выражения.
Но это не так.
Вместо этого он создает новое this
внутри каждой функции, которую вы определяете, и решает, что это означает, основываясь на том, как вы вызываете функцию, а не на том месте, где вы ее определили.
Эта первая точка похожа на случай переопределения любой переменной в дочерней области:
01
02
03
04
05
06
07
08
09
10
11
|
function foo () {
var bar = «bar»;
function baz () {
// Reusing variable names like this is called «shadowing»
var bar = «BAR»;
console.log(bar);
}
baz();
}
foo();
|
Замените bar
на this
, и все должно очиститься мгновенно!
Традиционно, чтобы заставить this
работать, поскольку мы ожидаем, что простые старые лексически переменные переменные будут работать, требуется один из двух обходных путей:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
var foo = {
name: ‘Foo’,
languages: [‘Spanish’, ‘French’, ‘Italian’],
speak_self : function speak_s () {
var self = this;
self.languages.forEach(function(language) {
console.log(self.name + » speaks » + language + «.»);
})
},
speak_bound : function speak_b () {
this.languages.forEach(function(language) {
console.log(this.name + » speaks » + language + «.»);
}.bind(foo));
}
};
|
В speak_self
мы сохраняем значение this
в переменной self
и используем эту переменную, чтобы получить speak_self
ссылку. В speak_bound
мы используем bind
чтобы навсегда указать this
на данный объект.
ES2015 приносит нам новую альтернативу: функции стрелок.
В отличие от «обычных» функций, функции стрелок не скрывают this
значение своих родительских областей, устанавливая их собственные. Скорее, они решают его значение лексически.
Другими словами, если вы используете this
в функции стрелки, JavaScript ищет ее значение, как и любую другую переменную.
Во-первых, он проверяет локальную область видимости для this
значения. Так как функции стрелок не устанавливают один, он не найдет один. Затем он проверяет родительскую область для значения this
. Если он найдет его, он будет использовать его.
Это позволяет нам переписать приведенный выше код следующим образом:
1
2
3
4
5
6
7
8
9
|
var foo = {
name: ‘Foo’,
languages: [‘Spanish’, ‘French’, ‘Italian’],
speak : function speak () {
this.languages.forEach((language) => {
console.log(this.name + » speaks » + language + «.»);
})
}
};
|
Если вы хотите получить более подробную информацию о функциях стрелок, взгляните на превосходный курс Envato Tuts + Instructor Дэн Веллмана по основам JavaScript ES6 , а также документацию MDN по функциям стрелок .
Вывод
Мы уже проделали большую работу! В этой статье вы узнали, что:
- Переменные регистрируются в своих областях во время компиляции и связываются с их значениями присваивания во время выполнения .
- Ссылаясь на переменные, объявленные с
let
илиconst
перед присваиванием выдаетReferenceError
, и что такие переменные ограничиваются ближайшим блоком. - Функции стрелок позволяют нам достигнуть лексического связывания
this
и обойти традиционное динамическое связывание.
Вы также видели два правила подъема:
- Первое правило подъема : выражения функций и объявления
var
доступны во всех областях, где они определены, но имеютundefined
значение до тех пор, пока не будут выполнены ваши операторы присваивания. - Второе правило подъема : чтобы имена объявлений функций и их тела были доступны во всех областях, где они определены.
Хороший следующий шаг — использовать ваши новомодные знания областей JavaScript, чтобы обернуть голову вокруг замыканий. Для этого, проверьте Сферы и закрытия Кайла Симпсона.
Наконец, об this
можно сказать гораздо больше, чем я смог осветить здесь. Если ключевое слово все еще выглядит как чёрная магия, взгляните на этот & Прототипы объектов, чтобы разобраться с этим.
А пока возьмите то, что вы узнали, и пишите меньше ошибок!
Изучите JavaScript: полное руководство
Мы создали полное руководство, которое поможет вам изучить JavaScript , независимо от того, начинаете ли вы как веб-разработчик или хотите изучать более сложные темы.