スクリプトの V8 ランタイムへの移行

Rhino ランタイムを使用している既存のスクリプトで V8 構文と機能を使用したい場合は、スクリプトを V8 に移行する必要があります。

Rhino ランタイムを使用して記述されたほとんどのスクリプトは、調整なしで V8 ランタイムを使用して動作できます。多くの場合、V8 構文と機能をスクリプトに追加する前提条件は、V8 ランタイムを有効にすることだけです。

ただし、V8 ランタイムを有効にした後にスクリプトが失敗したり、予期しない動作をしたりする可能性がある、非互換性その他の違いがいくつかあります。スクリプトを V8 を使用するように移行する際は、スクリプト プロジェクトでこれらの問題を検索し、見つかった問題を修正する必要があります。

V8 の移行手順

スクリプトを V8 に移行する手順は次のとおりです。

  1. スクリプトの V8 ランタイムを有効にする
  2. 以下の互換性のない機能をよく確認してください。スクリプトを確認し、互換性がないかどうかを判断します。互換性が 1 つ以上ある場合は、スクリプトコードを調整して問題を解消するか回避します。
  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 年の年は 2 桁の年を返しますが、それ以外の日付は 4 桁の年を返します。これは JavaScript 1.2 以前の動作です。

V8 ランタイムでは、Date.prototype.getYear() は ECMAScript 標準で求められるように、年から 1900 を引いた値を返します。

スクリプトを V8 に移行する場合は、常に Date.prototype.getFullYear() を使用してください。これは、日付に関係なく 4 桁の年を返します。

予約済みキーワードを名前として使用しないでください

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 条件付きキャッチ句をサポートしていません。

スクリプトを 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 には、ECMAScript 標準の一部ではなかった Object.prototype.toSource() メソッドが含まれていました。V8 ランタイムではサポートされていません。

スクリプトを V8 に移行する場合は、コードから Object.prototype.toSource() の使用をすべて削除してください。

その他の相違点

スクリプトの失敗につながる上記の非互換性に加えて、修正しないと予期しない V8 ランタイム スクリプトの動作につながる可能性があるその他の違いがいくつかあります。

以降のセクションでは、このような予期しない事態を回避するためにスクリプト コードを更新する方法について説明します。

言語 / 地域固有の日付と時刻の形式を調整する

Date メソッド toLocaleString()toLocaleDateString()toLocaleTimeString() は、Rhino と比較して 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 オブジェクトは、コンストラクタ パラメータまたはオブジェクト プロパティとして fileName または lineNumber をサポートしていません。

スクリプトを V8 に移行する際は、Error.fileNameError.lineNumber への依存関係をすべて削除してください

別の方法として、Error.prototype.stack を使用することもできます。このスタックも標準ではありませんが、Rhino と V8 の両方でサポートされています。2 つのプラットフォームで生成されるスタック トレース形式は若干異なります。

// 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 について考えてみましょう。プロジェクト 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 ランタイムで実行されるスタンドアロン スクリプトの場合、スクリプトのトリガーが正しく機能するように、ユーザーにスクリプトへの閲覧アクセス権を少なくとも付与する必要があります。