Question Utilisation de .apply () avec un opérateur 'new'. Est-ce possible?


En JavaScript, je veux créer une instance d'objet (via le new opérateur), mais passez un nombre arbitraire d'arguments au constructeur. Est-ce possible?

Ce que je veux faire est quelque chose comme ça (mais le code ci-dessous ne fonctionne pas):

function Something(){
    // init stuff
}
function createSomething(){
    return new Something.apply(null, arguments);
}
var s = createSomething(a,b,c); // 's' is an instance of Something

La réponse

D'après les réponses fournies ici, il est devenu évident qu'il n'y a pas de moyen intégré d'appeler .apply() avec le new opérateur. Cependant, les gens ont suggéré un certain nombre de solutions vraiment intéressantes au problème.

Ma solution préférée était celui de Matthew Crumley (Je l'ai modifié pour passer le arguments propriété):

var createSomething = (function() {
    function F(args) {
        return Something.apply(this, args);
    }
    F.prototype = Something.prototype;

    return function() {
        return new F(arguments);
    }
})();

425
2017-10-22 12:15


origine


Réponses:


Avec les ECMAScript5 Function.prototype.bind les choses deviennent assez propres:

function newCall(Cls) {
    return new (Function.prototype.bind.apply(Cls, arguments));
    // or even
    // return new (Cls.bind.apply(Cls, arguments));
    // if you know that Cls.bind has not been overwritten
}

Il peut être utilisé comme suit:

var s = newCall(Something, a, b, c);

ou même directement:

var s = new (Function.prototype.bind.call(Something, null, a, b, c));

var s = new (Function.prototype.bind.apply(Something, [null, a, b, c]));

Ceci et le solution basée sur eval sont les seuls qui fonctionnent toujours, même avec des constructeurs spéciaux comme Date:

var date = newCall(Date, 2012, 1);
console.log(date instanceof Date); // true

modifier

Un peu d'explication: Nous devons courir new sur une fonction qui prend un nombre limité d'arguments. le bind méthode nous permet de le faire comme ça:

var f = Cls.bind(anything, arg1, arg2, ...);
result = new f();

le anything paramètre n'a pas beaucoup d'importance, puisque le new réinitialisations de mots clés fle contexte Cependant, il est requis pour des raisons syntaxiques. Maintenant, pour le bind call: Nous devons passer un nombre variable d'arguments, donc cela fait l'affaire:

var f = Cls.bind.apply(Cls, [anything, arg1, arg2, ...]);
result = new f();

Enveloppons cela dans une fonction. Cls est passé comme arugment 0, donc ça va être notre anything.

function newCall(Cls /*, arg1, arg2, ... */) {
    var f = Cls.bind.apply(Cls, arguments);
    return new f();
}

En fait, le temporaire f variable n'est pas nécessaire du tout:

function newCall(Cls /*, arg1, arg2, ... */) {
    return new (Cls.bind.apply(Cls, arguments))();
}

Enfin, nous devrions nous assurer que bind est vraiment ce dont nous avons besoin. (Cls.bind peut avoir été écrasé). Alors remplacez-le par Function.prototype.bind, et nous obtenons le résultat final comme ci-dessus.


324
2018-01-12 22:20



Voici une solution généralisée qui peut appeler n'importe quel constructeur (sauf les constructeurs natifs qui se comportent différemment lorsqu'ils sont appelés comme des fonctions, comme String, Number, Date, etc.) avec un tableau d'arguments:

function construct(constructor, args) {
    function F() {
        return constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    return new F();
}

Un objet créé en appelant construct(Class, [1, 2, 3]) serait identique à un objet créé avec new Class(1, 2, 3).

Vous pouvez également créer une version plus spécifique afin de ne pas avoir à passer le constructeur à chaque fois. Ceci est également légèrement plus efficace, car il n'est pas nécessaire de créer une nouvelle instance de la fonction interne à chaque fois que vous l'appelez.

var createSomething = (function() {
    function F(args) {
        return Something.apply(this, args);
    }
    F.prototype = Something.prototype;

    return function(args) {
        return new F(args);
    }
})();

La raison de la création et de l'appel de la fonction anonyme externe est de conserver la fonction F de polluer l'espace de noms global. On l'appelle parfois le modèle de module.

[METTRE À JOUR]

Pour ceux qui veulent utiliser ceci dans TypeScript, puisque TS donne une erreur si F retourne quoi que ce soit:

function construct(constructor, args) {
    function F() : void {
        constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    return new F();
}

253
2017-10-22 16:52



Si votre environnement prend en charge L'opérateur de propagation ECMA Script 2015 (...), vous pouvez simplement l'utiliser comme ça

function Something() {
    // init stuff
}

function createSomething() {
    return new Something(...arguments);
}

Remarque: Maintenant que les spécifications d'ECMA Script 2015 sont publiées et que la plupart des moteurs JavaScript l'implémentent activement, ce serait la manière préférée de procéder.

Vous pouvez vérifier le support de l'opérateur Spread dans quelques environnements majeurs, ici.


28
2017-09-13 09:33



Supposons que vous ayez un constructeur Items qui amuse tous les arguments que vous lui lancez:

function Items () {
    this.elems = [].slice.call(arguments);
}

Items.prototype.sum = function () {
    return this.elems.reduce(function (sum, x) { return sum + x }, 0);
};

Vous pouvez créer une instance avec Object.create () puis .apply () avec cette instance:

var items = Object.create(Items.prototype);
Items.apply(items, [ 1, 2, 3, 4 ]);

console.log(items.sum());

Qui lors de l'exécution imprime 10 depuis 1 + 2 + 3 + 4 == 10:

$ node t.js
10

26
2018-05-19 17:18



En ES6, Reflect.construct() est très pratique:

Reflect.construct(F, args)

12
2017-07-21 09:00



@Matthieu Je pense qu'il est préférable de réparer la propriété constructeur aussi.

// Invoke new operator with arbitrary arguments
// Holy Grail pattern
function invoke(constructor, args) {
    var f;
    function F() {
        // constructor returns **this**
        return constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    f = new F();
    f.constructor = constructor;
    return f;
}

9
2017-12-18 11:09



Une version améliorée de la réponse de @ Matthew. Cette forme a les avantages de performance légère obtenue en stockant la classe temp dans une fermeture, ainsi que la flexibilité d'avoir une fonction capable d'être utilisée pour créer n'importe quelle classe

var applyCtor = function(){
    var tempCtor = function() {};
    return function(ctor, args){
        tempCtor.prototype = ctor.prototype;
        var instance = new tempCtor();
        ctor.prototype.constructor.apply(instance,args);
        return instance;
    }
}();

Cela serait utilisé en appelant applyCtor(class, [arg1, arg2, argn]);


8
2017-12-07 09:05



Vous pouvez déplacer les éléments init dans une méthode séparée de SomethingLe prototype:

function Something() {
    // Do nothing
}

Something.prototype.init = function() {
    // Do init stuff
};

function createSomething() {
    var s = new Something();
    s.init.apply(s, arguments);
    return s;
}

var s = createSomething(a,b,c); // 's' is an instance of Something

7
2017-10-22 13:35



Cette réponse est un peu tardive, mais il est possible que quiconque voit cela puisse l’utiliser. Il existe un moyen de retourner un nouvel objet en utilisant apply. Bien que cela nécessite un petit changement à votre déclaration d'objet.

function testNew() {
    if (!( this instanceof arguments.callee ))
        return arguments.callee.apply( new arguments.callee(), arguments );
    this.arg = Array.prototype.slice.call( arguments );
    return this;
}

testNew.prototype.addThem = function() {
    var newVal = 0,
        i = 0;
    for ( ; i < this.arg.length; i++ ) {
        newVal += this.arg[i];
    }
    return newVal;
}

testNew( 4, 8 ) === { arg : [ 4, 8 ] };
testNew( 1, 2, 3, 4, 5 ).addThem() === 15;

Pour le premier if déclaration à travailler dans testNew vous devez return this; au bas de la fonction. Donc, par exemple avec votre code:

function Something() {
    // init stuff
    return this;
}
function createSomething() {
    return Something.apply( new Something(), arguments );
}
var s = createSomething( a, b, c );

Mettre à jour: J'ai changé mon premier exemple pour additionner un nombre quelconque d'arguments, au lieu de deux.


6
2017-09-12 17:22