Rhino 运行时将于 2026 年 1 月 31 日或之后停用。如果您有使用 Rhino 运行时的现有脚本,则必须将该脚本迁移到 V8。
通常,向脚本添加 V8 语法和功能的唯一前提条件是启用 V8 运行时。不过,仍存在少量不兼容性和其他差异,可能会导致脚本在 V8 运行时中失败或表现出意外行为。在将脚本迁移为使用 V8 时,您必须在脚本项目中搜索这些问题,并更正发现的任何问题。
V8 迁移过程
如需将脚本迁移到 V8,请按以下步骤操作:
- 为脚本启用 V8 运行时。您可以使用 Apps 脚本项目的清单来检查
runtimeVersion
。 - 请仔细查看以下不兼容情况。 检查脚本,确定是否存在任何不兼容情况;如果存在一种或多种不兼容情况,请调整脚本代码以移除或避免相应问题。
- 请仔细查看以下其他区别。 检查脚本,确定列出的任何差异是否会影响代码的行为。调整脚本以修正此行为。
- 修正发现的所有不兼容问题或其他差异后,您就可以开始更新代码,以使用 V8 语法和其他功能。
- 完成代码调整后,请彻底测试脚本,确保其行为符合预期。
- 如果您的脚本是 Web 应用或已发布的插件,您必须创建新版本的脚本并进行 V8 调整,然后将部署指向新创建的版本。如需向用户提供 V8 版本,您必须使用此版本重新发布脚本。
- 如果您的脚本用作库,请为脚本创建新的版本化部署。将此新版本告知使用您库的所有脚本和用户,并指示他们更新到启用 V8 的版本。 验证您库的任何基于 Rhino 的旧版本是否不再处于有效使用状态或可访问状态。
- 验证脚本是否没有仍在旧版 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 时,请避免使用预留关键字来命名变量或函数。重命名任何变量或函数,以避免使用关键字名称。将关键字用作名称的常见情况包括 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 脚本项目直接使用 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); } |
避免使用条件 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.fileName
和 Error.lineNumber
在 V8 运行时中,标准 JavaScript Error
对象不支持将 fileName
或 lineNumber
用作构造函数参数或对象属性。
将脚本迁移到 V8 时,请移除对 Error.fileName
和 Error.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 内置函数(如 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 和项目 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')); } |
// V8 runtime // Project A function testPassingNonSharedProperties() { PropertiesService.getScriptProperties() .setProperty('project', 'Project-A'); B.setScriptProperties(); // Prints: Project-A Logger.log(B.getScriptProperties( PropertiesService, 'project')); } |
更新了对独立脚本的访问权限
对于在 V8 运行时中运行的独立脚本,您需要为用户提供对该脚本的至少查看权限,以便脚本的触发器正常运行。