Как я могу вызвать конструктор javascript, используя call или apply?

Как я могу обобщить функцию ниже, чтобы принять N аргументов? (Используя звонок или подать заявку?)

Есть ли программный способ применить аргументы к "новому"? Я не хочу, чтобы конструктор обрабатывался как простая функция.

/**
 * This higher level function takes a constructor and arguments
 * and returns a function, which when called will return the 
 * lazily constructed value.
 * 
 * All the arguments, except the first are pased to the constructor.
 * 
 * @param {Function} constructor
 */ 

function conthunktor(Constructor) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        console.log(args);
        if (args.length === 0) {
            return new Constructor();
        }
        if (args.length === 1) {
            return new Constructor(args[0]);
        }
        if (args.length === 2) {
            return new Constructor(args[0], args[1]);
        }
        if (args.length === 3) {
            return new Constructor(args[0], args[1], args[2]);
        }
        throw("too many arguments");    
    }
}

qUnit test:

test("conthunktorTest", function() {
    function MyConstructor(arg0, arg1) {
        this.arg0 = arg0;
        this.arg1 = arg1;
    }
    MyConstructor.prototype.toString = function() {
        return this.arg0 + " " + this.arg1;
    }

    var thunk = conthunktor(MyConstructor, "hello", "world");
    var my_object = thunk();
    deepEqual(my_object.toString(), "hello world");
});

7 ответов

Решение

Попробуй это:

function conthunktor(Constructor) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {

         var Temp = function(){}, // temporary constructor
             inst, ret; // other vars

         // Give the Temp constructor the Constructor's prototype
         Temp.prototype = Constructor.prototype;

         // Create a new instance
         inst = new Temp;

         // Call the original Constructor with the temp
         // instance as its context (i.e. its 'this' value)
         ret = Constructor.apply(inst, args);

         // If an object has been returned then return it otherwise
         // return the original instance.
         // (consistent with behaviour of the new operator)
         return Object(ret) === ret ? ret : inst;

    }
}

Вот как вы это делаете:

function applyToConstructor(constructor, argArray) {
    var args = [null].concat(argArray);
    var factoryFunction = constructor.bind.apply(constructor, args);
    return new factoryFunction();
}

var d = applyToConstructor(Date, [2008, 10, 8, 00, 16, 34, 254]);

Позвонить немного проще

function callConstructor(constructor) {
    var factoryFunction = constructor.bind.apply(constructor, arguments);
    return new factoryFunction();
}

var d = callConstructor(Date, 2008, 10, 8, 00, 16, 34, 254);

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

var dateFactory = applyToConstructor.bind(null, Date)
var d = dateFactory([2008, 10, 8, 00, 16, 34, 254]);

или же

var dateFactory = callConstructor.bind(null, Date)
var d = dateFactory(2008, 10, 8, 00, 16, 34, 254);

Он будет работать с любым конструктором, а не только со встроенными модулями или конструкторами, которые могут работать как функции (например, Date).

Однако для этого требуется функция Ecmascript 5.bind. Шимс, вероятно, не будет работать правильно.

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

function neu(constructor) {
    // http://www.ecma-international.org/ecma-262/5.1/#sec-13.2.2
    var instance = Object.create(constructor.prototype);
    var result = constructor.apply(instance, Array.prototype.slice.call(arguments, 1));

    // The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.
    return (result !== null && typeof result === 'object') ? result : instance;
}

function Person(first, last) {this.first = first;this.last = last};
Person.prototype.hi = function(){console.log(this.first, this.last);};

var p = neu(Person, "Neo", "Anderson");

И теперь, конечно, вы можете сделать .apply или же .call или же .bind на neu как обычно.

Например:

var personFactory = neu.bind(null, Person);
var d = personFactory("Harry", "Potter");

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

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

function applyConstructor(ctor, args) {
    var a = [];
    for (var i = 0; i < args.length; i++)
        a[i] = 'args[' + i + ']';
    return eval('new ctor(' + a.join() + ')');
}

ОБНОВЛЕНИЕ: Как только поддержка ES6 будет широко распространена, вы сможете написать это:

