Как проверить, является ли функция Javascript конструктором

Я заметил, что не все функции Javascript являются конструкторами.

var obj = Function.prototype;
console.log(typeof obj === 'function'); //true
obj(); //OK
new obj(); //TypeError: obj is not a constructor

Вопрос 1: Как проверить, является ли функция конструктором, чтобы ее можно было вызвать с помощью нового?

Вопрос 2: Когда я создаю функцию, можно ли сделать конструктор НЕ?

Ответы

Ответ 1

Немного предыстории:

ECMAScript 6+ различает вызываемые (можно вызывать без new) и конструируемые (можно вызывать с new) функции:

  • Функции, созданные с помощью синтаксиса функций стрелок или с помощью определения метода в классах или литералах объектов, не могут быть построены.
  • Функции, созданные с помощью синтаксиса class, не могут быть вызваны.
  • Функции, созданные любым другим способом (выражение/объявление функции, конструктор Function), можно вызывать и создавать.
  • Встроенные функции не конструируемы, если явно не указано иное.

О Function.prototype

Function.prototype - это так называемая встроенная функция ,которая не может быть реализована. Из спецификации:

Объекты встроенной функции, которые не определены как конструкторы, не реализуют внутренний метод [[Construct]], если иное не указано в описании конкретной функции.

Значение Function.prototype создается в самом начале инициализации во время выполнения. Это в основном пустая функция, и явно не указано, что она конструируема.


Как проверить, является ли функция конструктором, чтобы ее можно было вызывать с новым?

Нет встроенного способа сделать это. Вы можете try вызвать функцию с помощью new и либо проверить ошибку, либо вернуть true:

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    // verify err is the expected error and then
    return false;
  }
  return true;
}

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

Кроме того, это только скажет вам, может ли функция вызываться как конструктор, а не если она предназначена для вызова как конструктор. Для этого вам нужно ознакомиться с документацией или реализацией функции.

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

Когда я создаю функцию, как мне сделать ее НЕ конструктором?

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

var f = () => console.log('no constructable');

Функции стрелок по определению не конструируемы. В качестве альтернативы вы можете определить функцию как метод объекта или класса.

В противном случае вы можете проверить, вызывается ли функция с new (или чем-то подобным), проверив ее значение this, и выдать ошибку, если она:

function foo() {
  if (this instanceof foo) {
    throw new Error("Don't call 'foo' with new");
  }
}

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


Примеры

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    if (err.message.indexOf('is not a constructor') >= 0) {
      return false;
    }
  }
  return true;
}

function test(f, name) {
  console.log('${name} is constructable: ${isConstructor(f)}');
}

function foo(){}
test(foo, 'function declaration');
test(function(){}, 'function expression');
test(()=>{}, 'arrow function');

class Foo {}
test(Foo, 'class declaration');
test(class {}, 'class expression');

test({foo(){}}.foo, 'object method');

class Foo2 {
  static bar() {}
  bar() {}
}
test(Foo2.bar, 'static class method');
test(new Foo2().bar, 'class method');

test(new Function(), 'new Function()');

Ответ 2

Вы ищете, если у функции есть [[Construct]] внутренний метод. Внутренний метод IsConstructor подробно описывает шаги:

IsConstructor(argument)

ReturnIfAbrupt(argument).  // (Check if an exception has been thrown; Not important.)  
If Type(argument) is not Object, return false.  // argument === Object(argument), or (typeof argument === 'Object' || typeof argument === 'function')  
If argument has a [[Construct]] internal method, return true.  
Return false.

Теперь нам нужно найти места, где используется IsConstructor, но [[Construct]] не вызывается (обычно внутренним методом Construct.)

Я обнаружил, что он используется в функции String newTarget (new.target в js), которую можно использовать с Reflect.construct:

function is_constructor(f) {
  try {
    Reflect.construct(String, [], f);
  } catch (e) {
    return false;
  }
  return true;
}

(я мог бы использовать что-нибудь действительно, например, Reflect.construct(Array, [], f);, но String был первым)

