Как определить, вызывается ли функция как конструктор? - PullRequest
101 голосов
/ 15 декабря 2008

С учетом функции:

function x(arg) { return 30; }

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

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

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

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

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

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

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

Ответы [ 21 ]

88 голосов
/ 10 декабря 2009

ПРИМЕЧАНИЕ. Теперь это возможно в 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 в качестве конструктора»

61 голосов
/ 25 июня 2015

Начиная с 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"
52 голосов
/ 15 декабря 2008

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

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

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

19 голосов
/ 22 декабря 2008

ПРИМЕЧАНИЕ. Этот ответ был написан в 2008 , когда javascript был еще в ES3 с 1999 . С тех пор было добавлено много новых функций, поэтому теперь существуют лучшие решения. Этот ответ хранится по историческим причинам.

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

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 отсутствует встроенная поддержка для обнаружения вызова функции в качестве конструктора. Это самая близкая вещь, которую я придумал, и она должна работать, если вы не удалите свойство.

8 голосов
/ 15 декабря 2008

Два пути, по сути, одинаковые под капотом. Вы можете проверить, что такое область действия 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
  }
}
5 голосов
/ 15 марта 2012

Проверка типа экземпляра [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

Чтобы преодолеть это, некоторые предлагают: а) поместить некоторую информацию в поле экземпляра, например, «ConstructorFinished», и проверить его, или б) отслеживать ваши построенные объекты в списке. Мне неудобно с обоими, так как изменение каждого экземпляра 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]. Связанная функция всегда будет сохранять свой исходный [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");
            }
        });
4 голосов
/ 19 апреля 2015

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

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

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/

3 голосов
/ 13 декабря 2009

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

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

РЕДАКТИРОВАТЬ: добавление некоторых тестов

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
3 голосов
/ 16 сентября 2010

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

// Store instances in a variable to compare against the current this
// Based on Tim Down's 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"

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

3 голосов
/ 12 марта 2012

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

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

Вы можете получить глобальный объект, вызвав функцию как функцию (хе), возвращающую 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 выражений.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...