351 Stimmen

Wie kann man die Namen/Werte von Funktionsparametern dynamisch ermitteln?

Gibt es eine Möglichkeit, die Namen der Funktionsparameter einer Funktion dynamisch abzurufen?

Nehmen wir an, meine Funktion sieht wie folgt aus:

function doSomething(param1, param2, .... paramN){
   // fill an array with the parameter name and value
   // some other code 
}

Nun, wie würde ich eine Liste der Parameternamen und ihre Werte in ein Array von innerhalb der Funktion erhalten?

367voto

Jack Allan Punkte 13724

Die folgende Funktion gibt ein Array mit den Parameternamen jeder übergebenen Funktion zurück.

var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
function getParamNames(func) {
  var fnStr = func.toString().replace(STRIP_COMMENTS, '');
  var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
  if(result === null)
     result = [];
  return result;
}

Beispiel für die Verwendung:

getParamNames(getParamNames) // returns ['func']
getParamNames(function (a,b,c,d){}) // returns ['a','b','c','d']
getParamNames(function (a,/*b,c,*/d){}) // returns ['a','d']
getParamNames(function (){}) // returns []

Editar :

Mit der Erfindung von ES6 kann diese Funktion durch Standardparameter ausgelöst werden. Hier ist ein schneller Hack, der in den meisten Fällen funktionieren sollte:

var STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;

Ich sage "in den meisten Fällen", denn es gibt einige Dinge, die das System stören können

function (a=4*(5/3), b) {} // returns ['a']

Editar : Ich stelle außerdem fest, dass Vikasde die Parameterwerte auch in einem Array haben möchte. Dies ist bereits in einer lokalen Variable namens arguments vorhanden.

Auszug aus https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments :

Das Argumente-Objekt ist kein Array. Es ist einem Array ähnlich, hat aber außer der Länge keine Array-Eigenschaften. So verfügt es beispielsweise nicht über die Methode pop. Es kann jedoch in ein echtes Array konvertiert werden:

var args = Array.prototype.slice.call(arguments);

Wenn Array-Generika verfügbar sind, kann man stattdessen Folgendes verwenden:

var args = Array.slice(arguments);

131voto

Lambder Punkte 2832

Der folgende Code stammt aus AngularJS, das diese Technik für seinen Dependency-Injection-Mechanismus verwendet.

Und hier eine Erklärung dazu aus http://docs.angularjs.org/tutorial/step_05

Der Dependency Injector von Angular stellt Ihrem Controller Dienste zur Verfügung wenn der Controller konstruiert wird. Der Dependency Injector kümmert sich auch sich auch um die Erstellung von transitiven Abhängigkeiten, die der Dienst haben kann (Dienste hängen oft von anderen Diensten ab).

Beachten Sie, dass die Namen der Argumente von Bedeutung sind, da der Injektor diese verwendet, um die Abhängigkeiten nachzuschlagen.

/**
 * @ngdoc overview
 * @name AUTO
 * @description
 *
 * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}.
 */

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function annotate(fn) {
  var $inject,
      fnText,
      argDecl,
      last;

  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      $inject = [];
      fnText = fn.toString().replace(STRIP_COMMENTS, '');
      argDecl = fnText.match(FN_ARGS);
      forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
        arg.replace(FN_ARG, function(all, underscore, name){
          $inject.push(name);
        });
      });
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn')
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;
}

64voto

humbletim Punkte 1000

Hier ist eine aktualisierte Lösung, die versucht, alle oben genannten Probleme auf kompakte Weise zu lösen:

function $args(func) {  
    return (func + '')
      .replace(/[/][/].*$/mg,'') // strip single-line comments
      .replace(/\s+/g, '') // strip white space
      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
      .replace(/=[^,]+/g, '') // strip any ES6 defaults  
      .split(',').filter(Boolean); // split & filter [""]
}  

Gekürzte Testausgabe (die vollständigen Testfälle sind unten beigefügt):

'function (a,b,c)...' // returns ["a","b","c"]
'function ()...' // returns []
'function named(a, b, c) ...' // returns ["a","b","c"]
'function (a /* = 1 */, b /* = true */) ...' // returns ["a","b"]
'function fprintf(handle, fmt /*, ...*/) ...' // returns ["handle","fmt"]
'function( a, b = 1, c )...' // returns ["a","b","c"]
'function (a=4*(5/3), b) ...' // returns ["a","b"]
'function (a, // single-line comment xjunk) ...' // returns ["a","b"]
'function (a /* fooled you...' // returns ["a","b"]
'function (a /* function() yes */, \n /* no, */b)/* omg! */...' // returns ["a","b"]
'function ( A, b \n,c ,d \n ) \n ...' // returns ["A","b","c","d"]
'function (a,b)...' // returns ["a","b"]
'function $args(func) ...' // returns ["func"]
'null...' // returns ["null"]
'function Object() ...' // returns []

