Cómo migrar secuencias de comandos al entorno de ejecución V8

Si tienes una secuencia de comandos existente que usa el entorno de ejecución de Rhino y deseas usar la sintaxis y las funciones de V8, debes migrar la secuencia de comandos a V8.

La mayoría de las secuencias de comandos escritas con el entorno de ejecución de Rhino pueden funcionar con un entorno de ejecución V8 sin ajuste. A menudo, el único requisito previo para agregar la sintaxis y las funciones de V8 a una secuencia de comandos es habilitar el entorno de ejecución de V8.

Sin embargo, existe un pequeño conjunto de incompatibilidades y otras diferencias que pueden provocar que una secuencia de comandos falle o se comporte de forma inesperada después de habilitar el tiempo de ejecución V8. A medida que migras una secuencia de comandos para usar V8, debes buscar estos problemas en el proyecto de secuencia de comandos y corregir cualquiera que encuentres.

Procedimiento de migración de V8

Para migrar una secuencia de comandos a V8, sigue este procedimiento:

  1. Habilita el tiempo de ejecución V8 para la secuencia de comandos.
  2. Revisa con atención las incompatibilidades que se indican a continuación. Examina tu secuencia de comandos para determinar si hay alguna incompatibilidad. Si hay una o más incompatibilidades, ajusta el código de secuencia de comandos para quitar o evitar el problema.
  3. Revisa cuidadosamente otras diferencias que se indican a continuación. Examina tu secuencia de comandos para determinar si alguna de las diferencias enumeradas afecta el comportamiento del código. Ajusta el guion para corregir el comportamiento.
  4. Una vez que hayas corregido otras incompatibilidades u otras diferencias descubiertas, puedes comenzar a actualizar tu código para usar la sintaxis de V8 y otras funciones según lo desees.
  5. Después de finalizar los ajustes de código, prueba minuciosamente la secuencia de comandos para asegurarte de que se comporte como se espera.
  6. Si tu secuencia de comandos es una aplicación web o un complemento publicado, debes crear una versión nueva de la secuencia de comandos con los ajustes de V8. Si quieres que la versión V8 esté disponible para los usuarios, debes volver a publicar la secuencia de comandos con esa versión.

Incompatibilidades

Lamentablemente, el entorno de ejecución original de Apps Script basado en Rhino permitía varios comportamientos no estándar de ECMAScript. Dado que V8 cumple con los estándares, estos comportamientos no son compatibles después de la migración. Si no se corrigen estos problemas, se producen errores o un comportamiento incorrecto de la secuencia de comandos una vez que se habilita el tiempo de ejecución V8.

En las siguientes secciones, se describe cada uno de estos comportamientos y pasos que debes seguir para corregir tu código de secuencia de comandos durante la migración a V8.

Evita for each(variable in object)

Se agregó la declaración for each (variable in object) a JavaScript 1.6 y se quitó a favor de for...of.

Cuando migres tu secuencia de comandos a V8, evita usar declaraciones for each (variable in object).

En su lugar, usa for (variable in object):

// Rhino runtime
var obj = {a: 1, b: 2, c: 3};

// Don't use 'for each' in V8
for each (var value in obj) {
  Logger.log("value = %s", value);
}
      
// V8 runtime
var obj = {a: 1, b: 2, c: 3};

for (var key in obj) {  // OK in V8
  var value = obj[key];
  Logger.log("value = %s", value);
}
      

Evita Date.prototype.getYear()

En el entorno de ejecución original de Rhino, Date.prototype.getYear() muestra años de dos dígitos para los años desde 1900 hasta 1999, pero años de cuatro dígitos para otras fechas, que era el comportamiento en JavaScript 1.2 y anteriores.

En el tiempo de ejecución V8, Date.prototype.getYear() muestra el año menos 1,900, como lo requieren los estándares de ECMAScript.

Cuando migres la secuencia de comandos a V8, usa siempre Date.prototype.getFullYear(), que muestra un año de cuatro dígitos, sin importar la fecha.

Evita usar palabras clave reservadas como nombres

ECMAScript prohíbe el uso de ciertas palabras clave reservadas en los nombres de funciones y variables. El entorno de ejecución de Rhino permitía muchas de estas palabras, por lo que, si tu código las usa, debes cambiar el nombre de tus funciones o variables.

Cuando migres la secuencia de comandos a V8, evita nombrar variables o funciones con una de las palabras clave reservadas. Cambia el nombre de cualquier variable o función para evitar usar el nombre de la palabra clave. Los usos comunes de las palabras clave como nombres son class, import y export.

Evita reasignar variables const

En el entorno de ejecución original de Rhino, puedes declarar una variable usando const, lo que significa que el valor del símbolo nunca cambia y se ignoran las asignaciones futuras al símbolo.

