Javascript ― язык весьма оригинальный. Его можно любить, ненавидеть и даже бояться, но равнодушным он вас вряд ли оставит. Не знать или не понимать, с чем ты работаешь ― самая частая ошибка, допускаемая современными фронтенд‑разработчиками. Вам бы понравилось, если бы дантист, к которому вы пришли, не понимал, какой он инструмент использует и какие у него особенности работы? Очевидно, что нет. И рано или поздно, если вы действительно хотите стать профессионалами, вы разберётесь во всём, но как сделать так, чтоб это случилось раньше?

В этой статье в блоге ЛАНИТ хотелось бы показать, что о сложных вещах можно и нужно говорить просто.

Источник

Так уж вышло, что с давних пор мне периодически доводилось принимать участие (иногда это периодически превращается в «c завидным постоянством») в собеседованиях специалистов, работающих на Javascript. Многие из них позиционируют себя как разработчики на различных современных фреймворках. Самое удивительное, когда респондент хорошо отвечает на вопросы об особенностях работы фреймворков, но не может ответить на базовые вопросы по Javascript. И тут уж каких только оправданий не услышишь! Вероятно, я зануда, но у меня в голове не укладывается, как, например, можно считаться хорошим разработчиком на React, если ты банально не знаешь Javascript?

Так сложилось, что помимо уже квалифицированных специалистов, в компании также существует набор «малышей» (стажёров). Некоторые обучались самостоятельно, кто‑то заканчивал курсы, но всех их объединяет одно ― теория. Очевидно, что они так или иначе её учили, но она плохо откладывается в голове без практики, а иногда просто не до конца усваивается из‑за сухости или сложности формулировок. Нередко уже на этом этапе у будущих программистов возникает ошибочное мнение, что знать теорию вовсе не обязательно. Главное ― это уметь делать. Ведь программирование ― прикладная дисциплина. Что, собственно, является чистой правдой, но было бы только что «прикладывать». Работая в команде высококвалифицированных специалистов, не стоит ожидать, что они будут каждый раз расшифровывать всё так, чтобы вам было понятно с тем уровнем знаний, на котором вы остановились. Им проще использовать уже готовую и понятную терминологию. Поэтому, если вы хотите участвовать в процессе разработки не только как «руки, которые пишут код», рано или поздно вам придётся подтягивать свои знания. Важно не просто выучить термины, а понимать, что от вас хочет человек, который их употребляет. Вы не обязаны определять их одними и теми же словами, как по методичке. Главное ― вкладывать в них один и тот же смысл.

А как сделать так, чтобы у достаточно большого количества людей сложилось приблизительно одинаковое представление о самых базовых и банальных понятиях? Посадить читать документацию! Да, это вариант. Но, к сожалению, документация ― это не легкое художественное чтиво. Это, прежде всего, стандарт. Читать его можно и нужно, но для стажёров это кажется непосильной задачей. Так родилась идея написания небольшой методички (действительно небольшой), которая затрагивает только самые базовые понятия, но необходимые для всех, кто хочет начать свой путь в мире JavaScript. В ней мы с коллегами постарались переделать всё настолько простыми словами, чтобы не вызывать у читающих мигрень и желание срочно сменить вид деятельности.

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

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

Немного баек. Первый и самый частый провал на собеседовании происходит при попытке ответить на вопрос: в чём заключаются отличия переменных, созданных с помощью конструкции let и с помощью конструкции var? Здорово, что все знают, что отличие в области видимости. Классно, что многие отвечают, что у let область видимости блочная. Проблемы начинаются, когда задаешь вопрос: ну а у var какая? Самый популярный ответ, что у var область видимости глобальная (спойлеры — нет!), а самый курьёзный, на мой взгляд, ответ, что область видимости «более глобальная, чем у let«. Никогда не надо так говорить, во‑первых, программисту вы этим ничего не скажете, кроме того, что вы не знаете ответа. Во‑вторых, вы поставите в неловкое положение своего преподавателя русского языка. Более глобальная? Куда уж глобальнее?

Итак, какие же бывают области видимости?

  1. Глобальная область видимости ― это область видимости всего нашего скрипта. Так как мы говорим о фронтенд, то глобальная область видимости уже будет содержать такой объект, как window. Пока мы не перейдём на другую вкладку в браузере или новое окно, сколько бы скриптов поочередно не загружалось и не выполнялось ― они все будут в этой области видимости, и window для них будет одним и тем же объектом.

  2. Функциональная область видимости ― это область видимости, ограниченная curly braces при декларации функций. И именно этой областью видимости характеризуется переменная, созданная с помощью конструкции var.

  3. Блочная область видимости ― область видимости, ограниченная любым блоком curly braces, будь то функция, условие, цикл. И именно этой областью видимости характеризуется переменная, созданная с помощью конструкции let или константы.

А теперь немного наглядных примеров для закрепления.

// Я глобальная область видимости

function test() {
  // Я функциональная область видимости
  var a = 10;  
  console.log("Я - переменная а, существую только внутри функции test", a);
}

test();
console.log("А вот тут я - переменная а - не существую", a);

Вывод в консоль:

> Я - переменная а, существую только внутри функции test 10
> ReferenceError: а is not defined

// Я - глобальная область видимости
// Я - глобальная область видимости

if (true) { 
  // Я - блочная область видимости  
  var a = 10 // меня не ограничить блоком, буду видна везде  
  let b = 200 // я буду видна только внутри этого блока
}

console.log(a)
console.log(b)

Вывод в консоль:

> 10
> ReferenceError: b is not defined

Но на самом деле Javascript не был бы Javascript, если бы не был полон сюрпризов. Если не включать strict‑режим, то можно создавать переменные и без конструкции var, let. Они автоматически становятся параметрами объекта window.

// Я глобальная область видимости

test = 400 // я переменная, созданная малограмотным специалистом

console.log(window.test)
console.log(test)

Вывод в консоль:

> 400
> 400

