Область видимости и контекст в JavaScript

Александр Мальцев
Александр Мальцев
41K
10
Область видимости и контекст в JavaScript
Содержание:
  1. Области видимости
  2. Цепочка областей видимости
  3. Контекст и ключевое слово this
  4. Указание контекста с помощью call или apply
  5. Привязка контекста к функции
  6. Ключевое слово var
  7. Необъявленные переменные и строгий режим
  8. Комментарии

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

Области видимости

Область видимости – это некоторая сущность JavaScript, которая определяет границы действия переменных.

Создаются области видимости во время выполнения программы. Самая первая область, которая создаётся и которая включает в себя все остальные называется глобальной.

Именно в этой области определены такие переменные как window в веб-браузере и global в Node.js.

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

let num = 5;
const fruits = ['apple', 'pears', 'banana'];

Переменные объявленные в глобальной области видимости называются глобальными переменными. Такие переменные могут быть доступны в любой точке программы.

Кроме глобальной области видимости в JavaScript имеются ещё локальные. Они, создаются, когда интерпретатор, например, выполняет код блочной конструкции:

// глобальная переменная
let a = 5;
{
  // локальная переменная
  let b = 17;
}
Локальная и глобальные переменные в JavaScript

Причем такая локальная область видимости называется областью видимости блока.

Переменные, объявленные внутри блока с помощью let и const имеют область видимости ограниченную этим блоком. Т.е. они привязаны к нему и будет действовать только в его рамках. Переменные, объявленные в локальной области видимости называются локальными.

if (true) {
  // локальная переменная
  let b = 17;
  // выведем значение переменной b в консоль
  console.log(b); // 17
}
console.log(b); // Uncaught ReferenceError: b is not defined

Под блоком в JavaScript понимается любой код, который расположен в фигурных скобках { ... }. Блоки используются в конструкциях if, for, while и т.д. Даже тело функции является блоком, т.к. находится между фигурными скобками.

Кроме этого локальные области видимости также создаются вызовами функций и модулями. Они соответственно называются областью видимости функции и областью видимости модуля.

Пример, в котором создаётся две функциональные области видимости:

// глобальная область видимости
function salute(welcomeText) {
  console.log(welcomeText);
}

salute('Привет'); // вызов функции salute
salute('Здравствуйте'); // вызов функции salute

В этом примере в глобальной области видимости объявляется функция salute с помощью ключевого слова function. Затем эта функция вызывается два раза.

Область видимости функции создаётся для каждого вызова функции. Даже, когда мы вызываем одну и ту же функцию. При этом для каждого вызова создаётся своя отдельная область видимости.

В этом примере будут созданы две локальные области видимости уровня функции.

Создание областей видимости функций в JavaScript

Цепочка областей видимости

При создании локальной области видимости она всегда сохраняет ссылку на внешнюю область видимости. Эта ссылка используется для поиска переменных.

// глобальная область видимости
let a = 5;
let b = 8;
let c = 20;
function fnA() {
  a = 7;
  b = 10;
  let b = 11;
  b = 13;
  function fnB() {
    let c = 25;
    console.log(a); // 7
    console.log(b); // 13
    console.log(c); // 25
  }
  fnB();
}

fnA();

В момент выполнения console.log(a) мы имеем следующую картину:

Цепочка областей видимости в JavaScript

Начинается этот код с создания переменных a, b, c и fnA с соответствующими значениями в глобальной области видимости.

После этого вызывается функция fnA(). При её вызове создаётся область видимости функции, которая имеет ссылку на внешнюю область. В данном случае ей является глобальная область видимости.

Далее переменной a присваивается значение 7.

a = 7;

Но перед тем, как присвоить ей значение, нам необходимо сначала её найти. Поиск переменной всегда начинается с текущей области видимости. Но переменной и параметра a в области видимости вызова функции fnA() нет. Поэтому мы переходим по ссылке, в данном случае ведущую в глобальную область видимости и ищем переменную там. В данном примере такая переменная здесь имеется, и мы присваиваем ей значение 7. Таким образом, на этом шаге мы внутри функции fnA присвоили новое значение глобальной переменной a.

