web-dev-qa-db-fra.com

Comment puis-je appeler un constructeur javascript en utilisant call ou postuler?

Comment généraliser la fonction ci-dessous pour prendre N arguments? (Utiliser l'appel ou postuler?)

Existe-t-il un moyen programmatique d'appliquer des arguments à "nouveau"? Je ne veux pas que le constructeur soit traité comme une simple fonction.

/**
 * 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");    
    }
}

q Test unitaire:

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");
});
82
fadedbee

Essaye ça:

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;

    }
}
50
James

Voici comment procéder:

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]);

L'appel est un peu plus facile

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

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

Vous pouvez utiliser l'une de ces options pour créer des fonctions d'usine:

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

ou

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

Il fonctionnera avec n'importe quel constructeur, et pas seulement avec les constructeurs intégrés ou les constructeurs pouvant servir de fonctions (comme Date).

Cependant, il nécessite la fonction Ecmascript 5 .bind. Les cales ne fonctionneront probablement pas correctement.

Une approche différente, plus dans le style de certaines des autres réponses, consiste à créer une version fonctionnelle du new intégré. Cela ne fonctionnera pas sur tous les buildins (comme Date).

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");

Et maintenant, bien sûr, vous pouvez faire .apply ou .call ou .bind sur neu comme d'habitude.

Par exemple:

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

Je pense que la première solution que je donne est meilleure, car elle ne dépend pas de la réplication correcte de la sémantique d'un module intégré et fonctionne correctement avec les modules intégrés.

95
kybernetikos

Cette fonction est identique à new dans tous les cas. Il sera probablement beaucoup plus lent que la réponse de 999, alors n'utilisez-le que si vous en avez vraiment besoin.

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

MISE À JOUR: Une fois que le support ES6 est répandu, vous pourrez écrire ceci:

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

... mais vous n'en aurez pas besoin, car la fonction de bibliothèque standard Reflect.construct() fait exactement ce que vous cherchez!

14
Jason Orendorff

Une autre approche, qui nécessite de modifier le constructeur actuel appelé, mais me semble plus propre que d'utiliser eval () ou d'introduire une nouvelle fonction factice dans la chaîne de construction ... Gardez votre fonction conthunktor comme

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

Et modifiez les constructeurs appelés ...

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...
}

Vous pouvez donc essayer:

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

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

Dans ECMAScript 6, vous pouvez utiliser l'opérateur d'étalement pour appliquer un constructeur avec le nouveau mot clé à un tableau d'arguments:

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

L'utilisation d'un constructeur temporaire semble être la meilleure solution si Object.create N'est pas disponible.

Si Object.create Est disponible, son utilisation est une bien meilleure option. Sur Node.js, l'utilisation de Object.create Donne un code beaucoup plus rapide. Voici un exemple d'utilisation de 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;
}

(Évidemment, l'argument args est une liste d'arguments à appliquer.)

J'avais un morceau de code qui utilisait à l'origine eval pour lire un morceau de données créé par un autre outil. (Oui, eval est mauvais.) Cela instancierait un arbre de centaines à des milliers d'éléments. Fondamentalement, le moteur JavaScript était responsable de l'analyse et de l'exécution d'un tas d'expressions new ...(...). J'ai converti mon système pour analyser une structure JSON, ce qui signifie que mon code doit déterminer le constructeur à appeler pour chaque type d'objet dans l'arborescence. Lorsque j'ai exécuté le nouveau code dans ma suite de tests, j'ai été surpris de voir un ralentissement spectaculaire par rapport à la version eval.

  1. Suite de tests avec la version eval: 1 seconde.
  2. Suite de tests avec version JSON, utilisant un constructeur temporaire: 5 secondes.
  3. Suite de tests avec la version JSON, en utilisant Object.create: 1 seconde.

La suite de tests crée plusieurs arborescences. J'ai calculé que ma fonction applytoConstructor a été appelée environ 125 000 fois lorsque la suite de tests est exécutée.

3
Louis

Il existe une solution réutilisable pour ce cas. Pour chaque classe que vous souhaitez appeler avec la méthode apply ou call, vous devez appeler avant pour convertirToAllowApply ('classNameInString'); la classe doit être dans le même Scoope o global scoope (je n'essaye pas d'envoyer ns.className par exemple ...)

Il y a le code:

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);
1
danyg