Знание языка программирования не означает, что вы его понимаете или используете правильно. То же самое с JavaScript. Хотя это простой язык для изучения, есть много подводных камней для новичков, и даже для опытных программистов.
Одна вещь, которая смущает неопытных разработчиков, это то, как работает this
ключевое слово. Проще говоря, this
ссылочный псевдоним — он просто знает, на что именно он ссылается, это сложная часть.
Эта статья направлена на то, чтобы развеять путаницу и дать представление о внутренней работе this
ключевого слова.
Так что же это this
?
В двух словах, this
специальное ключевое слово-идентификатор, автоматически определяемое в области действия каждой функции, указывающее на «владельца» выполняемой функции. Но чтобы полностью понять его сложную природу, нам нужно ответить на два ключевых вопроса:
Как this
создано?
Каждый раз, когда вызывается функция JavaScript, создается новый объект, содержащий информацию о том, какие параметры были переданы, как была вызвана функция, откуда была вызвана функция и так далее. Одним из основных свойств этого объекта является ссылка this
, которая автоматически привязывается к объекту, функцией которого является метод.
Примечание: для любопытных это подробно описано в §10.4.3 Спецификации языка ECMAScript и разделах, на которые она ссылается.
var car = { brand: "Nissan", getBrand: function(){ console.log(this.brand); } }; car.getBrand(); // output: Nissan
В этом примере this
, используемый в this.brand
, является ссылкой на объект car
. Итак, this.brand
такой же, как car.brand
.
К чему this
относится?
Значение this
, переданное всем функциям, основано на контексте, в котором функция вызывается во время выполнения. Суть this
не в том, как и где объявляются функции, а в том, откуда они вызываются (т.е. в контексте).
Каждая строка кода JavaScript запускается в контексте выполнения . Объект, к которому this
относится, переопределяется каждый раз, когда вводится новый контекст выполнения, и остается фиксированным до тех пор, пока он не будет перемещен в другой контекст. Чтобы найти контекст выполнения (и this
привязку), нам нужно найти сайт вызова — место в коде, откуда вызывается функция (а не где она объявлена).
Продемонстрируем это на следующем примере:
var brand = 'Nissan'; var myCar = {brand: 'Honda'}; var getBrand = function() { console.log(this.brand); }; myCar.getBrand = getBrand; myCar.getBrand(); // output: Honda getBrand(); // output: Nissan
Несмотря на то, что myCar.getBrand()
и getBrand()
указывают на одну и ту же функцию, значение this
отличается, поскольку оно основано на контексте, в котором getBrand()
.
Как мы уже знаем, внутри функции this
связано с объектом, функцией которого является метод. В первом вызове функции объектом является myCar
, а во втором — объект window
( getBrand()
— то же самое, что window.getBrand()
). Таким образом, другой контекст дает другой результат.
Контексты вызова
Теперь давайте посмотрим, на что this
указывает, когда он помещается в разные контексты.
Глобальная сфера
Все среды выполнения JavaScript имеют уникальный объект, называемый глобальным объектом . В браузерах глобальным объектом является объект window
. В Node.js это называется global
объектом.
В глобальном контексте выполнения (вне какой-либо функции) this
относится к глобальному объекту, в строгом режиме или нет.
Локальная сфера
Внутри функции значение this
зависит от того, как вызывается функция. Есть три основных варианта:
Используется в простом вызове функции
Первый вариант — это вызов отдельной функции, где мы вызываем функцию напрямую.
function simpleCall(){ console.log(this); } simpleCall(); // output: the Window object
В этом случае значение this
не устанавливается вызовом. Так как код не выполняется в строгом режиме, значение this
всегда должно быть объектом, поэтому по умолчанию используется глобальный объект.
В строгом режиме значение this
остается неизменным при входе в контекст выполнения. Если он не определен, он остается неопределенным, как мы можем видеть в следующем примере:
function simpleCall(){ "use strict"; console.log(this); } simpleCall(); // output: undefined
Используется в методе объекта
Мы можем сохранить функцию в свойстве объекта, что превращает ее в метод, который мы можем вызвать через этот объект. Когда функция вызывается как метод объекта, this
значение устанавливается равным объекту, для которого вызывается метод.
var message = { content: "I'm a JavaScript Ninja!", showContent: function() { console.log(this.content); } }; message.showContent(); // output: I'm a JavaScript Ninja!
Здесь showContent()
является методом объекта message
, и поэтому this.content
равно message.content
.
Используется в функциях конструктора
Мы можем вызвать функцию через new
оператор. В этом случае функция становится конструктором — фабрикой для объектов. В отличие от простых вызовов функций и вызовов методов, описанных выше, вызов конструктора передает совершенно новый объект в качестве значения this
и неявно возвращает новый объект в качестве его результата.
Когда функция используется в качестве конструктора (с ключевым словом new
), this
значение привязывается к вновь созданному объекту. Если мы пропустим new
ключевое слово, то это будет обычная функция, которая будет указывать на объект window
.
function Message(content){ this.content = content; this.showContent = function(){ console.log(this.content); }; } var message = new Message("I'm JavaScript Ninja!"); message.showContent(); // output: I'm JavaScript Ninja!
В приведенном выше примере у нас есть функция конструктора с именем Message()
. Используя оператор new
мы создаем новый объект с именем message
. Мы также передаем функции конструктора строку, которую она устанавливает в качестве свойства content
нашего нового объекта. В последней строке кода мы видим, что эта строка успешно выводится, поскольку она указывает на вновь созданный объект, а не на саму функцию конструктора.
Как this
можно успешно манипулировать
В этом разделе мы рассмотрим некоторые встроенные механизмы для управления поведением this
.
В JavaScript все функции являются объектами, и поэтому они могут иметь методы. Два из этих методов, которыми обладают все функции, — apply () и call () . Мы можем использовать эти методы, чтобы изменить контекст на то, что нам нужно, и, таким образом, явно установить значение this
.
Метод apply()
принимает два аргумента: объект, для которого this
устанавливается, и (необязательный) массив аргументов для передачи в функцию.
Метод call()
работает точно так же, как apply()
, но мы передаем аргументы индивидуально, а не в массиве.
Давайте посмотрим на это в действии:
function warrior(speed, strength){ console.log( "Warrior: " + this.kind + ", weapon: " + this.weapon + ", speed: " + speed + ", strength: " + strength ); } var warrior1 = { kind: "ninja", weapon: "shuriken" }; var warrior2 = { kind: "samurai", weapon: "katana" }; warrior.call(warrior1, 9, 5); // output: Warrior: ninja, weapon: shuriken, speed: 9, strength: 5 warrior.apply(warrior2, [6, 10]); // output: Warrior: samurai, weapon: katana, speed: 6, strength: 10
Здесь у нас есть фабричная функция warrior()
, которая используется для создания разных типов воинов, используя разные объекты воинов. Таким образом, в этой фабричной функции this
будет указывать на различные объекты, которые мы передаем, используя call()
и / или apply()
.
В первом вызове функции мы используем метод call()
чтобы установить this
для объекта warrior1
, и передаем другие необходимые аргументы, разделенные запятыми. Во втором вызове функции мы делаем почти то же самое, но на этот раз мы warrior2
объект warrior2
и необходимые аргументы помещаются в массив.
Помимо apply()
и call()
ECMAScript 5 добавлен метод bind () , который также позволяет нам определять, какой конкретный объект будет связан с this
при вызове функции или метода. Давайте рассмотрим следующий пример:
function warrior(kind){ console.log( "Warrior: " + kind + ". Favorite weapon: " + this.weapon + ". Main mission: " + this.mission ); } var attributes = { weapon: "shuriken", mission: "espionage" }; var ninja = warrior.bind(attributes, "ninja"); ninja(); // output: Warrior: ninja. Favorite weapon: shuriken. Main mission: espionage
В этом примере метод bind()
используется аналогичным образом, но в отличие от методов call()
и apply()
warrior.bind()
создает новую функцию (с тем же телом и областью действия, что и warrior()
), а не изменение оригинальной функции warrior()
. Новая функция ведет себя так же, как и старая, но ее получатель привязан к объекту attributes
, а старая остается неизменной.
Резюме
Итак, это все. Это почти все, что вам нужно знать об this
ключевом слове, чтобы использовать его правильно и с большей уверенностью. Конечно, есть некоторые сложные моменты и некоторые общие проблемы, с которыми вы можете столкнуться по пути. Они будут рассмотрены в следующей статье, так что следите за обновлениями.