На следующей строчке мы присваиваем переменной b значение 10:

b = 10;

На текущий момент у нас ещё нет объявленной переменной bb в текущей области видимости. Поэтому мы также переходим по ссылке в глобальную область видимости и находим эту переменную там. После этого задаём ей новое значение. На этом этапе выполнения кода у нас в глобальной области видимости переменные a и b имеют соответственно значения 7 и 10.

Затем мы объявляем переменную b в локальной области функции, созданной вызовом fnA() и в этом же выражении сразу же ей присваиваем число 11:

let b = 11;

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

Теперь, если мы попытаемся в этой области видимости получить доступ к переменной b, то получим переменную, объявленную в этой локальной области видимости, но никак не переменную b из глобальной области видимости. Т.е. после объявления b в этой области видимости нам уже будет не доступна переменная b, объявленная в глобальной области видимости.

Таким образом, на следующей строчке будет использоваться переменная b, объявленная в текущей области видимости:

b = 13;

После этого объявляется функция fnB. Затем она вызывается fnB() и интерпретатор создаёт новую область видимости внутри fnA. Эта область видимости в свою очередь тоже содержит ссылку на внешнюю по отношению к ней область видимости. В данном случае, на ту, которая была создана ранее при вызове fnA(). В итоге у нас получается цепочка областей видимости.

Таким образом, цепочкой областей видимости (scope chain) можно назвать последовательность областей видимости, которые интерпретатор JavaScript использует для поиска переменных. При этом поиск всегда начинается с текущей области видимости и если только она не найдена в текущей, то происходит переход к следующей по цепочке и поиск переменной там и т.д.

На строчке console.log(a) для вывода значения указанной переменной в консоль, её сначала нужно получить. Поиск переменной, как мы уже отмечали выше, всегда начинается с текущей области видимости. Но так как этой переменной здесь нет, то выполняется переход по ссылке к следующей области, которая является по отношению к текущей внешней.

В этой области (в данном случае созданной в результате вызова fnA()) переменной a тоже нет. Но есть ссылка на следующую область, которая в данном случае является глобальной. Переходим по ней и пытаемся найти переменную там. В ней эта переменная есть. А, следовательно, берём эту переменную и выводим её значение в консоль. В данном случае, число 7.

Итак, поиск переменной интерпретатор JavaScript всегда начинает с текущей области видимости. Если она в ней имеется, то поиск прекращается и берётся эта переменная. В противном случае интерпретатор в поиске переменной переместится к следующей области, содержащейся в ссылке, и попробует отыскать её там. После этого действия повторяются, т.е. при отсутствии искомой переменной в просматриваемой области видимости, интерпретатор перемещается к следующей области посредством ссылки и пытается обнаружить её там.

В результате поиск всегда заканчивается одним из двух нижеприведённых сценариев:

  1. интерпретатор нашёл искомую переменную в какой-нибудь области; в этом случае он берёт эту переменную и останавливает её дальнейший поиск по цепочке в других областях видимости;
  2. интерпретатор в поиске переменной дошёл до глобальной области и не нашёл её там; в этом случае возникает ошибка, что переменной с указанным именем не существует.

Глобальная область видимости - это последнее звено в цепочке областей видимости Она не содержит ссылку на другую область, на ней всё заканчивается.

Рассмотрим ещё один очень интересный пример:

let num = 10;
function fnA() {
  console.log(num);
}
function fnB() {
  let num = 20;
  fnA();
}
fnB();

В этом примере в глобальной области видимости объявлены переменные num, fnA и fnB. При вызове функции fnB() у нас создаётся локальная область видимости, которая будет содержать ссылку на внешнюю, в данном случае на глобальную область видимости. В этой области у нас создаётся переменная с таким же именем num, а затем вызывается функция fnA(). При вызове функции fnA() у нас создаётся локальная область видимости, которая будет иметь в качестве ссылки глобальную область. Почему так? Потому что внешняя область определяется в зависимости от того, где объявлена функция, а не вызвана. А так как функция объявлена в глобальной области видимости, то не зависимого того где она вызвана, она будет содержать в качестве ссылки - ссылку на внешнюю область видимости, в зависимости от того где она объявлена.

