Область видимости и контекст в JavaScript
![Область видимости и контекст в JavaScript](/assets/images/covers/javascript-scope-and-context.png)
В этой статье мы изучим, что такое области видимости, глобальные и локальные переменные, контекст и ключевое слово this.
Области видимости
Область видимости – это некоторая сущность JavaScript, которая определяет границы действия переменных.
Создаются области видимости во время выполнения программы. Самая первая область, которая создаётся и которая включает в себя все остальные называется глобальной.
Именно в этой области определены такие переменные как window
в веб-браузере и global
в Node.js.
Вы также можете определять переменные в этой области. Для этого достаточно просто объявить переменные вне блока, функции и модуля. В этом случае они будут находиться в глобальной области видимости:
let num = 5;
const fruits = ['apple', 'pears', 'banana'];
Переменные объявленные в глобальной области видимости называются глобальными переменными. Такие переменные могут быть доступны в любой точке программы.
Кроме глобальной области видимости в JavaScript имеются ещё локальные. Они, создаются, когда интерпретатор, например, выполняет код блочной конструкции:
// глобальная переменная
let a = 5;
{
// локальная переменная
let b = 17;
}
![Локальная и глобальные переменные в JavaScript](/assets/img/javascript/scope-and-context/global-and-local-scope.png)
Причем такая локальная область видимости называется областью видимости блока.
Переменные, объявленные внутри блока с помощью 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](/assets/img/javascript/scope-and-context/function-scope.png)
Цепочка областей видимости
При создании локальной области видимости она всегда сохраняет ссылку на внешнюю область видимости. Эта ссылка используется для поиска переменных.
// глобальная область видимости
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](/assets/img/javascript/scope-and-context/scope-chain.png)
Начинается этот код с создания переменных a
, b
, c
и fnA
с соответствующими значениями в глобальной области видимости.
После этого вызывается функция fnA()
. При её вызове создаётся область видимости функции, которая имеет ссылку на внешнюю область. В данном случае ей является глобальная область видимости.
Далее переменной a
присваивается значение 7
.
a = 7;
Но перед тем, как присвоить ей значение, нам необходимо сначала её найти. Поиск переменной всегда начинается с текущей области видимости. Но переменной и параметра a
в области видимости вызова функции fnA()
нет. Поэтому мы переходим по ссылке, в данном случае ведущую в глобальную область видимости и ищем переменную там. В данном примере такая переменная здесь имеется, и мы присваиваем ей значение 7
. Таким образом, на этом шаге мы внутри функции fnA
присвоили новое значение глобальной переменной a
.
На следующей строчке мы присваиваем переменной b
значение 10
:
b = 10;
На текущий момент у нас ещё нет объявленной переменной b
b в текущей области видимости. Поэтому мы также переходим по ссылке в глобальную область видимости и находим эту переменную там. После этого задаём ей новое значение. На этом этапе выполнения кода у нас в глобальной области видимости переменные 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 всегда начинает с текущей области видимости. Если она в ней имеется, то поиск прекращается и берётся эта переменная. В противном случае интерпретатор в поиске переменной переместится к следующей области, содержащейся в ссылке, и попробует отыскать её там. После этого действия повторяются, т.е. при отсутствии искомой переменной в просматриваемой области видимости, интерпретатор перемещается к следующей области посредством ссылки и пытается обнаружить её там.
В результате поиск всегда заканчивается одним из двух нижеприведённых сценариев:
- интерпретатор нашёл искомую переменную в какой-нибудь области; в этом случае он берёт эту переменную и останавливает её дальнейший поиск по цепочке в других областях видимости;
- интерпретатор в поиске переменной дошёл до глобальной области и не нашёл её там; в этом случае возникает ошибка, что переменной с указанным именем не существует.
Глобальная область видимости - это последнее звено в цепочке областей видимости Она не содержит ссылку на другую область, на ней всё заканчивается.
Рассмотрим ещё один очень интересный пример:
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);
Таким образом, строгий режим запрещает использование не объявленных переменных.
Комментарии: 14
Здравствуйте! Помогите разобраться в моём примере, вроде всё делаю правильно, объявил глобальную переменную в самом начале цикла let objekt, но в конце кода не могу вывести результат в консоль, выводит undefined. В чем загвоздка может быть?
Добрый день!
У вас же асинхронный код. В этом случае функция, указанная вdone
выполнится только после получения ответа от сервера. Здесьconsole.log()
следует указывать внутри функции:Понял, а за пределами функции нельзя результат переменной получить?
Не совсем так. Просто инструкция
console.log(objekt);
, которую вы написали после циклаforEach
выполнится сразу, как только он будет завершён. А функция, указанная вdone
, выполнится только после получения успешного ответа от сервера. А это будет уже после выполнения инструкцииconsole.log(objekt);
, которую вы написали после циклаforEach
. Поэтому вы не можете получить таким образом этот результат.var drink = 'молоко';
function outputDrink() {
var drink = 'яблочный сок';
function displayDrink {
console.log(drink);
}
displayDrink();
}
outputDrink();
itchief.ru/assets/uploadify/3/e/b/3ebb568b32113bd8bbd8c04d2ea4697f.png
name = 'Афанасий',
score = 1300
}
var user0001 = {
name = 'Анастасия',
score = 2500
}
Брат, привет, в разделе про bind во втором примере ключи объектов записаны через = а не через:
и я правильно понимаю что второй объект должен называться user0002?
я просто чтобы понять хорошо твою статья переписываю слово в слово) и код весь проверяю)
10 будет, если, например, var воткнуть перед num = 20:
думаю, что-то такое и хотели написать