Контекст (и вечные проблемы его потери)

Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на Javascript. Код всегда выполняется внутри некоего контекста.

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

  1. Глобальный контекст выполнения ― базовый, используемый по умолчанию контекст выполнения. Если некий код находится не внутри какой‑нибудь функции, значит, он принадлежит глобальному контексту. Глобальный контекст характеризуется наличием глобального объекта, которым в случае с браузером является window. В программе может быть лишь один глобальный контекст.

  2. Контекст выполнения функции. Каждый раз, когда вызывается функция, для неё создается новый контекст. Каждая функция имеет собственный контекст выполнения.

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

А что же такое this? 

Многие предполагают, что this ― это и есть контекст, но это не совсем справедливо (а, может, и совсем несправедливо). This ― это ключевое слово, зарезервированное движком Javascript, при обращении к которому мы можем получить значение, зависящее от текущего контекста выполнения.

То есть:

  • this ― ключевое слово? ― да, как и во многих других языках, но поведение его в js весьма отличается,

  • this ― это контекст? ― нет, ни в коем случае не контекст в его первозданном виде,

  • значение this зависит от контекста? ― да, бесспорно.

Чему же равно this?

И вот это, пожалуй, самый интересный вопрос.

В глобальном контексте, о котором речь шла ранее, this представляет собой ссылку на глобальный объект window. И больше добавить нечего.

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

Простой вызов функции:

function myFunction() {
  console.log(this === window)
}

let functionalExpression = function () { 
  console.log(this === window)
}

myFunction()
functionalExpression()

Вывод в консоль:

> true
> true

Получаем следующее: при простом вызове функции значение this будет так же ссылкой на глобальный объект window. Но Javascript был бы не Javascript, если бы и тут не было нюансов.

function myFunction() {
  'use strict'    
  console.log(this)
}

let functionalExpression = function () {
  'use strict'
  console.log(this)
}

