Проблема конструкторов JavaScript и три способа её решения

в 8:52, , рубрики: javascript, Программирование

Введение

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

function Fubar (foo, bar) {
  this._foo = foo;
  this._bar = bar;
}

var snafu = new Fubar("Situation Normal", "All Fsked Up");

Когда мы вызываем функцию-конструктор при помощи ключевого слова new , то получаем новый объект, а контекст его конструктора устанавливается на сам объект. Если мы явно не возвращаем ничего из конструктора, то получаем сам объект в качестве результата. Таким образом, тело функции конструктора используется для инициализации вновь созданного объекта, прототипом которого будет содержимое свойства prototype конструктора, так что можно писать следующим образом:

Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}

snafu.concatenated()
  //=> 'Situation Normal All Fsked Up'

Используя оператор instanceof можно убедиться в том, что объект был создан при помощи определенного конструктора:

snafu instanceof Fubar
  //=> true

(Заставить работать instanceof «неправильно» возможно при в случаях с более продвинутыми идиомами, или же если вы — вредный тролль, собирающий исключения языка программирования и получающий наслаждение, истязая ими соискателей на собеседованиях. Однако, для наших целей instanceof работает достаточно хорошо.)

Проблема

Что происходит, если мы вызываем конструктор, случайно упустив ключевое слово new ?

var fubar = Fubar("Fsked Up", "Beyond All Recognition");

fubar
  //=> undefined

Чарльз-Зигмунд-Хуан!? Мы вызвали обычную функцию, которая ничего не возвращает, так что fubar будет undefined. Это не то, что нам нужно, даже хуже, потому что:

_foo
  //=> 'Fsked Up'

JavaScript устанавливает контекст в глобальную область видимости для выполнения обычной функции, так что мы только что туда намусорили. Ну это ещё как-то можно поправить:

function Fubar (foo, bar) {
  "use strict"

  this._foo = foo;
  this._bar = bar;
}

Fubar("Situation Normal", "All Fsked Up");
  //=> TypeError: Cannot set property '_foo' of undefined

Хотя использование «use strict» часто опускается в коде и в книгах, на продакшене его использование можно назвать практически обязательным из-за случаев, вроде описанного выше. Тем не менее, конструкторы, не предоставляющие возможность вызвать себя без ключевого слова new, являются потенциальной проблемой.

Так что же мы можем сделать с этим?

Решение №1 — автонаследование

Дэвид Херман объясняет автонаследование в своей книге Effective JavaScript. Когда мы вызываем конструктор при помощи new, псевдо-переменная this указывает на новый экземпляр нашего так-называемого «класса». Это можно использовать для того, чтобы определить: был ли вызван конструктор при помощи кодового слова new.

function Fubar (foo, bar) {
  "use strict"

  var obj,
      ret;

  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else return new Fubar(foo, bar);
}

Fubar("Situation Normal", "All Fsked Up");
  //=>
    { _foo: 'Situation Normal',
      _bar: 'All Fsked Up' }

Зачем делать так, чтобы оно работало без new? Одна из проблем, которые этот подход решает — невозможность вызова new Fubar(...). Рассмотрим пример:

function logsArguments (fn) {
  return function () {
    console.log.apply(this, arguments);
    return fn.apply(this, arguments)
  }
}

function sum2 (a, b) {
  return a + b;
}

var logsSum = logsArguments(sum2);

logsSum(2, 2)
  //=>
    2 2
    4

logsArguments декорирует функцию, логирующую свои аргументы, возвращая результат её вызова. Попробуем сделать то же самое при помощи Fubar:

function Fubar (foo, bar) {
  this._foo = foo;
  this._bar = bar;
}
Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}

var LoggingFubar = logsArguments(Fubar);

var snafu = new LoggingFubar("Situation Normal", "All Fsked Up");
  //=> Situation Normal All Fsked Up

snafu.concatenated()
  //=> TypeError: Object [object Object] has no method 'concatenated'

Это не работает, потому что snafu является экземпляром LoggingFubar, а не Fubar. Но если использовать автонаследование в Fubar:

function Fubar (foo, bar) {
  "use strict"

  var obj,
      ret;

  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else {
    obj = new Fubar();
    ret = Fubar.apply(obj, arguments);
    return ret === undefined
           ? obj
           : ret;
  }
}
Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}

var LoggingFubar = logsArguments(Fubar);

var snafu = new LoggingFubar("Situation Normal", "All Fsked Up");
  //=> Situation Normal All Fsked Up

snafu.concatenated()
  //=> 'Situation Normal All Fsked Up'

Теперь это работает, хотя, конечно же, snafu является экземпляром Fubar, а не LoggingFubar. Нельзя точно сказать, то ли это, чего мы добивались. Этот способ нельзя назвать более чем полезной абстракцией, не лишенной утечек, ровно как и нельзя сказать, что он «просто работает», хоть благодаря ему и становятся возможными некоторые вещи, которые при других подходах реализовать гораздо более сложно.

Решение №2 — использование перегруженной функции

Функция, проверяющая, является ли объект экземпляром определенного класса, может быть очень полезной. Если вас не пугает идея, что одна функция может делать две разные вещи, то можно сделать так, чтобы конструктор выполнял проверку на собственный instanceof .

function Fubar (foo, bar) {
  "use strict"

  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else return arguments[0] instanceof Fubar;
}

var snafu = new Fubar("Situation Normal", "All Fsked Up");

snafu
  //=>
    { _foo: 'Situation Normal',
      _bar: 'All Fsked Up' }

Fubar({})
  //=> false
Fubar(snafu)
  //=> true

Это дает возможность использовать конструктор как фильтр

var arrayOfSevereProblems = problems.filter(Fubar);

Решение №3 — выжечь огнем

Если насущной необходимости в авто-наследовании нет, а использование перегруженных функций по какой-либо причине не подходит, нам всё же может понадобиться способ избежать случайного вызова конструктора без использования ключевого слова new. Пускай
"use strict" и помогает, но и это не панацея. В этом режиме не будет выдана ошибка, если не попытаться записать значение в глоабольную область видимости, и если мы попытаемся что-то сделать, прежде чем записать-таки упомянутое значение, это произойдет не смотря ни на что.

Может быть, гораздо лучше взять дело в свои руки? Оливер Шеррер предлагает такое решение:

function Fubar (foo, bar) {
  "use strict"

  if (!(this instanceof Fubar)) {
      throw new Error("Fubar needs to be called with the new keyword");
  }

  this._foo = foo;
  this._bar = bar;
}

Fubar("Situation Normal", "All Fsked Up");
  //=> Error: Fubar needs to be called with the new keyword

Проще и безопаснее, чем просто полагаться на "use strict". Если необходимо сделать проверку на собственный instanceof , можно обернуть её в конструктор как метод функции:

Fubar.is = function (obj) {
  return obj instanceof Fubar;
}

var arrayOfSevereProblems = problems.filter(Fubar.is);

Заключение

Конструкторы, вызываемые без ключевого слова new, могут представлять потенциальную угрозу. Избежать этого можно тремя способами: автонаследованием, использованием перегруженных функций и принудительно бросая ошибку в случае неправильного вызова.

Оргинал статьи автора можно найти здесь.

К статье прилагается обсуждение на реддите.

Автор: benjie

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js