Главная » Статьи » Мастер JavaScript: что там с this?

Мастер JavaScript: что там с this?

Мастер JavaScript: что там с this?

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

Если вы когда-либо ломали голову из-за this JavaScript, вы определенно не одиноки.

const thisGuard = { firstName: 'Barnie', rank: 'this-guard', identify() { console.log(`Hi, I'm ${this.firstName}, and I'm a ${this.rank}`); }, }; // Что будет выведено через 1 секунду? setTimeout(thisGuard.identify, 1000);

Можете ли вы определить, что выводится из фрагмента кода, приведенного выше? Спойлер: Hi, I’m undefined, and I’m a undefined

Вероятно, это не то, что было задумано, исходя того, как выглядит код. Почему это происходит? Потому this заблудился где-то по пути.

Так, что там с this?

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

Лексическая и динамическая области видимости

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

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

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

Давайте рассмотрим пример с этим псевдо-JavaScript и представим, что он использует динамическую область видимости.

function dynamicBuilding() { console.log('I can see you, ' + guyFromTheOuterScope);
} function hiFromTheBuilding() { dynamicBuilding();
} function helloFromTheBuilding() { const guyFromTheOuterScope = 'Wayne'; dynamicBuilding();
} hiFromTheBuilding(); // I can see you, undefined
helloFromTheBuilding(); // I can see you, Wayne

Из dynamicBuilding() мы пытаемся ссылаться на переменную, она не объявляется ни в своей собственной области, ни в той области, в которой она находится; глобальный охват.

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

Ключевым моментом здесь является то, что dynamicBuilding() ведет себя по-разному в зависимости от того, откуда она вызывается. Область действия — или контекст выполнения  - откуда вызывается функция, называется точкой вызова функции. Приведенный выше пример — это псевдо-JavaScript. JavaScript работает не так, и не будет выполняться, как в примере.

4 способа, которыми привязывается «this»

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

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

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

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

Есть 4 разных способа привязки this в JavaScript, и все они ведут себя по-разному:

Ключевое слово new

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

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

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

Это также дает нам и 4 ключевых вопроса, которые вы будете задавать себе, чтобы определить, что this указывает на тот или иной контекст. Давайте рассмотрим каждый из этих способов.

Ключевое слово new

При использовании this в функции, первое, о чем вам нужно спросить себя, использовали ли вы ключевое слово new при вызове функции.

function thisGuard() { this.firstName = 'Barnie'; this.rank = 'this-guard'; console.log(this);
} const guard = new thisGuard();
// {
// firstName: "Barnie",
// rank: "this-guard",
// }

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

Создает пустой, простой объект JavaScript.

Связывает этот объект с другим объектом.

Передает вновь созданный объект из шага 1 в качестве контекста this.

Возвращает this, если функция не возвращает собственный объект.

Как мы видим в приведенном выше примере, this будет ссылкой на объект, который создан при вызове функции thisGuard() с ключевым словом new. В ES6 были введены классы.

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

class thisGuard { constructor() { this.firstName = 'Barnie'; this.rank = 'this-guard'; console.log(this); }
} const guard = new thisGuard();
// {
// firstName: "Barnie"
// rank: "this-guard"
// }

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

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

function thisGuard() { this.firstName = 'Barnie'; this.rank = 'this-guard'; console.log(this);
} const retiredThisGuard = { age: 68, retired: true,
}; thisGuard.apply(retiredThisGuard);
// {
// firstName: "Barnie",
// rank: "this-guard",
// age: 68,
// retired: true,
// }

В приведенном выше примере мы используем явную привязку, вызывая функцию thisGuard(), таким образом используя метод apply(), который принадлежит Function.prototype.

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

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

thisGuard.apply(retiredThisGuard, [arg1, arg2, arg3]);
thisGuard.call(retiredThisGuard, arg1, arg2, arg3);

Другой случай явной привязки — использование метода bind(), который был представлен в ES5. Мы вернемся к примеру этого чуть позже.

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

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

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

const thisGuard = { firstName: 'Barnie', rank: 'this-guard', identify() { console.log(`Hi, I'm ${this.firstName}, and I'm a ${this.rank}`); },
}; thisGuard.identify();
// Hi, I'm Barnie, and I'm a this-guard

В этом случае identify() является методом объекта thisGuard, и this будет неявно привязан к этому объекту. Стоит заметить, что явная привязка имеет приоритет над неявной привязкой, а жесткая привязка имеет приоритет над явной привязкой.

const thisGuard = { firstName: 'Barnie', rank: 'this-guard', identify() { console.log(`Hi, I'm ${this.firstName}, and I'm a ${this.rank}`); },
}; const explicitThisGuard = { firstName: 'James', rank: 'explicit-this-guard',
}; const hardThisGuard = { firstName: 'Jannice', rank: 'hard-this-guard',
}; thisGuard.identify();
// Hi, I'm Barnie, and I'm a this-guard thisGuard.identify.apply(explicitThisGuard);
// Hi, I'm James, and I'm a explicit-this-guard thisGuard.identify.bind(hardThisGuard)();
// Hi, I'm Jannice, and I'm a hard-this-guard

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

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

Если мы вернемся к самому первому примеру из этой статьи в том виде, в котором он был изначально, вы сможете понять, почему выводится undefined вместо ожидаемого firstName и rank:

const thisGuard = { firstName: 'Barnie', rank: 'this-guard', identify() { console.log(`Hi, I'm ${this.firstName}, and I'm a ${this.rank}`); },
}; // What will be printed after 1 second?
setTimeout(thisGuard.identify, 1000);

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

Зная, как this работает в JavaScript, мы можем легко решить эту проблему путем жесткого связывания this с объектом thisGuard.

// We hard-bind 'this' to the 'thisGuard' object
// Problem solved!
setTimeout(thisGuard.identify.bind(thisGuard), 1000);

Выражения функций стрелок

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

guyFromTheStreet = 'Jonathan'; const regularFunction = function() { console.log(this.guyFromTheStreet);
}; const objectFromTheStreet = { guyFromTheStreet: 'Michael', callRegular: regularFunction,
}; regularFunction();
// Jonathan objectFromTheStreet.callRegular();
// Michael

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

guyFromTheStreet = 'Jonathan'; const arrowFunction = () => { console.log(this.guyFromTheStreet);
}; const objectFromTheStreet = { guyFromTheStreet: 'Michael', callArrow: arrowFunction,
}; arrowFunction();
// Jonathan objectFromTheStreet.callArrow();
// Jonathan

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

Заключение

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

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

Автор: Simon LH

Источник: https://itnext.io/

Редакция: Команда webformyself.