Статья в разработке!

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

Область видимости (scope). Цепочка областей видимости

Область видимости определяет доступность (видимость) переменных и функций вида function declaration statement.

Области видимости создаются во время выполнения JavaScript программы (сценария).

В языке JavaScript (до ECMAScript 2015) выделяют 2 области видимости:

  • глобальная (переменная или функция, созданная в этой области видимости, может быть доступна из любой точки программы);
  • локальная или функциональная (переменная или функция, созданная в этой области видимости, может быть доступна только внутри неё).

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

// global scope (глобальная область видимости)
function salute(welcomeText) {
  console.log(welcomeText);
}
salute('Привет'); // вызов функции salute
salute('Здравствуйте'); // вызов функции salute
JavaScript - Область видимости (пример 1)

[[Scope]] - это скрытое внутреннее свойство функции, которое она получает во время вызова. Данное свойство содержит ссылку на ту область видимости, в которой данная функция была объявлена.

Пример:

// global scope (глобальная область видимости)
var num = 15;
function outputNum() {
  console.log(num);
}
outputNum(); // 15
JavaScript - Область видимости (пример 2)

В приведённом примере во время вызова функции outputNum будет создана локальная область, а также установлено в качестве значения [[Scope]] область в которой данная функция была объявлена.

Т.к. переменной num нет в текущей области, то интерпретатор посредством [[Scope]] перейдёт в область (в данном случае глобальную) и попытается найти её там. В этой области (глобальной) она есть. А это значит, что в качестве num будет использоваться значение переменной num, находящейся в глобальной области видимости.

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

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

Первый сценарий: интерпретатор встретил искомое имя идентификатора в какой-нибудь области. В этом случае он берёт его значение. Поиск искомого имени идентификатора в других областях видимости, т.е. дальнейшие перемещения по [[Scope]], не выполняются.

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

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

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

Последовательность областей видимости, которые интерпретатор использует при разрешении имени идентификатора, называется в JavaScript цепочкой областей видимости (scope chain).

В этом примере интерпретатор при разрешении переменной color дойдёт от текущей до глобальной области видимости:

// global scope (глобальная область видимости)
var color = 'green';
function outputColor() {
  function displayColor() {
    console.log(color);
  }
  displayColor(); // "green"
}
outputColor();
JavaScript - Область видимости (пример 3)

В этом примере значение переменной drink будет взято из локальной области, созданной во время вызова внешней функции:

// global scope (глобальная область видимости)
var drink = 'молоко';
function outputDrink() {
  var drink = 'яблочный сок';
  function displayDrink {
    console.log(drink);
  }
  displayDrink(); // "яблочный сок"
}
outputDrink();

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

var v1 = 15;
function f1() {
  var v2 = 10;
  function f2() {
    console.log('f2');
  }
}
console.log(v1); // 15
f1(); // вызов функции f1
f2(); // нельзя вызвать функцию f2, так как она не видна в глобальной области видимости
console.log(v2); // нельзя обратиться к v2, так как она не видна в глобальной области видимости

При разрешении имени, интерпретатор начинает поиск, начиная с текущей области видимости. Поэтому этот пример выведет в консоль текст "Дима".

// global scope
var name = 'Петя';
function getName() {
  var name = 'Иван';
  function displayName() {
    var name = 'Дима';
    console.log(name);  // "Дима"
  }
  displayName(); // вызов функции displayName
}
getName(); // вызов функции getName

Если в функции displayName не была бы объявлена переменная name, то интерпретатор не обнаружил бы её в области, созданной во время вызова этой функции. В этом случае он взял бы значение из области видимости, созданной во время вызова функции getName. В результате в консоль был бы выведен текст "Иван".

В этом примере функция f1 объявлена в глобальной области видимости. Поэтому свойство [[Scope]] функции f1, во время её вызова, будет содержать ссылку на глобальную область видимости даже несмотря на то, что данная функция вызвана в локальной области видимости, созданной во время вызова функции f2.

