スクリプトの 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..if 条件付きキャッチ句は標準に準拠していないため、V8 ランタイムではサポートされません。

スクリプトを V8 に移行する場合は、キャッチ本文内の 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 ランタイム スクリプトの動作が発生する可能性があります。

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

ロケール固有の日付と時刻の形式を調整する

V8 ランタイムでの Date メソッドの toLocaleString()toLocaleDateString()toLocaleTimeString() の動作は、Rhino とは異なります。

Rhino では、デフォルトの形式は long 形式であり、渡されるパラメータは無視されます。

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 ランタイムは、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 ランタイムで実行されるスタンドアロン スクリプトの場合、スクリプトのトリガーが正常に機能するには、少なくともスクリプトに対する表示アクセス権をユーザーに付与する必要があります。