Методы объектов, this

До этого мы говорили об объекте лишь как о хранилище значений. Теперь пойдём дальше и поговорим об объектах как о сущностях со своими функциями («методами»).

Методы у объектов

При объявлении объекта можно указать свойство-функцию, например:

var user = {
  name: 'Василий',

  // метод
  sayHi: function() {
    alert( 'Привет!' );
  }

};

// Вызов
user.sayHi();

Свойства-функции называют «методами» объектов. Их можно добавлять и удалять в любой момент, в том числе и явным присваиванием:

var user = {
  name: 'Василий'
};

user.sayHi = function() { // присвоили метод после создания объекта
  alert('Привет!');
};

// Вызов метода:
user.sayHi();

Доступ к объекту через this

Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызов user.sayHi() может захотеть вывести имя пользователя.

Для доступа к текущему объекту из метода используется ключевое слово this.

Значением this является объект перед «точкой», в контексте которого вызван метод, например:

var user = {
  name: 'Василий',

  sayHi: function() {
    alert( this.name );
  }
};

user.sayHi(); // sayHi в контексте user

Здесь при выполнении функции user.sayHi() в this будет храниться ссылка на текущий объект user.

Вместо this внутри sayHi можно было бы обратиться к объекту, используя переменную user:

...
  sayHi: function() {
    alert( user.name );
  }
...

…Однако, такое решение нестабильно. Если мы решим скопировать объект в другую переменную, например admin = user, а в переменную user записать что-то другое – обращение будет совсем не по адресу:

var user = {
  name: 'Василий',

  sayHi: function() {
    alert( user.name ); // приведёт к ошибке
  }
};

var admin = user;
user = null;

admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка!

Использование this гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана.

Через this метод может не только обратиться к любому свойству объекта, но и передать куда-то ссылку на сам объект целиком:

var user = {
  name: 'Василий',

  sayHi: function() {
    showName(this); // передать текущий объект в showName
  }
};

function showName(namedObj) {
  alert( namedObj.name );
}

user.sayHi(); // Василий

Подробнее про this

Любая функция может иметь в себе this. Совершенно неважно, объявлена ли она в объекте или отдельно от него.

Значение this называется контекстом вызова и будет определено в момент вызова функции.

Например, такая функция, объявленная без объекта, вполне допустима:

function sayHi() {
  alert( this.firstName );
}

Эта функция ещё не знает, каким будет this. Это выяснится при выполнении программы.

Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный this:

var user = { firstName: "Вася" };
var admin = { firstName: "Админ" };

function func() {
  alert( this.firstName );
}

user.f = func;
admin.g = func;

// this равен объекту перед точкой:
user.f(); // Вася
admin.g(); // Админ
admin['g'](); // Админ (не важно, доступ к объекту через точку или квадратные скобки)

Итак, значение this не зависит от того, как функция была создана, оно определяется исключительно в момент вызова.

Значение this при вызове без контекста

Если функция использует this – это подразумевает работу с объектом. Но и прямой вызов func() технически возможен.

Как правило, такая ситуация возникает при ошибке в разработке.

При этом this получает значение window, глобального объекта:

function func() {
  alert( this ); // выведет [object Window] или [object global]
}

func();

Таково поведение в старом стандарте.

А в режиме use strict вместо глобального объекта this будет undefined:

function func() {
  "use strict";
  alert( this ); // выведет undefined (кроме IE9-)
}

func();

Обычно если в функции используется this, то она, всё же, служит для вызова в контексте объекта, так что такая ситуация – скорее исключение.

Ссылочный тип

Контекст this никак не привязан к функции, даже если она создана в объявлении объекта. Чтобы this передался, нужно вызвать функцию именно через точку (или квадратные скобки).

Любой более хитрый вызов приведёт к потере контекста, например:

var user = {
  name: "Вася",
  hi: function() { alert(this.name); },
  bye: function() { alert("Пока"); }
};

user.hi(); // Вася (простой вызов работает)

// а теперь вызовем user.hi или user.bye в зависимости от имени
(user.name == "Вася" ? user.hi : user.bye)(); // undefined

В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. Но this при этом теряется.

Если хочется понять, почему, то причина кроется в деталях работы вызова obj.method().

Он ведь, на самом деле, состоит из двух независимых операций: точка . – получение свойства и скобки () – его вызов (предполагается, что это функция).

Функция, как мы говорили раньше, сама по себе не запоминает контекст. Чтобы «донести его» до скобок, JavaScript применяет «финт ушами» – точка возвращает не функцию, а значение специального «ссылочного» типа Reference Type.

Этот тип представляет собой связку «base-name-strict», где:

  • base – как раз объект,
  • name – имя свойства,
  • strict – вспомогательный флаг для передачи use strict.

То есть, ссылочный тип (Reference Type) – это своеобразное «три-в-одном». Он существует исключительно для целей спецификации, мы его не видим, поскольку любой оператор тут же от него избавляется:

  • Скобки () получают из base значение свойства name и вызывают в контексте base.
  • Другие операторы получают из base значение свойства name и используют, а остальные компоненты игнорируют.

Поэтому любая операция над результатом операции получения свойства, кроме вызова, приводит к потере контекста.

Аналогично работает и получение свойства через квадратные скобки obj[method].

Задачи

важность: 5

Каким будет результат? Почему?

var arr = ["a", "b"];

arr.push(function() {
  alert( this );
})

arr[2](); // ?

Вызов arr[2]() – это обращение к методу объекта obj[method](), в роли obj выступает arr, а в роли метода: 2.

Поэтому, как это бывает при вызове функции как метода, функция arr[2] получит this = arr и выведет массив:

var arr = ["a", "b"];