function $args(func) {  
    return (func + '')
      .replace(/[/][/].*$/mg,'') // strip single-line comments
      .replace(/\s+/g, '') // strip white space
      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
      .replace(/=[^,]+/g, '') // strip any ES6 defaults  
      .split(',').filter(Boolean); // split & filter [""]
}  

// test cases  
document.getElementById('console_info').innerHTML = (
[  
  // formatting -- typical  
  function(a,b,c){},  
  function(){},  
  function named(a, b,  c) {  
/* multiline body */  
  },  

  // default values -- conventional  
  function(a /* = 1 */, b /* = true */) { a = a||1; b=b||true; },  
  function fprintf(handle, fmt /*, ...*/) { },  

  // default values -- ES6  
  "function( a, b = 1, c ){}",  
  "function (a=4*(5/3), b) {}",  

  // embedded comments -- sardonic  
  function(a, // single-line comment xjunk) {}
    b //,c,d
  ) // single-line comment
  {},  
  function(a /* fooled you{*/,b){},  
  function /* are you kidding me? (){} */(a /* function() yes */,  
   /* no, */b)/* omg! */{/*}}*/},  

  // formatting -- sardonic  
  function  (  A,  b  
,c  ,d  
  )  
  {  
  },  

  // by reference  
  this.jQuery || function (a,b){return new e.fn.init(a,b,h)},
  $args,  

  // inadvertent non-function values  
  null,  
  Object  
].map(function(f) {
    var abbr = (f + '').replace(/\n/g, '\\n').replace(/\s+|[{]+$/g, ' ').split("{", 1)[0] + "...";
    return "    '" + abbr + "' // returns " + JSON.stringify($args(f));
  }).join("\n") + "\n"); // output for copy and paste as a markdown snippet

<pre id='console_info'></pre>

25voto

bubersson Punkte 749

Eine Lösung, die weniger fehleranfällig für Leerzeichen und Kommentare ist, wäre:

var fn = function(/* whoa) */ hi, you){};

fn.toString()
  .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'')
  .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
  .split(/,/)

["hi", "you"]

22voto

James Drew Punkte 578

Viele der Antworten auf hier verwenden Regexes, das ist in Ordnung, aber es behandelt nicht neue Ergänzungen zu der Sprache zu gut (wie Pfeil-Funktionen und Klassen). Außerdem ist zu beachten, dass, wenn Sie eine dieser Funktionen auf verkleinerten Code verwenden, wird es zu gehen. Es wird verwendet, was auch immer der minifizierte Name ist. Angular umgeht dies, indem es Ihnen erlaubt, ein geordnetes Array von Strings zu übergeben, das mit der Reihenfolge der Argumente übereinstimmt, wenn Sie sie im DI-Container registrieren. Also weiter mit der Lösung:

var esprima = require('esprima');
var _ = require('lodash');

const parseFunctionArguments = (func) => {
    // allows us to access properties that may or may not exist without throwing 
    // TypeError: Cannot set property 'x' of undefined
    const maybe = (x) => (x || {});

    // handle conversion to string and then to JSON AST
    const functionAsString = func.toString();
    const tree = esprima.parse(functionAsString);
    console.log(JSON.stringify(tree, null, 4))
    // We need to figure out where the main params are. Stupid arrow functions 
    const isArrowExpression = (maybe(_.first(tree.body)).type == 'ExpressionStatement');
    const params = isArrowExpression ? maybe(maybe(_.first(tree.body)).expression).params 
                                     : maybe(_.first(tree.body)).params;

    // extract out the param names from the JSON AST
    return _.map(params, 'name');
};

Dies behandelt das ursprüngliche Parse-Problem und einige weitere Funktionstypen (z. B. Pfeilfunktionen). Hier ist eine Vorstellung davon, was es kann und was nicht, wie es ist:

// I usually use mocha as the test runner and chai as the assertion library
describe('Extracts argument names from function signature. ', () => {
    const test = (func) => {
        const expectation = ['it', 'parses', 'me'];
        const result = parseFunctionArguments(toBeParsed);
        result.should.equal(expectation);
    } 

    it('Parses a function declaration.', () => {
        function toBeParsed(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses a functional expression.', () => {
        const toBeParsed = function(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses an arrow function', () => {
        const toBeParsed = (it, parses, me) => {};
        test(toBeParsed);
    });

    // ================= cases not currently handled ========================

    // It blows up on this type of messing. TBH if you do this it deserves to 
    // fail  On a tech note the params are pulled down in the function similar 
    // to how destructuring is handled by the ast.
    it('Parses complex default params', () => {
        function toBeParsed(it=4*(5/3), parses, me) {}
        test(toBeParsed);
    });

    // This passes back ['_ref'] as the params of the function. The _ref is a 
    // pointer to an VariableDeclarator where the  happens.
    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ({it, parses, me}){}
        test(toBeParsed);
    });

    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ([it, parses, me]){}
        test(toBeParsed);
    });

    // Classes while similar from an end result point of view to function
    // declarations are handled completely differently in the JS AST. 
    it('Parses a class constructor when passed through', () => {
        class ToBeParsed {
            constructor(it, parses, me) {}
        }
        test(ToBeParsed);
    });
});

Je nachdem, wofür Sie es verwenden möchten ES6 Proxies und destructuring kann Ihre beste Wette sein. Zum Beispiel, wenn Sie es für die Injektion von Abhängigkeiten (mit den Namen der Parameter) verwenden möchten, dann können Sie es wie folgt tun:

class GuiceJs {
    constructor() {
        this.modules = {}
    }
    resolve(name) {
        return this.getInjector()(this.modules[name]);
    }
    addModule(name, module) {
        this.modules[name] = module;
    }
    getInjector() {
        var container = this;

        return (klass) => {
            console.log(klass);
            var paramParser = new Proxy({}, {
                // The `get` handler is invoked whenever a get-call for
                // `injector.*` is made. We make a call to an external service
                // to actually hand back in the configured service. The proxy
                // allows us to bypass parsing the function params using
                // taditional regex or even the newer parser.
                get: (target, name) => container.resolve(name),

                // You shouldn't be able to set values on the injector.
                set: (target, name, value) => {
                    throw new Error(`Don't try to set ${name}! `);
                }
            })
            return new klass(paramParser);
        }
    }
}

Es ist nicht der fortschrittlichste Resolver, den es gibt, aber er gibt eine Vorstellung davon, wie man einen Proxy verwenden kann, um ihn zu handhaben, wenn man den args-Parser für einfache DI verwenden möchte. Es gibt jedoch einen kleinen Vorbehalt bei diesem Ansatz. Wir müssen Destrukturierungszuweisungen anstelle von normalen Params verwenden. Wenn wir den Injektor-Proxy übergeben, ist die Destrukturierung dasselbe wie der Aufruf des Getters auf dem Objekt.

class App {
   constructor({tweeter, timeline}) {
        this.tweeter = tweeter;
        this.timeline = timeline;
    }
}

class HttpClient {}

class TwitterApi {
    constructor({client}) {
        this.client = client;
    }
}

class Timeline {
    constructor({api}) {
        this.api = api;
    }
}

class Tweeter {
    constructor({api}) {
        this.api = api;
    }
}

// Ok so now for the business end of the injector!
const di = new GuiceJs();

di.addModule('client', HttpClient);
di.addModule('api', TwitterApi);
di.addModule('tweeter', Tweeter);
di.addModule('timeline', Timeline);
di.addModule('app', App);

var app = di.resolve('app');
console.log(JSON.stringify(app, null, 4));

Es wird Folgendes ausgegeben:

{
    "tweeter": {
        "api": {
            "client": {}
        }
    },
    "timeline": {
        "api": {
            "client": {}
        }
    }
}

Es hat die gesamte Anwendung verkabelt. Das beste Bit ist, dass die App einfach zu testen ist (Sie können einfach jede Klasse instanziieren und in Mocks/Stubs/etc übergeben). Auch wenn Sie Implementierungen austauschen müssen, können Sie das von einem einzigen Ort aus tun. All dies ist möglich, weil der JS Proxy-Objekte.

Hinweis: Es muss noch viel daran gearbeitet werden, bevor es produktionsreif ist, aber es vermittelt eine Vorstellung davon, wie es aussehen könnte.

Die Antwort kommt zwar etwas spät, aber vielleicht hilft sie anderen, die an das Gleiche denken.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X