Skip to content

Latest commit

 

History

History
1133 lines (764 loc) · 81.2 KB

File metadata and controls

1133 lines (764 loc) · 81.2 KB

Статья из "Вы не знаете JS: this и Прототипы Объектов"

Одним из наиболее запутанных механизмов в Javascript является ключевое слово this. Это специальное ключевое слово идентификатор, которое автоматически определяется внутри области видимости каждой функции, но к чему именно оно относится сбивает с толку даже опытных Javascript-разработчиков.

Любая достаточно продвинутая технология неотличима от магии. -- Артур Си. Клэрк

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

Примечание: Слово "this" — это достаточно распространенное местоимение в общих беседах. Поэтому, может быть очень сложно, особенно на словах, определить используем мы "this" как местоимение или же используем его, чтобы ссылаться на данное ключевое слово. Для ясности, я всегда буду использовать this для ссылки на специальное ключевое слово, а "this" или this или this в остальных случаях.

Зачем нужен this?

Раз механизм this такой запутанный даже для опытных JavaScript-разработчиков, можно задаться вопросом, а точно ли он полезный? Может у него больше недостатков, чем достоинств? Перед тем, как перейти к тому как он работает, мы должны проанализировать зачем он нужен.

Давайте попытаемся проиллюстрировать мотивацию и полезность механизма this:

function identify() {
	return this.name.toUpperCase();
}

function speak() {
	var greeting = "Hello, I'm " + identify.call( this );
	console.log( greeting );
}

var me = {
	name: "Kyle"
};

var you = {
	name: "Reader"
};

identify.call( me ); // KYLE
identify.call( you ); // READER

speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER

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

Этот фрагмент кода позволяет функциям identify() и speak() быть переиспользованными с разными объектами контекста (me и you), а не требовать новой версии функции для каждого объекта.

Вместо того, чтобы полагаться на this, вы могли бы явно передать объект контекста функциям identify() и speak().

function identify(context) {
	return context.name.toUpperCase();
}

function speak(context) {
	var greeting = "Hello, I'm " + identify( context );
	console.log( greeting );
}

identify( you ); // READER
speak( me ); // Hello, I'm KYLE

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

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

Заблуждения

Мы скоро объясним как this на самом деле работает, но сначала мы должны рассеять несколько заблуждений о том, как он на самом деле не работает.

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

Сама функция

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

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

Разработчики, незнакомые с механизмами JavaScript, часто думают, что ссылка на функцию как на объект (все функции в JavaScript являются объектами!) позволяет хранить состояния (значения в свойствах) между вызовами функций. Хотя это, конечно, возможно, но это имеет некоторые ограничения в использовании, остаток книги будет повествовать о многих других шаблонах для лучшего хранения состояния, чем объект функции.

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

Рассмотрим следующий код, где мы попытаемся отследить сколько раз функция (foo) была вызвана:

function foo(num) {
	console.log( "foo: " + num );

	// Отслеживаем сколько раз `foo` была вызвана
	this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
	if (i > 5) {
		foo( i );
	}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// Сколько раз была вызвана `foo`?
console.log( foo.count ); // 0 -- WTF?

foo.count до сих пор равен 0, даже не смотря на то, что 4 инструкции console.log очевидно показывают, что foo(..) на самом деле была вызвана 4 раза. Разочарование происходит от слишком буквального толкования того, что означает thisthis.count++).

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

Примечание: ответственный разработчик в этом месте должен спросить: "Если я увеличил свойство count, но оно не то, которое я ожидал, то какое count было мной увеличено?". На самом деле, если он копнет глубже, он обнаружит что случайно создал глобальную переменную count(смотрите в главе 2 как это произошло!), а её текущим значением является NaN. Конечно, после того, как он определит это, у него появится совсем другой ряд вопросов: "почему она стала глобальной и почему она имеет значение NaN, вместо правильного значения счетчика?". (см. главу 2).

Вместо того, чтобы остановиться на этом месте и копнуть глубже, чтобы узнать почему ссылка this не ведет себя как ожидалось, большинство разработчиков просто откладывают проблему целиком и ищут другие решения, например, создают другой объект для хранения свойства count:

function foo(num) {
	console.log( "foo: " + num );

	// отслеживаем сколько раз вызывалась `foo`
	data.count++;
}

var data = {
	count: 0
};

var i;

for (i=0; i<10; i++) {
	if (i > 5) {
		foo( i );
	}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// сколько раз вызывалась `foo`?
console.log( data.count ); // 4

Хоть это и верно, что этот подход "решает" проблему, к сожалению, это просто игнорирование реальной проблемы — недостатка понимания того, что значит this и как он работает и вместо этого возвращение в зону комфорта более простого механизма: области видимости.

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

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

Рассмотрим эти 2 функции:

function foo() {
	foo.count = 4; // `foo` ссылается на саму себя
}

setTimeout( function(){
	// анонимная функция (без имени), не может
	// ссылаться на себя
}, 10 );

В первой функции вызывалась "именованная функция", foo — это ссылка, которая может быть использована для ссылки на функцию из самой себя.

Но во втором примере функция обратного вызова, передаваемая в setTimeout(..), не имела имени идентификатора (так называемая "анонимная функция"), так что у неё нет правильного пути чтобы обратиться к её объекту.

Примечание: Старомодная, но ныне устаревшая и неиспользуемая ссылка arguments.callee внутри функции также указывает на объект функции, которая в данный момент выполняется. Эта ссылка обычно используется как возможность получить объект анонимной функции изнутри этой функции. Лучший подход, однако, состоит в том, чтобы избежать использования анонимных функций, по крайней мере тех, которые требуют обращения к себе изнутри, и вместо них использовать именованные функции. arguments.callee устарела и не должна использоваться.

Таким образом, другое решение нашего примера — это использовать идентификатор foo как ссылку на объект функции в каждом месте и вообще не использовать this, и это работает:

function foo(num) {
	console.log( "foo: " + num );

	// следим, сколько раз вызывается функция
	foo.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
	if (i > 5) {
		foo( i );
	}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// сколько раз `foo` была вызвана?
console.log( foo.count ); // 4

Однако, этот подход также является уклонением от фактического понимания this, и полностью зависит от области видимости переменной foo.

Еще один путь решения проблемы - это заставить this действительно указывать на объект функции foo:

function foo(num) {
	console.log( "foo: " + num );

	// следим, сколько раз вызывается функция
	// Заметьте: `this` теперь действительно ссылается на `foo`, это основано на том,
	// как `foo` вызывается (см. ниже)
	this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
	if (i > 5) {
		// используя `call(..)` мы гарантируем что `this`
		// ссылается на объект функции (`foo`) изнутри
		foo.call( foo, i );
	}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// сколько раз `foo` была вызвана?
console.log( foo.count ); // 4

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

Это область видимости функции

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

Для ясности, this, в любом случае, не ссылается на область видимости функции. Это правда, что внутри область видимости имеет вид объекта со свойствами для каждого определенного значения. Но "объект" области видимости не доступен в JavaScript коде. Это внутренняя часть механизма реализации языка (интерпретатора).

Рассмотрим код, который пытается (и безуспешно!) перейти границу и использовать this неявно ссылаясь на область видимости функции:

function foo() {
	var a = 2;
	this.bar();
}

function bar() {
	console.log( this.a );
}

foo(); //undefined

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

Во-первых, попытка ссылаться на функцию bar() как this.bar(). Это почти наверняка случайность, что это работает, но мы коротко объясним как это работает позже. Наиболее естественным путем вызвать bar() было бы опустить предшествующий this. и просто сделать ссылку на идентификатор.

Однако, разработчик, который писал этот код, пытался использовать this, чтобы создать мост между областями видимости foo() и bar() так, чтобы bar() получила доступ к переменной a внутри области видимости foo(). Не всякий мост возможен. Вы не можете использовать ссылку this, чтобы найти что-нибудь в области видимости. Это невозможно.

Каждый раз, когда вы чувствуете, что вы смешиваете поиски в области видимости с this, напоминайте себе: это не мост.

Что же такое this?

Оставив ошибочные предположения, давайте обратим наше внимание на то, как механизм this действительно работает.

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

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

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

Обзор (TL;DR)

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

Чтобы понять что такое this, вам сначала нужно понять чем this не является, несмотря на любые предположения или заблуждения, которые могут тянуть вас вниз. this — это не ссылка функции на саму себя и это не ссылка на область видимости функции.

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

Точка вызова

Чтобы понять привязку this, мы должны понять что такое точка вызова: это место в коде, где была вызвана функция (не там, где она объявлена). Мы должны исследовать точку вызова, чтобы ответить на вопрос: на что же этот this указывает?

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

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

Продемонстрируем стек вызовов и точку вызова:

function baz() {
    // стек вызовов: `baz`
    // поэтому наша точка вызова — глобальная область видимости

    console.log( "baz" );
    bar(); // <-- точка вызова для `bar`
}

function bar() {
    // стек вызовов: `baz` -> `bar`
    // поэтому наша точка вызова в `baz`

    console.log( "bar" );
    foo(); // <-- точка вызова для `foo`
}

function foo() {
    // стек вызовов: `baz` -> `bar` -> `foo`
    // поэтому наша точка вызова в `bar`

    console.log( "foo" );
}

baz(); // <-- точка вызова для `baz`

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

Примечание: Вы можете мысленно визуализировать стек вызовов посмотрев цепочку вызовов функций в том порядке, в котором мы это делали в комментариях в коде выше. Но это утомительно и чревато ошибками. Другой путь посмотреть стек вызовов — это использование инструмента отладки в вашем браузере. Во многих современных настольных браузерах есть встроенные инструменты разработчика, включающие JS-отладчик. В вышеприведенном коде вы могли бы поставить точку остановки в такой утилите на первой строке функции foo() или просто вставить оператор debugger; в первую строку. Как только вы запустите страницу, отладчик остановится в этом месте и покажет вам список функций, которые были вызваны, чтобы добраться до этой строки, каковые и будут являться необходимым стеком вызовов. Таким образом, если вы пытаетесь выяснить привязку this, используйте инструменты разработчика для получения стека вызовов, затем найдите второй элемент стека от его вершины и это и будет реальная точка вызова.

Ничего кроме правил

Теперь обратим наш взор на то, как точка вызова определяет на что будет указывать this во время выполнения функции.

Вам нужно изучить точку вызова и определить какое из 4 правил применяется. Сначала разъясним каждое из 4 правил по отдельности, а затем проиллюстрируем их порядок приоритета, для случаев когда к точке вызова могут применяться несколько правил сразу.

Привязка по умолчанию

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

Рассмотрим такой код:

function foo() {
	console.log( this.a );
}

var a = 2;

foo(); // 2

Первая вещь, которую можно отметить, если вы еще не сделали этого, то, что переменные, объявленные в глобальной области видимости, как например var a = 2, являются синонимами глобальных свойств-объектов с таким же именем. Они не являются копиями друг друга, они и есть одно и то же. Представляйте их как две стороны одной монеты.

Во-вторых, видно, что когда вызывается foo() this.a указывает на нашу глобальную переменную a. Почему? Потому что в этом случае, для this применяется привязка по умолчанию при вызове функции и поэтому this указывает на глобальный объект.

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

Когда включен strict mode, объект 'global' не подпадает под действие привязки по умолчанию, поэтому в противоположность обычному режиму this устанавливается в undefined.

function foo() {
	"use strict";

	console.log( this.a );
}

var a = 2;

foo(); // TypeError: `this` is `undefined`

Едва уловимая, но важная деталь: даже если все правила привязки this целиком основываются на точке вызова, глобальный объект подпадает под привязку по умолчанию только если содержимое foo() не выполняется в режиме strict mode; Состояние strict mode в точке вызова foo() не имеет значения.

function foo() {
	console.log( this.a );
}

var a = 2;

(function(){
	"use strict";

	foo(); // 2
})();

Примечание: К намеренному смешиванию включения и выключения strict mode в коде обычно относятся неодобрительно. Вся программа пожалуй должна быть либо строгой, либо нестрогой. Однако, иногда вы подключаете сторонние библиотеки, в которых этот режим строгости отличается от вашего, поэтому нужно отнестись с вниманием к таким едва уловимым деталям совместимости.

Неявная привязка

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

Рассмотрим:

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};

obj.foo(); // 2

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

Однако, точка вызова использует контекст obj, чтобы ссылаться на функцию, поэтому можно сказать, что объект obj "владеет" или "содержит" ссылку на функцию в момент вызова функции.

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

Поскольку obj является this для вызова foo(), this.a — синоним obj.a.

Только верхний/последний уровень ссылки на свойство объекта в цепочке имеет значение для точки вызова. Например:

function foo() {
	console.log( this.a );
}

var obj2 = {
	a: 42,
	foo: foo
};

var obj1 = {
	a: 2,
	obj2: obj2
};

obj1.obj2.foo(); // 42

Неявно потерянный

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

Представим такой код:

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};

var bar = obj.foo; // ссылка/алиас на функцию!

var a = "ой, глобальная"; // `a` также и свойство глобального объекта

bar(); // "ой, глобальная"

Несмотря на то, что bar по всей видимости ссылка на obj.foo, фактически, это на самом деле другая ссылка на саму foo. Более того, именно точка вызова тут имеет значение, а точкой вызова является bar(), который является прямым непривязанным вызовом, а следовательно применяется привязка по умолчанию.

Более неочевидный, более распространенный и более неожиданный путь получить такую ситуацию когда мы предполагаем передать функцию обратного вызова:

function foo() {
	console.log( this.a );
}

function doFoo(fn) {
	// `fn` — просто еще одна ссылка на `foo`

	fn(); // <-- точка вызова!
}

var obj = {
	a: 2,
	foo: foo
};

var a = "ой, глобальная"; // `a` еще и переменная в глобальном объекте

doFoo( obj.foo ); // "ой, глобальная"

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

Что если функция, в которую вы передаете функцию обратного вызова, не ваша собственная, а встроенная в язык? Никакой разницы, такой же результат.

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};