En el nuevo entorno de ejecución V8, la palabra clave const cumple con el estándar, y asignar a una variable declarada como const genera un error en el entorno de ejecución TypeError: Assignment to constant variable.

Cuando migres tu secuencia de comandos a V8, no intentes reasignar el valor de una variable const:

// Rhino runtime
const x = 1;
x = 2;          // No error
console.log(x); // Outputs 1
      
// V8 runtime
const x = 1;
x = 2;          // Throws TypeError
console.log(x); // Never executed
      

Evita los literales XML y el objeto XML

Esta extensión no estándar a ECMAScript permite que los proyectos de Apps Script usen la sintaxis XML directamente.

Cuando migres tu secuencia de comandos a V8, evita usar literales XML directos o el objeto XML.

En su lugar, usa XmlService para analizar XML:

// V8 runtime
var incompatibleXml1 = <container><item/></container>;             // Don't use
var incompatibleXml2 = new XML('<container><item/></container>');  // Don't use

var xml3 = XmlService.parse('<container><item/></container>');     // OK
      

No compiles funciones de iterador personalizadas con __iterator__.

JavaScript 1.7 agregó una función para permitir la adición de un iterador personalizado a cualquier clase mediante la declaración de una función __iterator__ en el prototipo de esa clase. Esto también se agregó al tiempo de ejecución de Rhino de Apps Script como conveniencia para el desarrollador. Sin embargo, esta función nunca formó parte del estándar ECMA-262 y se quitó de los motores de JavaScript compatibles con ECMAScript. Las secuencias de comandos que usan V8 no pueden usar esta construcción de iterador.

Cuando migres la secuencia de comandos a V8, evita la función __iterator__ para compilar iteradores personalizados. En su lugar, usa los iteradores de ECMAScript 6.

Considera la siguiente construcción del array:

// Create a sample array
var myArray = ['a', 'b', 'c'];
// Add a property to the array
myArray.foo = 'bar';

// The default behavior for an array is to return keys of all properties,
//  including 'foo'.
Logger.log("Normal for...in loop:");
for (var item in myArray) {
  Logger.log(item);            // Logs 0, 1, 2, foo
}

// To only log the array values with `for..in`, a custom iterator can be used.
      

En los siguientes ejemplos de código, se muestra cómo se puede construir un iterador en el entorno de ejecución de Rhino y cómo construir un iterador de reemplazo en el entorno de ejecución V8:

// Rhino runtime custom iterator
function ArrayIterator(array) {
  this.array = array;
  this.currentIndex = 0;
}

ArrayIterator.prototype.next = function() {
  if (this.currentIndex
      >= this.array.length) {
    throw StopIteration;
  }
  return "[" + this.currentIndex
    + "]=" + this.array[this.currentIndex++];
};

// Direct myArray to use the custom iterator
myArray.__iterator__ = function() {
  return new ArrayIterator(this);
}