Что дает следующие результаты:

// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);

// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)

& Lt; примечание & GT;

Единственное значение, которое я обнаружил, не работает, это Symbol, который, хотя new Symbol выбрасывает TypeError: Symbol is not a constructor в Firefox, is_constructor(Symbol) === true. Технически это правильный ответ, так как Symbol имеет внутренний метод [[Construct]] (что означает, что он также может быть разделен на подклассы), но использование new или super является специальным случаем для Symbol, чтобы вызвать ошибку ( Итак, Symbol является конструктором, сообщение об ошибке неверно, его просто нельзя использовать как единое целое.) Вы можете просто добавить if (f === Symbol) return false; в начало.

То же самое для чего-то вроде этого:

function not_a_constructor() {
  if (new.target) throw new TypeError('not_a_constructor is not a constructor.');
  return stuff(arguments);
}

is_constructor(not_a_constructor);  // true
new not_a_constructor;  // TypeError: not_a_constructor is not a constructor.

Таким образом, намерения функции быть конструктором не могут быть получены таким образом (пока не будет добавлено что-то вроде Symbol.is_constructor или какого-либо другого флага).

& Lt;/примечание & GT;

Ответ 3

Существует быстрый и простой способ определить, можно ли создать экземпляр функции, без необходимости прибегать к операторам try-catch (которые нельзя оптимизировать с помощью v8)

function isConstructor(obj) {
  return !!obj.prototype && !!obj.prototype.constructor.name;
}
  1. Сначала мы проверяем, является ли объект частью цепочки прототипов.
  2. Тогда мы исключаем анонимные функции

Существует предостережение: функции, названные внутри определения, все равно будут иметь свойство name и, таким образом, пройдут эту проверку, поэтому при использовании тестов для конструкторов функций следует соблюдать осторожность.

В следующем примере функция не является анонимной, но на самом деле называется myFunc. Это прототип может быть расширен как любой класс JS.

let myFunc = function () {};

:)

Ответ 4

С ES6 + Proxies можно протестировать [[Construct]] без фактического вызова конструктора. Вот фрагмент:

const handler={construct(){return handler}} //Must return ANY object, so reuse one
const isConstructor=x=>{
    try{
        return !!(new (new Proxy(x,handler))())
    }catch(e){
        return false
    }
}

Если переданный элемент не является объектом, конструктор Proxy выдает ошибку. Если это не конструктивный объект, то new выдает ошибку. Но если это конструктивный объект, то он возвращает объект handler без вызова его конструктора, который затем не отмечен в true.

Как и следовало ожидать, Symbol по-прежнему считается конструктором. Это потому, что это так, и реализация просто вызывает ошибку при вызове [[Construct]]. Это может иметь место в отношении ЛЮБОЙ пользовательской функции, которая генерирует ошибку, когда существует new.target, поэтому не представляется прав на то, что она специально отбирает ее в качестве дополнительной проверки, но не стесняйтесь делать это, если вы обнаружите, что это полезно.

Ответ 5

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

function isConstructor(func) {
    return (func && typeof func === "function" && func.prototype && func.prototype.constructor) === func;
}

Ответ 6

На вопрос 1, как насчет этого помощника?

Function.isConstructor = ({ prototype }) => Boolean(prototype) && Boolean(prototype.constructor)

Function.isConstructor(class {}); // true
Function.isConstructor(function() {}); // true
Function.isConstructor(() => {}); // false
Function.isConstructor("a string"); // false

Для вопроса 2, функция стрелки является решением. Его нельзя использовать в качестве конструктора, поскольку он не использует ту же область видимости, что и обычная функция, и не имеет прототипа (определение экземпляров, аналогичное определению класса для реального ООП)

const constructable = function() { console.log(this); };
const callable = () => { console.log(this); };

constructable(); // Window {}
callable(); // Window {}
new constructable(); // aConstructableFunction {}
new callable(); // Uncaught TypeError: callable is not a constructor