Статьи

Раскрытие внутренней работы ключевого слова «this» в JavaScript

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