[GAS] V8ランタイムを有効にし最新のECMAScriptを動かす

みんな大好きGoogleAppsScriptで、2020年2月よりES2015以降のナウい構文が利用できるようになりました。

Historically, Apps Script has been powered by Mozilla's Rhino JavaScript interpreter. While Rhino provided a convenient way for Apps Script to execute developer scripts, it also tied Apps Script to a specific JavaScript version (ES5). Apps Script developers can't use more modern JavaScript syntax and features in scripts using the Rhino runtime.

V8 Runtime Overview

これまではMozilla製のランタイムRhinoが裏側で動いており、ES5までの構文に対応していました。JavaScriptが劇的に進化したのはES2015(ES6)からです。現代においてES5までの構文しか使えないのはやはり辛い。classはもちろんconstやletもES2015からなのです。

GASは非常に便利なのですが、Node.jsでコードを書き慣れてくるに従ってこの点がほんと苦痛で仕方なかったw そんな折に満を持してGoogle謹製V8ランタイムの登場と相成ったというわけです。これで最新のECMAScriptが動作します。

設定する

V8ランタイムを有効にするのは非常に簡単で、2020年2月14日現在だとプロジェクトを起動すると画面上部に以下のようなメニューが現れますので「有効にする」をクリックするだけです。

もしくはメニューの「実行」から「Chrome V8 を搭載した新しい Apps Script ランタイムを有効にする」を選択します。

逆の手順をたどると無効にすることもできます。恐らく将来的にはV8がデフォルトになると思われるのでこの設定は一時的な物かもしれませんね。

実際に使ってみる

サンプルコード

動物クラスを継承した猫クラスを作ってみました。入門書で見かけるヤツですねw

// スーパークラス
class Animal{
  constructor(name, tail){
    this.name = name;  // 動物の名前
    this.tail = tail;  // 語尾につける文字
  }

  speak(text){
    const voice = `[${this.name}]: ${text}${this.tail}`;
    console.log(voice);
  }
}

// 猫クラス
class Cat extends Animal{
  constructor(name){
    super(name, "にゃー");
  }
}

// 実行用の関数
function go(){
  const Tama = new Cat("たま");
  Tama.speak("おはようございます");
}

ログを見ると無事に出力されていました。

このサンプルから以下の新機能は普通に利用できそうなことがわかりますね。

  • クラス、継承
  • let, const
  • テンプレート (バッククオート内で変数展開できる)

移行時の注意点

公式ドキュメントに記載されている内容を追ってみます。

for eachを使わない

JavaScriptの仕様からfor eachがすでに削除されているため、連想配列などの走査にはfor...in構文を利用します。

var obj = {a: 1, b: 2, c: 3};

// 旧
for each (var value in obj) {
  Logger.log("value = %s", value);
}

// 新
for (var key in obj) {
  var value = obj[key];
  Logger.log("value = %s", value);
}

getYear()を使わない

西暦の年が欲しい場合はgetFullYear()を利用します。

const now  = new Date();
const year = now.getFullYear();

getYear()はランタイム毎に以下のような挙動をになります。

Rhino
1900〜1999年の年数は2桁の年、それ以外は4桁の年数を返却
V8
常に1900を引いた年数を返却

そのためランタイムに依存せず常に4桁の西暦を返却するgetFullYear()がオススメされてているというわけです。

予約済みキーワードを利用しない

変数や関数名などに予約済みのキーワードを指定しないようにします。……ここだけ聞くと何当たり前のこと言ってるんだ感ありますねw これは今回ESのバージョンが上がったことでキーワードが増えたことによるものです。

たとえばclassなどはES5までは利用できていましたが、V8に移行する際は別の名前に変更する必要があります。ES2015で定義されているキーワードは以下の通りです。

  • break
  • case
  • catch
  • class
  • const
  • continue
  • debugger
  • default
  • delete
  • do
  • else
  • export
  • extends
  • finally
  • for
  • function
  • if
  • import
  • in
  • instanceof
  • new
  • return
  • super
  • switch
  • this
  • throw
  • try
  • typeof
  • var
  • void
  • while
  • with
  • yield

constで定義した変数の値を変更しない

