Знание языка программирования не означает, что вы его понимаете или используете правильно. То же самое с 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 ключевом слове, чтобы использовать его правильно и с большей уверенностью. Конечно, есть некоторые сложные моменты и некоторые общие проблемы, с которыми вы можете столкнуться по пути. Они будут рассмотрены в следующей статье, так что следите за обновлениями.