arr.push(function() {
  alert( this );
})

arr[2](); // "a","b",function
важность: 2

Каков будет результат этого кода?

var obj = {
  go: function() { alert(this) }
}

(obj.go)()

P.S. Есть подвох :)

Ошибка!

Попробуйте:

var obj = {
  go: function() {
    alert(this)
  }
}

(obj.go)() // error!

Причем сообщение об ошибке в большинстве браузеров не даёт понять, что на самом деле не так.

Ошибка возникла из-за того, что после объявления obj пропущена точка с запятой.

JavaScript игнорирует перевод строки перед скобкой (obj.go)() и читает этот код как:

var obj = { go:... }(obj.go)()

Интерпретатор попытается вычислить это выражение, которое обозначает вызов объекта { go: ... } как функции с аргументом (obj.go). При этом, естественно, возникнет ошибка.

важность: 3

Вызовы (1) и (2) в примере ниже работают не так, как (3) и (4):

"use strict"

var obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();            // (1) object

(obj.go)();          // (2) object

(method = obj.go)();      // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

В чём дело? Объясните логику работы this.

  1. Обычный вызов функции в контексте объекта.

  2. То же самое, скобки ни на что не влияют.

  3. Здесь не просто вызов obj.method(), а более сложный вызов вида (выражение).method(). Такой вызов работает, как если бы он был разбит на две строки:

    f = obj.go; // сначала вычислить выражение
    f();             // потом вызвать то, что получилось

    При этом f() выполняется как обычная функция, без передачи this.

  4. Здесь также слева от точки находится выражение, вызов аналогичен двум строкам.

В спецификации это объясняется при помощи специального внутреннего типа Reference Type.

Если подробнее – то obj.go() состоит из двух операций:

  1. Сначала получить свойство obj.go.
  2. Потом вызвать его как функцию.

Но откуда на шаге 2 получить this? Как раз для этого операция получения свойства obj.go возвращает значение особого типа Reference Type, который в дополнение к свойству go содержит информацию об obj. Далее, на втором шаге, вызов его при помощи скобок () правильно устанавливает this.

Любые другие операции, кроме вызова, превращают Reference Type в обычный тип, в данном случае – функцию go (так уж этот тип устроен).

Поэтому получается, что (method = obj.go) присваивает в переменную method функцию go, уже без всякой информации об объекте obj.

Аналогичная ситуация и в случае (4): оператор ИЛИ || делает из Reference Type обычную функцию.

важность: 5

Что выведет alert в этом коде? Почему?

var user = {
  firstName: "Василий",

  export: this
};

alert( user.export.firstName );

Ответ: undefined.

var user = {
  firstName: "Василий",

  export: this // (*)
};

alert( user.export.firstName );

Объявление объекта само по себе не влияет на this. Никаких функций, которые могли бы повлиять на контекст, здесь нет.

Так как код находится вообще вне любых функций, то this в нём равен window (в браузере так всегда для кода вне функций, вне зависимости от use strict).

Получается, что в строке (*) мы имеем export: window, так что далее alert(user.export.firstName) выводит свойство window.firstName, которое не определено.

важность: 5

Что выведет alert в этом коде? Почему?

var name = "";

var user = {
  name: "Василий",

  export: function() {
    return this;
  }

};

alert( user.export().name );

Ответ: Василий.

Вызов user.export() использует this, который равен объекту до точки, то есть внутри user.export() строка return this возвращает объект user.

В итоге выводится свойство name объекта user, равное "Василий".

важность: 5

Что выведет alert в этом коде? Почему?

var name = "";

var user = {
  name: "Василий",

  export: function() {
    return {
      value: this
    };
  }

};

alert( user.export().value.name );

Ответ: Василий.

Во время выполнения user.export() значение this = user.

При создании объекта { value: this }, в свойство value копируется ссылка на текущий контекст, то есть на user.

Получается что user.export().value == user.

var name = "";

var user = {
  name: "Василий",

  export: function() {
    return {
      value: this
    };
  }

};

alert( user.export().value == user ); // true
важность: 5

Создайте объект calculator с тремя методами:

  • read() запрашивает prompt два значения и сохраняет их как свойства объекта
  • sum() возвращает сумму этих двух значений
  • mul() возвращает произведение этих двух значений
var calculator = {
  ...ваш код...
}

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
Запустить демо

Открыть песочницу с тестами для задачи.

var calculator = {
  sum: function() {
    return this.a + this.b;
  },

  mul: function() {
    return this.a * this.b;
  },

  read: function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
}

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Открыть решение с тестами в песочнице.

важность: 2

Есть объект «лестница» ladder:

var ladder = {
  step: 0,
  up: function() { // вверх по лестнице
    this.step++;
  },
  down: function() { // вниз по лестнице
    this.step--;
  },
  showStep: function() { // вывести текущую ступеньку
    alert( this.step );
  }
};

Сейчас, если нужно последовательно вызвать несколько методов объекта, это можно сделать так:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

Модифицируйте код методов объекта, чтобы вызовы можно было делать цепочкой, вот так:

ladder.up().up().down().up().down().showStep(); // 1

Как видно, такая запись содержит «меньше букв» и может быть более наглядной.

Такой подход называется «чейнинг» (chaining) и используется, например, во фреймворке jQuery.

Решение состоит в том, чтобы каждый раз возвращать текущий объект. Это делается добавлением return this в конце каждого метода:

var ladder = {
  step: 0,
  up: function() {
    this.step++;
    return this;
  },
  down: function() {
    this.step--;
    return this;
  },
  showStep: function() {
    alert( this.step );
    return this;
  }
}

ladder.up().up().down().up().down().showStep(); // 1
Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.