Buscando el límite de argumentos para una función Javascript

17 Jun 2014

Introducción

Rara vez nos habremos preguntado cuál es el límite de parámetros que acepta una función. La razón de esto es simple: no solemos escribir llamadas con un número muy alto de argumentos ni funciones que las recojan. Eso sería inmanejables y un infierno para testear.

Sin embargo, existen un par de casos en los que conocer este límite puede ayudarnos a asegurar el correcto funcionamiento de nuestro script. El primero, viene de la mano de un viejo conocido: el método ‘apply’. El segundo, lo trae el nuevo estándar ECMAScript 6: el operador de propagación.

Veamos cómo nos puede afectar este límite, cuál es y cómo podemos bordearlo para garantizar la integridad de nuestras aplicaciones.

Los argumentos de una función

Por lo general, una función en Javascript no debería tener más de tres o cuatro argumentos. Según el Clean Coder de Robert Martin, realmente se considera una mala práctica manejar más de dos argumentos en una función independientemente del lenguaje en el que estemos trabajando. Con esta premisa en mente, nunca deberíamos encontrarnos un problema de límites.

var foo = function ( param1, param2 ) {
    // Good function
}
 
var bar = function (
    param1,
    param2,
    param3,
    param4,
    param5,
    param6,
    param7
) {
    // Bad function
}

Como solución estándar, lo habitual es que en el momento de necesitar más de dos parámetros, éstos se conviertan en un objeto para así pasar un único argumento a nuestra función. Como beneficio extra, el contar con un mapa nos permite no depender del orden en que recojemos los parámetros dentro de nuestra función, ganando así en flexibilidad.

var params = {
    param1: 'La',
    param2: 'donna',
    param3: 'e',
    param4: 'mobile',
    param5: 'cual',
    param6: 'piuma',
    param7: 'al',
    param8: 'vento'
};
 
var foo = function ( params ) {
    // Good: all expected values are grouped in the object 'params'
};

Salta a simple vista las ventajas de utilizar un objeto a modo de diccionario para manejar todos los posibles argumentos que necesitamos en nuestra función. Pero no siempre es posible ese esquema…

El método apply

El método ‘apply’ nos permite invocar a una función con dos valores: el contexto/valor de this, y todos sus posibles argumentos agrupados en un array (o un objeto similar a un array). Y eso último es lo que nos interesa ahora a nosotros: es el intérprete de Javascript el que de forma automática/desatendida recoge ese array en segundo plano, lo divide y pasa cada uno de sus elementos a la función como argumentos.

En un ejemplo sencillo:

var params = [ 'Hello', 'World' ];
 
var foo = function ( param1, param2 ) {
    console.info( param1, param2 );
}
 
foo.apply( this, params ); // Hello World

En este caso, nuestra función foo espera dos argumentos que son los que el intérprete Javascript le proporciona al dividir el array ‘params’ gracias al método ‘apply’.

Los problemas que puede dar apply

Veamos un ejemplo típico en el que no sabemos el número de argumentos que podemos recibir.

Queremos obtener el número/valor más alto de los que componen un array:

var foo = [ 34, 55, 1, 12, 43, 26, 38, 11, 7 ];

En este caso, necesitamos una función que nos devuelva el 55 como valor más alto. En lugar de escribir algo desde cero, usamos el objeto Math de Javascript, el cual tiene un método ‘max’ que hace exactamente eso:

console.info( Math.max( 45, 12, 99, 5 ) ); // 99

Genial, pero la pega es que ‘max’ no acepta un array directamente, sino que necesita que cada número sea un argumento. Si lo intentamos:

var foo = [ 34, 55, 1, 12, 43, 26, 38, 11, 7 ];
 
console.info( Math.max( foo ) ); // NaN

Error. Ok; hay que pasarle cada elemento del array ‘foo’ como un argumento independiente… Eso es exactamente lo que permite ‘apply’!

var foo = [ 34, 55, 1, 12, 43, 26, 38, 11, 7 ];
 
console.info( Math.max.apply( this, foo ) ); // 55

Ah! Genial! Ahora sabemos identificar el mayor número en un array sin tener que escribir una función para ello!

Si, pero la cuestión es: ¿y si el array que le pasamos a Math es muy grande? ¿Cuál es el límite de valores que puede manejar? Eso es lo que queremos averiguar! Probemos con 10.000 (diez mil):

var foo = [];
var length = 10000;
 
for ( let index = 0; index < length; index++ ) {
    let randomNumber = Math.round( Math.random() * length );
    foo.push( randomNumber );
}
 
console.info( Math.max.apply( this, foo ) ); // 9999 ~ 10000

