this и ScopeChain в EcmaScript

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Привет, Хабр!

В предыдущей статье мы рассматривали общую теории ООП в применении к 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]] — это свойство самой функции, содержит в себе иерархическую цепь лексически окружений порождающих контекстов

Надеюсь, статья была полезной. До будущих статей, друзья!
Источник: https://habr.com/ru/post/468943/


Интересные статьи

Интересные статьи

Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...
Привет, Хабр! Давно ничего не писал, большая загруженность на проекте крайние несколько недель, но сейчас появилось свободное время, поэтому решил представить вашему вниманию новую статью. ...
Как быстро определить, что на отдельно взятый сайт забили, и им никто не занимается? Если в подвале главной страницы в копирайте стоит не текущий год, а старый, то именно в этом году опека над са...
Если честно, к Д7 у меня несколько неоднозначное отношение. В некоторых местах я попискиваю от восторга, а в некоторых хочется топать ногами и ругаться неприличными словами.
Выпущенная для американского рынка в октябре 1992 года Sega CD первые несколько месяцев держалась хорошо, но быстро начала терять коммерческую популярность. В основном это было вызвано слишком ...