Миграция сценариев в среду выполнения V8

Если у вас есть существующий сценарий, использующий среду выполнения Rhino, и вы хотите использовать синтаксис и функции V8, вам необходимо перенести сценарий на V8 .

Большинство сценариев, написанных с использованием среды выполнения Rhino, могут работать с использованием среды выполнения V8 без каких-либо настроек. Зачастую единственным предварительным условием для добавления синтаксиса и функций V8 в сценарий является включение среды выполнения V8 .

Однако существует небольшой набор несовместимостей и других различий , которые могут привести к сбою сценария или непредвиденному поведению сценария после включения среды выполнения V8. При переносе сценария на использование версии 8 вам необходимо найти в проекте сценария эти проблемы и исправить все, что вы обнаружите.

Процедура миграции V8

Чтобы перенести сценарий на версию V8, выполните следующую процедуру:

  1. Включите среду выполнения V8 для сценария.
  2. Внимательно просмотрите несовместимости, перечисленные ниже. Проверьте свой сценарий, чтобы определить, присутствуют ли какие-либо несовместимости; Если присутствует одна или несколько несовместимостей, измените код сценария, чтобы устранить или избежать проблемы.
  3. Внимательно ознакомьтесь с другими различиями, перечисленными ниже. Изучите свой сценарий, чтобы определить, влияют ли какие-либо из перечисленных различий на поведение вашего кода. Настройте свой сценарий, чтобы исправить поведение.
  4. После устранения любых обнаруженных несовместимостей или других различий вы можете приступить к обновлению своего кода для использования синтаксиса V8 и других функций по желанию.
  5. После завершения корректировки кода тщательно протестируйте сценарий, чтобы убедиться, что он ведет себя должным образом.
  6. Если ваш сценарий представляет собой веб-приложение или опубликованную надстройку , вам необходимо создать новую версию сценария с настройками V8. Чтобы сделать версию V8 доступной для пользователей, необходимо повторно опубликовать сценарий с этой версией.

Несовместимости

К сожалению, исходная среда выполнения Apps Script на базе Rhino допускала несколько нестандартных вариантов поведения ECMAScript. Поскольку версия V8 соответствует стандартам, такое поведение не поддерживается после миграции. Если не исправить эти проблемы, это приведет к ошибкам или нарушению работы скрипта после включения среды выполнения V8.

В следующих разделах описывается каждое из этих действий и шаги, которые необходимо предпринять для исправления кода сценария во время миграции на версию V8.

Избегайте for each(variable in object)

Оператор for each (variable in object) был добавлен в JavaScript 1.6 и удален в пользу for...of .

При переносе сценария на версию V8 избегайте использования операторов for each (variable in object) .

Вместо этого используйте 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);
}
      

Избегайте Date.prototype.getYear()

В исходной среде выполнения Rhino Date.prototype.getYear() возвращает двухзначные годы для лет с 1900 по 1999 год и четырехзначные годы для других дат, как это было в JavaScript 1.2 и более ранних версиях.

В среде выполнения V8 Date.prototype.getYear() вместо этого возвращает год минус 1900, как того требуют стандарты ECMAScript.

При переносе сценария на версию V8 всегда используйте Date.prototype.getFullYear() , который возвращает четырехзначный год независимо от даты.

Избегайте использования зарезервированных ключевых слов в качестве имен.

ECMAScript запрещает использование определенных зарезервированных ключевых слов в именах функций и переменных. Среда выполнения Rhino допускает многие из этих слов, поэтому, если они используются в вашем коде, вам необходимо переименовать свои функции или переменные.

При переносе сценария на версию V8 избегайте именования переменных или функций с использованием одного из зарезервированных ключевых слов . Переименуйте любую переменную или функцию, чтобы избежать использования имени ключевого слова. Обычно ключевые слова в качестве имен используются в class , import и export .

Избегайте переназначения const переменных

В исходной среде выполнения Rhino вы можете объявить переменную, используя const , что означает, что значение символа никогда не меняется, а будущие присвоения символу игнорируются.

В новой среде выполнения V8 ключевое слово const соответствует стандарту, и его присвоение переменной, объявленной как const приводит к ошибке выполнения TypeError: Assignment to constant variable .

При переносе вашего скрипта в V8 не пытайтесь переназначить значение 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
      

Избегайте литералов XML и объекта XML

Это нестандартное расширение ECMAScript позволяет проектам Apps Script напрямую использовать синтаксис XML.

При переносе сценария на версию V8 избегайте использования прямых литералов XML или объекта XML .

Вместо этого используйте XmlService для анализа 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
      

Не создавайте собственные функции-итераторы, используя __iterator__

В JavaScript 1.7 добавлена ​​возможность добавлять собственный итератор к любому классу путем объявления функции __iterator__ в прототипе этого класса; это также было добавлено в среду выполнения Rhino Apps Script для удобства разработчика. Однако эта функция никогда не была частью стандарта ECMA-262 и была удалена из ECMAScript-совместимых механизмов JavaScript. Скрипты, использующие V8, не могут использовать эту конструкцию итератора.

При переносе сценария на V8 избегайте использования функции __iterator__ для создания пользовательских итераторов . Вместо этого используйте итераторы ECMAScript 6 .

Рассмотрим следующую конструкцию массива:

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

В следующих примерах кода показано, как можно создать итератор в среде выполнения Rhino и как создать замещающий итератор в среде выполнения 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);
}
      

Избегайте условных предложений catch

Среда выполнения V8 не поддерживает условные предложения catch..if , поскольку они не соответствуют стандартам.

При переносе сценария в V8 переместите все условия catch внутрь тела catch :

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

Избегайте использования Object.prototype.toSource()