// global scope
var num = 10;
function f1() {
  console.log(num);
}
function f2() {
  num = 20;
  f1(); // 10, т.к. [[Scope]] = global scope
}
f2(); // [[Scope]] = global scope

Поднятие (hoisting) функций и переменных

Функции вида function declaration statement можно использовать в JavaScript до их объявления. Это происходит из-за того что они поднимаются (hoisting) или другими словами «перемещаются» в начало текущего контекста.

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

Но поднятие в JavaScript выполняется не только функций вида function declaration statement, но и переменных, объявленных с помощью ключевого слова var. При этом поднятие осуществляется только самого объявления переменной.

console.log(width); // undefined
var width = 500;

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

var width;
console.log(width); // undefined
width = 500;

Локальные и глобальные переменные

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

Иными словами, глобальные переменные — это переменные, объявленные вне тела какой-либо функции, а локальные — это переменные, объявленные внутри тела какой-либо функции.

// глобальная область видимости
var age = 32; // глобальная переменная
function sayHello() { // объявление функции sayHello
  var myName = "Вася"; // локальная переменная (не будет доступна вне этой функции)
  console.log("Привет, " + myName);
}
function sayAge() { // обявление функции sayAge 
  console.log("Мне, " + age + " года"); // будет использоваться значение глобальной переменной, т.к. локальной переменной с именем age нет 
}
// вызов функции sayHello
sayHello(); // Привет, Вася
// вызов функции sayAge
sayAge(); // Мне 32 года
console.log("Возраст: " + age); //Возраст: 32
console.log("Моё имя: " + myName); // Ошибка, переменная myName не определена
Результат выполнение примера

В JavaScript до ECMAScript 2015 (6 версия) блочных областей видимости не было. Т.е. любая переменная созданная с помощью ключевого слова var внутри блока будет видима и за его пределами.

Пример:

if (true) {
  var subject = "Математика"; // переменная будет видна за пределами данного блока
}
console.log(subject); // "Математика"

JavaScript - Переменная subject видима за пределами блока

В ECMAScript 2015 (6) были введены ключевые слова let и const. Они предназначены для создания переменных и констант, видимость которых будет ограничено блоком в котором они объявлены. Блочная область видимости в JavaScript определяется посредством фигурных скобок.

Например, переменная subject не будет видна за пределами блока:

{
  let subject = "Математика"; // переменная не будет видна за пределами данного блока
}
console.log(subject); // Uncaught ReferenceError: subject is not defined

Кроме этого переменные, объявленные с помощью let и константы, созданные посредством const в JavaScript не поднимаются (hoisting). Т.е. к ним нельзя обратиться до их непосредственного объявления.

{
  console.log(osName); // Uncaught ReferenceError: subject is not defined
  let osName = "Linux"; 
}
console.log(subject);

Контекст функции. Ключевое слово this

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

Если функция не является методом объекта, то this будет указывать на window (глобальный объект).

function myFunc() {
  return this;
}
console.log(this); // window

В строгом режиме this в вышеприведённом примере будет равно undefined.

'use strict';
function myFunc() {
  return this;
}
console.log(this); // undefined

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

Пример 1:

var myObj = {
  propA: 27,
  methA: function() {
    return this.propA; // this = myObj
  }
}
myObj.methA(); // 27

Пример 2:

// объект myFish
var myFish = {
  fish : 'Треска',
  getFish: function() { // функция (метод объекта myFish)
    return this.fish;
  },
  getOwnObject: function() { // функция (метод объекта myFish)
    return this;
  }
}
// вывести в консоль результат выполнения метода getFish
console.log(myFish.getFish()); // "Треска"
// вывести в консоль результат выполнения метода getOwnObject
console.log(myFish.getOwnObject()); // объект myFish

Указание контекста функции. Методы call и apply

В JavaScript можно явно указать контекст, в котором необходимо вызвать функцию.

Первый способ - это использовать метод call.

Синтаксис:

// func - функция
// context -  контекст, в котором нужно вызвать функцию
// arg1, arg2, ... - список аргументов, которые нужно передать функции
funс.call(context[, arg1[, arg2[, ...]]]);

