将脚本迁移到 V8 运行时

Rhino 运行时将于 2026 年 1 月 31 日或之后停用。如果您有使用 Rhino 运行时的现有脚本,则必须将该脚本迁移到 V8。

通常,向脚本添加 V8 语法和功能的唯一前提条件是 启用 V8 运行时。 不过,有一小部分不兼容问题其他差异可能会导致脚本在 V8 运行时中失败或 行为异常。在将脚本迁移到 V8 时,您必须在脚本项目中搜索这些问题,并更正找到的任何问题。

V8 迁移过程

如需将脚本迁移到 V8,请按以下步骤操作:

  1. 为脚本启用 V8 运行时 。可以使用 Google Apps 脚本项目的 清单检查 runtimeVersion
  2. 仔细查看以下不兼容问题。 检查脚本,确定是否存在任何不兼容问题;如果存在一个或多个不兼容问题,请调整脚本代码以移除或避免该问题。
  3. 仔细查看以下其他差异。 检查脚本,确定列出的任何差异是否会影响代码的行为。调整脚本以更正行为。
  4. 更正发现的任何不兼容问题或其他 差异后,开始更新代码以使用 V8 语法和其他功能
  5. 完成代码调整后,请彻底测试脚本,确保其行为符合预期。
  6. 如果您的脚本是 Web 应用或已发布的 插件,则必须 使用 V8 调整创建一个新版本的 脚本,并将部署指向新创建的 版本。如需向用户提供 V8 版本,您必须使用此版本重新发布脚本。
  7. 如果您的脚本用作库,请为脚本创建一个新的版本化部署。将此新版本告知使用您的库的所有脚本和用户,并指示他们更新到启用 V8 的版本。 验证库的任何旧版(基于 Rhino)是否不再处于活跃使用状态或无法访问。
  8. 验证脚本的任何实例是否仍在旧版 Rhino 运行时中运行。验证所有 部署 是否都与 V8 上的版本相关联。归档旧部署。查看 所有版本,并删除未使用 V8 运行时的版本 。

不兼容性

遗憾的是,基于 Rhino 的原始 Apps 脚本运行时允许一些非标准的 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 时,请避免使用 保留关键字 之一来命名变量或函数。 重命名任何变量或函数,以避免使用关键字名称。关键字作为名称的常见用法包括 classimportexport

一个例外是,对象字面量允许使用保留关键字(在所有运行时中):

function class() {}     // Syntax error in V8.
var obj = { class: 1 }; // Allowed.

避免重新分配 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 脚本项目直接使用 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__ 函数,向任何类添加自定义迭代器;此功能还作为开发者便利功能添加到 Apps 脚本的 Rhino 运行时中。不过,此功能从未成为 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);
}
      

在 V8 运行时中,使用自定义迭代器遍历数组时,必须使用 for...of,因为 for..in 不接受可迭代对象。

避免使用条件 catch 子句

V8 运行时不支持 catch..if 条件 catch 子句,因为它们不符合标准。

将脚本迁移到 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 运行时脚本行为异常。

以下部分介绍了如何更新脚本代码以避免这些意外情况。

调整特定于语言区域的日期和时间格式

与 Rhino 相比,Date 方法 toLocaleString()toLocaleDateString()、 和 toLocaleTimeString() 在 V8 运行时中的行为有所不同。

在 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.fileNameError.lineNumber

在 V8 运行时中,标准 JavaScript Error 对象不支持将 fileNamelineNumber 作为构造函数参数 或对象属性。

将脚本迁移到 V8 时, 请移除对 Error.fileNameError.lineNumber 的任何依赖项。

一种替代方法是使用 Error.prototype.stack。 此堆栈也是非标准的,但在 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 脚本服务和 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 脚本服务和 ECMAScript 内置函数(如 MathDate)并列。

将脚本迁移到 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 和项目 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 运行时中的 JDBC 建议

在 V8 运行时中,我们为 JDBC 服务添加了新功能。

使用 executeBatch 进行批量操作

使用 executeBatch(params) 操作执行批量数据库操作。

以下示例展示了如何使用批处理将多行插入数据库:

以下是 Rhino 运行时(旧方法):

var conn = Jdbc.getCloudSqlConnection("jdbc:google:mysql://...");
var stmt = conn.prepareStatement("INSERT INTO employees (name, age) VALUES (?, ?)");
var params = [["John Doe", 30], ["John Smith", 25]];
for (var i = 0; i < params.length; i++) {
  stmt.setString(1, params[i][0]);
  stmt.setInt(2, params[i][1]);
  stmt.execute();
}

以下是 V8 运行时(新方法):

var conn = Jdbc.getCloudSqlConnection("jdbc:google:mysql://...");
var stmt = conn.prepareStatement("INSERT INTO employees (name, age) VALUES (?, ?)");
var params = [["John Doe", 30], ["John Smith", 25]];
stmt.executeBatch(params);

使用 getRows 获取结果集

使用 getRows(queryString) 通过一次调用获取结果集数据。queryString 由以逗号分隔的对 JdbcResultSet 的 getter 方法的调用组成,例如:"getString(1), getDouble('price'), getDate(3, 'UTC')"。支持的方法包括负责读取列数据的所有 getter 方法,例如,不支持 getHoldabilitygetMetaData 等。实参可以是整数列索引(从 1 开始)或单引号或双引号括起来的字符串列标签。

以下示例展示了如何从结果集中获取行:

以下是 Rhino 运行时(旧方法):

var conn = Jdbc.getCloudSqlConnection("jdbc:google:mysql://...");
var stmt = conn.createStatement();
var rs = stmt.executeQuery("SELECT name, age FROM employees");
while (rs.next()) {
  Logger.log(rs.getString('name') + ", " + rs.getInt('age'));
}

以下是 V8 运行时(新方法):

var conn = Jdbc.getCloudSqlConnection("jdbc:google:mysql://...");
var stmt = conn.createStatement();
var rs = stmt.executeQuery("SELECT name, age FROM employees");
var rows = rs.getRows("getString('name'), getInt('age')");
for (var i = 0; i < rows.length; i++) {
  Logger.log(rows[i][0] + ", " + rows[i][1]);
}

更新对独立脚本的访问权限

对于在 V8 运行时中运行的独立脚本,您需要为用户提供至少查看脚本的权限,以便脚本的触发器能够正常运行。