web-dev-qa-db-fra.com

Utilisation de .apply () avec le nouvel opérateur. Est-ce possible?

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

Ce que je veux faire, c'est quelque chose comme ceci (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, il est apparu qu'il n'y avait pas de moyen intégré d'appeler .apply() avec l'opérateur new. Cependant, les gens ont suggéré un certain nombre de solutions très intéressantes au problème.

Ma solution préférée était celle de Matthew Crumley (je l'ai modifiée pour passer la propriété arguments):

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

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

Avec 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 la 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 exécuter new sur une fonction prenant un nombre limité d'arguments. La méthode bind nous permet de le faire comme suit:

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

Le paramètre anything importe peu, car le mot clé new réinitialise le contexte de f. Cependant, cela est nécessaire pour des raisons syntaxiques. Maintenant, pour l’appel bind: Nous devons passer un nombre variable d’arguments.

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

Enveloppons cela dans une fonction. Cls est passé comme l'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, la variable temporaire f n'est pas du tout nécessaire:

_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é). Donc remplacez-le par _Function.prototype.bind_ et vous obtiendrez le résultat final comme ci-dessus.

351
user123444555621

Voici une solution généralisée qui peut appeler n'importe quel constructeur (à l'exception des constructeurs natifs qui se comportent différemment lorsqu'ils sont appelés en tant que 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 pour ne pas avoir à passer le constructeur à chaque fois. Cela 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 pour créer et appeler la fonction anonyme externe comme celle-ci est d'empêcher la fonction F de polluer l'espace de noms global. C'est parfois appelé le modèle de module.

[UPDATE]

Pour ceux qui veulent utiliser ceci dans TypeScript, puisque TS donne une erreur si F renvoie rien:

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

Si votre environnement prend en charge l'opérateur de diffusion d'ECMA Script 2015 (...) , vous pouvez simplement l'utiliser comme ceci

function Something() {
    // init stuff
}

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

Remarque: Maintenant que les spécifications du script ECMA 2015 sont publiées et que la plupart des moteurs JavaScript l'implémentent, cela serait la méthode préférée pour le faire.

Vous pouvez vérifier le support de l'opérateur Spread dans quelques-uns des principaux environnements, here .

30
thefourtheye

Supposons que vous ayez un constructeur Items qui contient tous les arguments que vous lui soumettez:

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
27
substack

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

Reflect.construct(F, args)
15
gfaceless

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

// 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
wukong

Une version améliorée de la réponse de @ Matthew. Cette forme présente les légers avantages en termes de performances obtenus en stockant la classe temp dans une fermeture, ainsi que la flexibilité de disposer d’une fonction capable de créer toute 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;
    }
}();

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

8
jordancpaul

Vous pouvez déplacer le contenu init dans une méthode distincte du prototype de Something:

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
Tim Down

Cette réponse est un peu tardive, mais je pense que quiconque le verra pourra peut-être 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 que la première instruction if fonctionne dans testNew, vous devez return this; au bas de la fonction. Donc, à titre d'exemple avec votre code:

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

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

6
Trevor Norris

Je viens de rencontrer ce problème et je l'ai résolu comme suit:

function instantiate(ctor) {
    switch (arguments.length) {
        case 1: return new ctor();
        case 2: return new ctor(arguments[1]);
        case 3: return new ctor(arguments[1], arguments[2]);
        case 4: return new ctor(arguments[1], arguments[2], arguments[3]);
        //...
        default: throw new Error('instantiate: too many parameters');
    }
}

function Thing(a, b, c) {
    console.log(a);
    console.log(b);
    console.log(c);
}

var thing = instantiate(Thing, 'abc', 123, {x:5});

Oui, c'est un peu moche, mais ça résout le problème, et c'est tellement simple.

6
alekop

si vous êtes intéressé par une solution basée sur eval

function createSomething() {
    var q = [];
    for(var i = 0; i < arguments.length; i++)
        q.Push("arguments[" + i + "]");
    return eval("new Something(" + q.join(",") + ")");
}
4
user187291

Cela marche!

var cls = Array; //eval('Array'); dynamically
var data = [2];
new cls(...data);
4
infinito84

Voyez aussi comment CoffeeScript le fait.

s = new Something([a,b,c]...)

