¿Cómo obtener nombres/valores de parámetros de funciones de forma dinámica?

Resuelto vikasde asked hace 15 años • 0 respuestas

¿Hay alguna manera de obtener los nombres de los parámetros de una función de forma dinámica?

Digamos que mi función se ve así:

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

Ahora, ¿cómo obtendría una lista de los nombres de los parámetros y sus valores en una matriz desde el interior de la función?

vikasde avatar Jun 17 '09 22:06 vikasde
Aceptado

La siguiente función devolverá una matriz de los nombres de los parámetros de cualquier función pasada.

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

Uso de ejemplo:

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 :

Con la invención de ES6, esta función se puede activar mediante parámetros predeterminados. Aquí hay un truco rápido que debería funcionar en la mayoría de los casos:

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

Digo la mayoría de los casos porque hay algunas cosas que lo harán tropezar.

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

Editar : También noto que vikasde también quiere los valores de los parámetros en una matriz. Esto ya se proporciona en una variable local denominada argumentos.

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

El objeto de argumentos no es una matriz. Es similar a un Array, pero no tiene ninguna propiedad de Array excepto la longitud. Por ejemplo, no tiene el método pop. Sin embargo, se puede convertir en un Array real:

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

Si los genéricos de Array están disponibles, se pueden usar los siguientes en su lugar:

var args = Array.slice(arguments);
Jack Allan avatar Mar 29 '2012 11:03 Jack Allan

A continuación se muestra el código tomado de AngularJS que utiliza la técnica para su mecanismo de inyección de dependencia.

Y aquí hay una explicación tomada de http://docs.angularjs.org/tutorial/step_05

El inyector de dependencia de Angular proporciona servicios a su controlador cuando se construye el controlador. El inyector de dependencias también se encarga de crear cualquier dependencia transitiva que pueda tener el servicio (los servicios a menudo dependen de otros servicios).

Tenga en cuenta que los nombres de los argumentos son importantes, porque el inyector los usa para buscar las dependencias.

/**
 * @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;
}
Lambder avatar Aug 24 '2012 11:08 Lambder

Aquí hay una solución actualizada que intenta abordar todos los casos extremos mencionados anteriormente de una manera compacta:

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 [""]
}  

Resultado de la prueba abreviado (los casos de prueba completos se adjuntan a continuación):

'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 []

Mostrar fragmento de código

humbletim avatar Jul 02 '2015 21:07 humbletim

La solución que es menos propensa a errores con espacios y comentarios sería:

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

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

["hi", "you"]
bubersson avatar Feb 02 '2013 08:02 bubersson

Muchas de las respuestas aquí usan expresiones regulares, esto está bien, pero no maneja muy bien las nuevas adiciones al lenguaje (como funciones de flecha y clases). También es de destacar que si usa alguna de estas funciones en código minimizado, desaparecerá 🔥. Utilizará cualquiera que sea el nombre minimizado. Angular soluciona esto permitiéndole pasar una matriz ordenada de cadenas que coincida con el orden de los argumentos al registrarlos en el contenedor DI. Así que vamos con la solución:

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');
};

Esto maneja el problema del análisis original y algunos tipos de funciones más (por ejemplo, funciones de flecha). He aquí una idea de lo que puede y no puede manejar tal como está:

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

Dependiendo de lo que desee utilizar para ES6 Proxies y la desestructuración puede ser su mejor opción. Por ejemplo, si desea usarlo para la inyección de dependencia (usando los nombres de los parámetros), puede hacerlo de la siguiente manera:

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

No es el solucionador más avanzado que existe, pero da una idea de cómo puede usar un Proxy para manejarlo si desea usar el analizador de argumentos para DI simple. Sin embargo, hay una pequeña advertencia en este enfoque. Necesitamos utilizar asignaciones desestructurantes en lugar de parámetros normales. Cuando pasamos el proxy del inyector, la desestructuración es lo mismo que llamar al captador del objeto.

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

Esto genera lo siguiente:

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

Está conectado toda la aplicación. Lo mejor es que la aplicación es fácil de probar (puedes simplemente crear una instancia de cada clase y pasar simulacros/stubs/etc). Además, si necesita intercambiar implementaciones, puede hacerlo desde un solo lugar. Todo esto es posible gracias a los objetos JS Proxy.

Nota: Hay mucho trabajo por hacer antes de que esté listo para su uso en producción, pero da una idea de cómo se vería.

La respuesta es un poco tardía, pero puede ayudar a otros que estén pensando en lo mismo. 👍

James Drew avatar Jan 07 '2017 18:01 James Drew