- Ключевое слово this в javascript — учимся определять контекст на практике
- 1. Теория
- 2. Разбираем задачу
- Вместо заключения
- Методы объекта, «this»
- Примеры методов
- Сокращённая запись метода
- Ключевое слово «this» в методах
- «this» не является фиксированным
- У стрелочных функций нет «this»
- Итого
- Задачи
- Использование «this» в литерале объекта
Ключевое слово this в javascript — учимся определять контекст на практике
По просьбам некоторых читателей решил написать топик про контекст в javascript. Новички javascript часто не понимают значение ключевого слова this в javascript. Данный топик будет интересен не только новичкам, а также тем, кто просто хочет освежить данный аспект в памяти. Посмотрите пример ниже. Если вы затрудняетесь ответить на вопрос «что будет выведено в логе» хотя бы в одном из пунктов или хотите просто посмотреть ответы — добро пожаловать под кат.
var f = function() < this.x = 5; (function() < this.x = 3; >)(); console.log(this.x); >; var obj = >; f(); new f(); obj.m(); new obj.m(); f.call(f); obj.m.call(f);
1. Теория
В отличие от многих других языков программирования ключевое слово this в javascript не привязывается к объекту, а зависит от контекста вызова. Для упрощения понимания будем рассматривать примеры применительно к браузеру, где глобальным объектом является window.
1.1. Простой вызов функции
В данном случае this внутри функции f равен глобальному объекту (например, в браузере это window, в Node.js — global).
Самовызывающиеся функции (self-invoking) работают по точно такому же принципу.
1.2. В конструкторе
function f() < this.x = 5; console.log(this === window); // false >var o = new f(); console.log(o.x === 5); // true
При вызове функции с использованием ключевого слова new функция выступает в роли конструктора, и в данном случе this указывает на создаваемый объект.
1.3. В методе объекта
var o = < f: function() < return this; >> console.log(o.f() === o); // true
Если функция запускается как свойство объекта, то в this будет ссылка на этот объект. При этом не имеет значения, откуда данная функция появилась в объекте, главное — как она вызывается, а именно какой объект стоит перед вызовом функции:
var o = < f: function() < return this; >> var o2 = ; console.log(o.f() === o);//true console.log(o2.f() === o2);//true
1.4. Методы apply, call
Методы apply и call позволяют задать контекст для выполняемой функции. Разница между apply и call — только в способе передачи параметров в функцию. Первый параметр обеих функций определяет контекст выполнения функции (то, чему будет равен this).
function f(a,b,c) < return a * b + c; >f.call(f, 1, 2, 3); // аргументы перечисляются через запятую; var args = [1,2,3]; f.apply(f, args); // // аргументы передаются в виде массива; // В обоих случаях вызовется функция f с аргументами a = 1, b = 2, c = 3;
function f() < >f.call(window); // this внутри функции f будет ссылаться на объект window f.call(f); //this внутри f будет ссылаться на f
function f() < console.log(this.toString()); // 123 >f.call(123); // this внутри функции f будет ссылаться на объект Number со значением 123
2. Разбираем задачу
Применим полученные знания к приведенной в начале топика задаче. Опять же для упрощения будем рассматривать примеры применительно к браузеру, где глобальным объектом является window.
2.1. f()
var f = function() < // Функция f вызывается с помощью простого вызова - f(), // поэтому this ссылается на глобальный объект this.x = 5; // window.x = 5; // В пункте 1.1 также указано, что в самовызывающихся функциях this также ссылается на глобальный объект (function() < this.x = 3; // window.x = 3 >)(); console.log(this.x); // console.log(window.x) >;
2.2. new f();
var f = function() < // Функция f вызывается с использованием ключевого слова new, // поэтому this ссылается на создаваемый объект (обозначим его как object) this.x = 5; // object.x = 5; // В пункте 1.1 также указано, что в самовызывающихся функциях this ссылается на глобальный объект (function() < this.x = 3; // window.x = 3 >)(); console.log(this.x); // console.log(object.x) >;
2.3. obj.m();
2.4. new obj.m();
2.5. f.call(f);
var f = function() < // Функция f вызывается с помощью метода call // первым параметром в call указана сама функция (точнее объект) f, поэтому // поэтому this ссылается на f this.x = 5; // f.x = 5; // В пункте 1.1 также указано, что в самовызывающихся функциях this ссылается на глобальный объект (function() < this.x = 3; // window.x = 3 >)(); console.log(this.x); // console.log(f.x) >;
2.6. obj.m.call(f);
Внимание: Если данный пример рассматривать отдельно от остальных, то в логе будет не 5, а undefined. Попробуйте внимательно разобрать пример и объяснить поведение
var f = function() < this.x = 5; (function() < this.x = 3; >)(); console.log(this.x); >; var obj = >; obj.m.call(f);
Вместо заключения
В статье я постарался описать, как работает ключевое слово this в javascript. В ближайшее время я скорее всего напишу статью, в которой описывается для чего нужно знать эти тонкости (например, jQuery.proxy)
P.S. Если вы заметили ошибки/неточности или хотите что-то уточнить/добавить — напишите в ЛС, поправлю.
Методы объекта, «this»
Объекты обычно создаются, чтобы представлять сущности реального мира, будь то пользователи, заказы и так далее:
// Объект пользователя let user = < name: "John", age: 30 >;
И так же, как и в реальном мире, пользователь может совершать действия: выбирать что-то из корзины покупок, авторизовываться, выходить из системы, оплачивать и т.п.
Такие действия в JavaScript представлены функциями в свойствах.
Примеры методов
Для начала давайте научим нашего пользователя user здороваться:
let user = < name: "John", age: 30 >; user.sayHi = function() < alert("Привет!"); >; user.sayHi(); // Привет!
Здесь мы просто использовали Function Expression (функциональное выражение), чтобы создать функцию приветствия, и присвоили её свойству user.sayHi нашего объекта.
Затем мы можем вызвать ee как user.sayHi() . Теперь пользователь может говорить!
Функцию, которая является свойством объекта, называют методом этого объекта.
Итак, мы получили метод sayHi объекта user .
Конечно, мы могли бы использовать заранее объявленную функцию в качестве метода, вот так:
let user = < // . >; // сначала, объявляем function sayHi() < alert("Привет!"); >// затем добавляем в качестве метода user.sayHi = sayHi; user.sayHi(); // Привет!
Когда мы пишем наш код, используя объекты для представления сущностей реального мира, – это называется объектно-ориентированным программированием или сокращённо: «ООП».
ООП является большой предметной областью и интересной наукой самой по себе. Как выбрать правильные сущности? Как организовать взаимодействие между ними? Это – создание архитектуры, и на эту тему есть отличные книги, такие как «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» авторов Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес или «Объектно-ориентированный анализ и проектирование с примерами приложений» Гради Буча, а также ещё множество других книг.
Сокращённая запись метода
Существует более короткий синтаксис для методов в литерале объекта:
// эти объекты делают одно и то же user = < sayHi: function() < alert("Привет"); >>; // сокращённая запись выглядит лучше, не так ли? user = < sayHi() < // то же самое, что и "sayHi: function()" alert("Привет"); > >;
Как было показано, мы можем пропустить ключевое слово «function» и просто написать sayHi() .
Нужно отметить, что эти две записи не полностью эквивалентны. Есть тонкие различия, связанные с наследованием объектов (что будет рассмотрено позже), но на данном этапе изучения это неважно. Почти во всех случаях сокращённый синтаксис предпочтителен.
Ключевое слово «this» в методах
Как правило, методу объекта обычно требуется доступ к информации, хранящейся в объекте, для выполнения своей работы.
Например, коду внутри user.sayHi() может потребоваться имя пользователя, которое хранится в объекте user .
Для доступа к информации внутри объекта метод может использовать ключевое слово this .
Значение this – это объект «перед точкой», который используется для вызова метода.
let user = < name: "John", age: 30, sayHi() < // "this" - это "текущий объект". alert(this.name); >>; user.sayHi(); // John
Здесь во время выполнения кода user.sayHi() значением this будет являться user (ссылка на объект user ).
Технически также возможно получить доступ к объекту без ключевого слова this , обратившись к нему через внешнюю переменную (в которой хранится ссылка на этот объект):
…Но такой код ненадёжен. Если мы решим скопировать ссылку на объект user в другую переменную, например, admin = user , и перезапишем переменную user чем-то другим, тогда будет осуществлён доступ к неправильному объекту при вызове метода из admin .
let user = < name: "John", age: 30, sayHi() < alert( user.name ); // приведёт к ошибке >>; let admin = user; user = null; // перезапишем переменную для наглядности, теперь она не хранит ссылку на объект. admin.sayHi(); // TypeError: Cannot read property 'name' of null
Если бы мы использовали this.name вместо user.name внутри alert , тогда этот код бы сработал.
«this» не является фиксированным
В JavaScript ключевое слово «this» ведёт себя иначе, чем в большинстве других языков программирования. Его можно использовать в любой функции, даже если это не метод объекта.
В следующем примере нет синтаксической ошибки:
Значение this вычисляется во время выполнения кода, в зависимости от контекста.
Например, здесь одна и та же функция назначена двум разным объектам и имеет различное значение «this» в вызовах:
let user = < name: "John" >; let admin = < name: "Admin" >; function sayHi() < alert( this.name ); >// используем одну и ту же функцию в двух объектах user.f = sayHi; admin.f = sayHi; // эти вызовы имеют разное значение this // "this" внутри функции - это объект "перед точкой" user.f(); // John (this == user) admin.f(); // Admin (this == admin) admin['f'](); // Admin (нет разницы между использованием точки или квадратных скобок для доступа к объекту)
Правило простое: если вызывается obj.f() , то во время вызова f , this – это obj . Так что, в приведённом выше примере это либо user , либо admin .
Мы даже можем вызвать функцию вообще без объекта:
function sayHi() < alert(this); >sayHi(); // undefined
В строгом режиме ( «use strict» ) в таком коде значением this будет являться undefined . Если мы попытаемся получить доступ к this.name – это вызовет ошибку.
В нестрогом режиме значением this в таком случае будет глобальный объект ( window в браузерe, мы вернёмся к этому позже в главе Глобальный объект). Это – исторически сложившееся поведение this , которое исправляется использованием строгого режима ( «use strict» ).
Обычно подобный вызов является ошибкой программирования. Если внутри функции используется this , тогда она ожидает, что будет вызвана в контексте какого-либо объекта.
Если вы до этого изучали другие языки программирования, то вы, вероятно, привыкли к идее «фиксированного this » – когда методы, определённые в объекте, всегда имеют this , ссылающееся на этот объект.
В JavaScript this является «свободным», его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а скорее от того, какой объект вызывает метод (какой объект стоит «перед точкой»).
Эта концепция вычисления this в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок.
Здесь наша позиция заключается не в том, чтобы судить, является ли это архитектурное решение в языке хорошим или плохим. Скоро мы поймем, как с этим работать, как получить выгоду и избежать проблем.
У стрелочных функций нет «this»
Стрелочные функции особенные: у них нет своего «собственного» this . Если мы ссылаемся на this внутри такой функции, то оно берётся из внешней «нормальной» функции.
Например, здесь arrow() использует значение this из внешнего метода user.sayHi() :
let user = < firstName: "Ilya", sayHi() < let arrow = () =>alert(this.firstName); arrow(); > >; user.sayHi(); // Ilya
Это особенность стрелочных функций. Она полезна, когда мы на самом деле не хотим иметь отдельное this , а скорее хотим взять его из внешнего контекста. Позже в главе Повторяем стрелочные функции мы увидим больше примеров на эту тему.
Итого
- Функции, которые находятся в свойствах объекта, называются «методами».
- Методы позволяют объектам «действовать»: object.doSomething() .
- Методы могут ссылаться на объект через this .
Значение this определяется во время исполнения кода.
- При объявлении любой функции в ней можно использовать this , но этот this не имеет значения до тех пор, пока функция не будет вызвана.
- Функция может быть скопирована между объектами (из одного объекта в другой).
- Когда функция вызывается синтаксисом «метода» – object.method() , значением this во время вызова является object .
Также ещё раз заметим, что стрелочные функции являются особенными – у них нет this . Когда внутри стрелочной функции обращаются к this , то его значение берётся извне.
Задачи
Использование «this» в литерале объекта
Здесь функция makeUser возвращает объект.
Каким будет результат при обращении к свойству объекта ref ? Почему?
function makeUser() < return < name: "John", ref: this >; > let user = makeUser(); alert( user.ref.name ); // Каким будет результат?
Ответ: ошибка.
function makeUser() < return < name: "John", ref: this >; > let user = makeUser(); alert( user.ref.name ); // Error: Cannot read property 'name' of undefined
Это потому, что правила, которые определяют значение this , никак не смотрят на объявление объекта. Важен лишь момент вызова.
Здесь значение this внутри makeUser() равно undefined , потому что оно вызывается как функция, а не через «точечный» синтаксис как метод.
Значение this одно для всей функции, блоки кода и объектные литералы на него не влияют.
Таким образом, ref: this фактически принимает текущее this функции makeUser() .
Мы можем переписать функцию и вернуть то же самое this со значением undefined :
function makeUser() < return this; // на этот раз нет литерала объекта >alert( makeUser().name ); // Error: Cannot read property 'name' of undefined
Как вы можете видеть, результат alert( makeUser().name ) совпадает с результатом alert( user.ref.name ) из предыдущего примера.
Вот противоположный случай:
function makeUser() < return < name: "John", ref() < return this; >>; > let user = makeUser(); alert( user.ref().name ); // John
Теперь это работает, поскольку user.ref() – это метод. И значением this становится объект перед точкой . .