Пример:

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

Второй способ - это использовать метод apply. Данный метод аналогичен call. Единственное отличие apply от call заключается в том, что аргументы в нём указываются посредством массива. Этот вариант передачи аргументов является очень удобным, особенно тогда когда их количество заранее неизвестно.

Синтаксис:

// func - функция
// context -  контекст, в котором нужно вызвать функцию
// argArray - массив аргументов, которые нужно передать функции
funс.apply(context[, argArray]);

В качестве контекста методам call и apply, кроме объекта, можно ещё установить специальные значения null и undefined.

Не в строгом режиме в этом случае this будет указывать на объект window.

В строгом режиме this будет равно null.

Привязка функции к контексту (метод bind)

Функция (метод) в JavaScript не сохраняет контекст (объект) к которому она относится.

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

Например:

'use strict';
var mouse = {
  model: 'M100',
  getModel: function() {
    return this.model;
  }
}
// сохраним в переменную ссылку на метод
var getModelMouse = mouse.getModel;
// вызовем метод getModelMouse и выведем его результат в консоль
console.log(getModelMouse()); // Uncaught TypeError: Cannot read property 'model' of undefined

В результате вызова getModelMouse будет возвращена ошибка, т.к. контекст, к которому относится данный метод, будет потерян.

Метод bind предназначен для явной привязки контекста (this) к функции. Он в отличие от методов call и apply не вызывает функцию. Механизм этого метода заключается в создании функции-обёртки, которая будет устанавливать необходимый контекст целевой функции.

Кроме этого, эта функция-обёртка будет результатом, который метод bind будет возвращать в результате своего выполнения.

Метод bind также позволяет задать аргументы, которые необходимо передать функции в момент её вызова. Данные аргументы будут переданы целевой функции до аргументов, которые ей будут установлены явно при её вызове.

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

// func - функция
// context -  контекст, с которым нужно связать функцию
// arg1, arg2, ... - список аргументов, которые нужно передать функции
func.bind(context[, arg1[, arg2[, ...]]])

Например, изменим вышеприведённый пример. А именно, не просто сохраним ссылку на метод, а выполним это с привязкой его к объекту mouse:

// сохраним в переменную ссылку на метод с привязкой его к контексту mouse
var getModelMouse = mouse.getModel.bind(mouse);
// вызовем метод getModelMouse и выведем его результат в консоль
console.log(getModelMouse()); // "M100"

В этом примере привяжем функцию changeScore к разным объектам. Функция changeScore, привязанная к первому объекту будет доступна по идентификатору changeScoreUser0001, а ко второму - по changeScoreUser0002.

'use strict';

function changeScore(amount) {
  this.score = this.score + amount;
  console.log(this.score);
}

var user0001 = {
  name = 'Афанасий',
  score = 1300
}

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

var changeScoreUser0001 = changeScore.bind(user0001);
var changeScoreUser0002 = changeScore.bind(user0002);

changeScoreUser0001(400); // 1700
changeScoreUser0001(-800); // 900

changeScoreUser0002(-700); // 1800
changeScoreUser0001(300); // 2100

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

'use strict';

function checkInRange(min, max, value) {
  if (typeof value !== 'number') {
    return false;
  }
  return value >= min && value <= max;
}

var checkInRange10_20 = checkInRange.bind(null, 10, 20);

console.log(checkInRange10_20(15)); // true
console.log(checkInRange10_20(25)); // false








var myName = "Женя"; // глобальная переменная, т.к. объявлена вне тела какой-либо функции
function sayHello() { // объявление функции
  var myName1 = "Вася";  // локальная переменная
  console.log("Привет, " + myName);
  console.log("Привет, " + myName1);
  function sayHi() { // функции, объявленная внутри тела функции sayHello
    var myName2 = "Дима"; // локальная переменная 
    console.log("Привет, " + myName);
    console.log("Привет, " + myName1);
    console.log("Привет, " + myName2);
  }
  sayHi();
}
sayHello();
JavaScript - Цепочка областей видимости (пример 1)
JavaScript - Цепочка областей видимости (пример 1)

