將指令碼遷移至 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 這段期間,但其他日期則是四位數的年份,這是 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 執行階段指令碼行為。

以下各節說明如何更新指令碼程式碼,避免發生非預期的意外。

調整語言代碼專屬的日期和時間格式

在 V8 執行階段中,Date 方法 toLocaleString()toLocaleDateString()toLocaleTimeString() 的行為與 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 的建構函式做為 a instanceof b 的替代方案。使用方式: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 執行階段上執行的獨立指令碼,您至少必須為使用者提供指令碼的檢視權限,指令碼的觸發條件才能正常運作。