Привет!
В предыдущей статье мы рассматривали общую теории ООП в применении к EcmaScript и популярное заблуждение начинающих разработчиков относительно отличия ООП в JS и классических языках.
Сегодня мы поговорим о двух других не менее важных концепциях EcmaScript, а именно связи сущности с контекстом исполнения (this и есть эта самая связь) и связи сущности с порождающим контекстом(ScopeChain).
Итак, начнём!
this
На собеседованиях в ответ на вопрос: «Расскажите подробнее про this.». Начинающие разработчики, как правило, дают очень туманные ответы: "this – это объект «перед точкой», который использовался для вызова метода", "this — контекст, в котором был вызвана функция" и т.д.…
На самом деле, ситуация с этим центральным для EcmaScript языков понятием обстоит несколько сложнее. Разберёмся по порядку.
Допустим, у нас есть программа на языке JavaScript, в которой есть переменные объявленные глобально; глобальные функции; локальные функции(объявленные внутри других функций), функции, возвращаемые из функций.
const a = 10;
const b = 20;
const x = {
a: 15,
b: 25,
}
function foo(){
return this.a + this.b;
}
function bar () {
const a = 30;
return a + b;
}
function fooBaz(){
function test () {
return this.a + this.b;
}
return test();
}
function fooBar() {
const a = 40;
const b = 50;
return function () {
return a + b;
}
}
fooBar()();
При передаче управления исполняемому коду осуществляется вход в контекст исполнения. Исполняемы код — это любой код, который мы выполняем в данный момент времени, это может быть и глобальный код, и код какой- либо функции.
Контекст исполнения — это абстракция, с помощью которой типизируют и разграничивают код. С точки зрения этой абстракции, код делится на глобальный(любые подключённые скрипты, инлайновые скрипты) и код функций(код вложенных функций не относится к контексту родительских функций).
Есть ещё третий тип — EvalCode. В рамках этой статьи мы им пренебрежём.
Логически совокупность контекстов исполнения представляет собой стек, работающий по принципу Last-in-First-out(lifo). Дном стека всегда является глобальный контекст, а вершиной текущий исполняемый. Каждый раз при вызове функции осуществляется вход в её контекст. При завершении функции её контекст завершается. Отработанные контексты удаляются из стека последовательно и в обратном порядке.
Взглянем ещё раз на код выше. У нас есть вызов функции fooBar в глобальном коде. В функции fooBar мы возвращаем анонимную функцию, которую сразу вызываем. Со стеком происходят следующие изменения: в него попадает глобальный контекст — при вызове fooBar её контекст попадает в стек — контекст fooBar завершается, возвращает анонимную функцию и удаляется из стека — происходит вызов анонимной функции, её контекст попадает в стек — анонимная функция отрабатывает, возвращает значение и её контекст удаляется из стека — по завершению скрипта глобальный контекст удаляется из стека.
Контекст исполнения можно условно представить как объект. Одним из свойств этого объекта будет Лексическое окружение(Lexical Environment, LO).
Лексическое окружение содержит в себе:
- все объявления переменных контекста
- все декларации функций
- все формальные параметры функции(если речь идёт о контексте функций)
При входе в контекст исполнения интерпритатор сканирует контекст. Все объявления переменных и декларации функций поднимаются к началу контекста. Переменные создаются равными undefined, а функции полностью готовыми к использованию.
this также является свойством контекста исполнения, но никак не самим контекстом, как отвечают некоторые начинающие разработчики на собеседованиях! this определяется при входе в контекст и остаётся неизменным до конца срока жизни контекста(пока контекст не удалится из стека).
В глобальном контексте исполнения this определяется в зависимости от strict mode: при выключенном strict mode в this находится глобальный объект(в браузере он проксирован на верхний уровень в объект window), при 'use strict' this равен undefined.
this в контексте функций — вопрос гораздо более интересный!
this в функциях определяется вызывающей стороной и зависит от синтаксиса вызова. Например, как мы знаем, есть методы, которые позволяют закрепить this жёстко при вызове(call, apply) и метод, который позволяет создать обёртку с «закреплённым this» (bind). В этих ситуациях мы явным способом указываем this и никаких сомнений в его определении быть не может.
При обычном вызове функции ситуация гораздо более сложная!
Понять как проставляется this в функциях, нам поможет один из встроенных типов EcmaScript — ReferenceType. Это один из внутренних типов, доступных на уровне реализации. Логически он представляет собой объект с двумя свойствами base(ссылка на некий базовый объект для которого возвращается ReferenceType), propertyName(строковое представление идентификатора объекта для которого возвращается ReferenceType).
ReferenceType возвращается для всех объявлений переменных, деклараций функции и обращения к свойству(именно этот случай нас интересует с точки зрения понимания this).
Правило определения this для функций, вызванных обычным способом:
Если слева от скобок активации функции находится ReferenceType, то в this
функции проставляется base этого ReferenceType. Если слева от скобок любой другой тип, то в this
проставляется глобальный объект или undefined
(на самом деле проставляется null
, но т.к. null не имеет определённого значения с точки зрения ecmascript, то он приводится к глобальному объекту, ссылка на который может быть равна undefined
в зависимости от strict mode).
Разберём пример:
const x = 0;
const obj = {
x: 10,
foo: function() {
return this.x;
}
}
obj.foo();// вернёт 10 т.к. слева от скобок ReferenceType свойство base которого указывает на объект obj
const test = obj.foo;// присвоим метод объекта в глобальную переменную
test();// вернёт 0 т.к. вызов test() эквивалентен вызову ГО.test(),т.е. свойство base укажет на глобальный объект, а в глобальном объекте х присвоено 0.
Думаю, способ определения проиллюстрирован наглядно. Сейчас рассмотрим несколько менее очевидных случаев.
Функциональные выражения
Вернёмся на секунду к нашему ReferenceType. У этого типа есть встроенный метод GetValue, который возвращает истинный тип получаемого через ReferenceType объекта. В зоне выражения GetValue всегда срабатывает.
Пример:
(function (){
return this;// this проставляется глобальный объект или undefined в зависимости от strict mode
})()
Это происходит из-за того, что в зоне выражения у нас всегда срабатывает GetValue. GetValue возвращает тип Function и слева от скобок активации получается не ReferenceType. Вспомним наше правило определения this: Если слева от скобок любой другой тип, то в this
проставляется глобальный объект или undefined
(на самом деле проставляется null
, но т.к. null не имеет определённого значения с точки зрения ecmascript, то он приводится к глобальному объекту, ссылка на который может быть равна undefined в зависимости от strict mode).
Зоной выражения считаются: присваивание(=), операторы || или иные логические операторы, тернарный оператор, инициализатор массива, перечисление через запятую.
const x = 0;
const obj = {
x: 10,
foo: function() {
return this.x;
}
}
obj.foo();
//приведём вызов этого метод объекта в зону выражения
//сработают ли скобки?
(obj.foo)(); //не сработают, данный вызов эквивалентен предыдущему, GetValue не отрабатывает
//присваивание сработает?
(obj.foo = obj.foo)(); // с обоих сторон от оператора присваивания срабатывает GetValue, поэтому результатом будет тип Fuction, а не ReferenceType, следовательно вернёт 0 из глобального объекта(вспоминай правило определения this)
// операторы || или иные операторы сравнения, тернарный оператор и т.д.?
(obj.foo || obj.foo)();//вернёт 0 по тем же причинам, что и предыдущий пример
//инициализатор массива
[obj.foo][0]();//вернёт 0 по тем же причинам, что и предыдущий пример
//и т.д.
Идентичная ситуация в именованных функциональных выражениях. Даже при рекурсивном вызове this глобальный объект или undefined
this вложенных функций вызываемых в родительской
Также немаловажная ситуация!
const x = 0;
function foo() {
function bar(){
return this.x;
}
return bar();
}
const obj = {x:10};
obj.test = foo;
obj.test();//вернёт undefined
Это связано с тем, что вызов bar()
эквивалентен вызову LE_foo.bar
, а объект лексического окружения проставляет undefined в качестве this.
Функции-конструкторы
Как я писал выше:
this в функциях определяется вызывающей стороной и зависит от синтаксиса вызова.
Функции-конструкторы мы активируем с помощью ключевого слова new. Особенность этого способа активации функции в том, что вызывается внутренний метод функции [[construct]], который проводит определённые операции(механизм создания сущностей конструкторами разберём во второй или третьей статье по ООП!) и вызывает внутренний метод [[call]], который проставляет в this созданную инстанции функции-конструктора.
Цепь областей видимости(Scope Chain)
Цепь областей видимости также является свойством контекста исполнения как и this. Она представляет собой список объектов лексических окружений текущего контекста и всех порождающих контекстов. Именно в этой цепи происходит поиск переменных при разрешении имён идентификаторов.
Обратите внимание: this связывает функцию с контекстом исполнения, а ScopeChain с порождающими контекстами.
Спецификация утверждает, что ScopeChain это массив:
SC = [LO, LO1, LO2,..., LOglobal];
Однако, в некоторых реализациях, например в JS, цепь областей видимости реализована через связанные списки.
Для того чтобы лучше разобраться со ScopeChain, обсудим жизненный цикл функций. Он подразделяется на этап создания и этап выполнения.
В момент создания функции ей присваивается внутреннее свойство [[SCOPE]].
В [[SCOPE]] записывается иерархическая цепь объектов лексических окружений вышестоящих(порождающих) контекстов. Это свойство остаётся неизменным до тех пор пока функция не уничтожена сборщиком мусора.
Обратите внимание! [[SCOPE]] в отличии от ScopeChain являертся свойством самой функции, а не её контекста.
При вызове функции инициализируется и наполняется её контекст исполнения. Контексту проставляется ScopeChain = LO(самой функции) + [[SCOPE]](иерархическая цепь LO пораждающих контекстов).
Разрешение имён идентификаторов — последовательный опрос объектов LO в цепи ScopeChain слева направо. На выходе получается ReferenceType свойство base которого указывает на объект LO, в котором был найден искомый идентификатор, а PropertyName будет являться строковым представлением имени идентификатора.
Именно так под капотом устроено Замыкание! Замыкание это по сути результат поиска в ScopeChain всех переменных, идентификаторы которых присутствуют в функции.
const x = 10;
function foo () {
return x;
}
(function (){
const x = 20;
foo();//вернёт 10 т.к. на этапе создания в <b><i>[[SCOPE]]</i></b> foo был записан объект окружения в котором она была создана
})()
Следующим примером проиллюстрирует цикл жизни [[SCOPE]].
function foo () {
const x = 10;
const y = 20;
return function () {
return [x,y];
}
}
const x = 30;
const bar = foo();//присвоили переменной анонимную функцию, контекст функции foo отработал и завершился
bar();//вернёт [10,20] т.к. [[SCOPE]] свойство самой функции foo и существует даже после того как её контекст завершился
Важным исключение является функция-конструктор. Для этого типа функций [[SCOPE]] всегда указывает на глобальный объект.
Также не стоит забывать, что если у какого-то из звеньев в цепи ScopeChain есть прототип, то поиск будет осуществляться и в прототипе тоже.
Заключение
Вынесем ключевые идеи тезисно:
- this — это связь сущности с контекстом исполнения
- ScopeChain — это связь сущности со всеми порождающими контекстами
- this и ScopeChain — это свойства контекста исполнения
- this функций определяется вызывающей стороной и зависит от синтаксиса вызова
- ScopeChain — это лексическое окружение текущего контекста + [[Scope]]
- [[Scope]] — это свойство самой функции, содержит в себе иерархическую цепь лексически окружений порождающих контекстов
Надеюсь, статья была полезной. До будущих статей, друзья!
Автор: Alex_Shcherbackov