В локальной области видимости, созданной во время выполнения функции sayHi можно получить значение переменной myName, myName1 и myName2. Переменная myName находится в текущей области выполнения,

  • myName2 - расположена в текущем контексте;
  • myName1 - расположена в родительской функции sayHello;
  • myName - расположена в глобальном контексте.

А вот в функции sayHello() доступны только 2 переменные:

  • myName1 - расположена в текущем контексте;
  • myName - расположена в глобальном контекст.

Рассмотрим ещё один пример:

// глобальный контекст
var myName = "Женя"; //глобальная переменная
function sayHello() { // контекст функции
  console.log("Привет, " + myName); // переменная name берётся из глобального контекста 
}
function sayHi() { // контекст функции
   var myName = "Дима"; // локальная переменная функции sayHi
    console.log("Привет, " + myName); // переменная name берётся из текущего контекста
}
// вызов функции sayHi
sayHi();
// вызов функции sayHello
sayHello();
JavaScript - Цепочка областей видимости (пример 2)

Функция sayHi() выведет в консоль "Привет, Дима". Это обусловлено тем, что переменная name существует в текущем контексте.

Функция sayHello() отобразит в консоли запись "Привет, Женя". Переменная name найдена в родительском (глобальном) контексте.

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

myColor = "Зелёный"; //глобальная переменная
// объявление функции getColor
function getColor() {
  var myColor = "Синий"; //локальная переменная
  console.log("Цвет: " + myColor); //вывод значения локальной переменной
  console.log("Цвет: " + window.myColor); //вывод глобальной переменной
}
// вызов функции getColor
getColor();
JavaScript - Получение доступа к глобальной переменной через window
JavaScript - Получение доступа к глобальной переменной через window

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

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

Если Вы не используете строгий режим, то JavaScript позволяет создавать переменные без ключевого слова var. С такими переменными необходимо быть очень осторожными. Это связано с тем, что такая переменная вне зависимости от контекста, в котором она создана, автоматически становится глобальной. А это может привести к непредсказуемым результатам.

Рассмотрим следующий пример:

function sayHello() {
  var myName = "Вася"; // переменная не существует вне этой функции
}
sayHello();
console.log("Привет, " + myName); //строчка выдаст ошибку: myName - не определена.

JavaScript - Ключевое слово var (пример 1)

Если избавиться от ключевого слова var, то тогда переменная будет объявлена как глобальная переменная:

function sayHello() {
  myName = "Вася"; // переменная будет глобальной, несмотря на то, что она объявлена в функции
}
sayHello();
console.log("Привет, " + myName); //строчка вернёт: Привет, Вася.

JavaScript - Ключевое слово var (пример 2)

Некоторые особенности языка JavaScript при работе с переменными и функциями

У языка JavaScript есть одна интересная особенность, которая связана с тем как браузер понимает код, в котором использование переменной идёт до её объявления.

Рассмотрим эту ситуацию на следующем примере:

function sayHello() {
  console.log("Привет, " + myName);
  var myName = "Вася"; // переменная не существует вне этой функции
}
sayHello();

JavaScript - Некоторые особенности языка при работе с переменными

В этом примере вывод значения переменной myName в консоль идёт до её объявления и инициализации.

Как браузер понимает этот код? Браузер объявления переменных всегда поднимает вверх их функциональной области видимости.

Т.е. браузер будет видеть вышепредставленный код следующим образом:

function sayHello() {
  var name;
  console.log("Привет, " + name);
  name = "Вася";
}
sayHello(); 

В результате в консоли отобразится сообщение "Привет, undefined".

Кроме переменных, вверх их функциональной области видимости браузер поднимает ещё функции. Т.е. код JavaScript позволяет вызвать функцию до её объявления.

var name = "Вася";
sayHello(); // вызов функции sayHello
// объявление функции sayHello
function sayHello() {
  console.log(name);
}

JavaScript - Некоторые особенности языка при работе с функциями

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

Поэтому следует придерживаться следующих правил:

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