將指令碼遷移至 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);
}
      

避免使用條件式 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 執行階段指令碼行為。

下列各節將說明如何更新指令碼,避免發生這些意外情況。

調整特定語言代碼的日期和時間格式

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 不同。也就是說,程式碼中對「global 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 服務和 MathDate 等 ECMAScript 內建項目旁。

將指令碼遷移至 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"

請考慮 Project A 和 Project B,其中 Project A 會將 Project 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 執行階段中,將非共用資源傳遞至程式庫是可行的。程式庫會使用傳遞的非共用資源。

請勿將非共用資源做為函式參數傳遞。請務必在使用非共用資源的相同指令碼中宣告這些資源。

請考慮 Project A 和 Project B,其中 Project A 會將 Project 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 執行階段執行的獨立指令碼,您必須至少為使用者提供指令碼的檢視權限,才能讓指令碼的觸發事件正常運作。