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

Для функции:

function x(arg) { return 30; }

Вы можете назвать это двумя способами:

result = x(4);
result = new x(4);

Первый возвращает 30, второй возвращает объект.

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

Независимо от вашего решения, он также должен работать со следующим вызовом:

var Z = new x(); 
Z.lolol = x; 
Z.lolol();

Все решения, которые в настоящее время считают, что Z.lolol() вызывает его как конструктор.

Ответ 1

ПРИМЕЧАНИЕ. Теперь это возможно в ES2015 и более поздних версиях. См. ответ Даниэля Вайнера.

Я не думаю, что вы хотите [до ES2015]. Внутри функции недостаточно информации, чтобы сделать надежный вывод.

Глядя на спецификацию ECMAScript 3rd edition, шаги, выполняемые при вызове new x(), по существу:

  • Создать новый объект
  • Назначьте свой внутренний свойство [[Prototype]] свойству прототипа x
  • Вызвать x как обычно, передав ему новый объект как this
  • Если вызов x возвратил объект, верните его, в противном случае верните новый объект

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

function x(y) {
    var isConstructor = false;
    if (this instanceof x // <- You could use arguments.callee instead of x here,
                          // except in in EcmaScript 5 strict mode.
            && !this.__previouslyConstructedByX) {
        isConstructor = true;
        this.__previouslyConstructedByX = true;
    }
    alert(isConstructor);
}

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

(*) "экземпляр" является неточным термином, но достаточно близок и более кратким, чем "объект, который был создан путем вызова x в качестве конструктора"

Ответ 2

1) Вы можете проверить this.constructor:

function x(y)
{
    if (this.constructor == x)
        alert('called with new');
    else
         alert('called as function');
}

2) Да, возвращаемое значение просто отбрасывается при использовании в контексте new

Ответ 3

Как и в ECMAScript 6, это возможно с помощью new.target. new.target будет установлен, если функция вызывается с помощью new (или с Reflect.construct, которая действует как new), в противном случае это undefined.

function Foo() {
    if (new.target) {
       console.log('called with new');
    } else {
       console.log('not called with new');
    }
}

new Foo(); // "called with new"
Foo(); // "not called with new"
Foo.call({}); // "not called with new"

Ответ 4

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

function x() {
    if ( (this instanceof arguments.callee) ) {
      alert("called as constructor");
    } else {
      alert("called as function");
    }
}

Обновление Как указано в claudiu, приведенный выше код не работает, если назначить конструктору тот же объект, который он создал. Я никогда не писал код, который делает это, и он видел, как кто-то еще делает этого истребителя.

Пример Клавдия:

var Z = new x();
Z.lolol = x;
Z.lolol();

Добавив свойство к объекту, можно определить, был ли объект инициализирован.

function x() {
    if ( (this instanceof arguments.callee && !this.hasOwnProperty("__ClaudiusCornerCase")) ) {
        this.__ClaudiusCornerCase=1;
        alert("called as constructor");
    } else {
        alert("called as function");
    }
}

Даже если вышеописанный код не будет удален, вы удалите добавленное свойство. Однако вы можете переписать его любым желаемым значением, в том числе undefined, и оно все еще работает. Но если вы его удалите, он сломается.

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

Ответ 5

Два способа, по сути, то же самое под капотом. Вы можете проверить, что такое область this, или вы можете проверить, что такое this.constructor.

Если вы вызвали метод как конструктор this, будет новый экземпляр класса, если вы вызовите метод как метод this, будет объектом контекста методов. Подобным же образом конструктор объекта будет сам метод, если он будет вызван как новый, и конструктор объекта системы в противном случае. Это ясно, как грязь, но это должно помочь:

var a = {};

a.foo = function () 
{
  if(this==a) //'a' because the context of foo is the parent 'a'
  {
    //method call
  }
  else
  {
    //constructor call
  }
}

var bar = function () 
{
  if(this==window) //and 'window' is the default context here
  {
    //method call
  }
  else
  {
    //constructor call
  }
}

a.baz = function ()
{
  if(this.constructor==a.baz); //or whatever chain you need to reference this method
  {
    //constructor call
  }
  else
  {
    //method call
  }
}

Ответ 6

Проверка типа экземпляра [this] внутри конструктора - путь. Проблема в том, что без каких-либо дополнительных атак этот подход подвержен ошибкам. Однако есть решение.

Предположим, что мы имеем дело с функцией ClassA(). Рудиментарный подход:

    function ClassA() {
        if (this instanceof arguments.callee) {
            console.log("called as a constructor");
        } else {
            console.log("called as a function");
        }
    }

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

    var instance = new ClassA;
    instance.classAFunction = ClassA;
    instance.classAFunction(); // <-- this will appear as constructor call

    ClassA.apply(instance); //<-- this too

Чтобы преодолеть это, некоторые полагают, что либо a) помещает некоторую информацию в поле экземпляра, например, "ConstructorFinished" и проверяет его, либо b) сохраняет следы ваших построенных объектов в списке. Мне неудобно и то и другое, так как изменение каждого экземпляра ClassA слишком инвазивное и дорогостоящее для функции, связанной с типом. Сбор всех объектов в списке может обеспечить сбор мусора и проблемы с ресурсами, если ClassA будет иметь много экземпляров.

Путь к тому, чтобы иметь возможность контролировать выполнение функции ClassA. Простой подход:

    function createConstructor(typeFunction) {
        return typeFunction.bind({});
    }

    var ClassA = createConstructor(
        function ClassA() {
            if (this instanceof arguments.callee) {
                console.log("called as a function");
                return;
            }
            console.log("called as a constructor");
        });

    var instance = new ClassA();

Это эффективно предотвратит все попытки обмануть значение [this]. Связанная функция всегда будет сохранять свой оригинальный [этот] контекст, если вы не вызываете его с помощью оператора new.

Расширенная версия возвращает возможность применения конструктора к произвольным объектам. Некоторые виды использования могут использовать конструктор как типичный преобразователь или обеспечить вызываемую цепочку конструкторов базового класса в сценариях наследования.

    function createConstructor(typeFunction) {
        var result = typeFunction.bind({});
        result.apply = function (ths, args) {
            try {
                typeFunction.inApplyMode = true;
                typeFunction.apply(ths, args);
            } finally {
                delete typeFunction.inApplyMode;
            }
        };
        return result;
    }

    var ClassA = createConstructor(
        function ClassA() {
            if (this instanceof arguments.callee && !arguments.callee.inApplyMode) {
                console.log("called as a constructor");
            } else {
                console.log("called as a function");
            }
        });

Ответ 7

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

ОБНОВЛЕНИЕ: благодаря TwilightSun решение уже завершено, даже для теста Claudiu! спасибо вам, ребята!!!

function Something()
{
    this.constructed;

    if (Something.prototype.isPrototypeOf(this) && !this.constructed)
    {
        console.log("called as a c'tor"); this.constructed = true;
    }
    else
    {
        console.log("called as a function");
    }
}

Something(); //"called as a function"
new Something(); //"called as a c'tor"

: https://jsfiddle.net/9cqtppuf/

Ответ 8

Расширение решения Gregs, это прекрасно работает с тестовыми примерами, которые вы предоставили:

function x(y) {
    if( this.constructor == arguments.callee && !this._constructed ) {
        this._constructed = true;
        alert('called with new');
    } else {
        alert('called as function');
    }
}

EDIT: добавление некоторых тестовых примеров

x(4);             // OK, function
var X = new x(4); // OK, new

var Z = new x();  // OK, new
Z.lolol = x; 
Z.lolol();        // OK, function

var Y = x;
Y();              // OK, function
var y = new Y();  // OK, new
y.lolol = Y;
y.lolol();        // OK, function

Ответ 9

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

// Store instances in a variable to compare against the current this
// Based on Tim Down solution where instances are tracked
var Klass = (function () {
    // Store references to each instance in a "class"-level closure
    var instances = [];

    // The actual constructor function
    return function () {
        if (this instanceof Klass && instances.indexOf(this) === -1) {
            instances.push(this);
            console.log("constructor");
        } else {
            console.log("not constructor");
        }
    };
}());

var instance = new Klass();  // "constructor"
instance.klass = Klass;
instance.klass();            // "not constructor"

В большинстве случаев я, вероятно, просто проверю экземпляр.

Ответ 10

Нет надежного способа отличить, как функция вызывается в JavaScript-коде. 1

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

Вы можете получить глобальный объект, получив функцию, называемую функцией (heh), возвращающую this.

Моя интуиция заключается в том, что в спецификации ECMAScript 1.3 конструкторы, которые имеют определенное поведение при вызове функции, должны различать, как они были вызваны с использованием этого сравнения:

function MyClass () {
    if ( this === (function () { return this; })() ) {
        // called as a function
    }
    else {
        // called as a constructor
    }
}

В любом случае, любой может просто использовать функцию или конструктор call или apply и установить this на что угодно. Но таким образом вы можете избежать "инициализации" глобального объекта:

function MyClass () {
    if ( this === (function () { return this; })() ) {
        // Maybe the caller forgot the "new" keyword
        return new MyClass();
    }
    else {
        // initialize
    }
}

<суб > 1. Хост (например, реализация) может определить разницу, если он реализует эквивалент внутренних свойств [[Call]] и [[Construct]]. Первый вызывается для выражений функции или метода, а последний вызывается для выражений new.

Ответ 11

От Джона Ресига:

function makecls() {

   return function(args) {

        if( this instanceof arguments.callee) {
            if ( typeof this.init == "function")
                this.init.apply(this, args.callee ? args : arguments)
        }else{
            return new arguments.callee(args);
        }
    };
}

var User = makecls();

User.prototype.init = function(first, last){

    this.name = first + last;
};

var user = User("John", "Resig");

user.name

Ответ 12

Если вы собираетесь взломать, то instanceof является минимальным решением после new.target, как и другими ответами. Но с помощью решения instanceof он не смог бы выполнить этот пример:

let inst = new x;
x.call(inst);

В сочетании с решением @TimDown вы можете использовать ES6 WeakSet, если вы хотите, чтобы совместимость со старыми версиями ECMAScript предотвращала помещение свойств внутри экземпляров. Ну, WeakSet будет использоваться для того, чтобы не использовать неиспользуемые объекты для сбора мусора. new.target не будет совместим в том же исходном коде, что и синтаксис ES6. ECMAScript указывает, что идентификаторы не могут быть одним из зарезервированных слов, а new в любом случае не является объектом.

(function factory()
{
    'use strict';
    var log = console.log;

    function x()
    {
        log(isConstructing(this) ?
            'Constructing' :
            'Not constructing'
        );
    }

    var isConstructing, tracks;
    var hasOwnProperty = {}.hasOwnProperty;

    if (typeof WeakMap === 'function')
    {
        tracks = new WeakSet;
        isConstructing = function(inst)
        {
            if (inst instanceof x)
            {
                return tracks.has(inst) ?
                    false : !!tracks.add(inst);
            }
            return false;
        }
    } else {
        isConstructing = function(inst)
        {
            return inst._constructed ?
                false : inst._constructed = true;
        };
    }
    var z = new x; // Constructing
    x.call(z)      // Not constructing
})();

Оператор ECMAScript 3 instanceof - указанный как:

11.8.6 Оператор instanceof
--- Произведение RelationalExpression: RelationalExpression instanceof Вычисление ShiftExpression следующим образом:
--- 1. Оцените RelationalExpression.
--- 2. Вызвать GetValue (Результат (1)).
--- 3. Оцените выражение Shift.
--- 4. Вызовите GetValue (Результат (3)).
--- 5. Если Result (4) не является объектом, бросьте исключение TypeError.
--- 6. Если Result (4) не имеет метода [[HasInstance]], бросьте исключение TypeError.
--- 7. Вызвать метод [[HasInstance]] результата (4) с параметром Result (2).
--- 8. Результат возврата (7).
15.3.5.3 [[HasInstance]] (V)
--- Предположим, что F является объектом Function.
--- Когда метод [[HasInstance]] из F вызывается со значением V, предпринимаются следующие шаги:
--- 1. Если V не является объектом, верните false.
--- 2. Вызвать метод [[Get]] F с именем свойства "prototype".
--- 3. Пусть O - результат (2).
--- 4. Если O не является объектом, бросьте исключение TypeError.
--- 5. Пусть V - значение свойства [[Prototype]] V.
--- 6. Если V - ** null **, верните false.
--- 7. Если O и V относятся к одному и тому же объекту или относятся к объектам, связанным друг с другом (13.1.2), верните true.
--- 8. Перейдите к шагу 5.

И это означает, что он будет возвращать значение левой стороны после перехода к его прототипу, пока он не станет объектом, или пока он не будет равен прототипу объекта правой стороны с указанным методом [[HasInstance]]. Что означает, что он проверяет, является ли левая сторона экземпляром правой стороны, тем не менее, потребляя все внутренние прототипы левой стороны.

function x() {
    if (this instanceof x) {
        /* Probably invoked as constructor */
    } else return 30;
}

Ответ 13

В моем тестировании для http://packagesinjavascript.wordpress.com/ Я нашел тест, если (this == window) работает кросс-браузер во всех случаях, так что тот, с которым я закончил использовать.