devient:

var s;
s = (function(func, args, ctor) {
  ctor.prototype = func.prototype;
  var child = new ctor, result = func.apply(child, args);
  return Object(result) === result ? result : child;
})(Something, [a, b, c], function(){});
3
mbarkhau

Cette approche de constructeur fonctionne à la fois avec et sans le mot clé new:

function Something(foo, bar){
  if (!(this instanceof Something)){
    var obj = Object.create(Something.prototype);
    return Something.apply(obj, arguments);
  }
  this.foo = foo;
  this.bar = bar;
  return this;
}

Il assume le support de Object.create mais vous pouvez toujours le remplir si vous supportez des navigateurs plus anciens. Voir la table de support sur MDN ici .

Voici un JSBin pour le voir en action avec la sortie de la console .

2
muffinresearch

Solution sans ES6 ou polyfill:

var obj = _new(Demo).apply(["X", "Y", "Z"]);


function _new(constr)
{
    function createNamedFunction(name)
    {
        return (new Function("return function " + name + "() { };"))();
    }

    var func = createNamedFunction(constr.name);
    func.prototype = constr.prototype;
    var self = new func();

    return { apply: function(args) {
        constr.apply(self, args);
        return self;
    } };
}

function Demo()
{
    for(var index in arguments)
    {
        this['arg' + (parseInt(index) + 1)] = arguments[index];
    }
}
Demo.prototype.tagged = true;


console.log(obj);
console.log(obj.tagged);


sortie

Démo {arg1: "X", arg2: "Y", arg3: "Z"}


... ou "plus court" façon:

var func = new Function("return function " + Demo.name + "() { };")();
func.prototype = Demo.prototype;
var obj = new func();

Demo.apply(obj, ["X", "Y", "Z"]);


edit:
Je pense que cela pourrait être une bonne solution:

this.forConstructor = function(constr)
{
    return { apply: function(args)
    {
        let name = constr.name.replace('-', '_');

        let func = (new Function('args', name + '_', " return function " + name + "() { " + name + "_.apply(this, args); }"))(args, constr);
        func.constructor = constr;
        func.prototype = constr.prototype;

        return new func(args);
    }};
}
2
Martin Wantke

Ce one-liner devrait le faire:

new (Function.prototype.bind.apply(Something, [null].concat(arguments)));
1
aleemb
function createSomething() {
    var args = Array.prototype.concat.apply([null], arguments);
    return new (Function.prototype.bind.apply(Something, args));
}

Si votre navigateur cible ne prend pas en charge ECMAScript 5 Function.prototype.bind _, le code ne fonctionnera pas. Cependant, il est peu probable que vous voyiez tableau de compatibilité .

1
user2683246

Vous ne pouvez pas appeler un constructeur avec un nombre variable d'arguments comme vous le souhaitez avec l'opérateur new.

Ce que vous pouvez faire, c'est changer légèrement le constructeur. Au lieu de:

function Something() {
    // deal with the "arguments" array
}
var obj = new Something.apply(null, [0, 0]);  // doesn't work!

Faites ceci à la place:

function Something(args) {
    // shorter, but will substitute a default if args.x is 0, false, "" etc.
    this.x = args.x || SOME_DEFAULT_VALUE;

    // longer, but will only put in a default if args.x is not supplied
    this.x = (args.x !== undefined) ? args.x : SOME_DEFAULT_VALUE;
}
var obj = new Something({x: 0, y: 0});

Ou si vous devez utiliser un tableau:

function Something(args) {
    var x = args[0];
    var y = args[1];
}
var obj = new Something([0, 0]);
1
Anthony Mills

@Matthew modifié réponse. Ici, je peux passer n'importe quel nombre de paramètres pour fonctionner comme d'habitude (pas de tableau). De plus, "Quelque chose" n'est pas codé en dur dans:

function createObject( constr ) {   
  var args =  arguments;
  var wrapper =  function() {  
    return constr.apply( this, Array.prototype.slice.call(args, 1) );
  }

  wrapper.prototype =  constr.prototype;
  return  new wrapper();
}


function Something() {
    // init stuff
};

var obj1 =     createObject( Something, 1, 2, 3 );
var same =     new Something( 1, 2, 3 );
1
Eugen Konkov

