Как стать автором
Обновить

Комментарии 43

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

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

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

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

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

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

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

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

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

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

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

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 свой путь , с оглядкой на актуальные проблемы и потребности

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

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

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

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

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

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

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

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

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

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

( theIncapsulatedVariable ) => () => theIncapsulatedVariable

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

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

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

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

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

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

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

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

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

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

function foo() {
var x;
bar();
x = 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;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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) зачем-то создаёт новые идентичные объекты на каждой итерации с разными ссылками

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ну вот! Страху нагнали в конце. Страх в начале надо ставить - дольше эмоции и стимул к разбору явления

Жесть какая... И весь интернет на этом написан...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий