Статьи

Сфера Гроккинга в JavaScript

Область действия, или набор правил, определяющих, где находятся ваши переменные, является одним из самых основных понятий любого языка программирования. На самом деле это настолько фундаментально, что легко забыть, какими тонкими могут быть правила!

Точное понимание того, как движок JavaScript «думает» о области видимости, не даст вам написать типичные ошибки, которые может вызвать подъем, подготовит вас к тому, чтобы обернуть голову вокруг замыканий, и сделает вас намного ближе к тому, чтобы никогда больше не писать ошибок.

… Ну, в любом случае, это поможет вам понять подъемы и затворы.

В этой статье мы рассмотрим:

  • основы областей применения в JavaScript
  • как интерпретатор решает, какие переменные принадлежат к какой области видимости
  • как на самом деле работает подъем
  • как ключевые слова ES6 let и let изменить игру

Давайте погрузимся в.

Если вы хотите узнать больше о ES6 и о том, как использовать синтаксис и функции для улучшения и упрощения кода JavaScript, почему бы не попробовать эти два курса:

  • ES6
    Основы JavaScript ES6
    Дэн Веллман
  • JavaScript
    Методы рефакторинга JavaScript
    Паван Подила

Если вы написали хотя бы строку JavaScript раньше, вы будете знать, что где вы определяете свои переменные, определяет, где вы можете их использовать . Тот факт, что видимость переменной зависит от структуры вашего исходного кода, называется лексической областью действия.

Существует три способа создания области видимости в JavaScript:

  1. Создать функцию , Переменные, объявленные внутри функций, видны только внутри этой функции, в том числе во вложенных функциях.
  2. Объявите переменные с помощью let или const внутри блока кода . Такие объявления видны только внутри блока.
  3. Создать блок 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, две вещи заставляют его работать.

  1. Во-первых, ваш источник компилируется.
  2. Затем скомпилированный код выполняется.

В течение шаг компиляции , движок JavaScript:

  1. принимает к сведению все ваши имена переменных
  2. регистрирует их в соответствующем объеме
  3. резервирует место для своих ценностей

Только во время выполнения движок JavaScript фактически устанавливает значение ссылок на переменные, равное их значениям присваивания. До тех пор они не undefined .

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 .

Поскольку внутри функции больше нет объявлений переменных, компилятор возвращается в глобальную область видимости. И поскольку там больше нет объявлений переменных, этот этап завершен.

Обратите внимание, что мы еще ничего не запустили . Задача компилятора на этом этапе — просто убедиться, что он знает имя каждого; это не волнует, что они делают.

На данный момент наша программа знает, что:

  1. В глобальной области видимости есть переменная first_name .
  2. В глобальной области видимости есть функция popup .
  3. В области popup last_name есть переменная с именем last_name .
  4. Значения first_name и last_name не undefined .

И не важно, что мы присваиваем эти переменные значения в другом месте нашего кода. Двигатель позаботится об этом во время исполнения .

На следующем шаге движок снова читает наш код, но на этот раз выполняет его.

Сначала он читает строку, var first_name = "Peleke" . Для этого движок ищет переменную с именем first_name . Поскольку компилятор уже зарегистрировал переменную с таким именем, движок находит ее и устанавливает ее значение "Peleke" .

Далее он читает строку function popup (first_name) . Поскольку мы здесь не выполняем функцию, движок не заинтересован и пропускает ее.

Наконец, он читает popup(first_name) строку popup(first_name) . Так как мы выполняем функцию здесь, движок:

  1. ищет значение popup
  2. ищет значение first_name
  3. выполняет popup как функцию, передавая значение first_name в качестве параметра

Когда он выполняет popup , он проходит тот же процесс, но на этот раз внутри popup функции. Это:

  1. ищет переменную с именем last_name
  2. устанавливает значение last_name равным "Sengstacke"
  3. ищет 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!»);
}

Если вы запустите этот код, вы заметите три вещи:

  1. Вы можете обратиться к foo прежде чем назначить ему, но его значение не undefined .
  2. Вы можете вызвать broken до того, как определите его, но вы получите TypeError .
  3. Вы можете вызвать bar прежде, чем определите его, и он будет работать как нужно.

Под подъемом подразумевается тот факт, что JavaScript делает все объявленные нами имена переменных доступными повсюду в их областях — в том числе и до того, как мы присваиваем им.

В этом фрагменте есть три случая, о которых вам нужно знать в своем собственном коде, поэтому мы рассмотрим каждый из них по одному.

Помните, когда компилятор JavaScript читает строку вроде var foo = "bar" , это:

  1. регистрирует имя foo в ближайшей области видимости
  2. устанавливает значение 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, не так ли?

Для просмотра:

  1. Имена как объявлений переменных, так и выражений функций доступны во всей их области видимости, но их значения не undefined до назначения.
  2. Имена и определения объявлений функций доступны во всей их области, даже до их определений.

Теперь давайте взглянем на два новых инструмента, которые работают немного по-другому: let и 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 , с двумя ключевыми отличиями:

  1. Вы должны присвоить значение при объявлении с помощью const .
  2. Вы не можете переназначить значения переменной, объявленной с помощью 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 не имеет ничего общего с областью видимости. И, на самом деле, 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 , независимо от того, начинаете ли вы как веб-разработчик или хотите изучать более сложные темы.