В JavaScript ключевое слово this может быть очень коварно. Эта происходит из-за разного поведения функций в зависимости от способа их вызова. Что? Функции можно вызывать по-разному? Ага! Есть 4 основных способа вызова функций. Посмотрим, как работает каждый, и как они обходятся с this.
- Вызов метода
- Вызов функции
- Вызов конструктора
- Вызов через apply
Вызов метода
Первый способ использования функций выглядит наиболее привычно. При переходе с классического языка программирования (C#, Java) вы наверняка привыкли использовать классы как единственные строительные блоки программы. Ничто не может существовать вне класса. У вас есть функции только как методы классов. И абсолютно ясно, к чему относится this.
Отлично, таким же образом работает вывоз метода в JavaScript. Если вы определите объект Object, и сделаете одно из его свойств функцией, затем вызовете этот метод, тогда this будет объектом Object, к которому принадлежит эта функция.
var Obj = { something: 'строка', changeSomething: function() { //this == Obj return this.something.toUpperCase(); } }; Obj.changeSomething(); //вернёт 'СТРОКА'
Когда бы мы ни вызвали changeSomething объекта Obj таким способом, мы знаем, что это будет Obj, и нет необходимости постоянно повторять название объекта. Это также будет верно, если мы клонируем объект. this будет указывать на объект, владеющий методом.
Вызов функции
Второй способ может выглядеть немного странно, и его поведение также может показаться странным, но, как только вы лучше поймете особенности JavaScript, вы увидите логику такого поведения (точно-точно!). Следующий способ обычно называется «вызов функции», он заключается в определении функции самой по себе, а не как метода класса. В JavaScript это возможно, поскольку функции являются объектами первого рода. Их можно передавать как аргументы, сохранять в переменных, и делать прочие классные вещи. Предположим, что мы всё это знаем.
Когда мы объявляем функцию, не связанную с объектом, при вызове такой функции this связывается с глобальным объектом (в большинстве случаев это window). В основном потому, что каждая функция должна быть привязана к объекту. Если она не привязана к объекту другими способами вызова, то по-умолчанию этим объектом является объект верхнего уровня. Это кажется достаточно ясным при объявлении глобальных функций.
var logWindow = function() { //this == window console.log(this); }; // оба способа одинаковы function logWindow() { //this == window console.log(this); } logWindow() // выводит объект window в Firebug
Тем не менее, такое поведение сохраняется где бы вы не определили такую функцию, а не только на глобальном уровне. Это создаёт людям большие проблемы. Предположим, что у нас есть функция объекта, и вызовем метод. Мы знаем, что this будет связано с объектом-владельцем. Но JavaScript позволяет объявлять функции внутри функций. Это можно делать для создания обработчиков событий, обратных вызовов, или просто служебных функций, которые используются много раз в методе. И внутри таких функций this больше не связан с объектом-владельцем. На самом деле, внутри этих областей видимости this связывается с глобальным объектом.
// упс var Thing = { name: 'Я принадлежал Thing', woops: function() { //this == Thing var concat = function(first,second) { //this == window this.name = first + second; }; concat(this.name, ', но только что изменил window!'); } }; Thing.woops(); //устанавливает window.name = 'Я принадлежал Thing, но только что изменил window!'
Здесь метод woops определяет внутреннюю функцию (что очевидно), помогающую получению результата. Он передаёт ей 2 строки и обращается к this, которым, как мы надеялись, является Thing. Но затем внутренняя функция concat изменяет свойство объекта this. Итак, в этой функции this привязано к window, так что мы начали портить свойства window. Просто замечательно.
Решение этой проблемы не такое уж и трудное. Большинство просто сохраняют this в другой переменной до вызова внутренней функции, например self, и затем, внутри используем self вместо this. Тот же пример с использованием этого способа:
//решение с self var Thing = { name: 'я принадлежал Thing', woops: function() { //this == Thing var self = this; var concat = function(first,second) { //this == window //self == Thing self.name = first + second; }; concat(this.name, ', и до сих пор принадлежу. Уау!'); } }; Thing.woops(); //устанавливает Thing.name = Я принадлежал Thing, и до сих пор принадлежу. Уау!'
Вызов конструктора
Третий путь — использовать функцию как конструктор — нарушает все правила предыдущих двух способов. Когда вы используете оператор new на функции, то во время выполнения этой функции this связан с только что созданным объектом. Этот объект получает прототип функции как прототип объекта. И любое использование this внутри этого конструктора будет изменять ваш новый объект во время его создания.
Этот способ довольно привычный, если вы привыкли к классическим языкам. Мы можем написать функцию, предназначенную для создания новых объектов. Эта функция будет выполнять команды на только что созданном объекте и вернёт его.
var Dog = function(name) { //this == совершенно новый object ({}); this.name = name; this.age = (Math.random() * 5) + 1; }; var myDog = new Dog('Спайк'); //myDog.name == 'Спайк' //myDog.age == 2 var yourDog = new Dog('Спот'); //yourDog.name == 'Спот' //yourDog.age == 4
Когда myDog присваевается new Dog, this применяется в отношении нового объекта myDog. Позже, когда мы присваеваем new Dog переменной yourDog, this применяется уже к yourDog. Ясно?
Изящность состоит в том, что такая функция может находиться где угодно. Это может быть внутренняя функция, или глобальная либо метод объекта. Когда вы используете принцип конструктора, изменяется способ работы функции.
Однако, нужно предостеречь. Если вы создали функцию-конструктор, и использовали ее, но случайно пропустили ключевое слово new, то механизм связывания этой функции будет нарушен. В других классических языках вы бы получили ошибку компилятора или что-нибудь подобное в таком случае. А в Javascript вы просто испортите объект, которому принадлежит функция, будь то какой-то определённый объект или window.
var Dog = function(name) { this.name = name; this.age = (Math.random() * 5) + 1; }; var notADog = Dog('Лэсси'); //notADog неопределен //window.name == 'Лэсси'; //window.age == 1;
В этом примере мы испортили window; и даже не получили ссылку на window в переменной notADog, поскольку наша функция Dog не возвращает значения обычным способом. Она возвращает значение только тогда, когда используется как конструктор.
Лично у меня с этим проблем нет, поскольку всякий раз, когда я хочу получить от функции новый объект, я пишу new на автомате. Старая привычка. Однако есть хорошая практика для смягчения этй проблемы — называть конструкторы с большой буквы, а все прочие функции с маленькой. Это поможет увидеть ошибку, если пропущено слово new.
Если вы все-таки иногда допускаете такой промах и портите объекты, которые не предполагали портить, вы можете добавить небольшой фрагмент к каждому своему конструктору, чтобы убедиться, что функция была вызвана через new.
// Проверка в конструкторе var Dog = function(name) { if(!(this instanceof Dog)) { throw new Error('Ты забыл "new", болван!'); } this.name = name; }; var notADog = Dog('Барт'); // вызовет ошибку
Вызов через apply
И наконец, способ точного управления значением this в функции, независимо от того, чем было бы оно по-умолчанию. Как я говорил выше, функции — это объекты, а это значит, что у них могут быть методы. У всех функций есть метод apply. При его использовании вызывается объект-владелец (функция), связывая указанный объект с this.
var addMessages = function(mess1, mess2, mess3) { // Обычно this == window this.message = mess1 + mess2 + mess3; }; var test = {}; var args = ['this ', 'будет', ' test']; addMessage.apply(test, args);
Хотя addMessage, будучи глобальной функцией, в обычной ситуации связана с window, при использовании метода apply мы можем передать ей другой объект для связывания. Второй аргумент при вызове apply — массив аргументов вызываемой функции.
Есть другой способ сделать почти то же самое — использовать метод call. Единственное заметное отличие состоит в том, что вместо одного массива аргументов вы передаёте их по отдельности.
addMessage.call(test, 'this ', 'будет', ' test');
Можно использовать эти методы у любой функции. Любой. Например, на методах или прототипах. Иногда можно видеть, как используется метод apply класса Array на аргументе-массиве, или на коллекции элементов, поскольку они не являются настоящими массивами и не имеют методов в своих прототипах. Но с использованием apply вы в любом случае можете применять к ним эти методы.
Заключение
Функции в Javascript очень забавные. Но без большого опыта работы с Javascript эти особенности могут поставить вас в тупик. Даже если вы уже писали на Javascript, вы можете не знать, почему некоторые вещи работают именно так, а не иначе. Знание этих аспектов однозначно помогло мне улучшить свой Javascript код.
Я узнал об этом из двух источников. Из моего опыта использования Javascript я узнал о всех 4х этих способах, и действительно понял приципы конструкторов и методов. Но остальные 2 были просто чем-то с чем нужно смириться. Тем, что помогло мне разобраться с остальным и даже дало мне немного больше по каждому способу, была книга Крокфорда «Javascript: The Good Parts». Я начал читать эту короткую книгу в начале года, и глава о функциях запомнилась мне больше всего.




"Их можно передавать как аргументы, сохранять в переменных, и делать прочие классные вещи."
Я бы сказал, не почти, а просто супер классные вещи ;)