-Stijn

Ответ 14

Возможно, я ошибаюсь, но (ценой паразита) следующий код выглядит как решение:

function x(arg) {
    //console.debug('_' in this ? 'function' : 'constructor'); //WRONG!!!
    //
    // RIGHT(as accepted)
    console.debug((this instanceof x && !('_' in this)) ? 'function' : 'constructor');
    this._ = 1;
    return 30;
}
var result1 = x(4),     // function
    result2 = new x(4), // constructor
    Z = new x();        // constructor
Z.lolol = x; 
Z.lolol();              // function

Ответ 15

Используйте this instanceof arguments.callee (необязательно заменяя arguments.callee на функцию, в которой он работает, что повышает производительность), чтобы проверить, вызвано ли что-то как конструктор. Не использовать this.constructor, поскольку это можно легко изменить.

Ответ 16

Тим Даун Я думаю, что это правильно. Я думаю, что как только вы дойдете до того, что считаете, что вам нужно различать два режима вызова, вам не следует использовать ключевое слово "this". this является ненадежным, и это может быть глобальный объект, или это может быть какой-то совершенно другой объект. дело в том, что наличие функции с этими различными режимами активации, некоторые из которых работают так, как вы планировали, другие делают что-то совершенно дикое, нежелательно. Я думаю, может быть, вы пытаетесь понять это из-за этого.

Существует идиоматический способ создания функции-конструктора, которая ведет себя одинаково независимо от того, как она называется. будь то Thing(), новый Thing() или foo.Thing(). Это происходит следующим образом:

function Thing () {
   var that = Object.create(Thing.prototype);
   that.foo="bar";
   that.bar="baz";
   return that;
}

где Object.create - это новый стандартный метод ecmascript 5, который может быть реализован в обычном javascript следующим образом:

if(!Object.create) {
    Object.create = function(Function){
        // WebReflection Revision
       return function(Object){
           Function.prototype = Object;
           return new Function;
    }}(function(){});
}

Object.create примет объект как параметр и вернет новый объект с переданным в объекте в качестве своего прототипа.

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

Ответ 17

Если вы не хотите помещать свойство __previouslyConstructedByX в объект - потому что оно загрязняет открытый интерфейс объекта и может быть легко перезаписано - просто не возвращайте экземпляр x:

function x() {

    if(this instanceof x) {
        console.log("You invoked the new keyword!");
        return that;
    }
    else {
        console.log("No new keyword");
        return undefined;
    }

}

x();
var Z = new x(); 
Z.lolol = x; 
Z.lolol();
new Z.lolol();

Теперь функция x никогда не возвращает объект типа x, поэтому (я думаю) this instanceof x оценивает только true, когда функция вызывается с ключевым словом new.

Недостаток заключается в том, что это эффективно устраняет поведение instanceof, но в зависимости от того, насколько вы его используете (я не склонен), это может быть не проблема.


Если вы хотите вернуть оба случая 30, вы можете вернуть экземпляр Number вместо экземпляра x:

function x() {

    if(this instanceof x) {
        console.log("You invoked the new keyword!");
        var that = {};
        return new Number(30);
    }
    else {
        console.log("No new");
        return 30;
    }

}

console.log(x());
var Z = new x();
console.log(Z);
Z.lolol = x;
console.log(Z.lolol());
console.log(new Z.lolol());

Ответ 18

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

Кажется, достаточно проверить наличие "this" в начале вашей функции:

function RGB(red, green, blue) {
    if (this) {
        throw new Error("RGB can't be instantiated");
    }

    var result = "#";
    result += toHex(red);
    result += toHex(green);
    result += toHex(blue);

    function toHex(dec) {
        var result = dec.toString(16);

        if (result.length < 2) {
            result = "0" + result;
        }

        return result;
    }

    return result;
}

Во всяком случае, в конце концов, я просто решил превратить псевдокласс RGB() в функцию rgb(), поэтому я просто не буду пытаться его создать, поэтому вообще не нужно проверять безопасность. Но это будет зависеть от того, что вы пытаетесь сделать.

Ответ 19

function createConstructor(func) {
    return func.bind(Object.create(null));
}

var myClass = createConstructor(function myClass() {
    if (this instanceof myClass) {
        console.log('You used the "new" keyword');
    } else {
        console.log('You did NOT use the "new" keyword');
        return;
    }
    // constructor logic here
    // ...
});