El bucle ‘for’ va añadiendo al array números ‘aleatorios’ para que al final el ‘max’ los compruebe y extraiga el mayor. Como la función nativa ‘random’ de Javascript apesta, siempre obtendremos un valor igual, o muy cercano, al del número de elementos. He ejecutado este ejemplo varias veces y no he conseguido un valor máximo por debajo de 9998 para 10000 elementos… La ‘aleatoriedad’ es un tema en el que no vamos a entrar, pero la cuestión es que el método ‘max’ ha soportado 10000 argumentos. Subamos!

var length = 50000; // Ok
//...
 
var length = 100000; // OK
//...
 
var length = 200000; // OK
//...
 
var length = 500000; // OK
//...
 
var length = 500001; // CRASH!
// RangeError: arguments array passed to Function.prototype.apply is too large

Bien; ahí se quedó. Estoy haciendo pruebas sobre un Firefox v.30 y el límite queda claramente fijado en 500000 argumentos (quinientos mil).

Hago pruebas con otros navegadores y obtengo este resumen:

IE10 IE11 Firefox v.30 Chrome v.35 Safari v.7
Máx 253717 253717 500000 125408 65536
Excepción Out of stack space Out of stack space Arguments array passed to Function.prototype.apply is too large Maximum call stack size exceeded RangeError: Maximum call stack size exceeded

NOTA: Gracias a ITZombieCrwod por sus métricas para Safari 🙂

Disparidad absoluta. Firefox, o su motor SpiderMonkey, es el que más rango soporta, con un número redondo además; el resto va fijando sus límites en valores más extraños. Si necesitamos pues fijar un rasero, tenemos que mirar al menor valor obtenido, en este caso Safari: su motor ‘solo’ puede operar con algo más de 65000 argumentos.

Y ahí estaría nuestro límite. No podemos intentar superar ese registro cuando trabajo con ‘apply’ porque obtenemos un error.

Evitando el excedernos con los argumentos

Conocer el número máximo de argumentos soportados por cada navegador no va evitar por si solo los errores. Cuando trabajamos por ejemplo con la respuesta que nos devuelve una API externa, puede darse el caso de que dezconozcamos completamente el rango de valores que pueden llegar. En esos casos, hay que buscar alguna forma de evitar un stack mayor del permitido. Veamos cómo hacerlo.

var getSecureMax = function ( arr ) {
    var QUANTUM = 50000,
        result = 0;
 
    for (
        let index = 0, length = arr.length;
        index < length;
        index += QUANTUM
    ) {
        let subArray = arr.slice( index, index + QUANTUM ),
            subMax = Math.max.apply( null, subArray );
 
        result = Math.max( subMax, result );
    }
 
    return result;
};
 
var foo = [ 123, 32, 55, 66, 23, 76, 12, 34 ]; // Put here thousands of values
console.log( getSecureMax( foo ) );

Aquí la idea es tener una función que compute nuestro resultado de forma secuencial dividiendo el array en trozos manejables. El número de ‘trozos’ o argumentos que consideramos seguro lo declaramos en una ‘pseudo-constante’ que en este ejemplo hemos llamado QUANTUM. He escogido el valor 50000 como seguro, pero podría reducirse aún más si tenemos en perspectivas navegadores antiguos.

El resto del código es relativamente simple: dividir el array mediante un ‘slice’ para ir computando los segmentos de forma secuencial o escalonada. Con ello, nos aseguramos de que al método ‘apply’ nunca llegarán más argumentos de los que fijemos en la constante QUANTUM.

Limitaciones del operador de propagación

Como vimos en un pasado post, con ECMAScript 6 llega un nuevo operador que podemos utilizar a la hora de manipular los argumentos en una función: el operador de propagación. Su uso recuerda al del viejo ‘arguments‘: recoge todos los argumentos que se envían o recibe una función en forma de array.

Podemos retomar el ejemplo anterior modificando únicamente la forma en que llamamos a Math.max:

var foo = [];
var length = 500000;
 
for ( let index = 0; index < length; index++ ) {
    let randomNumber = Math.round( Math.random() * length );
    foo.push( randomNumber );
}
 
console.info( Math.max( ...foo ) ); // ~ 500000

Me encanta la elegancia de este operador pero, en este momento, la cuestión está en sus límites. Los 500000 los soporta correctamente; pero…

var length = 500001; // RangeError: too many function arguments

Nada; tenemos la misma limitación: cuando se trata de llamadas a funciones, el límite de argumentos es igual tanto para apply como para el operador de arrastre.

Pero podemos encontrar un escenario donde obtener un resultado diferente…

