將指令碼遷移至 V8 執行階段

如果您有使用 Rhino 執行階段的指令碼,而且想要使用 V8 語法和功能,則必須將指令碼遷移至 V8

大多數使用 Rhino 執行階段編寫的指令碼都能透過 V8 執行階段運作,而不會進行調整。一般來說,在指令碼中加入 V8 語法和功能的唯一先決條件是啟用 V8 執行階段

不過,在啟用 V8 執行階段後,仍有少數不相容其他差異,可能導致指令碼失敗或出現非預期的行為。遷移指令碼以使用 V8 時,必須在指令碼專案中搜尋這些問題,並修正所發現的問題。

V8 遷移程序

如要將指令碼遷移至 V8,請按照下列步驟操作:

  1. 為指令碼啟用 V8 執行階段
  2. 請詳閱下列不相容性說明。請檢查指令碼,確認是否存在任何不相容問題。如果存在一或多個不相容問題,請調整指令碼程式碼加以移除或避免問題。
  3. 請詳閱下列其他差異。 檢查指令碼,判斷這裡列出的有任何差異是否影響程式碼的行為。請調整指令碼來修正行為。
  4. 修正發現的不相容問題或其他差異後,您就可以開始更新程式碼,視需要使用 V8 語法和其他功能
  5. 完成程式碼調整後,請徹底測試指令碼,確保指令碼可正常運作。
  6. 如果指令碼是網頁應用程式或已發布外掛程式,您必須透過 V8 調整項目建立新版本的指令碼。如要向使用者提供 V8 版本,您必須使用這個版本重新發布指令碼。

不相容性

很抱歉,原始以 Rhino 為基礎的 Apps Script 執行階段允許多種非標準 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 年之間二位數的年份,其他日期則傳回四位數的年份,其他日期則為 4 位數的年份,也就是 JavaScript 1.2 以下版本的行為。

在 V8 執行階段中,Date.prototype.getYear() 會改為根據 ECMAScript 標準的要求,改為傳回減 1900 的年份。

將指令碼遷移至 V8 時,請一律使用 Date.prototype.getFullYear(),此方法會傳回四位數的年份,無論日期為何。

避免使用保留關鍵字做為名稱

ECMAScript 禁止在函式和變數名稱中使用特定保留關鍵字。Rhino 執行階段允許許多這類字詞,因此如果您的程式碼使用了這些字詞,您就必須重新命名函式或變數。

將指令碼遷移至 V8 時,請避免使用保留關鍵字的變數或函式命名。請重新命名任何變數或函式,避免使用關鍵字名稱。常見的關鍵字名稱為 classimportexport

避免重新指派 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__ 函式,以便將自訂疊代器新增至任何子句。這也適用於 Apps Script 的 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 執行階段不支援 catch..if 條件式擷取子句,因為這些子句與標準不符。

將指令碼遷移至 V8 時,請將主體主體中的所有擷取條件移至

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

在 V8 之前,標準 JavaScript Error 物件不支援 fileNamelineNumber 做為建構函式參數或物件屬性。

將指令碼遷移至 V8 時,請移除 Error.fileNameError.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 內建項目 (例如 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 在其中使用 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 在其中使用 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 執行階段執行的獨立指令碼,您必須至少為使用者提供該指令碼的檢視權限,指令碼的觸發條件才能正常運作。