Logger.log("With custom Rhino iterator:");
for (var item in myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      
// V8 runtime (ECMAScript 6) custom iterator
myArray[Symbol.iterator] = function() {
  var currentIndex = 0;
  var array = this;

  return {
    next: function() {
      if (currentIndex < array.length) {
        return {
          value: "[${currentIndex}]="
            + array[currentIndex++],
          done: false};
      } else {
        return {done: true};
      }
    }
  };
}

Logger.log("With V8 custom iterator:");
// Must use for...of since
//   for...in doesn't expect an iterable.
for (var item of myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      

Evita las cláusulas de captura condicionales

El entorno de ejecución V8 no admite cláusulas de captura condicional catch..if, ya que no cumplen con los estándares.

Cuando migres tu secuencia de comandos a V8, mueve las condicionales de captura dentro del cuerpo de captura:

// Rhino runtime

try {
  doSomething();
} catch (e if e instanceof TypeError) {  // Don't use
  // Handle exception
}
      
// V8 runtime
try {
  doSomething();
} catch (e) {
  if (e instanceof TypeError) {
    // Handle exception
  }
}

Evita usar Object.prototype.toSource()

JavaScript 1.3 contenía un método Object.prototype.toSource() que nunca fue parte de ningún estándar ECMAScript. No es compatible con el entorno de ejecución V8.

Cuando migres la secuencia de comandos a V8, quita cualquier uso de Object.prototype.toSource() del código.

Otras diferencias

Además de las incompatibilidades anteriores que pueden causar fallas en las secuencias de comandos, existen otras diferencias que, si no se corrigen, pueden generar un comportamiento inesperado de la secuencia de comandos en el tiempo de ejecución V8.

En las siguientes secciones, se explica cómo actualizar el código de la secuencia de comandos para evitar estas sorpresas inesperadas.

Cómo ajustar el formato de fecha y hora específico de la configuración regional

Los métodos toLocaleString(), toLocaleDateString() y toLocaleTimeString() de Date se comportan de manera diferente en el tiempo de ejecución de V8 en comparación con Rhino.

En Rhino, el formato predeterminado es el formato largo y se ignoran todos los parámetros que se pasan.

En el entorno de ejecución V8, el formato predeterminado es el formato corto y los parámetros que se pasan se manejan según el estándar ECMA (consulta la documentación de toLocaleDateString() para obtener más detalles).

Cuando migres tu secuencia de comandos a V8, prueba y ajusta las expectativas de tu código con respecto al resultado de métodos de fecha y hora específicos de la configuración regional:

// Rhino runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "December 21, 2012" in Rhino
console.log(event.toLocaleDateString());

// Also outputs "December 21, 2012",
//  ignoring the parameters passed in.
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
// V8 runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "12/21/2012" in V8
console.log(event.toLocaleDateString());

// Outputs "21. Dezember 2012"
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
      

Evita usar Error.fileName y Error.lineNumber

En la versión 8 untime, el objeto Error estándar de JavaScript no admite fileName ni lineNumber como parámetros de constructor o propiedades del objeto.

Cuando migres tu secuencia de comandos a V8, quita cualquier dependencia de Error.fileName y Error.lineNumber.

Una alternativa es usar Error.prototype.stack. Esta pila tampoco es estándar, pero es compatible con Rhino y V8. El formato del seguimiento de pila que producen las dos plataformas es ligeramente diferente:

// Rhino runtime Error.prototype.stack
// stack trace format
at filename:92 (innerFunction)
at filename:97 (outerFunction)


// V8 runtime Error.prototype.stack
// stack trace format
Error: error message
at innerFunction (filename:92:11)
at outerFunction (filename:97:5)
      

Cómo ajustar el manejo de objetos de enumeración enum en cadena

En el entorno de ejecución original de Rhino, el uso del método JSON.stringify() de JavaScript en un objeto enum solo muestra {}.

En V8, el uso del mismo método en un objeto enum recupera el nombre enum.

Cuando migres tu secuencia de comandos a V8, prueba y ajusta las expectativas de tu código en relación con el resultado de JSON.stringify() en los objetos de enumeración:

// Rhino runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to {}
// V8 runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to "BUBBLE"

Cómo ajustar el manejo de parámetros no definidos

En el tiempo de ejecución original de Rhino, pasar undefined a un método como parámetro generó que se pasara la string "undefined" a ese método.

En V8, pasar undefined a los métodos equivale a pasar null.

Cuando migres tu secuencia de comandos a V8, prueba y ajusta las expectativas de tu código en relación con los parámetros undefined:

// Rhino runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has the string
// "undefined"  as its value.
      
// V8 runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has no content, as
// setValue(null) removes content from
// ranges.

Ajusta el manejo de los this globales

El entorno de ejecución de Rhino define un contexto especial implícito para las secuencias de comandos que lo usan. El código de secuencia de comandos se ejecuta en este contexto implícito, distinto del this global real. Esto significa que las referencias al "this global" en el código en realidad se evalúan en función del contexto especial, que solo contiene el código y las variables definidas en la secuencia de comandos. Los servicios integrados de Apps Script y los objetos de ECMAScript se excluyen de este uso de this. La situación fue similar a la de esta estructura de JavaScript:

// Rhino runtime

// Apps Script built-in services defined here, in the actual global context.
var SpreadsheetApp = {
  openById: function() { ... }
  getActive: function() { ... }
  // etc.
};

function() {
  // Implicit special context; all your code goes here. If the global this
  // is referenced in your code, it only contains elements from this context.

  // Any global variables you defined.
  var x = 42;

  // Your script functions.
  function myFunction() {
    ...
  }
  // End of your code.
}();

En V8, se quita el contexto especial implícito. Las variables y funciones globales definidas en la secuencia de comandos se colocan en el contexto global, junto a los servicios integrados de Apps Script y las funciones integradas de ECMAScript, como Math y Date.

Cuando migres tu secuencia de comandos a V8, prueba y ajusta las expectativas de tu código con respecto al uso de this en un contexto global. En la mayoría de los casos, las diferencias solo son evidentes si el código examina las claves o los nombres de las propiedades del objeto this global:

// Rhino runtime
var myGlobal = 5;

function myFunction() {

  // Only logs [myFunction, myGlobal];
  console.log(Object.keys(this));

  // Only logs [myFunction, myGlobal];
  console.log(
    Object.getOwnPropertyNames(this));
}





      
// V8 runtime
var myGlobal = 5;

function myFunction() {

  // Logs an array that includes the names
  // of Apps Script services
  // (CalendarApp, GmailApp, etc.) in
  // addition to myFunction and myGlobal.
  console.log(Object.keys(this));

  // Logs an array that includes the same
  // values as above, and also includes
  // ECMAScript built-ins like Math, Date,
  // and Object.
  console.log(
    Object.getOwnPropertyNames(this));
}

Ajusta el manejo de instanceof en las bibliotecas

El uso de instanceof en una biblioteca en un objeto que se pasa como parámetro en una función de otro proyecto puede generar falsos negativos. En el tiempo de ejecución V8, un proyecto y sus bibliotecas se ejecutan en diferentes contextos de ejecución y, por lo tanto, tienen diferentes globales y cadenas de prototipos.

Ten en cuenta que este es solo el caso si tu biblioteca usa instanceof en un objeto que no se crea en tu proyecto. Su uso en un objeto que se crea en tu proyecto, ya sea en la misma secuencia de comandos o en una diferente dentro de tu proyecto, debería funcionar como se espera.

Si un proyecto que se ejecuta en V8 usa tu secuencia de comandos como biblioteca, verifica si la secuencia de comandos usa instanceof en un parámetro que se pasará desde otro proyecto. Ajusta el uso de instanceof y usa otras alternativas posibles según tu caso de uso.

Una alternativa para a instanceof b puede ser usar el constructor de a en los casos en que no necesites buscar en toda la cadena del prototipo y solo verificar el constructor. Uso: a.constructor.name == "b"

Considera el Proyecto A y el Proyecto B, en el que el Proyecto A usa el Proyecto B como biblioteca.

//Rhino runtime

//Project A

function caller() {
   var date = new Date();
   // Returns true
   return B.callee(date);
}

//Project B

function callee(date) {
   // Returns true
   return(date instanceof Date);
}

      
//V8 runtime

//Project A

function caller() {
   var date = new Date();
   // Returns false
   return B.callee(date);
}

//Project B

function callee(date) {
   // Incorrectly returns false
   return(date instanceof Date);
   // Consider using return (date.constructor.name ==
   // “Date”) instead.
   // return (date.constructor.name == “Date”) -> Returns
   // true
}

Otra alternativa puede ser introducir una función que verifique instanceof en el proyecto principal y pasar la función además de otros parámetros cuando llame a una función de biblioteca. La función pasada se puede usar para verificar instanceof dentro de la biblioteca.

//V8 runtime

//Project A

function caller() {
   var date = new Date();
   // Returns True
   return B.callee(date, date => date instanceof Date);
}

//Project B

function callee(date, checkInstanceOf) {
  // Returns True
  return checkInstanceOf(date);
}
      

Ajusta el paso de recursos no compartidos a las bibliotecas

Pasar un recurso no compartido de la secuencia de comandos principal a una biblioteca funciona de manera diferente en el tiempo de ejecución V8.

En el entorno de ejecución de Rhino, pasar un recurso no compartido no funcionará. En su lugar, la biblioteca usa su propio recurso.

En el tiempo de ejecución V8, funciona el paso de un recurso no compartido a la biblioteca. La biblioteca usa el recurso no compartido que se pasó.

No pases recursos no compartidos como parámetros de función. Siempre declara los recursos no compartidos en la misma secuencia de comandos que los usa.

Considera el Proyecto A y el Proyecto B, en el que el Proyecto A usa el Proyecto B como biblioteca. En este ejemplo, PropertiesService es un recurso no compartido.

// Rhino runtime
// Project A
function testPassingNonSharedProperties() {
  PropertiesService.getScriptProperties()
      .setProperty('project', 'Project-A');
  B.setScriptProperties();
  // Prints: Project-B
  Logger.log(B.getScriptProperties(
      PropertiesService, 'project'));
}

//Project B function setScriptProperties() { PropertiesService.getScriptProperties() .setProperty('project', 'Project-B'); } function getScriptProperties( propertiesService, key) { return propertiesService.getScriptProperties() .getProperty(key); }

// V8 runtime
// Project A
function testPassingNonSharedProperties() {
  PropertiesService.getScriptProperties()
      .setProperty('project', 'Project-A');
  B.setScriptProperties();
  // Prints: Project-A
  Logger.log(B.getScriptProperties(
      PropertiesService, 'project'));
}

// Project B function setProperties() { PropertiesService.getScriptProperties() .setProperty('project', 'Project-B'); } function getScriptProperties( propertiesService, key) { return propertiesService.getScriptProperties() .getProperty(key); }

Actualiza el acceso a secuencias de comandos independientes

En el caso de las secuencias de comandos independientes que se ejecutan en el entorno de ejecución de V8, debes proporcionarles a los usuarios acceso de lectura, como mínimo, a la secuencia de comandos para que sus activadores funcionen correctamente.