La excepción de los arrays con el operador de arrastre

Como vimos en su día, este operador puede resultar interesante también para concatenar arrays de un modo claro y legible:

// Concat two arrays
var foo = [ 'En', 'un', 'lugar', 'de', 'la', 'Mancha' ],
    bar = [ 'de', 'cuyo', 'nombre', 'no', 'quiero', 'acordarme' ],
 
    // ECMAScript 6 style
    ES6Style = [ ...foo, ...bar ];

El caso anterior es también similar al uso de un ‘apply’ pero sobre un objeto nativo de Javascript. Podríamos decir que ese código es equivalente a:

var arrayConcat = [];
Array.prototype.push.apply( arrayConcat, foo );
Array.prototype.push.apply( arrayConcat, bar );

o quizá también:

var arrayConcat = [];
arrayConcat.push.apply( arrayConcat, foo );
arrayConcat.push.apply( arrayConcat, bar );

No vamos a entrar en si un ‘concat’ nativo haría lo mismo, sino que nos fijamos en el ‘apply’ implícito que tenemos ahí para ver sus limitaciones a través del operador de propagación. Montamos de nuevo las pruebas y probamos inicialmente con 10000

var length = 10000,
    foo = new Array( length );
 
var func = function () {
    console.info( arguments.length );
}
 
console.info( func( ...foo ) ); // 10000

Ok; sin problemas. En este caso no usamos bucles para rellenar el array; nos basta con acceder a la propiedad ‘length’ del constructor. Pese a que no es la forma más segura de hacerlo, para este ejemplo es perfectamente válida. Sigamos probando:

var length = 100000; // 100000
//...
 
var length = 1000000; // 1000000
//...
 
var length = 10000000; // 10000000
//...
 
var length = 100000000; // 100000000 (!)
//...
 
var length = 100000000; // Browser congelado unos segundos...
// Out of memory

Interesante!! Parece que cuando se trata del operador de propagación sobre un array, no hay más límite que el de la memoria del navegador. Eso quiere decir que la cantidad de argumentos puede así multiplicarse varias veces por encima del valor tope que vimos antes… En este caso, podemos decir que no existe límite en tanto al número de argumentos que acepta una función, sino al número de elementos que soporta un array.

Desconozco si esto es así porque estamos en un estadio aún poco maduro con estos operadores o si por el contrario es el comportamiento final esperado.

Conclusión

Con los ejemplos anteriores, dejamos claro que cuando hablamos sobre el número de argumentos que soporta una función, ya sea de forma directa, usando ‘apply’ o con el nuevo operador de arrastre, cada navegador establece sus propios límites. Conocerlos no deja de ser una curiosidad, aunque tenerlo presente puede ayudar a prevenir errores cuando trabajamos con datos de terceros y desconocemos su número.

Sortear este problema no es sin embargo demasiado complejo: hemos visto como con un pequeño algoritmo, podemos dividir el stack final en grupos ‘seguros’ más pequeños que podemos computar por separado de forma secuencial. Con esto, damos un pasito más en nuestras aplicaciones a la hora de ofrecer solidez en escenarios que precisen de manejar una enorme cantidad de datos.

Más:

{6} Comentarios.

  1. josejuan

    Una forma rápida de llegar al “número mágico” podría ser:

    function getMaxStackArgs() {
      var test =
        function (size) {
          try {
            Math.max.apply(this, new Array(size));
            return true;
          } catch(_) {
            return false;
          }
        }
      var minimum = 1, maximum = 1;
      do {
        minimum = maximum;
        maximum < <= 1;
        console.log("Testing " + maximum + "...");
      } while(test(maximum));
      while(minimum < maximum) {
        var m = (minimum + maximum) >> 1;
        console.log("Testing {" + minimum + " < = " + m + " <= " + maximum+ "...");
        if(test(m)) minimum = m;
        else        maximum = m - 1;
      }
      return minimum;
    }
    
    console.log(getMaxStackArgs());
    
    • Carlos Benítez

      Perfecto;
      con esa función podemos comprobar fácilmente el número de argumentos soportados en varios entornos y navegadores.

      Gran aporte; gracias!

      Saludos!

  2. ITZombieCrwod

    En el caso de Safari [Version 7.0.4 (9537.76.4)] el número máximo de argumentos para una función es:
    65536.

    Con salida: RangeError: Maximum call stack size exceeded.

  3. Pedro

    Genial! justo lo que estaba buscando 🙂

  4. Joffre sanchez

    65536 me parece un numero lógico es 2 elevado a la potencia 16, pero en el caso de los otros, me gustaría saber su motivo

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *