244 lines
9.5 KiB
Markdown
244 lines
9.5 KiB
Markdown
# Методы объектов, this
|
||
|
||
До этого мы говорили об объекте лишь как о хранилище значений. Теперь пойдём дальше и поговорим об объектах как о сущностях со своими функциями ("методами").
|
||
[cut]
|
||
|
||
## Методы у объектов
|
||
|
||
При объявлении объекта можно указать свойство-функцию, например:
|
||
|
||
```js
|
||
//+ run
|
||
var user = {
|
||
name: 'Василий',
|
||
|
||
*!*
|
||
// метод
|
||
*/!*
|
||
sayHi: function() {
|
||
alert('Привет!');
|
||
}
|
||
|
||
};
|
||
|
||
*!*
|
||
// Вызов
|
||
user.sayHi();
|
||
*/!*
|
||
```
|
||
|
||
Свойства-функции называют "методами" объектов. Их можно добавлять и удалять в любой момент, в том числе и явным присваиванием:
|
||
|
||
```js
|
||
//+ run
|
||
var user = {
|
||
name: 'Василий'
|
||
};
|
||
|
||
*!*
|
||
user.sayHi = function() { // присвоили метод после создания объекта
|
||
alert('Привет!');
|
||
};
|
||
*/!*
|
||
|
||
// Вызов метода:
|
||
*!*user.sayHi();*/!*
|
||
```
|
||
|
||
## Доступ к объекту через this
|
||
|
||
Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызов `user.sayHi()` может захотеть вывести имя пользователя.
|
||
|
||
**Для доступа к текущему объекту из метода используется ключевое слово `this`**.
|
||
|
||
Значением `this` является объект перед "точкой", в контексте которого вызван метод, например:
|
||
|
||
```js
|
||
//+ run
|
||
var user = {
|
||
name: 'Василий',
|
||
|
||
sayHi: function() {
|
||
alert( *!*this.name*/!* );
|
||
}
|
||
};
|
||
|
||
user.sayHi(); // sayHi в контексте user
|
||
```
|
||
|
||
Здесь при выполнении функции `user.sayHi()` в `this` будет храниться ссылка на текущий объект `user`.
|
||
|
||
Вместо `this` внутри `sayHi` можно было бы обратиться к объекту, используя переменную `user`:
|
||
|
||
```js
|
||
...
|
||
sayHi: function() {
|
||
alert( *!*user.name*/!* );
|
||
}
|
||
...
|
||
```
|
||
|
||
...Однако, такое решение нестабильно. Если мы решим скопировать объект в другую переменную, например `admin = user`, а в переменную `user` записать что-то другое -- обращение будет совсем не по адресу:
|
||
|
||
```js
|
||
//+ run
|
||
var user = {
|
||
name: 'Василий',
|
||
|
||
sayHi: function() {
|
||
alert( *!*user.name*/!* ); // приведёт к ошибке
|
||
}
|
||
};
|
||
|
||
var admin = user;
|
||
user = null;
|
||
|
||
admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка!
|
||
```
|
||
|
||
Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана.
|
||
|
||
Через `this` метод может не только обратиться к любому свойству объекта, но и передать куда-то ссылку на сам объект целиком:
|
||
|
||
```js
|
||
//+ run
|
||
var user = {
|
||
name: 'Василий',
|
||
|
||
*!*
|
||
sayHi: function() {
|
||
showName(this); // передать текущий объект в showName
|
||
}
|
||
*/!*
|
||
};
|
||
|
||
function showName(namedObj) {
|
||
alert( namedObj.name );
|
||
}
|
||
|
||
user.sayHi(); // Василий
|
||
```
|
||
|
||
## Подробнее про this
|
||
|
||
Любая функция может иметь в себе `this`. Совершенно неважно, объявлена ли она в объекте или отдельно от него.
|
||
|
||
Значение `this` называется *контекстом вызова* и будет определено в момент вызова функции.
|
||
|
||
Например, такая функция, объявленная без объекта, вполне допустима:
|
||
|
||
```js
|
||
function sayHi() {
|
||
alert( *!*this.firstName*/!* );
|
||
}
|
||
```
|
||
|
||
Эта функция ещё не знает, каким будет `this`. Это выяснится при выполнении программы.
|
||
|
||
**Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный `this`:**
|
||
|
||
```js
|
||
//+ run
|
||
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`, глобального объекта:
|
||
|
||
```js
|
||
//+ run
|
||
function func() {
|
||
alert(this); // выведет [object Window] или [object global]
|
||
}
|
||
|
||
func();
|
||
```
|
||
|
||
Таково поведение в старом стандарте.
|
||
|
||
А в режиме `use strict` вместо глобального объекта `this` будет `undefined`:
|
||
|
||
```js
|
||
//+ run
|
||
function func() {
|
||
"use strict";
|
||
alert(this); // выведет undefined (кроме IE9-)
|
||
}
|
||
|
||
func();
|
||
```
|
||
|
||
Обычно если в функции используется `this`, то она, всё же, служит для вызова в контексте объекта, так что такая ситуация -- скорее исключение.
|
||
|
||
## Ссылочный тип
|
||
|
||
Контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта. Чтобы `this` передался, нужно вызвать функцию именно через точку (или квадратные скобки).
|
||
|
||
Любой более хитрый вызов приведёт к потере контекста, например:
|
||
|
||
```js
|
||
//+ run
|
||
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](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-reference-specification-type).
|
||
|
||
Этот тип представляет собой связку "base-name-strict", где:
|
||
<ul>
|
||
<li>*base* -- как раз объект,</li>
|
||
<li>*name* -- имя свойства,</li>
|
||
<li>*strict* -- вспомогательный флаг для передачи `use strict`.</li>
|
||
</ul>
|
||
|
||
То есть, ссылочный тип (Reference Type) -- это своеобразное "три-в-одном". Он существует исключительно для целей спецификации, мы его не видим, поскольку любой оператор тут же от него избавляется:
|
||
|
||
<ul>
|
||
<li>Скобки `()` получают из `base` значение свойства `name` и вызывают в контексте base.</li>
|
||
<li>Другие операторы получают из `base` значение свойства `name` и используют, а остальные компоненты игнорируют.</li>
|
||
</ul>
|
||
|
||
Поэтому любая операция над результатом операции получения свойства, кроме вызова, приводит к потере контекста.
|
||
|
||
Аналогично работает и получение свойства через квадратные скобки `obj[method]`.
|
||
|
||
|
||
|