JavaScript 1.3 содержал метод Object.prototype.toSource() , который никогда не был частью какого-либо стандарта ECMAScript. Он не поддерживается в среде выполнения V8.

При переносе сценария на версию V8 удалите из кода любое использование Object.prototype.toSource() .

Другие различия

Помимо вышеперечисленных несовместимостей, которые могут вызвать сбои сценариев, существует несколько других отличий, которые, если их не исправить, могут привести к неожиданному поведению сценариев во время выполнения V8.

В следующих разделах объясняется, как обновить код сценария, чтобы избежать этих неожиданных сюрпризов.

Настройте форматирование даты и времени в зависимости от локали.

Методы Date toLocaleString() , toLocaleDateString() и toLocaleTimeString() ведут себя по-разному во время выполнения V8 по сравнению с Rhino.

В Rhino форматом по умолчанию является длинный формат , и любые передаваемые параметры игнорируются .

В среде выполнения V8 форматом по умолчанию является короткий формат , а передаваемые параметры обрабатываются в соответствии со стандартом ECMA (подробности см. в документации toLocaleDateString() ).

При переносе сценария на версию V8 протестируйте и скорректируйте ожидания вашего кода в отношении вывода методов даты и времени, зависящих от локали :

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

Избегайте использования Error.fileName и Error.lineNumber

В untime версии V8 стандартный объект Error JavaScript не поддерживает fileName или lineNumber в качестве параметров конструктора или свойств объекта.

При переносе сценария в версию V8 удалите любую зависимость от Error.fileName и Error.lineNumber .

Альтернативой является использование Error.prototype.stack . Этот стек также нестандартен, но поддерживается как в Rhino, так и в V8. Формат трассировки стека, создаваемой двумя платформами, немного отличается:

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

Настройка обработки строковых объектов перечисления.

В исходной среде выполнения Rhino использование метода JavaScript JSON.stringify() для объекта перечисления возвращает только {} .

В V8 использование того же метода для объекта перечисления позволяет перенастроить имя перечисления.

При переносе вашего скрипта на V8 протестируйте и скорректируйте ожидания вашего кода относительно вывода JSON.stringify() для объектов перечисления :

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

Настройка обработки неопределенных параметров

В исходной среде выполнения Rhino передача undefined методу в качестве параметра приводила к передаче этому методу строки "undefined" .

В V8 передача undefined методам эквивалентна передаче null .

При переносе скрипта на V8 протестируйте и скорректируйте ожидания вашего кода в отношении 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.

Настройте обработку глобального this

Среда выполнения Rhino определяет неявный специальный контекст для сценариев, которые ее используют. Код сценария выполняется в этом неявном контексте, отличном от фактического глобального this . Это означает, что ссылки на «глобальное this » в коде фактически оцениваются как специальный контекст, который содержит только код и переменные, определенные в скрипте. Встроенные службы Apps Script и объекты ECMAScript исключены из использования this . Эта ситуация была похожа на эту структуру 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.
}();

В V8 неявный специальный контекст удален. Глобальные переменные и функции, определенные в скрипте, размещаются в глобальном контексте рядом со встроенными службами Apps Script и встроенными модулями ECMAScript, такими как Math и Date .

При переносе вашего сценария на V8 протестируйте и скорректируйте ожидания вашего кода относительно this использования в глобальном контексте. В большинстве случаев различия очевидны только в том случае, если ваш код проверяет ключи или имена свойств глобального объекта this :

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

Настройте обработку instanceof в библиотеках

Использование instanceof в библиотеке для объекта, который передается в качестве параметра в функцию из другого проекта, может привести к ложноотрицательным результатам. В среде выполнения V8 проект и его библиотеки выполняются в разных контекстах выполнения и, следовательно, имеют разные глобальные переменные и цепочки прототипов.

Обратите внимание, что это происходит только в том случае, если ваша библиотека использует instanceof для объекта, который не создан в вашем проекте. Использование его на объекте, созданном в вашем проекте, будь то в том же или другом скрипте внутри вашего проекта, должно работать должным образом.

Если проект, работающий на V8, использует ваш сценарий в качестве библиотеки, проверьте, использует ли ваш сценарий instanceof для параметра, который будет передан из другого проекта. Настройте использование instanceof и используйте другие возможные альтернативы в соответствии с вашим вариантом использования.

Одной из альтернатив a instanceof b может быть использование конструктора a в тех случаях, когда вам не нужно искать всю цепочку прототипов и просто проверять конструктор. Использование: a.constructor.name == "b"

Рассмотрим проект A и проект B, где проект A использует проект B в качестве библиотеки.

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

Другой альтернативой может быть введение функции, которая проверяет instanceof в основном проекте и передает эту функцию в дополнение к другим параметрам при вызове библиотечной функции. Переданную функцию затем можно использовать для проверки instanceof внутри библиотеки.

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

Настройте передачу несовместно используемых ресурсов в библиотеки.

Передача частного ресурса из основного сценария в библиотеку работает по-другому в среде выполнения V8.

В среде выполнения Rhino передача неразделяемого ресурса не будет работать. Вместо этого библиотека использует собственный ресурс.

В среде выполнения V8 работает передача неразделяемого ресурса в библиотеку. Библиотека использует переданный необщий ресурс.

Не передавайте неразделяемые ресурсы в качестве параметров функции. Всегда объявляйте неразделяемые ресурсы в том же сценарии, в котором они используются.

Рассмотрим проекты A и Project B, где проект A использует проект B в качестве библиотеки. В этом примере PropertiesService — это частный ресурс.

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

Обновить доступ к автономным скриптам

Для автономных сценариев, работающих в среде выполнения V8, необходимо предоставить пользователям хотя бы доступ для просмотра сценария, чтобы триггеры сценария работали правильно.