myFunction(
functionalExpression()

Добавим strict режим в наши функции и посмотрим результат:

> undefined
> undefined

И, кстати, данная запись будет работать аналогично.

'use strict'

function myFunction() {
  console.log(this)
}

let functionalExpression = function () {
  console.log(this)
}

myFunction()
functionalExpression()

В общем, разобрались. При простом вызове функции значение this может быть ссылкой на глобальный объект window или undefined, в зависимости от того, используется ли strict‑режим.

Функции конструкторы

Теперь рассмотрим поведение ключевого слова this в функциях-конструкторах:

function MyConstructor(propName) {
  this.name = 'Default'    
    
  if (propName) {
    this.name = propName    
  }
    
  return this     
    /* return тут только для наглядности, так как функция конструктор        
    по умолчанию возвращает создаваемый объект */
 }
 
 let defaultConstructor = new MyConstructor()
 let constructorWithName = new MyConstructor('MyConstructor')
 
 console.log(defaultConstructor)
 console.log(constructorWithName)

Смотрим в консоль:

> MyConstructor {name: 'Default'}
> MyConstructor {name: 'MyConstructor'}

Что имеем? Если функция вызывается как конструктор, то внутри неё, соответственно, создаётся новый объект, и значение this будет ссылаться на него. Это будет работать одинаково, независимо от strict‑режима.

Вызов функции как метода объекта

Преобразим нашу функцию-конструктор следующим образом (на этот раз специально уберём return this, чтобы стало понятно, что поведение функции не изменится).

function MyConstructor(propName) {
  this.name = 'Default'    
   
  if (propName) {
    this.name = propName    
  }
    
  function logThis () {
    console.log(this)    
  }
    
    this.log = logThis
}

let defaultConstructor = new MyConstructor()
let constructorWithName = new MyConstructor('MyConstructor')

defaultConstructor.log()
constructorWithName.log()

Вывод в консоль:

> MyConstructor {name: 'Default', log: ƒ}
> MyConstructor {name: 'MyConstructor', log: ƒ}

В данном случае log вызывается как метод объекта, и значение this у него будет равно объекту, методом которого является log.

С объектами можно ведь и напрямую работать.

let a = {
  name: 'Name',
  log: function() {
    console.log(this)    
  }
}

a.log()

Вывод в консоль:

> {name: 'Name', log: ƒ}

Значение this всё ещё объект, методом которого является log.

Вызов функции как метода прототипа объекта

Вспомним, что у нас также появились классы в Javascript, и они работают не совсем как функции‑конструкторы. Это позволяет неявно добавить методы не самому объекту, созданному на основе класса, а его прототипу, что для нас является разницей в вызове.

class Hero {
  constructor(heroName) {
    this.heroName = heroName || 'Default'    
}
  log() {
    console.log(this);    
  }
}

const batman = new Hero('Batman');
batman.log()

Однако в консоли видно, что значение this всё ещё является ссылкой на создаваемый объект. Вывод в консоль:

> Hero {heroName: 'Batman'}

А также видно, что log больше не является методом объекта, но если раскрыть детальную информацию, то можно увидеть следующее:

> Hero {heroName: 'Batman'}
  >  heroName: "Batman"
  >  [[Prototype]]: Object
      > constructor: class Hero
      > log: ƒ log()
      > [[Prototype]]: Object

Вызов функции с помощью методов call и apply

Существуют два метода прототипа Function, которые позволяют вызвать функцию и искусственно задать значение, которое будет связано с ключевым словом this. Это методы call и apply. Тут разница в том, что мы не вызываем саму функцию непосредственно, но вызываем методы, которые сделают это за нас.

function myFunction(c, d) {
  this.c = c
  this.d = d
  console.log(this)
}

let fakeThis = {a: 1, b: 3}

// Первый параметр - это объект, который следует использовать как
// 'this', последующие параметры передаются
// как аргументы при вызове функции
myFunction.call(fakeThis, 5, 7)

// Первый параметр - объект, который следует использовать как
// 'this', второй параметр - массив,
// элементы которого используются как аргументы при вызове функции
myFunction.apply(fakeThis, [10, 20])

console.log(‘fakeThis’, fakeThis)

После вызова функции методами call и apply дополнительно выведем в консоль объект fakeThis.

> {a: 1, b: 3, c: 5, d: 7}
> {a: 1, b: 3, c: 10, d: 20}
> fakeThis {a: 1, b: 3, c: 10, d: 20}

Следует быть весьма внимательным при использовании данных методов, ведь передача по ссылке ― вещь весьма коварная. Если контекст, от которого зависит значение this, ― штука, динамически создаваемая для каждого вызова функции, то вот наш объект fakeThis не такой. Поэтому, производя изменения с this внутри функции, мы ещё и дважды изменили параметры объекта fakeThis.

Вызов функции как обработчика события DOM

Когда функция используется как обработчик событий, this присваивается элементу, с которого начинается событие (некоторые браузеры не следуют этому соглашению для обработчиков, добавленных динамически с помощью всех методов, кроме addEventListener).

Проверяем.

Для начала создаём «красивейшую кнопку», да простят меня все, у кого ещё осталось чувство прекрасного.

А потом создаём для неё асинхронного слушателя событий и колбеком ему передаём функцию.

function callback(e) {
  // Всегда true
  console.log('currentTarget', this === e.currentTarget)
  // true, когда currentTarget и target один объект
  console.log('target', this === e.target)
}

// Получить список каждого элемента в документе
let elements = document.getElementsByTagName('button');

// Добавить callback как обработчика кликов
for (var i = 0; i < elements.length; i++) {
  elements[i].addEventListener('click', callback)
}

А теперь жмём на кнопку и смотрим в консоль.

> currentTarget true
> target true

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

<button onclick="alert(this.tagName.toLowerCase());">
  Показать this
</button>

Когда код вызван из инлайнового обработчика, this указывает на DOM-элемент, в котором расположен код события. Но это не актуально для вложенных функций.

<button onclick="alert((function() {return this;} ()));">
  Показать вложенный this
</button>

В этом случае this вложенной функции не будет установлен, так что будет возвращена ссылка на window объект.

IIFE - immediately invoked function expression

Они же - самовызывающиеся функции.

(function(){
  console.log(this === window)
})()

Вывод в консоль:

> true

Как теряется контекст?

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

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

class Hero {
  constructor(heroName) {
    this.heroName = heroName || 'Default'    
  }    

  log() {
    console.log(this.heroName);
  }    

  asyncLog() {
    setTimeout(this.log, 5000)    
  }
}

const batman = new Hero('Batman')
batman.log()
batman.asyncLog()

В общем, добавили прототипу нашего объекта ещё один метод логирования, да ко всему прочему сделали его асинхронным.

В итоге в конце скрипта вызываем синхронный log, а за ним asyncLog и сравниваем результаты.

> Batman
> undefined

И вот она — магия во всей красе. Мы его потеряли. Хотя, казалось бы, асинхронный лог не делает ничего сверхъестественного, кроме как выполняет тот же log, но с задержкой.

Почему же имя героя исчезло?

По умолчанию внутри window.setTimeout() this устанавливается в объект window.

И ещё раз модифицируем наш класс. Вспоминаем, что у нас где‑то была «красивейшая кнопка».

Вот она, если кто забыл. Освежаем память.

И делаем так, что выводить в консоль имя героя мы хотим при клике на кнопку.

class Hero {    
  constructor(heroName) {
    this.heroName = heroName || 'Default'
    // Получить список каждого элемента в документе   
    let elements = document.getElementsByTagName('button');

    // Добавить callback как обработчика кликов        
    for (let i = 0; i < elements.length; i++) {
        elements[i].addEventListener('click', this.log)
    }    
  }    
  
  log() {
    console.log(this.heroName);
  }
}

const batman = new Hero('Batman')

Нажмём на кнопку.

> undefined

Тут опять же все прозрачно. У нас в колбеки ивент листенеров this отправляется ссылка на элемент DOM, а у него опять же нет heroName.

Для примера более чем достаточно. Какой делаем вывод? Что «контекст не теряется», он просто становится другим и уже не удовлетворяет наши потребности.

Исправление ошибок привязки значения к this

Были у нас способы привязки значения к this с помощью методов прототипа Function call и apply, но в нашей ситуации они нам не очень помогут (вообще не помогут). Мы не можем сразу вызвать функцию callback, ведь она ещё не дождалась своего часа.

Но в современном Javascript есть способы справиться с ситуацией.

Привязка значения к this с помощью Function.prototype.bind()

Метод этот пришел вместе с ES5, и в отличие от своих соратников call и apply, он не вызывает функцию. В результате своего исполнения он возвращает новую функцию, которая при выполнении будет устанавливать в значение this то, что мы передали в качестве первого аргумента.

Применяем на практике.

class Hero {    
  constructor(heroName) {
    this.heroName = heroName || 'Default'
    // Получить список каждого элемента в документе
    let elements = document.getElementsByTagName('button'); 

    // Добавить callback как обработчика кликов       
    for (let i = 0; i < elements.length; i++) {
      elements[i].addEventListener('click', this.log.bind(this))
    }    
  }    
  
  log() {
    console.log(this.heroName);
  }
}

const batman = new Hero('Batman')

Жмём на нашу изящную кнопку и смотрим в консоль.

> Batman

Спойлер! C setTimeout тоже сработает.

Arrow functions и их отношения с контекстом

А вот уже ES6 представила нам новую возможность борьбы за this. Этим способом стали стрелочные функции. Все дело в том, что в отличие от обычных, они не создают собственного контекста. Они наследуют контекст, в котором были созданы, а вместе с этим у них отсутствует своё привязывание значения к this. Где бы и как они не вызывались, это никак не влияет на значение this.

Смотрим на практике.

class Hero {
  constructor(heroName) {
    this.heroName = heroName || 'Default'
    // Получить список каждого элемента в документе
    let elements = document.getElementsByTagName('button');
    // Добавить callback как обработчика кликов
    for (let i = 0; i < elements.length; i++) { 
      elements[i].addEventListener('click', this.log)
    }    
  }
  
  log = () => {
    console.log(this.heroName);    
  }
}

const batman = new Hero('Batman')

Мы просто переделали log, теперь это функциональное выражение, которое описано стрелочной функцией. Жмём и смотрим.

>  Batman

Идеально!

Кстати, если попробовать вызвать метод bind для привязывания конкретного значения к this, это не сработает. Можете проверить самостоятельно.

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

let a = {
  name: 'Name',
  log: function() {
    console.log(this)    
  },
  logArrow: () => console.log(this)
}

a.log()
a.logArrow()

Сразу модифицировали объект, добавили атрибут logArrow, который описан стрелочным функциональным выражением, и пробуем узнать, что у нас за значение this в log и logArrow.

> {name: 'Name', log: ƒ, logArrow: ƒ}
> Window {window: Window, self: Window, document: document, name: 'MyConstructor', location: Location, ...}

И понимаем, что в данной ситуации arrow function ведёт себя не так, как хотелось бы. Почему? Да всё просто. Описанный таким образом объект ― это не функция, так что контекста у него нет. Наш logArrow унаследовал тот, что ближе лежал, ― глобальный. this, стало быть, ссылка на window, как и полагается в глобальном контексте.

На этом с контекстом хотелось бы уже закончить.

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

  1. Синтаксис, но это настолько очевидно, что и говорить не стоит.

  2. Наличие неявного return (хотя это тоже можно отнести к синтаксису, вероятно).

  3. Контекст и привязка значения к this. У обычных функций контекст динамически создаётся в зависимости от того, как они были вызваны, у стрелочных наследуется контекст, в котором она была создана.

  4. Использование call, apply и bind. У обычной функции значение к this можно привязать с помощью Function.prototype методов call, apply и bind. Их можно использовать со стрелочной функцией, но изменить значение this не выйдет.

  5. Функции‑конструкторы. Обычные функции можно использовать как конструктор, стрелочные ― нет. Это связано с тем, что они не создают свой контекст.

  6. Обычные функции могут быть и функциональными выражениями, и именованными функциями. Стрелочные выступают только как функциональные выражения.

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

  8. У стрелочных функций нет массива arguments. В большинстве случаев лучшей заменой объекта arguments в стрелочных функциях являются остаточные параметры.

  9. В стрелочный функциях не может быть использовано ключевое слово yield (за исключением случаев, когда разрешается использовать в функциях, вложенных в тело стрелочной функции). Как следствие, стрелочные функции не могут быть использованы как генераторы.

Если кому‑то пришлась по нраву наша манера повествования или, может, кто‑то смог узнать что‑то новое для себя, мы будем очень рады предоставить вам полную версию нашей методички. Маякните, если она нужна ― можем выложить вторым постом.

Комментарии (41)


  1. LordDarklight
    00.00.0000 00:00

    Эх... замороченный этот JavaScript!

    А что там в TypeScript на ту же тему?

    Про стрелочные функции, всё же стоит отдельную статью написать с примерами, если планировался цикл статей. Тогда данную статью надо было называть как-нибудь так "Нюансы определения контекста в JavaScript в браузерах"; ну и, наверное, имело смысл уточнять про нюансы с отсылкой на конкретные браузерные движки, где они по-разному ведут себя


    1. OkoloWEB Автор
      00.00.0000 00:00
      +10

      TypeScript, да простят меня все, кому не понравится данное утверждение, это всего лишь синтаксический сахар над JavaScript, перед выполнением он будет так же собираться в JS и работать по всем правилам обработки JS


      1. Kanut
        00.00.0000 00:00
        +7

        Какой ЯП не возьми в итоге это всё равно синтаксический сахар над машинным кодом, который потом "будет собираться и работать по правилам машинного кода" Но при этом почему-то люди предпочитают использовать ЯП :)


      1. Kenya-West
        00.00.0000 00:00
        +1

        Технически вы уже не правы, ибо на TS можно написать всё. Правда, в такое дерьмо, как писать полноценный код на системе типов TS, я добровольно не полезу, иначе у меня есть неплохой шанс оказаться в дурке... Но в бытовом, "утилитарном" смысле правы на 100%.

        Вроде на данный момент не существует движка, который бы работал с Typescript напрямую. Даже Deno и то транспилирует код в JS, а непонятному AssemblyScript и его возможности компиляции TS в WASM напрямую я не верю - ибо их некая "JavaScript Standard Library" явно не покрывает и 50% возможностей V8.

        Так что, в 99% случаев TS надо превращать в JS... Пока что.


        1. maeris
          00.00.0000 00:00
          +2

          Да полно вам, у нас тут в дурке тепло и уютно.

          AssemblyScript и его возможности компиляции TS в WASM напрямую я не верю

          И правильно не верите. Они там не поддерживают даже замыкания.


        1. LordDarklight
          00.00.0000 00:00

          TypeScript проектировался как надстройка над JS. А браузеры умели ранее выполнять только JS код (Java-апплеты и наивные компоненты не в счёт). Но, сейчас появился ещё WebAssembly, который тоже исполняется движками браузеров и в него уже умеют компилироваться не мало ЯП, в т.ч даже C++ (лично запускал Doom 3 в браузере). Но TypeScript пока не компилируется в WebAssembly, насколько мне известно. Тут есть проблема, так как среда выполнения WebAssembly логически отделена от среды JS - и их взаимодействие идёт через проксирование с потерей производительности. А весь DOM API страницы пока доступен только через JS-среду. Но то ли будет далее - WASM пока очень молод. Но сама идея писать клиентскую логику не только на JS (и для JS) очень сильно будоражит - и тот же, к примеру Microsoft Blazor,дующий возможность разрабатывать код браузерного клиента на C# штука очень интересная! (но C# в браузере сейчас уже это не только Blazor). Другие ЯП - другие заморочки другие возможности!

          Кроме TypeScript в JS-код умеют транслироваться и другие ЯП - их тоже не мало, я тут не спец, поэтому назову только парочку: Scala, Kotlin.

          И, свой же вопрос, я могу переадресовать этим ЯП - где общая стандартизация архитектуры языка программирования изначально продиктована уже не особенностями JavaScipt - и логика определения контекста для this более строгая и чётка, наверное, даже если идёт трансляция в JS, или я ошибаюсь, вот в чём был мой вопрос?

          Про AssemblyScript думаю говорить смысла нет - это предыстория WebAssembly и уже не актуально! WASM пока тоже ещё молод и бурно развивается, но он не строится вокруг нюансов JS движка, который уже не особо молодой и искорёженный кучей спецификаций-надстроек. У WASM свой путь , с оглядкой на актуальные проблемы и потребности


      1. LordDarklight
        00.00.0000 00:00

        Да, но сборщик вправе делать свои проверки, устанавливать свои правила, делать дополнительную кодогенерацию. Тем самым организуя в своих исходных терминах, грубо говоря, более чётко контролируемое поведение. Например, при определении объектов (всеми доступными для TypeScript) способами гарантировать, что под this всегда будет ссылка на контекст объекта. Даже при вызове в событии. А какие-то неоднозначные варианты вообще запрещать (при наличии более адекватной альтернативы)


      1. LordDarklight
        00.00.0000 00:00

        Я не спец ту - поэтому и спрашиваю - есть ли разница в указанных нюансах в JavaScript и TypeScript.

        А кроме TypeScript есть, к примеру, Kotlin, Scala - тоже умеют транслироваться в JS - но там уже архитектура ЯП в первую очередь диктовалась другим фреймворком и более чёткой логикой поведения те же контекстов, к примеру, не думаю, что они там так же скачут как в чистом JavaScript - об этом и был вопрос


      1. YuryB
        00.00.0000 00:00

        ну и что в какой среде он работает? если на уровне языка есть ограничения, то для вас этих "правил обработки JS" не существует, если вы конечно явно не хитрите


  1. Ernesto
    00.00.0000 00:00
    +4

    Спасибо. Выкладывайте продолжение :)


  1. teilarerJs
    00.00.0000 00:00
    +1

    Функциональная область видимости ― это область видимости, ограниченная curly braces при декларации функций. И именно этой областью видимости характеризуется переменная, созданная с помощью конструкции var.

    А если переменная var объявлена вне функции, то какая у неё область видимости?


    1. OkoloWEB Автор
      00.00.0000 00:00
      +3

      Не надо заниматься подменой понятий, вы говорите о том, в какой области переменная создана, в описанном вами случае она будет создана в глобальной области видимости и доступна именно там, а если говорить то том "какая область видимости у переменной, созданной с помощью конструкции var", то ответом будет - функциональная или область видимости текущего контекста, это всего лишь означает, что единственным способом инкапсулировать данную переменную будет размещение ее внутри блока фигурных скобок, но не любых фигурных скобок, а только тех, которые ограничивают тело функции


      1. DmitryOlkhovoi
        00.00.0000 00:00
        +2

        Та проще - функциональная видимость, а let дает блочную


      1. demimurych
        00.00.0000 00:00
        -1

        Смотрите, я инкапсулирую переменную без единой фигурной скобки:

        ( theIncapsulatedVariable ) => () => theIncapsulatedVariable

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

        Согласно спецификации - в JavaScript нет областей видимости, но есть Realm + Enviroment.

        Согласно спецификации - в JavaScript все зависит от RunTime семантики: Statement и Decloration.

        Согласно спецификации - в JavaScript оперирование идентификаторами дейтсвительно происходит в рамках Runing Execution Context, но только в той части которая отвечает за поиск текущего окружения, формирования которого никак не зависит от него же (Execution Context).


    1. savostin
      00.00.0000 00:00

      Если я правильно понял, вопрос про "всплытие".


      1. LordDarklight
        00.00.0000 00:00

        Средство мощное - реализация корявая :-(


        1. savostin
          00.00.0000 00:00

          Вы о чем? Я об этом:

          Интерпретатор JavaScript всегда незаметно для нас перемещает («поднимает») объявления функций и переменных в начало области видимости. Формальные параметры функций и встроенные переменные языка, очевидно, изначально уже находятся в начале. Это значит, что этот код:

          function foo() {
          bar();
          var x = 1;
          }
          

          на самом деле интерпретируется так:

          function foo() {
          var x;
          bar();
          x = 1;
          }
          


          1. LordDarklight
            00.00.0000 00:00
            +1

            О том, что не интуитивно всё. Ну идёт в разрез с куда более общепринятой практикой восприятия. Но тут да так уж сложилось исторически - это сейчас в большинстве ЯП иная модель, но всё-равно - просто изначально неудачный дизайн, хоть это и может звучать предвзято.

            Вообще - с моей точки зрения:

            - Явное объявление переменных - должно именно объявлять новую переменную (и тут могут быть два подхода если имена пересекаются - оба допустимы: либо ошибка, либо объявление новой переменной в границах текущей локальной области видимости

            - Неявное объявление переменных - априори зло!

            - Ничего не имею против указанного вами примера - если это объявление переменной остаётся только внутри функции, не затрагивая изменение вышестоящего контекста

            - Но есть вот такой пример:

            function foo(a,b) {
            bar();
            if (a)
            {var x = 1}
            if (b)
            {var x = 2}
            alert(x)
            }

            Тоже, вполне себе хороший пример. Неудачно только ключевое слово var, хотя когда него вводили - уверен - было всё очень даже удачно. Просто привычка другая. Да есть ключевое слово let - с моей точки зрения тоже не особо удачное, но будь их смысл наоборот - было немного удачнее let - допустим - т.е. предположим, что переменная может уже существовать. А для переменных локального контекста лучше было бы loc (local).

            Но куда хуже примеры из статьи про "всплытие"

            var x = 1; 
            function foo(a) { 
                if (a) { 
                    var x = 10; 
                } 
                alert(x); //вывод: 10
            } 
            
            var x = 1; 
            function bar() { 
                x = 10; 
                return; 
                function x() {} 
            } 
            alert(x); //вывод 1

            Не - ну всё логично, конечно - по сознание ломается - и это недостатки ЯП.

            Но спорить не буду ибо логика тут есть и мне попрой в Cи подобных языках тоже вот неудобно когда:

            static int foo(bool a)
                {
                  if (a)
                    {var x = 1;}
                  else
                    {var x = 2;}
                    return x*3; //error CS0103: The name 'x' does not exist in the current context
                }

            Как и неудобно

             object bar(object a)
                {
                  if (a is int v)
                    {return v+1;}
                  if (a is bool v) //error CS0128: A local variable or function named 'v' is already defined in this scope
                    {return !v;}
                  if (a is double v) //error CS0128: A local variable or function named 'v' is already defined in this scope
                    {return v;}
                    
                    return null;
                }

            На JS в первый пример как раз нормально бы работал (без типов, конечно), а во втором примере.... не возьмусь писать на JS (т.к. завязан на типы) - но тут ни let ни var подходы JS не подходят - это недостаток ЯП (C#) - объявление переменной внутри условия if не является локальным только для вложенного блока внутри if

            TypeScript более топорный, чем C# и в нём нет такой потребности и возможности объявления переменной v
            function bar(a : object)
                {
                  if (typeof a == "number")
                    {return a+1;}
                  if (typeof a == "string")
                    {return !a;}
                  if (typeof a == "boolean")
                    {return a;}
                    
                    return null;
                }

            мои примеры несколько надуманны и сделаны на коленке. Но в целом в реальном коде такие случае бывают достаточно часто хоть и порой в других конструкциях.

            Тем более не стоит ещё и забывать про кодогенерацию, где код может собираться из разных блоков и проблема пересечения идентификаторов становится ещё острее (причём тут возможны обе схемы: должны быть одинаковыми ли должны быть разными такие объявления)


            1. OldCold
              00.00.0000 00:00
              +2

              Не совсем понял проблему с пересечением идентификаторов. По мне достаточно очевидно, что если ты в каком-то блоке кода запрашиваешь переменную a, то нужно пытаться найти эту a по коду выше, и выше и выше...

              Возможно для вас настоящей проблемой является не "пересечение идентификаторов", а "всплытие". Попробуйте переписать сложные примеры БЕЗ всплытия. Т.е. все var заменить на let и const, а все function a(){} на const a = () => {} . Вы приятно удивитесь, на сколько упростится и преобразится язык.

              А если сверху это все засыпать TypeScript, то все язык становится просто идеальным.

              По большей части существует очень много ЧАСТИЧНО "надуманных" проблем языка. Которые усугубляют "сениоры" на собеседованиях. Связаны они как раз с тем, что описано в статье. С таким "функционалом", который не несет под собой практически никаких преимуществ, но крайне сильно расширяет возможности "выстрелить себе в ногу". И при этом в реальных проектах эти же "сениоры" не будут в здравом уме (крайне на это надеюсь) использовать и пропускать в ревью код с таким "ухищрениями". И получается ситуация на собесе на вакансию на React мы про всплытие, области видимости и прямое обращение к API DOM мы спросим, статью на Хабре напишем (собрав плюсы и прорекламировав свою компанию), но при этом, если ты к нам устроишься, то код такой писать даже не вздумай.


  1. ruslanyar
    00.00.0000 00:00
    +5

    Спасибо за статью!

    С удовольствием почитал бы полную версию вашей методички.


  1. astenix
    00.00.0000 00:00
    +3

    Не останавливайтесь, серьёзно.


  1. Format-X22
    00.00.0000 00:00

    Про var это интересно и это теоретически встречается в реальном коде, правда вот как 8 лет по сути стандарту ES6 и с тех пор должна быть суровая веская причина использовать var. На практике года так с 2016 не видел var ни разу. Это конечно более полезный вопрос чем какой результат будет у i++ + ++i, но всё же.

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


  1. maeris
    00.00.0000 00:00

    А если не пользоваться var, this, call, apply, bind, function и class, то можно из "весьма оригинального" языка сделать даже production-ready язык! Даже IIFE расставлять не нужно, когда переменные адекватно к областям видимости принадлежат. Правильный набор ключевых слов для такой статьи был бы где-то такой: arrow function, closure, TDZ, spread operator, rest parameters. Содержимое статьи относится к разработке на JS только в случае, если вы сениор, которому нужно разгрести какое-то древнейшее legacy.


    1. maeris
      00.00.0000 00:00
      +4

      Например, в одном случае это выглядело бы так

      type Hero = {name: string};
      const Hero = (name = 'Default'): Hero => ({name});
      const log = ({name}: Hero) => console.log(name);
      const asyncLog = (hero: Hero) => setTimeout(() => log(hero), 5000);
      
      const batman = Hero('Batman');
      log(batman);
      asyncLog(batman);
      

      А в другом как-то так:

      const subscribe = (hero: Hero) => {
      	const logMe = () => log(hero);
      	const elements = [...document.querySelectorAll('button')];
      	for (const element of elements) {
      		element.addEventListener('click', logMe);
      	}
      };
      

      А так у вас получается что-то странное:

      • getElementsByTagName создаёт живой список элементов по тегу, и если у вас нет намерения создавать новые кнопки в процессе итерации по другим кнопкам, его использовать, пожалуй, не стоит.

      • let elements = ... как бы говорит нам, что мы собираемся elements где-то мутировать, но нигде этого не происходит

      • в i < elements.length на каждой итерации length не бесплатный, он действительно идёт в DOM перезапрашивать текущее количество тегов, хотя, казалось бы, зачем тормоза разводить, если можно посчитать один раз

      • мы всё равно итерируемся по элементам и не пользуемся индексами, но зачем-то используется legacy цикл

      • this.log.bind(this) зачем-то создаёт новые идентичные объекты на каждой итерации с разными ссылками


  1. OldCold
    00.00.0000 00:00

    Самое удивительное, когда респондент хорошо отвечает на вопросы об особенностях работы фреймворков, но не может ответить на базовые вопросы по Javascript. И тут уж каких только оправданий не услышишь! Вероятно, я зануда, но у меня в голове не укладывается, как, например, можно считаться хорошим разработчиком на React, если ты банально не знаешь Javascript?

    Элементарно! Чтобы водить авто тебе не нужно знать как он устроен.


    Также не обязательно знать инструментарий, которым ты НЕ пользуешься. Вместо этого можно совершенствовать знания инструментария, которым ты пользуешься. А 90% ситуаций из статьи НЕ пройдут strict mode, линтер, typescript и ревью.


    1. vasyakolobok77
      00.00.0000 00:00

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


  1. leonofv
    00.00.0000 00:00
    +1

    нада вторым постом


  1. valex83
    00.00.0000 00:00
    +4

    Пардон, но почему "curly braces" не написать как "фигурные скобки"?


  1. cjdieic37
    00.00.0000 00:00
    +1

    Спасибо. Выкладывайте методичку!


  1. Val_SA
    00.00.0000 00:00

    Короче, как я понял, все проблемы в JS из за this))))

    P.S. Не кидайтесь помидорами это шутка


    1. Alexandroppolus
      00.00.0000 00:00

      Проблемы больше из-за того, что (специальный) аргумент функции зачем-то называют "контекстом", и это всех запутывает.


  1. demsp
    00.00.0000 00:00

    var уже не используется в современных скриптах?


    1. savostin
      00.00.0000 00:00
      +1

      Я Вас удивлю, наверное:


      1. maeris
        00.00.0000 00:00
        +1

        W3schools не имеют никакого отношения к w3c, например.


    1. maeris
      00.00.0000 00:00
      +1

      var используется там, где нужна производительность. На создание дополнительных областей видимости для let/const нужны ресурсы.


    1. interhin
      00.00.0000 00:00
      +1

      В легаси коде разве что, хотя мб есть индивидумы которые как-то используют особенности var.


  1. rqdkmndh
    00.00.0000 00:00

    А почему про Temporal Dead Zone не упомянули, когда шла речь об областях видимости? На собесах этим вопросом могут каждого второго уложить, если не каждого первого.

    func();
    // ReferenceError
    let Name = "Ivan";
    function func() {
        console.log(`${ Name }, hello!`);
    }

    не могут объяснить, почему ошибка.


    1. valera545
      00.00.0000 00:00

      Странно, тут-то всё на виду. Вообще, последовательность определений сущностей в JS проста — всё по порядку, кроме объявленных функций.


    1. slca
      00.00.0000 00:00

      Это скорее вопрос про всплытие function declaration и порядок выполнения, чем про область видимости.


  1. Ellen-jagger
    00.00.0000 00:00

    спасибо огромное вашейкоманде за продуланную работу.

    а можно продолжение тоже выложить?)


  1. demimurych
    00.00.0000 00:00
    -1

    Все это конечно же имеет право на существование, тем более если это кому-то делает жизнь проще.

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

    Под спойлер я положил несоответствия, описание которых, возможно, окажутся полезными тем, кто действительно преследует цель понять JavaScript.

    Заметки

    В чём заключаются отличия переменных, созданных с помощью конструкции let и с помощью конструкции var? Здорово, что все знают, что отличие в области видимости.

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

    В рамках которой, заявлена концепция Host - Realm - Enviroment. Которая, на мой взгляд, намного доходчевее описывает работу с идентификаторами в JS, нежели устоявшийся в среде программистов жаргон областей видимости.

     

    Итак, какие же бывают области видимости?
    Глобальная область видимости.
    Функциональная область видимости ― это область видимости, ограниченная curly braces при декларации функций.
    Блочная область видимости

    Даже если рассматривать работу с идентификаторами в JS, с точки зрения "областей видимости", Автор сильно упрощает утверждая, что функциональная область видимости ограничена curly braces. В чем можно убедиться на простом примере:

    ( theVar) => ( ) => ( ) => theVar;

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

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

    2. Требует уточнения формулировка того - чем является наш скрипт? Это классический скрипт или это модуль? Или может быть это ServiceWorker?

    3. Как следствие, упущением следует считать забытую область видимости в пределах модуля.

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

     

    Так как мы говорим о фронтенд, то глобальная область видимости уже будет содержать такой объект, как window

    Объект window безусловно является одним из самых часто встречаемых объектов в случае, когда программист работает с фронтендом в браузере.

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

    Тем более что чем выше квалификация специалиста, тем больше он сталкивается именно с ними: SharedWorkerGlobalScope, WorkerGlobalScope, DedicatedWorkerGlobalScope, ServiceWorkerGlobalScope и т.д.

     

    Но на самом деле Javascript не был бы Javascript, если бы не был полон сюрпризов. 

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

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

    Что не удивительно, когда узнаешь, что фундаментальные нормы языка вдохновлены паттернами, которым на момент их внедрения в JS было по 20 и больше лет.

     

    можно создавать переменные и без конструкции var, let. Они автоматически становятся параметрами объекта window.

    Нет не становятся. Не один идентификатор, который заявлен при помощи Let/Const Declaration, ни при каких условиях не становится property глобального обьекта. В чем можно убедиться и на простом примере:

    var theVar=1;
    let theLet=1;
    console.log( theVar, theLet); // 1 1
    console.log(window['theVar'], window['theLet']); // 1 undefined
    console.log(globalThis['theVar'], globalThis['theLet']);// 1 undefined

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

     

    Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на Javascript. Код всегда выполняется внутри некоего контекста.

    Начали за здравие, потому как сказали все в соответствии со спецификацией, а дальше продолжили за упокой

     

    Существует в общей сложности три типа контекста, хотя на практике мы работаем чаще всего с двумя первыми:
    Глобальный контекст выполнения, Контекст выполнения функции, Контекст выполнения функции eval.

    Конечно же ничего подобного в JS нет. То есть все Execution Context формируются совершенно одинаково вне зависимости от того для чего они создавались.

    Отличаются они только тем, какое окружение Environment подключено к running execution context. При этом, в рамках одного и того же running execution context окружение может меняться и в случае RunTime семнатики выполняемого в нем Statement.

     

    This ― это ключевое слово, зарезервированное движком Javascript, при обращении к которому мы можем получить значение, зависящее от текущего контекста выполнения.

    this действительно зависит от running execution context, но только в той его части, что именно в момент RunTime Semantics (то есть именно в момент выполнения того или иного выражения) this может быть установлен.

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

     

    Чему же равно this? И вот это, пожалуй, самый интересный вопрос.

    Это очень простой вопрос, если знать как в рамках спецификации работает JavaScript. В stric режиме this всегда undefined. И может быть установлен только в одном случае - в случае вычисления MemberExpression выражения, которое является частью CallExpression выражения.

    Кажется страшно сложным? Ничуть если понимать что MemberExpression это любое выражение которое вычисляет доступ к property объекта:

    obj.prop;
    obj['prop'];

    а CallExpression, это банальный вызов функции:

    func();
    obj.prop();
    obj[‘prop’]();

    Складываем первое MemberExpression и второе CallExpression и получаем ответ: связывание this происходит только в случае вызова функции в дот (или аналогичной нотации). И будет оно связано с тем, что было перед точкой. Например:

    obj.prop(); // this будет связан с obj
    obj['prop'](); // this будет связан с obj 
    let func = obj.prop;
    func(); // this останется связан со значением, которое было связано с ним до вызова функции. Так как вызов был не в дот нотации. Как и любой другой вызов функции или метода.

    Как итог - очень просто запомнить: в текущей спецификации языка JS, this может быть установлен либо явным образом используя методы apply call bind, либо образом при котором функция или метод вызывается как property (dot нотация) обьекта. 
     

    Функции конструкторы, [...] return тут только для наглядности, так как функция конструктор по умолчанию возвращает создаваемый объект

    Следует явным образом обозначить, что объект связанный с this в функции конструктора, возвращается только в случае, если в функции конструктора return либо отсутствует вообще, либо возвращает что-то отличное от Object.

    То есть в случае если конструктор возвращает посредством return statement какой либо объект, то все манипуляции с this не имеют никакого значения.

     

    Вспомним, что у нас также появились классы в Javascript, и они работают не совсем как функции‑конструкторы. Это позволяет неявно добавить методы не самому объекту, созданному на основе класса, а его прототипу, что для нас является разницей в вызове.

    Это не добавляет методы его прототипу, а создает новый прототип который является первым объектом в цепочке прототипов. Что абсолютно идентично, если бы Вы заявили все те-же методы, как prototype.property для функции конструктора.

       

    Однако в консоли видно, что значение this всё ещё является ссылкой на создаваемый объект.

    По той простой причине, которую я описал выше. А именно: согласно спецификации, this связывается со значением в момент вызова и только в случае MemberExpression (дот нотации).

    То есть когда Вы пишите выражение anyObj.anyProp() то для любой функции anyProp вне зависимости от того, каким образом она будет вызвана, this будет связан с anyObj.

    То есть неважно - это функция в цепочке прототипов, или это функция как метот самого обьекта, или это вообще прокси - this будет связан с anyProp.

     

    Вызов функции как обработчика события DOM

    Поведение этого вызова, никак не лимитируется JavaScript. И описывается только спецификацией того API, которое связано с этим вызовом.

    В данном случае - это API стандарта HTML5, которое устанавливает значение this в значение элемента к которому привязано событие, что является нарушением спецификации JS.

    Правильным вызовом было бы как раз вызов привязанной функции без установки this. Тем не менее, поведение API никак не регламентируется спецификацией JS и называется host implementation. Иными словами - host может делать все что он хочет.

     

    IIFE - immediately invoked function expression

    ( function(){
      console.log(this === window)
    })() 

    Очередной жаргон, который не имеет ничего общего со спецификацией.

    В рамках спецификации - это обычный function expression который является частью CallExpression. Термин immediately invoked function expression пример безграмотного, с точки зрения архитектуры языка, сленга.

     

    При определённых условиях контекст легко потерять и потом сидеть и ломать голову, почему же так произошло.

    Контекст невозможно потерять по двум причинам:

    1. в JS явным образом, управлять Execution Context - нельзя. Исключение - в Non Strict Mode используя with statement

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

     class Hero {
       // [...] ненужный код поскипан
       asyncLog() {
         setTimeout(this.log, 5000)
       }
     } 
    const batman = new Hero('Batman')
    batman.log()
    batman.asyncLog()

    И вот она — магия во всей красе. Мы его потеряли. Хотя, казалось бы, асинхронный лог не делает ничего сверхъестественного, кроме как выполняет тот же log, но с задержкой.

    Никто ничего не потерял. Вы в setTimeout передали ссылку на функцию this.log которая по истечении таймера и вызвалась как функция.  То есть она была вызвана без dot нотации, которая является единственной возможностью установить this.  

     

    По умолчанию внутри window.setTimeout() this устанавливается в объект window.

    Это не так. SetTimeout вызвал то, что ему передали, строго в соответствии со спецификацией JS. А именно функцию asyncLog.

     

    Arrow functions и их отношения с контекстом.
    А вот уже ES6 представила нам новую возможность борьбы за this.

    Это откровенная чушь. Arrow function создавались для возможности максимально эффективной реализации FP парадигмы программирования. В рамках которой, даже пользуясь жаргоном автора - никакого this быть не может. 

     

    Все дело в том, что в отличие от обычных, они не создают собственного контекста.

    Еще как создают. Только с одним но - в рамках runtime semantics вызов arrow function не приводит к связыванию this.

    Говоря колхозным языком, когда происходит вызов arrow function - то с this вообще ничего не происходит. То есть если Вы внтури arrow function используете this, то получаете доступ к тому значению, которое связано с this в рамках общих правил работы с идентификаторами, а именно произойдет поиск по цепочке окружений, которые являются родительскими для текущего окружения.

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