Контекст и ключевое слово this

Кроме области видимости в JavaScript имеется ещё контекст (context). Контекст – это то, на что указывает this.

По сути this – это объект, которому «принадлежит» выполняемый в данный момент код.

1. В контексте глобального объекта (вне модулей и функций) this – это глобальный объект.

let num = 10;
console.log(this); // window

2. Внутри функции this зависит от того, как вызывается функция.

2.1. Если функция вызывается не как метод объекта, то this в не строгом режиме указывает на глобальный объект, а в строгом – undefined.

function fnA() {
  console.log(this); // window
}
function fnB() {
  'use strict';
  console.log(this); // undefined
}
fnA();
fnB();
2.2. Когда функция вызывается как метод, this – это объект, который использовался для его вызова:
Object.prototype.getThis = function () {
  return this;
}
const objectA = {}
const objectB = {}

console.log(objectA.getThis() === objectA); // true
console.log(objectB.getThis() === objectB); // true

3. Внутри класса this указывает на новый объект, который будет создан с помощью new:

class Person {
  getThis() {
    return this;
  }
}
const personA = new Person();
const personB = new Person();

console.log(personA.getThis() === personA); // true
console.log(personB.getThis() === personB); // true

4. В модуле на верхнем уровне this – это undefined.

<script type="module">
  console.log(this);
</script>

Стрелочные функции нет имеют собственного this. Если внутри стрелочной функции происходит обращение к this, она берёт его снаружи.

Указание контекста с помощью call или apply

В JavaScript при вызове функции можно установить нужный this, т.е. контекст, в котором она должна выполняться. Осуществляется это с помощью метода call или apply.

Синтаксис использования метода call:

// myFunc – некоторая функция
// thisArg – значение this
// arg1, arg2, ..., argN – аргументы для функции
myFunc.call(thisArg, arg1, arg2, ..., argN);

Например:

// объект user
const user = {
  name: 'Василий',
  age: 27
}
// объявление функции getAge
const getAge = function() {
  return this.age;
}
// вызов функции в контексте объекта user
console.log(getAge.call(user)); // 27

Метод apply аналогичен call. Единственное отличие в том, что аргументы в apply передаются в виде массива.

Синтаксис метода apply:

// myFunc – некоторая функция
// thisArg – значение this
// argsArray – аргументы в виде массива
myFunc.call(thisArg, argsArray);

В качестве thisArg методам call и apply, кроме объекта, можно также установить значение null или undefined. В не строгом режиме эти значения будут заменены ссылкой на глобальный объект.

Привязка контекста к функции

У функций имеется метод bind. С помощью него можно установить определённый this, в рамках которого она должна выполняться. В качестве результата bind() возвращает новую связанную с этим this функцию. Выполнение связанной функции приводит к вызову исходной функции, но в указанном this.

Синтаксис метода bind:

// newFn – новая функция, которая будет выполнять fn в указанном this
// fn – исходная функция
// thisArg – значение this
// arg1, ... , argN – аргументы для функции
const newFn = fn.bind(thisArg, arg1, ... , argN)

1. Использование bind для привязки функции:

let person = {
  fullName: 'Alexander Maltsev',
  getFullName() {
    console.log(this.fullName);
  }
};

setTimeout(person.getFullName.bind(person), 1000); // Alexander Maltsev

Так как функция setTimeout является методом объекта window, то внутри неё this указывает на глобальный объект. Чтобы this внутри функции был person, его нужно привязать с помощью метода bind(). То есть, как это и сделано в примере.

2. Применение bind для использования методов другого объекта:

// объект runner имеет метод run
const runner = {
  name: 'Бегун',
  run(speed) {
    console.log(this.name + ' бежит со скоростью ' + speed + ' километров в час.');
  }
};
// объект flyer имеет метод fly
const flyer = {
  name: 'Флайер',
  fly(speed) {
    console.log(this.name + ' летит со скоростью ' + speed + ' километров в час.');
  }
};
const fly = flyer.fly.bind(runner, 50);
fly(); // Бегун летит со скоростью 50 километров в час.

