4 сентября 2022 г.

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

Продвинутая возможность языка

Эта статья охватывает продвинутую тему, чтобы лучше понять некоторые нестандартные случаи.

Она несильно важна. Многие опытные разработчики прекрасно живут, даже не подозревая об этом. Читайте дальше, если хотите узнать, как все работает под капотом.

Некоторые хитрые способы вызова метода приводят к потере значения 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 == "John" ? user.hi : user.bye)(); // Ошибка!

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

Ссылочный тип: объяснение

Присмотревшись поближе, в выражении obj.method() можно заметить две операции:

  1. Сначала оператор точка '.' возвращает свойство объекта – его метод (obj.method).
  2. Затем скобки () вызывают этот метод (исполняется код метода).

Итак, каким же образом информация о this передаётся из первой части во вторую?

Если мы поместим эти операции в отдельные строки, то значение this, естественно, будет потеряно:

let user = {
  name: "John",
  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']() (они делают то же самое). Существуют различные способы решения этой проблемы: одним из таких является func.bind().

Итого

Ссылочный тип – это внутренний тип языка.

Чтение свойства, например, с точкой . в obj.method() возвращает не точное значение свойства, а специальное значение «ссылочного типа», в котором хранится как значение свойства, так и объект, из которого оно было взято.

Это нужно для последующего вызова метода (), чтобы получить объект и установить для него this.

Для всех остальных операций ссылочный тип автоматически становится значением свойства (в нашем случае функцией).

Вся механика скрыта от наших глаз. Это имеет значение только в особых случаях, например, когда метод динамически извлекается из объекта с использованием выражения.

Задачи

важность: 2

Каким будет результат выполнения этого кода?

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

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

Ошибка!

Попробуйте запустить:

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // ошибка!

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

Ошибка появляется, потому что точка с запятой пропущена после user = {...}.

JavaScript не вставляет автоматически точку с запятой перед круглой скобкой (user.go)(), поэтому читает этот код так:

let user = { go:... }(user.go)()

Теперь мы тоже можем увидеть, что такое объединённое выражение синтаксически является вызовом объекта { go: ... } как функции с аргументом (user.go). И это происходит в той же строчке с объявлением переменной let user, т.е. объект user ещё даже не определён, поэтому получается ошибка.

Если мы вставим точку с запятой – всё заработает:

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

Обратите внимание, что круглые скобки вокруг (user.go) ничего не значат. Обычно они определяют последовательность операций (оператор группировки), но здесь вызов метода через точку . срабатывает первым в любом случае, поэтому группировка ни на что не влияет. Только точка с запятой имеет значение.

важность: 3

В представленном ниже коде мы намерены вызвать obj.go() метод 4 раза подряд.

Но вызовы (1) и (2) работают иначе, чем (3) и (4). Почему?

let obj, method;

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

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

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

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

Вот как это объясняется.

  1. Это обычный вызов метода объекта через точку ., и this ссылается на объект перед точкой.

  2. Здесь то же самое. Круглые скобки (оператор группировки) тут не изменяют порядок выполнения операций – доступ к методу через точку в любом случае срабатывает первым.

  3. Здесь мы имеем более сложный вызов (expression).method(). Такой вызов работает, как если бы он был разделён на 2 строчки:

    f = obj.go; // вычисляется выражение (переменная f ссылается на код функции)
    f();        // вызов функции, на которую ссылается f

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

  4. Тут похожая ситуация на случай (3) – идёт потеря значения this.

Чтобы объяснить поведение в примерах (3) и (4), нам нужно помнить, что доступ к свойству (через точку или квадратные скобки) возвращает специальное значение ссылочного типа (Reference Type).

За исключением вызова метода, любая другая операция (подобно операции присваивания = или сравнения через логические операторы, например ||) превращает это значение в обычное, которое не несёт информации, позволяющей установить this.

Карта учебника

Комментарии

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