Skip to content

Latest commit

 

History

History
281 lines (191 loc) · 22 KB

File metadata and controls

281 lines (191 loc) · 22 KB

Вы не знаете JS: This и Прототипы Объектов

Глава 1: this (тут) или That (там)?

Одним из наиболее запутанных механизмов в 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 — это привязка, которая создается во время вызова функции, и на что она ссылается определяется тем, где и при каких условиях функция была вызвана.