var a = "ой, глобальная"; // `a` еще и переменная в глобальном объекте

setTimeout( obj.foo, 100 ); // "ой, глобальная"

Поразмышляйте над этой грубой теоретической псевдо-реализацией setTimeout(), которая есть в качестве встроенной в JavaScript-среде:

function setTimeout(fn,delay) {
	// подождать (так или иначе) `delay` миллисекунд
	fn(); // <-- точка вызова!
}

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

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

Явная привязка

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

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

У "всех" функций в языке есть несколько инструментов, доступных для них (через их [[Прототип]], о котором подробности будут позже), которые могут оказаться полезными в решении этой задачи. Говоря конкретнее, у функций есть методы call(..) и apply(..) . Технически, управляющие среды JavaScript иногда обеспечивают функции, которые настолько специфичны, что у них нет такой функциональности. Но таких мало. Абсолютное большинство предоставляемых функций и конечно все функции, которые создаете вы сами, безусловно имеют доступ к call(..) и apply(..).

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

Представим такой код:

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2
};

foo.call( obj ); // 2

Вызов foo с явной привязкой посредством foo.call(..) позволяет нам указать, что this будет obj.

Если в качестве привязки this вы передадите примитивное значение (типа string, boolean или number), то это примитивное значение будет обернуто в свою объектную форму (new String(..), new Boolean(..) или new Number(..) соответственно). Часто это называют "упаковка".

