Внутренняя реализация: Ссылочный тип
Продвинутая возможность языка
Этот раздел объясняет сложную тему, чтобы лучше понимать некоторые запутанные случаи.
Если вы хотите продвигаться быстрее, его можно пропустить или отложить.
Некоторые хитрые способы вызова метода приводят к потере значения this
, например:
let user = {
name: "Джон",
hi() { alert(this.name); },
bye() { alert("Пока"); }
};
user.hi(); // Джон (простой вызов метода работает хорошо)
// теперь давайте попробуем вызывать user.hi или user.bye
// в зависимости от имени пользователя user.name
(user.name == "Джон" ? user.hi : user.bye)(); // Ошибка!
В последней строчке кода используется условный оператор ?
, который определяет, какой будет вызван метод (user.hi
или user.bye
) в зависимости от выполнения условия. В данном случае будет выбран user.hi
.
Затем метод тут же вызывается с помощью скобок ()
. Но вызов не работает как положено!
Вы можете видеть, что при вызове будет ошибка, потому что значением "this"
внутри функции становится undefined
(полагаем, что у нас строгий режим).
Так работает (доступ к методу объекта через точку):
user.hi();
Так уже не работает (вызываемый метод вычисляется):
(user.name == "Джон" ? user.hi : user.bye)(); // Ошибка!
Почему? Если мы хотим понять, почему так происходит, давайте разберёмся (заглянем под капот), как работает вызов методов (obj.method()
).
Присмотревшись поближе, в выражении obj.method()
можно заметить две операции:
- Сначала оператор точка
'.'
возвращает свойство объекта – его метод (obj.method
). - Затем скобки
()
вызывают этот метод (исполняется код метода).
Итак, каким же образом информация о this
передаётся из первой части во вторую?
Если мы поместим эти операции в отдельные строки, то значение this
, естественно, будет потеряно:
let user = {
name: "Джон",
hi() { alert(this.name); }
};
// разделим получение метода объекта и его вызов в разных строках
let hi = user.hi;
hi(); // Ошибка, потому что значением this является undefined
Здесь hi = user.hi
сохраняет функцию в переменной, и далее в последней строке она вызывается полностью сама по себе, без объекта, так что нет this
.
Для работы вызовов типа user.hi()
, JavaScript использует трюк – точка '.'
возвращает не саму функцию, а специальное значение «ссылочного типа», называемого Reference Type.
Этот ссылочный тип (Reference Type) является внутренним типом. Мы не можем явно использовать его, но он используется внутри языка.
Значение ссылочного типа – это «триплет»: комбинация из трёх значений (base, name, strict)
, где:
base
– это объект.name
– это имя свойства объекта.strict
– это режим исполнения. Является true, если действует строгий режим (use strict
).
Результатом доступа к свойству user.hi
является не функция, а значение ссылочного типа. Для user.hi
в строгом режиме оно будет таким:
// значение ссылочного типа (Reference Type)
(user, "hi", true)
Когда скобки ()
применяются к значению ссылочного типа (происходит вызов), то они получают полную информацию об объекте и его методе, и могут поставить правильный this
(=user
в данном случае, по base
).
Ссылочный тип – исключительно внутренний, промежуточный, используемый, чтобы передать информацию от точки .
до вызывающих скобок ()
.
При любой другой операции, например, присваивании hi = user.hi
, ссылочный тип заменяется на собственно значение user.hi
(функцию), и дальше работа уже идёт только с ней. Поэтому дальнейший вызов происходит уже без this
.
Таким образом, значение this
передаётся правильно, только если функция вызывается напрямую с использованием синтаксиса точки obj.method()
или квадратных скобок obj['method']()
(они делают то же самое). Позднее в этом учебнике мы изучим различные варианты решения проблемы потери значения this
. Например, такие как func.bind().
У стрелочных функций нет «this»
Стрелочные функции особенные: у них нет своего «собственного» this
. Если мы используем this
внутри стрелочной функции, то его значение берётся из внешней «нормальной» функции.
Например, здесь arrow()
использует значение this
из внешнего метода user.sayHi()
:
let user = {
firstName: "Илья",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Илья
Это является особенностью стрелочных функций. Они полезны, когда мы на самом деле не хотим иметь отдельное значение this
, а хотим брать его из внешнего контекста. Позднее в главе Повторяем стрелочные функции мы увидим больше примеров на эту тему.
Итого
- Функции, которые находятся в объекте в качестве его свойств, называются «методами».
- Методы позволяют объектам «действовать»:
object.doSomething()
. - Методы могут ссылаться на объект через
this
.
Значение this
определяется во время исполнения кода.
- При объявлении любой функции в ней можно использовать
this
, но этотthis
не имеет значения до тех пор, пока функция не будет вызвана. - Эта функция может быть скопирована между объектами (из одного объекта в другой).
- Когда функция вызывается синтаксисом «метода» –
object.method()
, значениемthis
во время вызова является объект перед точкой.
Также ещё раз заметим, что стрелочные функции являются особенными – у них нет this
. Когда внутри стрелочной функции обращаются к this
, то его значение берётся снаружи.
Практика
Создайте калькулятор
важность: 5
Создайте объект calculator
(калькулятор) с тремя методами:
read()
(читать) запрашивает два значения и сохраняет их как свойства объекта.sum()
(суммировать) возвращает сумму сохранённых значений.mul()
(умножить) перемножает сохранённые значения и возвращает результат.
let calculator = {
// ... ваш код ...
};
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
Цепь вызовов
важность: 2
Это ladder
(лестница) – объект, который позволяет подниматься вверх и спускаться:
let ladder = {
step: 0,
up() {
this.step++;
},
down() {
this.step--;
},
showStep: function() { // показывает текущую ступеньку
alert( this.step );
}
};
Теперь, если нам нужно сделать несколько последовательных вызовов, мы можем выполнить это так:
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
Измените код методов up
, down
и showStep
таким образом, чтобы их вызов можно было сделать по цепочке, например так:
ladder.up().up().down().showStep(); // 1
Такой подход широко используется в библиотеках JavaScript.