В этом примере показано как можно использовать метод одного объекта для другого без создания в нём его копии.

3. Создание функции с заранее заданными начальными аргументами:

const sum = function (a, b) {
  console.log(a + b);
}
const add7 = sum.bind(null, 7);
add7(5); // 12
add7(9); // 16

4. Привязывания одной функции к разным объектам:

const users = {
  'm-1': { total: 1000 },
  'm-2': { total: 700 }
}
const getFee = function (fee) {
  this.total -= fee;
  console.log(this.total);
}
const getFeeM1 = getFee.bind(users['m-1']);
const getFeeM2 = getFee.bind(users['m-2']);
getFeeM2(100); // 600
getFeeM2(100); // 500

В этом примере мы с помощью bind привязали одну функцию getFee к разным объектам. Для объекта users['m-1'] её следует использовать как getFeeM1, а для users['m-2'] – как getFeeM2.

Ключевое слово var

Создавать переменные посредством var и функций посредством Function Declaration не рекомендуется. Но понимать, как работает код, в котором эти вещи используются необходимо.

1. Переменные, объявление с помощью ключевого слова var, имеют область видимости функции:

{
  var a = 5;
  let b = 7;
  function myFunc() {
    var c = 5;
  }
}
myFunc();
console.log(a); // 5
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined

Т.е. var в отличие от let и const создаёт переменные, которые ограничены областью видимости функции, а не блоком.

2. Ключевое слово var в отличие от let и const, создаёт глобальные переменные, которые будут являться свойствами глобального объекта window в веб-браузере и global в Node.js:

var a = 5;
let b = 7;
console.log(window.a); // 5
console.log(window.b); // undefined

Переменные, созданные в глобальной области видимости с помощью ключевых слов let, const и class, не являются свойствами глобального объекта.

3. Переменные, объявленные с использованием var поднимаются (hoisting) в начало текущего контекста. При этом поднимается только само объявление переменной:

console.log(`Ширина: ${width}`); // Ширина: undefined
var width = 500;

Этот код интерпретатор воспринимает так:

var width;
console.log(width); // Ширина: undefined
width = 500;

Но в JavaScript поднимаются не только переменные, созданные с помощью var, но и функции, объявленные как function declaration:

// вызываем функцию greet
greet();
// объявляем функцию greet
function greet() {
  console.log('Привет!');
}

В этом примере мы вызываем функцию greet() до её объявления.

Необъявленные переменные и строгий режим

При попытке присвоить значение переменной, которая раньше нигде не была объявлена, ошибки не будет:

function sayHello() {
  myName = 'Вася';
  console.log(`Привет, ${myName}!`);
}
sayHello(); // Привет, Вася!
console.log(myName); // Вася

Переменная myName в этом примере будет объявлена автоматически, причём это будет сделано в глобальной области видимости. Тем самым после завершения выполнения функции sayHello(), эта переменная будет нам доступна и мы можем вывести её значение в консоль.

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

Но писать код без объявления переменных не рекомендуется, и чтобы избежать их автоматического создания можно использовать строгий режим (на английском strict mode).

Строгий режим – это просто инструкция интерпретатору JavaScript, которая предотвращает выполнение определенных действий и создает больше исключений.

Для того чтобы включить строгий режим достаточно просто добавить следующую строку:

'use strict';

Строгий режим можно включить ко всему сценарию или к отдельным функциям и модулям.

Чтобы применить строгий режим ко всему сценарию, просто добавьте в самом вверху кода перед первой строчкой 'use strict'.

После включения строго режима при выполнении примера, приведённого выше, вы получите ошибку:

function sayHello() {
  myName = 'Вася'; // Переменная не
  console.log(`Привет, ${myName}!`);
}
sayHello();
console.log(myName);

Таким образом, строгий режим запрещает использование не объявленных переменных.