solutions de Matthew Crumley dans CoffeeScript:

construct = (constructor, args) ->
    F = -> constructor.apply this, args
    F.prototype = constructor.prototype
    new F

ou

createSomething = (->
    F = (args) -> Something.apply this, args
    F.prototype = Something.prototype
    return -> new Something arguments
)()
1
Benjie

Toute fonction (même un constructeur) peut prendre un nombre variable d'arguments. Chaque fonction a une variable "arguments" qui peut être convertie en tableau avec [].slice.call(arguments).

function Something(){
  this.options  = [].slice.call(arguments);

  this.toString = function (){
    return this.options.toString();
  };
}

var s = new Something(1, 2, 3, 4);
console.log( 's.options === "1,2,3,4":', (s.options == '1,2,3,4') );

var z = new Something(9, 10, 11);
console.log( 'z.options === "9,10,11":', (z.options == '9,10,11') );

Les tests ci-dessus produisent la sortie suivante:

s.options === "1,2,3,4": true
z.options === "9,10,11": true
0
Wil Moore III

Alors que les autres approches sont réalisables, elles sont trop complexes. Dans Clojure, vous créez généralement une fonction qui instancie des types/enregistrements et utilisez cette fonction comme mécanisme d'instanciation. Traduire ceci en JavaScript:

function Person(surname, name){
  this.surname = surname;
  this.name = name;
}

function person(surname, name){ 
  return new Person(surname, name);
}

En adoptant cette approche, vous évitez d'utiliser new sauf comme décrit ci-dessus. Et cette fonction, bien sûr, ne présente aucun problème avec apply ou un nombre quelconque d’autres fonctionnalités de programmation fonctionnelles.

var doe  = _.partial(person, "Doe");
var john = doe("John");
var jane = doe("Jane");

En utilisant cette approche, tous vos constructeurs de type (par exemple Person) sont des constructeurs Vanilla, ne rien faire. Vous venez de passer des arguments et de les affecter à des propriétés du même nom. Les détails poilus vont dans la fonction constructeur (par exemple person).

Il n’est pas difficile de créer ces fonctions de constructeur supplémentaires car c’est de toute façon une bonne pratique. Elles peuvent être pratiques car elles vous permettent d’avoir éventuellement plusieurs fonctions de constructeur avec des nuances différentes.

0
Mario

Il est également intéressant de voir comment le problème de la réutilisation du constructeur temporaire F() a été résolu en utilisant arguments.callee, autrement dit la fonction créateur/fabrique elle-même: http://www.dhtmlkitchen.com /? category =/JavaScript/& date = 2008/05/11/& entry = Décorateur-Usine-Aspect

0
polaretto
function FooFactory() {
    var prototype, F = function(){};

    function Foo() {
        var args = Array.prototype.slice.call(arguments),
            i;     
        for (i = 0, this.args = {}; i < args.length; i +=1) {
            this.args[i] = args[i];
        }
        this.bar = 'baz';
        this.print();

        return this;
    }

    prototype = Foo.prototype;
    prototype.print = function () {
        console.log(this.bar);
    };

    F.prototype = prototype;

    return Foo.apply(new F(), Array.prototype.slice.call(arguments));
}

var foo = FooFactory('a', 'b', 'c', 'd', {}, function (){});
console.log('foo:',foo);
foo.print();
0
user2217522

En fait, la méthode la plus simple est:

function Something (a, b) {
  this.a = a;
  this.b = b;
}
function createSomething(){
    return Something;
}
s = new (createSomething())(1, 2); 
// s == Something {a: 1, b: 2}
0
user3184743

Voici ma version de createSomething:

function createSomething() {
    var obj = {};
    obj = Something.apply(obj, arguments) || obj;
    obj.__proto__ = Something.prototype; //Object.setPrototypeOf(obj, Something.prototype); 
    return o;
}

Sur cette base, j'ai essayé de simuler le mot clé new de JavaScript:

//JavaScript 'new' keyword simulation
function new2() {
    var obj = {}, args = Array.prototype.slice.call(arguments), fn = args.shift();
    obj = fn.apply(obj, args) || obj;
    Object.setPrototypeOf(obj, fn.prototype); //or: obj.__proto__ = fn.prototype;
    return obj;
}