以下のコードは実行時エラーとなります。constで最初に代入した値は変更できなくなります。逆にRhinoで変更が可能だったということの方がちょっと衝撃でしたw

const PI = 3.14159265;
PI = 3; //エラー 

ちなみに以下のようにオブジェクトを代入し、オブジェクト内の変数の値が変わるのは構いません。(別のオブジェクトを代入するとエラー)

class Animal{
  constructor(){
    this._name = null;
  }

  set name (text){
    this._name = text;
  }
}

function go(){
  const Cat = new Animal();  // インスタンスを作成し代入
  Cat.name = "たま";         // エラーにはならない

  // 別の何かを入れるとエラー
  Cat = new Animal();
}

XMLを利用する場合はXmlService()を使う

最近はJSでXMLを使う機会はめっきり減った印象ですが、旧来の利用方法から変更になりました。

// 旧
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

実行順に注意が必要

グローバル変数を複数ファイルで扱う場合に注意が必要です。

以下の2つのファイルを用意し、「1st.gs」のmyFunction()を実行すると実行時エラーとなります。

// 1st.gs
var globalVar = calculate();
function myFunction() {
  Logger.log("globalVar = %s", globalVar);
}
// 2nd.gs
function calculate() {
  return Math.random();
}

2nd.gsに存在するハズのcalculate()が存在しないと言われています。

「ReferenceError: calculate is not defined(行 1、ファイル「1st」)」

Rhinoではすべてのファイルを読み込んだ後に実行されていたためエラーにはならなかったのですが、順番に1ファイルずつ読み込み、読み込んだ瞬間にグローバル変数として記述している部分が実行されるためこのようなエラーとなるようです。

なので回避する手軽な手段としては関数やクラスの中に入れてしまうやり方でしょうか。

// 1st.gs
function myFunction() {
  var globalVar = calculate();
  Logger.log("globalVar = %s", globalVar);
}

異なるファイル間で依存し合うグローバル変数は推奨されないようです。

条件付きのtry〜catchを使わない

catch()の中で条件式の記述がV8ではサポートされていません。

// 旧
try {
  doSomething();
}
catch (e if e instanceof TypeError) {  // Don't use
  // Handle exception
}

// 新
try {
  doSomething();
}
catch (e) {
  if (e instanceof TypeError) {
    // Handle exception
  }
}

日付の出力形式が異なる

var event = new Date();

//-----------------
// 旧
//-----------------
// "December 21, 2012"
event.toLocaleDateString(); 

// "December 21, 2012"
// (引数はすべて無視される)
event.toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' }));


//-----------------
// 新
//-----------------
// "12/21/2012"
event.toLocaleDateString();

// "21. Dezember 2012"
event.toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' }));

thisの内容が異なる

関数内のthisの内容が異なります。

var myGlobal = 5;
function myFunction() {
  console.log(Object.keys(this));
}

上記のコードを実行した場合、ランタイムによって次のような内容になります。

Rhino
[myFunction, myGlobal]
V8
[ 'Browser', 'CacheService', 'CalendarApp', 'CardService', 'Charts', 'ContactsApp', 'ContentService', 'DataStudioApp', 'DocumentApp', 'DriveApp', 'ErrorService', 'FormApp', 'GmailApp', 'GroupsApp', 'HtmlService', 'LanguageApp', 'LinearOptimizationService', 'LockService', 'Logger', 'MailApp', 'Maps', 'MimeType', 'PropertiesService', 'ScriptApp', 'ScriptProperties', 'Session', 'SitesApp', 'SlidesApp', 'SoapService', 'SpreadsheetApp', 'UiApp', 'UrlFetchApp', 'UserProperties', 'Utilities', 'XmlService', 'console', 'Jdbc', 'myGlobal', 'myFunction' ]

その他

  • __iterator__を使わない(別の記述をする)
  • toSource()を使わない(削除する必要あり)
  • Error.fileNameError.lineNumberを使わない
  • enumオブジェクトをJSON.stringify()した際の挙動が異なる
  • 関数に渡すundefinedの扱いが異なる
    • Rhinoだと文字列として渡されると書いてあったのですが、そっち側を再現できませんでした。

詳しくは以下のページを参照してください。 developers.google.com

参考ページ