function applyConstructor(ctor, args) {
    return new ctor(...args);
}

... но вам не нужно, потому что стандартная функция библиотеки Reflect.construct() делает именно то, что вы ищете!

В ECMAScript 6 вы можете использовать оператор распространения, чтобы применить конструктор с ключевым словом new к массиву аргументов:

var dateFields = [2014, 09, 20, 19, 31, 59, 999];
var date = new Date(...dateFields);
console.log(date);  // Date 2014-10-20T15:01:59.999Z

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

function conthunktor(Constructor) {
  // Call the constructor
  return Constructor.apply(null, Array.prototype.slice.call(arguments, 1));
}

И измените вызываемые конструкторы...

function MyConstructor(a, b, c) {
  if(!(this instanceof MyConstructor)) {
    return new MyConstructor(a, b, c);
  }
  this.a = a;
  this.b = b;
  this.c = c;
  // The rest of your constructor...
}

Так что вы можете попробовать:

var myInstance = conthunktor(MyConstructor, 1, 2, 3);

var sum = myInstance.a + myInstance.b + myInstance.c; // sum is 6

Использование временного конструктора представляется наилучшим решением, если Object.create не доступен.

Если Object.create доступно, то использование его является гораздо лучшим вариантом. На Node.js, используя Object.create приводит к гораздо более быстрому коду. Вот пример того, как Object.create может быть использован:

function applyToConstructor(ctor, args) {
    var new_obj = Object.create(ctor.prototype);
    var ctor_ret = ctor.apply(new_obj, args);

    // Some constructors return a value; make sure to use it!
    return ctor_ret !== undefined ? ctor_ret: new_obj;
}

(Очевидно, что args Аргумент - это список аргументов для применения.)

У меня был кусок кода, который первоначально использовал eval читать часть данных, созданных другим инструментом. (Да, eval это зло.) Это создало бы дерево от сотен до тысяч элементов. По сути, движок JavaScript отвечал за разбор и выполнение множества new ...(...) выражения. Я преобразовал свою систему для анализа структуры JSON, а это значит, что мой код должен определять, какой конструктор вызывать для каждого типа объекта в дереве. Когда я запустил новый код в моем наборе тестов, я был удивлен, увидев резкое замедление по сравнению с eval версия.

  1. Тестовый набор с eval Версия: 1 секунда.
  2. Набор тестов с версией JSON с использованием временного конструктора: 5 секунд.
  3. Набор тестов с версией JSON, используя Object.create: 1 секунда

Набор тестов создает несколько деревьев. Я рассчитал мой applytoConstructor функция вызывалась около 125 000 раз при запуске набора тестов.

Для этого случая есть приемлемое решение. Для каждого Класса, который вы хотите вызывать с помощью метода apply или call, вы должны вызывать его для convertToAllowApply('classNameInString'); Класс должен быть в том же Scoope или глобальном Scoope (я не пытаюсь отправить ns.className, например...)

Есть код:

function convertToAllowApply(kName){
    var n = '\n', t = '\t';
    var scrit = 
        'var oldKlass = ' + kName + ';' + n +
        kName + '.prototype.__Creates__ = oldKlass;' + n +

        kName + ' = function(){' + n +
            t + 'if(!(this instanceof ' + kName + ')){'+ n +
                t + t + 'obj = new ' + kName + ';'+ n +
                t + t + kName + '.prototype.__Creates__.apply(obj, arguments);'+ n +
                t + t + 'return obj;' + n +
            t + '}' + n +
        '}' + n +
        kName + '.prototype = oldKlass.prototype;';

    var convert = new Function(scrit);

    convert();
}

// USE CASE:

myKlass = function(){
    this.data = Array.prototype.slice.call(arguments,0);
    console.log('this: ', this);
}

myKlass.prototype.prop = 'myName is myKlass';
myKlass.prototype.method = function(){
    console.log(this);
}

convertToAllowApply('myKlass');

var t1 = myKlass.apply(null, [1,2,3]);
console.log('t1 is: ', t1);
Другие вопросы по тегам