*Примечание: * В отношении привязки this call(..) и apply(..) идентичны. Они по-разному ведут себя с дополнительными параметрами, но мы не будем сейчас на этом останавливаться.

К сожалению, явная привязка сама по себе все-таки не предлагает никакого решения для указанной ранее проблемы "потери" функцией ее привязки this, либо оставляет это на усмотрение фреймворка.

Жесткая привязка

Но поиграв с вариациями на тему явной привязки на самом деле можно получить желаемое. Пример:

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2
};

var bar = function() {
	foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` жестко привязывает `this` в `foo` к `obj`
// поэтому его нельзя перекрыть
bar.call( window ); // 2

Давайте изучим как работает этот вариант. Мы создаем функцию bar(), которая внутри вручную вызывает foo.call(obj), таким образом принудительно вызывая foo с привязкой obj для this. Неважно как вы потом вызовете функцию bar, она всегда будет вручную вызывать foo с obj. Такая привязка одновременно явная и сильная, поэтому мы называем ее жесткой привязкой.

Самый типичный способ обернуть функцию с жесткой привязкой — создать сквозную обертку, передающую все параметры и возвращающую полученное значение:

function foo(something) {
	console.log( this.a, something );
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = function() {
	return foo.apply( obj, arguments );
};

var b = bar( 3 ); // 2 3
console.log( b ); // 5

Еще один способ выразить этот шаблон — создать переиспользуемую вспомогательную функцию:

function foo(something) {
	console.log( this.a, something );
	return this.a + something;
}

// простая вспомогательная функция `bind`
function bind(fn, obj) {
	return function() {
		return fn.apply( obj, arguments );
	};
}

var obj = {
	a: 2
};

var bar = bind( foo, obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

Поскольку жесткая привязка — очень распространеный шаблон, он есть как встроенный инструмент в ES5: Function.prototype.bind, а используется вот так:

function foo(something) {
	console.log( this.a, something );
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..) возвращает новую функцию, в которой жестко задан вызов оригинальной функции с именно тем контекстом this, который вы указываете.

Примечание: Начиная с ES6, в функции жесткой привязки, выдаваемой bind(..), есть свойство .name, наследуемое от исходной функции. Например: у bar = foo.bind(..) должно быть в bar.name значение "bound foo", которое является названием вызова функции, которое должно отражаться в стеке вызовов.

"Контексты" в вызовах API

Функции многих библиотек, и разумеется многие встроенные в язык JavaScript и во внешнее окружение функции, предоставляют необязательный параметр, обычно называемый "контекст", который спроектирован как обходной вариант для вас, чтобы не пользоваться bind(..), чтобы гарантировать, что ваша функция обратного вызова использует данный this.

Например:

function foo(el) {
	console.log( el, this.id );
}

var obj = {
	id: "awesome"
};

// используем `obj` как `this` для вызовов `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome  2 awesome  3 awesome

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

Привязка new

Четвертое и последнее правило привязки this потребует от нас переосмысления самого распространенного заблуждения о функциях и объектах в JavaScript.

В традиционных классо-ориентированных языках, "конструкторы" — это особые методы, связанные с классами, таким образом, что когда создается экземпляр класса с помощью операции new, вызывается конструктор этого класса. Обычно это выглядит как-то так:

something = new MyClass(..);

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

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

Например, функция Number(..) действует как конструктор, цитируя спецификацию ES5.1:

15.7.2 Конструктор Number

Когда Number вызывается как часть выражения new, оно является конструктором: оно инициализирует только что созданный объект.

Так что, практически любая старенькая функция, включая встроенные объектные функции, такие как Number(..) (см. главу 3), могут вызываться с new перед ними и это превратит такой вызов функции в вызов конструктора. Это важное, но едва уловимое различие: нет такой вещи как "функции-конструкторы", а скорее есть вызовы, конструирующие из функций.

Когда функция вызывается с указанием перед ней new, также известный как вызов конструктора, автоматически выполняются следующие вещи:

  1. Создается новенький объект (т.е. конструируется) прямо из воздуха
  2. Только что сконструированный объект связывается с [[Прототипом]]
  3. Только что сконструированный объект устанавливается как привязка this для этого вызова функции
  4. За исключением тех случаев, когда функция возвращает свой собственный альтернативный объект, вызов функции с new автоматически вернет только что сконструированный объект.

Пункты 1, 3 и 4 применимы к нашему текущему обсуждению. Сейчас мы пропустим пункт 2 и вернемся к нему в главе 5.

Взглянем на такой код:

function foo(a) {
	this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

Вызывая foo(..) с new впереди нее, мы конструируем новый объект и устанавливаем этот новый объект как this для вызова foo(..). Таким образом new — единственный путь, которым this при вызове функции может быть привязан. Мы называем это привязкой new.

Всё по порядку

Итак, теперь мы раскрыли 4 правила привязки this в вызовах функций. Всё, что вам нужно сделать — это найти точку вызова и исследовать ее, чтобы понять какое правило применяется. Но что если к точке вызова можно применить несколько соответствующих правил? Должен быть порядок очередности применения этих правил, а потому далее мы покажем в каком порядке применяются эти правила.

Думаю, совершенно ясно, что привязка по умолчанию имеет самый низкий приоритет из четырех. Поэтому мы отложим ее в сторону.

Что должно идти раньше: неявная привязка или явная привязка? Давайте проверим:

function foo() {
	console.log( this.a );
}

var obj1 = {
	a: 2,
	foo: foo
};

var obj2 = {
	a: 3,
	foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

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

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

function foo(something) {
	this.a = something;
}

var obj1 = {
	foo: foo
};

var obj2 = {};

obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

Хорошо, привязка new более приоритетна, чем неявная привязка. Но как вы думаете: привязка new более или менее приоритетна, чем явная привязка?

Примечание: new и call/apply не могут использоваться вместе, поэтому new foo.call(obj1) не корректно, чтобы сравнить напрямую привязку new с явной привязкой. Но мы все-таки можем использовать жесткую привязку, чтобы проверить приоритет этих двух правил.

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

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

Давайте проверим:

function foo(something) {
	this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

Ого! bar жестко связан с obj1, но new bar(3) не меняет obj1.a на значение 3 что было бы ожидаемо нами. Вместо этого жестко связанныйobj1) вызов bar(..) может быть перекрыт с new. Поскольку был применен new, обратно мы получили новый созданный объект, который мы назвали baz, и в результате видно, что в baz.a значение 3.

Это должно быть удивительно с учетом ранее рассмотренной "фальшивой" вспомогательной функции привязки:

function bind(fn, obj) {
	return function() {
		fn.apply( obj, arguments );
	};
}

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

Но встроенная Function.prototype.bind(..) из ES5 — более сложная, даже очень на самом деле. Вот (немного отформатированный) полифиллинг кода, предоставленный со страницы MDN для функции bind(..):

if (!Function.prototype.bind) {
	Function.prototype.bind = function(oThis) {
		if (typeof this !== "function") {
			// наиболее подходящая вещь в ECMAScript 5
			// внутренняя функция IsCallable
			throw new TypeError( "Function.prototype.bind - what " +
				"is trying to be bound is not callable"
			);
		}

		var aArgs = Array.prototype.slice.call( arguments, 1 ),
			fToBind = this,
			fNOP = function(){},
			fBound = function(){
				return fToBind.apply(
					(
						this instanceof fNOP &&
						oThis ? this : oThis
					),
					aArgs.concat( Array.prototype.slice.call( arguments ) )
				);
			}
		;

		fNOP.prototype = this.prototype;
		fBound.prototype = new fNOP();

		return fBound;
	};
}

Примечание: Полифиллинг bind(..), показанный выше, отличается от встроенной bind(..) в ES5, учитывающей функции жесткой привязки, которые используются с new (см. ниже почему это может быть полезно). Поскольку полифиллинг не может создавать функцию без .prototype так, как это делает встроенная утилита, есть едва уловимый окольный путь, чтобы приблизиться к такому же поведению. Двигайтесь осторожно, если планируете использовать new вместе с функцией жесткой привязки и полагаетесь на этот полифиллинг.

Часть, которая позволяет перекрыть new:

this instanceof fNOP &&
oThis ? this : oThis

// ... and:

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

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

Почему перекрытие операцией new жесткой привязки может быть полезным?

Основная причина такого поведения — чтобы создать функцию (которую можно использовать вместе с new для конструирования объектов), которая фактически игнорирует жесткую привязку this, но которая инициализирует некоторые или все аргументы функции. Одной из возможностей bind(..) является умение сделать аргументы, переданные после после аргумента, привязки this, стандартными аргументами по умолчанию для предшествующей функции (технически называемое "частичным применением", которое является подмножеством "карринга").

Пример:

function foo(p1,p2) {
	this.val = p1 + p2;
}

// используем здесь `null`, т.к. нам нет дела до 
// жесткой привязки `this` в этом сценарии, и она 
// будет переопределена вызовом с операцией `new` в любом случае!
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2

Определяем this

Теперь можно кратко сформулировать правила для определения this по точке вызова функции, в порядке их приоритета. Зададим вопросы в том же порядке и остановимся как только будет применено первое же правило.

  1. Функция вызвана с new (привязка new)? Раз так, то this — новый сконструированный объект.

    var bar = new foo()

  2. Функция вызвана с call или apply (явная привязка), даже скрыто внутри жесткой привязки в bind? Раз так, this — явно указанный объект.

    var bar = foo.call( obj2 )

  3. Функция вызвана с контекстом (неявная привязка), иначе называемым как владеющий или содержащий объект? Раз так, this является тем самым объектом контекста.

    var bar = obj1.foo()

  4. В противном случае, будет this по умолчанию (привязка по умолчанию). В режиме strict mode, это будет undefined, иначе будет объект global.

    var bar = foo()

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

Исключения привязок

Как обычно, из "правил" есть несколько исключений.

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

Проигнорированный this

Если вы передаете null или undefined в качестве параметра привязки this в call, apply или bind, то эти значения фактически игнорируются, а взамен к вызову применяется правило привязки по умолчанию.

function foo() {
	console.log( this.a );
}

var a = 2;

foo.call( null ); // 2

Зачем вам бы понадобилось намеренно передавать что-то подобное null в качестве привязки this?

Довольно распространено использовать apply(..) для распаковки массива значений в качестве параметров вызова функции. Аналогично и bind(..) может каррировать параметры (предварительно заданные значения), что может быть очень полезно.

function foo(a,b) {
	console.log( "a:" + a + ", b:" + b );
}

// распакуем массив как параметры
foo.apply( null, [2, 3] ); // a:2, b:3

// каррируем с помощью `bind(..)`
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

Обa этих инструмента требуют указания привязки this в качестве первого параметра. Если рассматриваемым функциям не важен this, то вам нужно -значение-заменитель, и null — это похоже разумный выбор, как мы видели выше.

Примечание: В этой книге мы не уделим этому внимания, но в ES6 есть операция расширения ..., которая дает возможности синтаксически "развернуть" массив как параметры без необходимости использования apply(..), например как в foo(...[1,2]), что равносильно foo(1,2) — синтаксически избегая привязки this, раз она не нужна. К сожалению, в ES6 нет синтаксической замены каррингу, поэтому параметр this вызова bind(..) все еще требует внимания.

Однако, есть некоторая скрытая "опасность" в том, чтобы всегда использовать null, когда вам не нужна привязка this. Если вы когда-нибудь воспользуетесь этим при вызове функции (например, функции сторонней библиотеки, которой вы не управляете) и эта функция все-таки воспользуется ссылкой на this, сработает правило привязки по умолчанию, что повлечет за собой ненамеренно ссылку (или еще хуже, мутацию!) на объект global (window в браузере).

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

Более безопасный this

Пожалуй в некоторой степени "более безопасная" практика — передавать особым образом настроенный объект для this, который гарантирует отсутствие побочных эффектов в вашей программе. Заимствуя терминологию из сетевых (и военных) технологий, мы можем создать объект "DMZ" (демилитаризованной зоны (de-militarized zone)) — не более чем полностью пустой, неделегированный (см. главы 5 и 6) объект.

Если всегда передавать DMZ-объект для привязок this, которые не требуются, то мы можем быть уверены в том, что любое скрытое/неожидаемое использование this будет ограничено пустым объектом, который защитит объект global нашей программы от побочных эффектов.

Поскольку этот объект совершенно пустой, лично я люблю давать его переменной имя ø (математический символ пустого множества в нижнем регистре). На многих клавиатурах (как например US-раскладка на Mac), этот символ легко можно ввести с помощью +o (option+o). В некоторых системах есть возможность назначать горячие клавиши на определенные символы. Если вам не нравится символ ø или на вашей клавиатуре сложно набрать такой символ, вы конечно же можете назвать переменную как вам угодно.

Как бы вы ни назвали ее, самый простой путь получить абсолютно пустой объект — это Object.create(null) (см. главу 5). Object.create(null) — похож на { }, но без передачи Object.prototype, поэтому он "более пустой", чем просто { }.

function foo(a,b) {
	console.log( "a:" + a + ", b:" + b );
}

// наш пустой DMZ-объект
var ø = Object.create( null );

// распаковываем массив как параметры
foo.apply( ø, [2, 3] ); // a:2, b:3

// каррируем с помощью `bind(..)`
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

Не только функционально "безопаснее", но еще и стилистически выгоднее использовать ø, что семантически отражает желаение "Я хочу, чтобы this был пустым" немного точнее, чем null. Но опять таки, называйте свой DMZ-объект как хотите.

Косвенность

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

Самый распространенный путь появления косвенных ссылок — при присваивании:

function foo() {
	console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2

Результатом выражения присваивания p.foo = o.foo будет всего лишь ссылка на внутренний объект функции. В силу этого, настоящая точка вызова - это просто foo(), а не p.foo() или o.foo() как вы могли бы предположить. Согласно вышеприведенным правилам будет применено правило привязки по умолчанию.

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

Смягчение привязки

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

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

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

if (!Function.prototype.softBind) {
	Function.prototype.softBind = function(obj) {
		var fn = this,
			curried = [].slice.call( arguments, 1 ),
			bound = function bound() {
				return fn.apply(
					(!this ||
						(typeof window !== "undefined" &&
							this === window) ||
						(typeof global !== "undefined" &&
							this === global)
					) ? obj : this,
					curried.concat.apply( curried, arguments )
				);
			};
		bound.prototype = Object.create( fn.prototype );
		return bound;
	};
}

Инструмент softBind(..), представленный здесь, работает подобно встроенному в ES5 инструменту bind(..), за исключением нашего поведения мягкой привязки. Он делает обертку указанной функции с логикой, которая проверяет this в момент вызова и если это global или undefined, использует указанное заранее альтернативное умолчание (obj). В противном случае this остается как есть. Также этот инструмент дает возможность опционального карринга (см. ранее обсуждениеbind(..)).

Продемонстрируем его в действии:

function foo() {
   console.log("name: " + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2   <---- смотрите!!!

fooOBJ.call( obj3 ); // name: obj3   <---- смотрите!

setTimeout( obj2.foo, 10 ); // name: obj   <---- возврат к мягкой привяке

Для мягкопривязанной версии функции foo() можно вручную привязать this к obj2 или obj3 как показано выше, но он возвращается к obj в случае применения привязки по умолчанию.

Лексический this

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

Стрелочные функции обозначаются не ключевым словом function, а операцией =>, так называемой "жирной стрелкой". Вместо использования четырех стандартных this-правил, стрелочные функции заимствуют привязку this из окружающей (функции или глобальной) области видимости.

Проиллюстрируем лексическую область видимости стрелочной функции:

function foo() {
	// возвращаем стрелочную функцию
	return (a) => {
		// Здесь `this` лексически заимствован из `foo()`
		console.log( this.a );
	};
}

var obj1 = {
	a: 2
};

var obj2 = {
	a: 3
};

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, а не 3!

Стрелочная функция, созданная в foo(), лексически захватывает любой this в foo() во время ее вызова. Поскольку в foo() this был привязан к obj1, bar (ссылка на возвращаемую стрелочную функцию) также будет с привязкой this к obj1. Лексическая привязка стрелочной функции не может быть перекрыта (даже с помощью new!).

Самый распространенный вариант использования стрелочной функции — обычно при использовании функций обратного вызова, таких как обработчики событий или таймеры:

function foo() {
	setTimeout(() => {
		// Здесь `this` лексически заимствован из `foo()`
		console.log( this.a );
	},100);
}

var obj = {
	a: 2
};

foo.call( obj ); // 2

Несмотря на то, что стрелочные функции предоставляют альтернативу применению bind(..) к функции, чтобы гарантировать определенный this, что может выглядеть весьма привлекательно, важно отметить, что они фактически запрещают традиционный механизм this в пользу более понятной лексической области видимости. До ES6, у нас уже был довольно распространенный шаблон для выполнения такой задачи, который по сути почти неотличим от сущности стрелочных функций ES6:

function foo() {
	var self = this; // лексический захват `this`
	setTimeout( function(){
		console.log( self.a );
	}, 100 );
}

var obj = {
	a: 2
};

foo.call( obj ); // 2

В том время как self = this и стрелочные функции обе кажутся хорошим "решением" при нежелании использовать bind(..), они фактически убегают от this вместо того, чтобы понять и научиться использовать его.

Если вы застали себя пишущим код в стиле this, но большую часть или всё время вы сводите на нет механизм this с помощью трюков лексической конструкции self = this или стрелочной функции, возможно вам следует сделать что-то одно из этого:

  1. Использовать только лексическую область видимости и забыть о фальшивости кода в стиле this.

  2. Полностью научиться использовать механизмы this-стиля, включая применение bind(..), где необходимо, и попытаться избегать трюков "лексического this" с помощью self = this и стрелочной функции.

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

Обзор

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

  1. Вызвана с new? Используем только что созданный объект.

  2. Вызвана с помощью call или apply (или bind)? Используем указанный объект.

  3. Вызвана с объектом контекста, владеющего вызовом функции? Используем этот объект контекста.

  4. По умолчанию: undefined в режиме strict mode, в противном случае объект global.

Остерегайтесь случайного/неумышленного вызова с применением правила привязки по умолчанию. В случаях, когда вам нужно "безопасно" игнорировать привязку this, "DMZ"-объект, подобный ø = Object.create(null), — хорошая замена, защищающая объект global от непредусмотренных побочных эффектов.

Вместо четырех стандартных правил привязки стрелочные функции ES6 используют лексическую область видимости для привязки this, что означает, что они заимствуют привязку this (какой бы она ни была) от вызова своей окружающей функции. Они по существу являются синтаксической заменой self = this в до-ES6 коде.