Skip to content

Latest commit

 

History

History
857 lines (577 loc) · 59.8 KB

File metadata and controls

857 lines (577 loc) · 59.8 KB

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

Глава 2: Весь this теперь приобретает смысл!

В главе 1 мы отбросили различные ложные представления о 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 коде.