Je l'ai testé et il semble que cela fonctionne parfaitement pour tous les scénarios. Cela fonctionne aussi sur des constructeurs natifs comme Date. Voici quelques tests:

//test
new2(Something);
new2(Something, 1, 2);

new2(Date);         //"Tue May 13 2014 01:01:09 GMT-0700" == new Date()
new2(Array);        //[]                                  == new Array()
new2(Array, 3);     //[undefined × 3]                     == new Array(3)
new2(Object);       //Object {}                           == new Object()
new2(Object, 2);    //Number {}                           == new Object(2)
new2(Object, "s");  //String {0: "s", length: 1}          == new Object("s")
new2(Object, true); //Boolean {}                          == new Object(true)
0
advncd

Oui nous pouvons, javascript est plus de prototype inheritance dans la nature.

function Actor(name, age){
  this.name = name;
  this.age = age;
}

Actor.prototype.name = "unknown";
Actor.prototype.age = "unknown";

Actor.prototype.getName = function() {
    return this.name;
};

Actor.prototype.getAge = function() {
    return this.age;
};

lorsque nous créons un objet avec "new", notre objet créé INHERITS getAge (), mais si nous utilisions apply(...) or call(...) pour appeler Actor, nous transmettons un objet pour "this" mais l'objet que nous passons WON'T hérite de Actor.prototype

sauf si nous passons directement apply ou appelons Actor.prototype mais ensuite .... "this" désignera "Actor.prototype" et this.name écrira à: Actor.prototype.name. Ceci affecte donc tous les autres objets créés avec Actor...depuis que nous écrasons le prototype plutôt que l'instance

var rajini = new Actor('Rajinikanth', 31);
console.log(rajini);
console.log(rajini.getName());
console.log(rajini.getAge());

var kamal = new Actor('kamal', 18);
console.log(kamal);
console.log(kamal.getName());
console.log(kamal.getAge());

Essayons avec apply

var vijay = Actor.apply(null, ["pandaram", 33]);
if (vijay === undefined) {
    console.log("Actor(....) didn't return anything 
           since we didn't call it with new");
}

var ajith = {};
Actor.apply(ajith, ['ajith', 25]);
console.log(ajith); //Object {name: "ajith", age: 25}
try {
    ajith.getName();
} catch (E) {
    console.log("Error since we didn't inherit ajith.prototype");
}
console.log(Actor.prototype.age); //Unknown
console.log(Actor.prototype.name); //Unknown

En passant Actor.prototype à Actor.call() comme premier argument, lorsque la fonction Actor () est exécutée, elle exécute this.name=name, puisque "ceci" désignera Actor.prototype, this.name=name; means Actor.prototype.name=name;

var simbhu = Actor.apply(Actor.prototype, ['simbhu', 28]);
if (simbhu === undefined) {
    console.log("Still undefined since the function didn't return anything.");
}
console.log(Actor.prototype.age); //simbhu
console.log(Actor.prototype.name); //28

var copy = Actor.prototype;
var dhanush = Actor.apply(copy, ["dhanush", 11]);
console.log(dhanush);
console.log("But now we've corrupted Parent.prototype in order to inherit");
console.log(Actor.prototype.age); //11
console.log(Actor.prototype.name); //dhanush

Pour revenir à la question originale: comment utiliser new operator with apply, voici mon point de vue ....

Function.prototype.new = function(){
    var constructor = this;
    function fn() {return constructor.apply(this, args)}
    var args = Array.prototype.slice.call(arguments);
    fn.prototype = this.prototype;
    return new fn
};

var thalaivar = Actor.new.apply(Parent, ["Thalaivar", 30]);
console.log(thalaivar);
0
Thalaivar

Une solution révisée de la réponse de @ jordancpaul.

var applyCtor = function(ctor, args)
{
    var instance = new ctor();
    ctor.prototype.constructor.apply(instance, args);
    return instance;
}; 
0
tech-e

depuis ES6, cela est possible via l'opérateur Spread, voir https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Apply_for_new

Cette réponse était déjà, en quelque sorte donnée dans le commentaire https://stackoverflow.com/a/42027742/704981 , mais semble avoir été omise par la plupart

0
Paul