Комментарии: 10

  1. Maksim G
    Maksim G
    2021-09-27 12:50:59
    Добрый день! подскажите в этой строчке случайно не пропущены скобки после имени функции displayDrink при объявлении ее??

    var drink = 'молоко';
    function outputDrink() {
    var drink = 'яблочный сок';
    function displayDrink {
    console.log(drink);
    }
    displayDrink();
    }
    outputDrink();

    itchief.ru/assets/uploadify/3/e/b/3ebb568b32113bd8bbd8c04d2ea4697f.png
  1. Александр Мальцев
    Александр Мальцев
    2021-09-27 14:58:18
    Привет! Спасибо, поправил.
  • Maksim G
    Maksim G
    2021-09-28 11:07:21
    var user0001 = {
    name = 'Афанасий',
    score = 1300
    }

    var user0001 = {
    name = 'Анастасия',
    score = 2500
    }

    Брат, привет, в разделе про bind во втором примере ключи объектов записаны через = а не через:
    и я правильно понимаю что второй объект должен называться user0002?
    я просто чтобы понять хорошо твою статья переписываю слово в слово) и код весь проверяю)
  • Александр Мальцев
    Александр Мальцев
    2021-09-28 15:01:21
    Да, правильно. Спасибо, поправил!
  • Станислав
    Станислав
    2021-05-19 18:21:15
    Результат же 20, так f2 изменит перменную, которая находится в глобальной области видимости. А вот если бы в функции f2 создавалась переменная с таким же идентификатором, то результат был бы равен 10. Разве не так?
    // global scope
    var num = 10;
    function f1() {
      console.log(num);
    }
    function f2() {
      num = 20;
      f1(); // 10, т.к. [[Scope]] = global scope
    }
    f2(); // [[Scope]] = global scope
    
    1. Данила Фадеев
      Данила Фадеев
      2021-05-26 15:31:05
      да.
      10 будет, если, например, var воткнуть перед num = 20:
      var num = 10;
      function f1() {
        console.log(num);
      }
      function f2() {
        var num = 20;
        f1(); // 10, т.к. [[Scope]] = global scope
      }
      f2(); // [[Scope]] = global scope
      
      думаю, что-то такое и хотели написать
    2. Александр Мальцев
      Александр Мальцев
      2021-05-27 14:27:13
      Правильно! var пропустил. Поправил пример в статье.
    3. Александр Мальцев
      Александр Мальцев
      2021-05-27 14:34:49
      Ага, в примере var пропустил. Иначе просто изменяется переменная в глобальной видимости, и то, что хотел передать этим примером теряет смысл.
  • SinGlEBW
    SinGlEBW
    2021-01-21 16:36:42
    Я знал что есть глобальная и локальная область, но не знал что каждый вызов создаёт локальную область. Вот попался только)). Делал popup и нужно было блокировать скрол. Засунул в функцию создание событий при открытии popup и если закрыть вызывалась та же функция но чуть ниже удаляя события. События конечно удалялись но не из той параллели.

    priceBtn.addEventListener('click', openPrice)
    priceList.addEventListener('click', closePrice);
    
    function openPrice(ev){
      priceList.style.display = "flex"; 
      blockingScroll(true, priceTable, window.scrollY)
    }
    
    function closePrice(ev){
      if(ev.target == priceList || ev.target == priceClose){
        priceList.style.display = "none"; 
        blockingScroll(false)
      }
    }
    
    function blockingScroll(bool, untouchable = null, saveScrollToTheOpen = null){
      function evWheel (ev) {
       
        if(!untouchable.contains(ev.target)){
          console.dir('Wheel');
          ev.preventDefault(); return; 
        }
        this.scrollTo(0, saveScrollToTheOpen);
      } 
      function evScroll(){
        console.dir('Scroll');
        this.scrollTo(0, saveScrollToTheOpen)
      } 
     
      if(bool){
        console.dir('создание');
        window.addEventListener('wheel', evWheel, { passive: false })
        window.addEventListener('scroll', evScroll)
        return;
      }
      console.dir('удаление');
      window.removeEventListener('wheel', evWheel, { passive: false })
      window.removeEventListener('scroll', evScroll)
    }
    
    
    1. Александр Мальцев
      Александр Мальцев
      2021-01-31 13:13:34
      Наши ошибки – это самые хорошие учителя. Без них никак.