Google Apps ScriptのV8 Runtime対応を検証してみた【GAS】

2020年2月6日、ついにGoogle Apps ScriptのJavaScript基盤でV8エンジンが利用可能になりました。これまでのGASの実行基盤はES2015ベースとは言え、一部が使えていただけで、最新のJavaScript環境で利用可能になっている構文は使えない状態にあったので、これでGASの書き方が一気に変わってくるかもしれない。

これまではMozillaのRhinoというエンジンで動いていたようで。現在はまだ選択式なので、V8にするためには設定が必要だけれど、いずれは現在のエンジンは廃止される見通しなので、今のうちにV8での書き方に慣れておく必要はあるかもしれない。今回はこのV8でどういう構文が使えるようになったのか?一個ずつ検証すると共に、GASの実行スピードも検証してみたい。

※注意が必要なのが、GASはデフォルトでV8がONになってしまってます。。。新規作成時に要注意です

今回使用するスプレッドシート

今回のスプレッドシートのappsscript.jsonには、runtimeVersionとしてV8を指定しています。

※Logger.logの結果が出るのがなんだか遅い。App Scriptダッシュボードのほうは割とすぐ出る。

有効にする手順

スクリプトエディターの場合

今回のV8 Runtimeを使うには以下の手順が必要です。本来はスクリプトエディタの「実行」の中に「Chrome V8を搭載した新しいApps Scriptランタイムを有効にする」というものをクリックして有効にすればそのスクリプト内で有効になります。まだ、表示されていない人は手動で変更も可能。無効にするをクリックをすれば元に戻ります。

図:メニューから簡単に切り替えられるよ

手動で変更する場合は以下の手順。詳細はmanifest structureを参照してください。

  1. スクリプトエディタを開く
  2. メニューの「表示」⇒「マニフェストファイルを表示」をクリック
  3. appsscript.jsonを開き、1行以下のようなものをを追加する
  4. 保存すれば、すぐに使えるようになります。
  5. もとに戻す場合には、この行を削除すればオッケーです。(DEPRECATED_ES5を指定しても可)
//追加する行はこちら
"runtimeVersion":"V8"

図:簡単に使えるようになります

claspの場合

Google Apps Scriptは、Node.js上のTypescript開発環境Claspでも開発ができます。clasp上でもV8 Runtimeが動かせるようになっています。手順としては以下の通り。こちらを参考にclaspをES2019に対応させる必要があります。

  1. clasp createで新しいプロジェクトを作成する
  2. 生成されたappscript.jsonを編集する
  3. 以下のようにコードに追記する
  4. また、typescriptを使う場合にはtsconfig.jsonも編集しておく
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

図:appscript.jsonの編集箇所

{
  "compilerOptions": {
    "target": "ES2019"
  }
}

図:tsconfig.jsonの編集箇所

基本、スクリプトエディターの時と変わらないですね。

V8対応検証

V8対応といっても、現時点でES6 Moduleのimport, exportにはまだ対応していないとの事なので、それ以外のものについて検証をしてみたい。

もはや、Google Apps Scriptというより、Google Apps Script + JavaScriptのサーバレス実行環境になったという印象。これは初心者の人にとっても非常に有用で、GASの流儀だけに縛られずに最新のJavaScriptのノウハウやメソッドをそのまま持ち込めるので、楽になります。ES2023までの数々のものに多く対応しています。本エントリーはES2019までのメソッドについて調査しています。

最新のECMAScript仕様のメソッド

ES2017の仕様に準拠した環境をリリースって話だったので、使えないよねって思って、ES2019で追加されたと言ういくつかのコードも試してみてる。その内の1つ、配列データをフラットにするflat()flatMap()を使ってみたら、使えました。ふむふむ。クライアント側で動かすわけじゃないから、割と最新のメソッド遠慮なく入れてきてる感じですかね。

2024年6月現在、試してみたところECMAScript2023まで対応を確認しました。相変わらずES6のModuleには未対応ではあるものの、公式でも最新のECMAScriptに対応を謳ってる。このV8リリース当時はES2019まで対応でしたが、やはりChromeのアプデと共にバージョンアップしていくようです。とはいえ、Class構文についてはStatic変数やPrivate Propertyには未対応なのでES2023対応と言っても全てに対応してるわけじゃなさそうです。

//flat
function flatman(){
  Logger.log([[1, 2], 3, 4].flat());

  var flatdata = shingeki.flatMap(data => data.favorite);
  Logger.log(flatdata);
}

const shingeki = [
  {
    name: "ミカサ",
    favorite: [ "エレン", "パン", "紅茶"]
  },
  {
    name: "エレン",
    favorite: ["自由", "チーハン"]
  },
  {
    name: "アルミン",
    favorite: [ "本", "海", "コーヒー"]
  }
];

配列同士でキーのペアを作ってオブジェクトを生成するObject.fromEntriesなども使えました。これは意外と便利かもしれない。

//配列からオブジェクトを生成
function keypair(){
  var pairs = Object.fromEntries([["id", 200], ["name", "エレン・イェーガー"]]);
  Logger.log(pairs);
}

ES2015に加わっていたtrimに加え、trimStarttrimEndも使えるようになっていました。

letとconst

これまでのGoogle Apps Scriptは変数の宣言は、PrivateもGlobalも「var」で宣言する事しかできませんでした。そのため、宣言した変数はもちろん、書き換える事ができるので、定数を宣言できないが為に面倒臭いことになったりしました。また、varはJavaScriptの巻き上げという仕様によって、頭で宣言しようが後で宣言しようが、頭で宣言したものとみなされる仕組み。

今回より、定数であるconstが利用できるようになり、またブロック内(例:For文の中のみとか)でだけ参照変数letも使えるようになり、変数何入っていたっけ?みたいな事が減ります。

※以前は、const自体あったものの、初期値を入れたあとに再代入しようとしてもエラーにならず、初期値が表示されるというオカシナ挙動でした。

※constはグローバル定数として宣言は出来ません。グローバルはvarで宣言し、各関数内でconstに代入するしかありません。

//消費税率
const eatin = 1.10;
const takeout = 1.08;

//eatinで食べて帰る
function priceman(){
  //変数と配列の宣言
  var items = ["e","t","t","e","t","t","e"];  //eがeatin, tがtakeout
  var result = 0;
  
  for(let i = 0;i<items.length;i++){
    //配列から1個値を取り出す
    let tempret = items[i];
    let hantei;
    let price;
    
    //条件分岐
    switch(tempret){
      case "e":
        //イートイン価格で計算
        hantei = false;
        price = 100;
        result += taxcalc(hantei,price);
        break;
      case "t":
        //お持ち帰り価格で計算
        hantei = true;
        price = 80;
        result += taxcalc(hantei,price);
        break;
    }
  }
  
  //計算結果を出す
  SpreadsheetApp.getUi().alert(result);
  
}

//消費税を計算するよ
function taxcalc(item,price){
  //計算結果を格納する変数
  var result = 0;
  
  //itemが持ち帰りか?その場で食べるかで変更する
  //trueならばtakeout, falseならばeatin
  if(item == true){
    result = price * takeout;
  }else{
    result = price * eatin;
  }

  //計算結果を
  return result;
}
  • 同一ブロック内でletで同じ変数を宣言するとSyntaxError: Identifier 'price' has already been declaredとなりエラーとなる。
  • ブロック外でletを参照すると、ReferenceError: price is not definedとなりエラーとなる。
  • constに対して、値を再代入しようとすると、TypeError: Assignment to constant variable.となりエラーとなる。

グローバルでconstを宣言するだけでなく、関数内でもconstは宣言可能。但し、letと同じくブロック内で有効となるので、関数内で使うなら、頭のほうで宣言しておくほうが迷わなくて済みます。

getYear()の仕様変更

古いRhinoの場合、1999年より前の1900年までは、getYear()では、2桁の年を返し、getFullYear()で4桁の年を返す仕様でした。また、2000年以降はgetFullYear()では、もちろん4桁の年を返してくれます。

しかし、V8からはgetFullYear()については同じく4桁で返すようになっているのですが、問題は2020/2/1をgetYear()で取得すると120が返ってくる・・・これは年から1900を引いた数が出てしまうので、もし古いコードでgetYearを使っている場合、大きくご動作する可能性があります。

V8ではgetYear()を使わず常に、getFullYear()を使うよう推奨されています。

function newgetyear(){
  var test = new Date("1978/10/6");
  Logger.log(test.getYear())
  Logger.log(test.getFullYear());
  
  test = new Date("2020/2/1");
  Logger.log(test.getYear());
  
}

図:2000年以降のgetYear()は挙動が変わってるので要注意

onOpenのメニューでオブジェクトメソッド呼び出し

これまでのカスタムメニューを作るui.createMenuでは、addItemで呼び出せるのは単一の関数のみでした。しかし、V8からは、クラスが使えるようになったこともあって、オブジェクトメソッド形式のものも呼び出せるようになりました。オブジェクトメソッドとは、オブジェクト名menuの中に複数のメソッドがある場合、menu.item1やmenu.item2といった形で関数を呼び出すものです。(SpreadsheetだけじゃなくSlideやFormでも同様です)。

//メニューを作成
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('🍎 カスタムメニュー')
      .addItem('1個目のメニュー', 'menu.item1')
      .addSeparator()
      .addSubMenu(ui.createMenu('サブメニュー')
          .addItem('2個目のメニュー', 'menu.item2'))
      .addToUi();
}

//メニューから呼び出す関数
var menu = {
  item1: function() {
    SpreadsheetApp.getUi().alert('おはようございます。');
  },
  item2: function() {
    SpreadsheetApp.getUi().alert('こんばんは。');
  }
}

アロー関数

アロー関数はこれまでの、function taxcalc(){}といった記述ではなく、=>といった矢印で処理を記述するような感じになります。もちろん、これまでの記法も利用可能。{}を使わない程度のものなら、1文で書くことも可能。引数がない場合には()だけでOKとなります。

前述のtaxcalc関数をアロー関数で書くとこうなります。

//アロー関数でtaxcalcを作成
const taxcalc2 = (item,price) => {
  //計算結果を格納する変数
  var result = 0;
  
  //itemが持ち帰りか?その場で食べるかで変更する
  //trueならばtakeout, falseならばeatin
  if(item == true){
    result = price * takeout;
  }else{
    result = price * eatin;
  }

  //計算結果を
  return result;
}

Class構文

これまでのJavaScriptことGoogle Apps Scriptでは、様々な関数を用意してそれを呼び出すようなシンプルな構造でした。今回のアップデートによって、Google Apps ScriptにてClassが使えるようになり、Javaに代表されるようなオブジェクト指向型言語のようにコードを記述する事が可能になります。

単発のコードだけで見ると、別に関数でいいじゃないかと思いがちですが、Classというものは単なる引数与えて答えもらうだけのものではなく、いくつものメソッド、プロパティを持って様々な処理を総合的に引き受けてくれるものなので、コードの再利用性が高まります。また、その内容も実に深いので、クラスは嫌いという人もいたりします。クラスの理解はこちらのサイトがとても参考になります。なかなか最初はサッと使えないかもしれませんが、身につけたら大きな武器になります。

※Class構文だけ別エントリーで作成しました。そちらで詳しく解説しています。

Google Apps Scriptでクラス構文を活用する【GAS】

//eatinで食べて帰る
function priceman3(){
  //変数と配列の宣言
  var items = ["e","t","t","e","t","t","e"];  //eがeatin, tがtakeout
  var result = 0;

  for(let i = 0;i<items.length;i++){
    //配列から1個値を取り出す
    let tempret = items[i];
    let hantei;
    let price;

    //条件分岐
    switch(tempret){
      case "e":
        //イートイン価格で計算
        hantei = false;
        price = 100;
        
        //クラスを使う
        var ret = new taxcalc3(hantei, price);
        result += ret.calcret(hantei,price);
        
        //税額だけログ出力
        Logger.log(ret.zeikin);
        
        break;
      case "t":
        //お持ち帰り価格で計算
        hantei = true;
        price = 80;
        
        //クラスを使う
        var ret = new taxcalc3(hantei, price);
        result += ret.calcret(hantei,price);
        
        //税額だけログ出力
        Logger.log(ret.zeikin);
        
        break;
    }
  }
  
  //計算結果を出す
  var ui = SpreadsheetApp.getUi();
  ui.alert(result)
  
  //staticなメッセージを取得
  ui.alert(taxcalc3.takasugi());
  
}

class taxcalc3 {
  constructor(item, price) { //クラスの構造体
    this.item = item;
    this.price = price;
    
    //税額を取得しておく
    if(item == true){
      this.zei = takeout;
    }else{
      this.zei = eatin;
    }
  }
  
  //クラス内に用意したメソッド
  //税計算して返す
  calcret() { 
    //計算結果を格納する変数
    let result = 0;
    
    //itemが持ち帰りか?その場で食べるかで変更する
    //trueならばtakeout, falseならばeatin
    result = this.price * this.zei;
    
    //結果を返す
    return result;
  }
  
  //消費税額だけ取得
  get zeikin(){
    return this.zei;
  }
  
  //staticな構文
  static takasugi(){
    return "税金高すぎ";
  }
}
  • taxcalc3には3つのメソッドが用意してあります。メインのcalcret、getterであるzeikin, staticなtakasugi
  • 使う側は、newでtaxcalc3インスタンスを用意して、その中のcalcretを呼び出し、引数を与えて処理をしてもらう
  • staticはnewでインスタンスを用意せずとも、taxcalc3.takasugi()でいきなり呼び出せます。
  • getterだけでなく、プロパティに値をセットするsetterもあります。
  • 他にもextendsな継承、superなオーバーライド、プロパティの直接参照を防ぐSymbolなど覚える事山のごとしです。
  • Javaなどではおなじみの継承して、新しいクラスを用意してなんてやり方が、Google Apps Scriptでも出来るようになるわけですね。
  • ret.zeikinでgetterであるzeikinメソッドを実行し、単品の税率がLogger.logに残るようにしています。

分割代入

ある配列に何個もある数値をそれぞれの変数に一個ずつ取り出すようなコード。これをスパッとまとめて用意した変数に一発で代入するのが、分割代入です。スプレッドシートの範囲のデータを取得した時に、1レコードの各列の値を使って何かする時に、効果を発揮しますね。

//分割代入で値を取得しつつ、マージン計算した結果を返す
function marginman(){
  //スプレッドシートの範囲を取得
  var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("商品台帳").getRange("B2:C").getValues();
  var len = ss.length;

  //計算結果を格納する
  var result = 0.0;
  
  //ループで売価を計算する
  for(let i = 0;i<len;i++){
    //1行取り出す
    let array = ss[i];
    
    //分割代入
    var [price,margin] = array;

    //計算してresultに加算
    result += Number(price) * Number(margin);
    
  }
  
  //答えを返す
  SpreadsheetApp.getUi().alert(result); 
}
  • PriceとMarginの2つの変数に1次元配列の値をぶっこんでいます。
  • 今回は2個だけですが、これが結構な量ある場合には、非常に有用です。

テンプレートリテラル

これまでは、例えばUrlfetchAppなどで変数を取得し、URLを組み立てるような場合には、+演算子などを利用して組み立てていたと思います。テンプレートリテラルを使う事で、これをもうちょっと簡単に組み立てられます。Vue.jsなどでは結構見かけるような記法ですね。

ただ、まだGAS側のコードの認識がおかしいのか、正しい記法なのに、https:の部分だけ黒文字という状態です。またこの時URLをくくっているのは「シングルコーテーション」ではなく「アクサングラーブ(バッククオート)」という記号になるので、シングルコーテーションだとエラーになります。似てるけれど違います。記号と読みを参照してみてください。入力方法は、Shift+@で入力できます。

//特定のシート(現在見てるシート)のみPDF化する
function makepdf() {   
  //アクティブシートのIDとGIDを取得する
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("大手町");
  var sheetID = sheet.getSheetId();
  var key = ss.getId();
  var ui = SpreadsheetApp.getUi();
  var token = ScriptApp.getOAuthToken();

  //テンプレートリテラルでURLの組み立て
  var url = `https://docs.google.com/spreadsheets/d/${key}/export?gid=${sheetID}&format=pdf&portrait=false&size=A4&gridlines=false&fitw=true`

  //PDF生成するURLをfetchする
  var pdf = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' +  token}}).getBlob().setName("test" + ".pdf");
    
  //作成したPDFファイルをメールに添付して送る
  var mail = GetUser();
  var subject = 'PDF送りましたよっと。'
  var body = 'テストPDFの送信パート2'
  MailApp.sendEmail(mail, subject, body, {attachments:pdf});
  
  ui.alert("画像PDFが送信されました。")

}

//現在のユーザのアドレスを取得
function GetUser() {
  var objUser = Session.getActiveUser();
  return objUser.getEmail();
}
  • アクサングラーブ(バッククオート)でURLを括っています。
  • ${key}で変数keyを取得しています。
  • ${sheetID}で変数sheetIDを取得しています。

関数のデフォルト引数

これまでのGoogle Apps Scriptでは、functionで関数を作った場合、デフォルトの引数は存在しません。故にundefinedになります。そのため、引数指定がなかった場合の処理を用意しておく必要がありました。しかし、関数のデフォルト引数を指定出来るようになったので、指定がない場合には、既定値を持って処理する事が可能です。

//既定値を指定した関数
function omikuji(param=1) {
  let result;
  switch(param){
    case 1:
      result = "大凶";
      break;
    case 2:
      result = "凶";
      break;
    case 3:
      result = "大吉"
      break;
  }
  
  return result;
}

//おみくじをひく
function choicemikuji(){
  //引数なしで呼び出し
  SpreadsheetApp.getUi().alert(omikuji());
}
  • omikuji関数の既定値は1でセットしてあります。
  • 呼び出す時、引数なしでomikujiを引くと、大凶が返ってきます。
  • 複数引数に規定値をセットする場合には、function omikuji(param1=1,param2="てんびん座"){}といったような指定になります。
  • 引数にundefinedを指定した場合には、引数は1になります。
  • 引数で演算させた結果を利用、例えば第一引数が1で第二引数が2, 第三引数では、1 + 2の結果を使うといったような書き方も可能になっています。

複数行に渡る文字列

これまでのGoogle Apps Scriptでは、改行をするような形での文字列の場合には、行末に「\n」という改行コードをつけ、各行はコーテーションでくくる。そしてそれぞれの行を+演算子で結合して、、、なんてやって、複数行に渡る文字列を作っていました。改行コードがなければ、例えばダイアログの中で改行されず、1行に全部つながった文字になってしまうわけです。

しかし、今回のアプデではこれも「アクサングラーブ(バッククオート)」記号を持ってくくれば、見ているまま適当に改行すれば改行して表示してくれるようになります。コーテーションで括るとエラーになりますので、注意。

//複数行に渡る文字列を表示
function multistring() {
  var multiline = `スパゲティのソースはどうしてもミートソースじゃなければならない。
                  ミートソースには深い思い出があるからだ!!
                  貧乏学生時代の自分を支えてくれたソウルフードだからだ`;
  
  SpreadsheetApp.getUi().alert(multiline);
}

図:いちいち改行コードとか鬱陶しいのを入れなくて良い

Promiseで順番に処理

Google Apps Scriptは基本的には同期処理なのですが、時々非同期にパーンって処理が進んじゃって、なんだか思ってた答えと違うものが返ってくるシーンがあります。Node.jsのように非同期が当たり前の処理だとCallbackを利用したり、Promiseで順番に処理をしたり・・・

そんなPromiseも使えるようになってるみたい。

var choco = "";

//順番に関数を実行する
function tabezakari() {
  Promise.all([
    kinoko(),
    takenoko(),
    suginoko()
  ])
  .then(function(ret) {
    SpreadsheetApp.getUi().alert(choco)
  })
}

function kinoko(){
  choco += "きのこたけのこす〜ぎのこ〜";
}

function takenoko(){
  choco += "じゃんけんぽ〜んでかくれんぼ〜";
}

function suginoko(){
  choco += "きのこたけのこみ〜つけた〜";
}

async function baseball() {
  await Playball(); // 終わるまで待つ
  Logger.log('game set'); // 終わってから実行される!
}

function Playball(){
  Logger.log("Playball");
}

//finallyを使ってみる(ES2018)
function promiseman(){
  new Promise((resolve, reject) => {
    var word = "おはようございます。";
    resolve(word);
  }).then((ret) => {
    console.log(ret);
    console.log('今日は雲丹醤油でご飯。')
  }).catch(e => {
    console.log('エラーを捕捉')
  }).finally(() => {
    console.log('実に美味しい')
  })
}
  • HTML Serviceのgoogle.script.runは非同期実行なので、順番に実行させるにはPromiseが必須です(元からHTML側はPromiseが使えます)
  • Promise.allで順番に実行が可能です。他のPromise記法も大丈夫。
  • async/awaitの記法も問題なく実行できました。

図:totocoの雲丹醤油美味しいです

また、複数のresolveを利用して確実に同期処理をするには以下のようなコードを使うと良いでしょう(特にgoogle.script.runなどを使うケースでは)

Google Apps Scriptでウェブアプリケーション作成入門【GAS】

//確実に同期的に処理をする
Promise.resolve()
.then(function () {
  return new Promise(function(resolve, reject) {
    setTimeout(function () {
        //ここに処理1を記述
        google.script.run.withSuccessHandler(function(ret) {
            resolve('タスク1の処理を完了');
        }).exportlog();
    }, 10);
  });
})
.then(function(value) {
  return new Promise(function(resolve, reject) {
    setTimeout(function () {
     //ここに処理2を記述
        google.script.run.withSuccessHandler(function(ret) {
            resolve('タスク2の処理を完了');
        }).transferlog();
    }, 10);
  });
})
.then(function (value) {
 //終了処理
  
}).catch(function (err) {
  //エラー発生時の処理
  console.log(err);
});
  • 上記の例だと、exportlogとtransferlogの2つのGAS側関数を順番に実行しています
  • 本来、google.script.runは非同期で実行されてしまうので、並列して書くと実行順番が保証されませんが、Promise.resolveを使う事で、期待通りの処理になります。

イテレータ

Google Apps ScriptでもDriveAppなどでファイルのリストを取得する際に利用されているものですね。配列のようなものに対して繰り返し作業をして値をどうこうする仕組みなので、配列もイテレータのオブジェクトと言えます。

これをスプレッドシートの処理などで使うとこれまでは、二次元配列で取得されたシートのデータを取り出すのに、for文とカウンタを利用して、取り出していたものが、for-ofという今回より使えるようになったものと組み合わせると簡単に取得が可能になります。カウンタのlengthとかも気にする必要がないので、Goodですね。

//シートのレコードを1行ずつイテレータで取り出す
function onerecord() {
  //スプレッドシートのデータを取得する
  var obj = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("商品台帳").getRange("A2:C").getValues();
  
  //イテレータで1行ずつ取り出す
  var iterator = obj[Symbol.iterator](); 
  for(var v of iterator){
    Logger.log(v);
  }
}

//非同期イテレータは使えない・・・・
async function* geturls() {
  for (const url of urls) {
    let response = await fetch(url);
    let iterable = response.text();
    yield iterable;
  }
}
  • シートデータをobjに全部入れておく
  • イテレータを用意して、イテレータを回す。すると、vに1行文のデータが入ってくる
  • 現状は、asyncと組み合わせて非同期イテレータみたいなことはできないようです。

ジェネレータ

イテレータと異なりジェネレータは理屈を理解するのに少し時間がかかります。この関数は、呼び出されてもその全てを実行してから返す事がなく、呼び出す毎にあらかじめ設定しておいた値を返す仕組みです。よって、連続で呼び出しても毎回答えが異なる。

特徴的なのは、yieldで必ず止まり、値を返す。二回目は次のyieldで止まって値を返すを繰り返してくれます。最終的に設定してるyieldの数を上回る回数回した場合には、doneがtrueという形で返ってくるという変わった関数です。呼び出される関数側はfunction*という形でアスタリスクが付くのも特徴です。

//じぇねれーた関数(3個いれておく)
function* myGenerator() {
  yield 'きのこ';//1回目のnext関数で返す
  yield 'たけのこ';//2回目のnext関数で返す
  yield 'すぎのこ';//3回目のnext関数で返す
}

//ジェネレータを実行
function generator(){
  //ジェネレータを使う
  var gen = myGenerator()
  
  //1回ずつジェネレータを呼び出す
  console.log(gen.next()); 
  console.log(gen.next()); 
  console.log(gen.next()); 
  console.log(gen.next()); 

}
  • ジェネレータを回す場合には、.nextで次の回を回せます。
  • ジェネレータに引数を渡すことも可能です。

for eachの変更

地味ながら、ここは修正が必要になる項目です。これまでもGoogle Apps Scriptでfor eachが利用できましたが、V8からは記法が変わります。eachを削るだけです。Rhino使おうとするとSyntaxError: Unexpected identifierとして弾かれる。

//キャラのステータス
var obj  = {
  hp : 100,
  mp : 50,
  speed : 125,
  stamina : 40,
  power : 68,
  wisdom : 45,
  luck : 255,
  defence : 70,
}

//ステータスを連続出力
function statussheet(){
  //foreachで流す
  for (var key in obj) {
    //能力値を取り出す
    var value = obj[key];
    
    //ログ出力
    Logger.log(`${key}の値は、${value}です。`)
  }
     
}

図:能力値のオブジェクトから値を取り出してみた

Symbol

Symbolとは何か?唯一無二のuuidのようなものを生成する関数で、かといって生成されるのは文字列じゃないので、console.logしても文字列は出てこない。これも以前のGoogle Apps Scriptでは使えず、使おうとするとReferenceError: 「Symbol」が定義されていません。というエラーが出ます。型名もsymbol型という・・・

また、このSymbolはオブジェクトのプロパティのキーとしても使えるという事なので、連想配列な{}の中に於いて、key名となることができる。二度同じ値は生成されないということで、絶対に被らない変数というものを用意等に使ったりするけれど、文字列じゃないので、uuidのような感じでは利用できない。

個人で使う機会あるかなぁというと、あまり無いかもしれない。チームで開発してる場合、同じようなモジュールでプロパティ名も同じ、けれどちょっと違う挙動なんて時に、参照する名前が同じで困るなんて事を避けられるくらいかなぁ。

Setオブジェクト

配列のような集合体であるSetオブジェクトが使えるようになりました。同じデータを重複して塊に含めることは出来ない仕様。データの追加はaddで行い、存在確認はhasで行う。deleteで削除ができ、sizeで個数を調べることができる。配列のようにインデックスを指定して値を取ることは出来ない。clearをすると空っぽになる。

値の取得はループなどで取り出すことが可能(追加順で出てくる)。

function settest(){
  //新規のsetを用意する
  var set = new Set();
  
  //新規setにデータを登録していく
  set.add("しめじ");
  set.add("まつたけ");
  set.add("しいたけ");
  set.add("エリンギ");
  
  //確認対象
  let target = "たもぎたけ";
  
  //setオブジェクトの件数をしらべる
  Logger.log(set.size);
  
  //値を取り出してみる
  for (var value of set) {
    Logger.log(value);
  }
  
  //存在確認
  if(set.has(`${target}`)) {
    Logger.log(`${target}は、在庫あり`); 
  }else{
    Logger.log(`${target}は、在庫ないようです。`); 
  }
}

オブジェクトを配列で返す

ES2017で規定されている、オブジェクトの各値を配列で返す、もしくは二次元配列で返すObject.valuesおよびObject.entriesが使えるようになりました。これによって、例えばJSONファイルを取得した時に、中に入ってる値を配列に突っ込みたい時、ループでどうこうしなくて済みます。

//キャラのステータス
var obj  = {
  hp : 100,
  mp : 50,
  speed : 125,
  stamina : 40,
  power : 68,
  wisdom : 45,
  luck : 255,
  defence : 70,
}

//object.values使えるかな
function objvan(){
  var array = Object.values(obj);
  Logger.log(array);
}

//object.entries使えるかな
function objvan2(){
  var array = Object.entries(obj);
  Logger.log(array);
}

配列の検索

これまで、Google Apps Scriptでの配列の検索は、indexOfを使ったりループで回してチェックしてみたりしてました。今回のアップデートより、includesメソッドが使えるようになったので、存在確認や末尾から探すなんてことも可能になりました。

//配列化からincludesで探す
function objsearch(){
  
  //配列化する
  var array = ["しいたけ","しめじ","まつたけ","えのきだけ","エリンギ"];
  
  //配列から探す
  var ret = array.includes("たもぎたけ")
  
  //結果の出力
  Logger.log(ret);

}

累乗計算

実はいままでのGoogle Apps Scriptでは、累乗はMath.pow(2,8)といったメソッドを使っていました。ループを使って自前で作っていた人もいるみたいです。あまり使う機会はないかもしれませんが、以下のような簡単な構文で計算結果が出せるようになりました。

function ruijou(){

  Logger.log(2 ** 8)

}

スプレッド構文

配列データの中身をconcatのようにつなげたり、足したりできる構文です。他の関数と組み合わせて、配列データを食わせて色々よしなにできます。この時使うのが「...」この点々。以下の関数では、101が答えとして返ってくる。

それだけじゃなく、複製したり、Array.prototype.push.applyのように配列をどんどんpushしたり、文字列を配列にしてみたりと、色々と加工ができる。

※ES2018で修正されたオブジェクトに対してのスプレッド構文も問題なく動きました。

//2つの配列つなげて、最大値を出す
function mathematica(){
  const data = [15, -3, 78, 1];
  const data2 = [40, 11, 101, 9];
  console.log(Math.max(...data, ...data2));
}

//オブジェクトでもやってみる(ES2018)
function spreadman(){
  const { a, b, ...c } = { a: 1, b: 2, x: 3, y: 4, z: 5}

  console.log(c);
}

図:オブジェクトに対してスプレッド構文で入れてみた

整数だけ取り出す

結構Math関係はGoogle Apps Scriptは使えていたのですが、それでも新しいメソッドは未対応だったので、自前で作ってたなんてケースは多いはず。そんな一つにMath.trunc。小数点の値をぶちこむと、整数部分だけ取り出してくれる。こんな感じの地味なんだけれど、すごく助かるものが、使えるようになった事が今回の対応の一番の利点なのではないかと思う。

//trunc
function truncman(){
  Logger.log(Math.trunc(6.5));
}

正規表現

これまでもGoogle Apps Scriptでは正規表現が使えていたわけですが、ES2018で追加されているsフラグも使えるようになりました。「 . が完全にすべてのワードにmatchするようになる」ものです。特殊な改行コードやユニコード記号(U+2029など)も対象になるわけでっす。

他にもES2018で強化された正規表現の機能(後読みアサーション、replace()との併用、

function regexman() {
  var test = /foo.bar/s.test('foo\nbar');
  Logger.log(test);
}

//trueが返ってくる

function regexman2(){
  var test = '123456'.match(/123(?=456)/) 
  Logger.log(test)
  
  var test ='123456'.match(/(?<=123)456/) 
  Logger.log(test);
  
}

//[456]が返ってくる

function regexman3(){
  let word = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
  let result = '2020-02-08'.replace(word, '$<day>/$<month>/$<year>');
  Logger.log(result);
}

//02/08/2020が返ってくる

function regexman4(){
  let regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
  let result = regex.exec('2020-02-08');
  Logger.log(result.groups.year);
}

//2020が返ってくる

Mapオブジェクト

ES2015より追加されたMapオブジェクトが利用可能になっています。配列の処理のパターンがこれでまた1つ加わりますね。キーと値の組み合わせを持った配列等の処理をする場合に効果があります。

反復処理はforeachに似たようなスタイルです。早速、Mapオブジェクトを利用したクラスを作られた方がいる。JSONオブジェクトの扱いが楽になるなぁ。

//Mapオブジェクトの基本
function mappu(){
  var map = new Map([["taro", 1000], ["hanako", "桜餅"]]);
  console.log(map.size); 
  
  map.clear();
  console.log(map.size);
}

//mapの反復処理
function maploop(){
  var items = [1,2,3,4,5];
   
  var result = items.map(function(value,key) {
      console.log("[" + key + ", " + value + "]" );
  });

}

String Padding

String Paddingとは規定の桁数や文字数があり、それに満たない文字の場合には、指定の文字で埋めてあげるもの。具体的にいうと10桁の商品コードがあって、5桁文しかないコードの場合、10桁になるように頭に0で埋めるといったことがメソッドで可能になります。社内アプリとかだと結構使うシーンがありますね。

padStartで前埋め、padEndで後ろ埋めになります。

function leftpadman(){
  //指定の文字
  var temp = "1234"

  //0埋め
  var ret = temp.padStart(10,"0");
  
  //結果は0000001234となる
  Logger.log(ret);
}

関数の引数最後にカンマ

よくやってしまうのですが、functionの引数に複数の引数を取る時、2つしか入れず、最後にカンマ(通称ケツカンマ)をつけたままにしてしまうことがあります。Google Apps Scriptでこれをやると、仮パラメータがありません。と怒られます。V8エンジンではこれに対してエラーがでなくなるようになります。

function test(price,margin,){

  Logger.log(price * margin)

}

日付の出力形式の変更

これまでの日付形式はnew Date()した値は、toLocaleDateStringですと「December 21, 2012」みたいな表記でした。これが今回からは「12/21/2012」となり、また、引数指定でja-JPなどを指定すると、2012年12月21日で返ってくるようになりました。

function dateman(){
  // V8 runtime
  var event = new Date();

  //日本の日付表記で取得する
  Logger.log(event.toLocaleDateString(
      'ja-JP',
      { year: 'numeric',
        month: 'long',
        day: 'numeric' }));
        
}

undefinedな値をsetValueすると

これまでスプレッドシートなどで指定のセルにsetValueする際に、引数の値がundefinedな時には、undefinedという文字がセルに入ってしまっていました。しかし、V8エンジンからは、null値が入るようになります。

function nullpo(){
   SpreadsheetApp.getActiveRange().setValue(undefined);
}

ファイルの順番に意味がある

スクリプトエディタの場合

古いGoogle Apps Scriptでは問題なく実行できたのに、V8からは実行すると「ReferenceError: xxxx is not defined」と出て、なぜかエラーになるようになりました。これは、V8エンジンの特性で、スクリプトエディタの左パネルに於いてファイルが並んでいますが、上から順番に参照されるので、参照された次のファイルに関数がある場合、まだ読み込んでない関数を呼び出そうとすることで発生します。

特にこれはグローバル変数で引き起こされます。

//最初に読み込まれるファイル
//グローバル変数で次のtest2.gsのranran関数を呼び出し格納
var gvalue = ranran();

//グローバル変数の値を表示する
function v8test() {
  var ui = SpreadsheetApp.getUi();
  ui.alert(`${gvalue}という値が入っています`);
}
//次に読み込まれるファイル
//ランダムな値を返す関数
function ranran() {
  return Math.random();
}

上記の場合、v8test関数を実行するとエラーになります。実行時v8testの前にranran関数が読み込まれていないからです。同じ、test1.gs内であれば、v8testの下に記述しても問題なく実行されます。そのため、同じ関数名の関数が複数どこかに入っている場合、最後に読み込まれたものが有効になるようです。

故に左サイドに関数を作り込んでいく上で、上から順番に読み込まれて実行されるという事を意識する必要性があります。また、同じファイル内ならば順番は意識する必要はありません。

※ならば、左サイドパネルのファイルの位置、好きなように上下できるようにしてほしいものだ。

claspの場合

V8状態でclaspでGAS側へpushする場合は少し様相が異なるようです。なんの指定もなく今まで通り関数やクラスを作って、pushした場合、ファイルの順番が「アルファベット順」で並ぶことになり、場合によっては他のファイルを参照してる場合、同様にエラーが発生する(つまり、Axxx.gsとBxxx.gsがあった場合、BのクラスをAが参照してもまだBは読み込まれていないことになる)

エディタのリスト順よりも優先してしまうという事ですね。

これを正す為には、clasp.jsonファイルの中に以下のようなfilePushOrder項目で順番にpushさせるよう指示を加えて上げる必要があるようです。

"filePushOrder": [
  "bxxx.js",  //クラスファイルが入ってる
  "axxx.js"   //クラスを参照するコードが入ってる
]

bxxx.jsからまず上げて、次にaxxx.jsをあげる。こうすることで、bxxx.jsがエディタ上のリストで言えば先に出てくるようになるので、bxxx.jsのクラスを無事にaxxx.jsが参照できるようになるという訳です。

※コメントで情報いただきました。大変ありがとうございます。

ライブラリを使う場合の注意点

GASのライブラリを使う場合、呼び出し元のプロジェクトがRhinoで、呼び出し先のライブラリ側がV8の場合、呼び出し元でそのライブラリを使った場合、V8でなければ使えないメソッド等の場合エラーとなります(逆にV8では使えないメソッドをライブラリ側で使っていた場合も同じ)。

呼び出された側のエンジンで動く事になるので、ライブラリ運用してる人は要注意事項です。必ず、両方のエンジンは同じにする事。ライブラリ側でうまく動いてるから問題なしと思ったら、それを使っていたプロジェクト側がV8ではなくエラーがバンバン出ることになります。

図:安易にライブラリ側はV8にすると厄介な事に

予約語に注意

これまでのGoogle Apps Scriptは予約語については緩い感じだったようですが、V8からは予約語が厳しい。予約語になってるワードを変数名や関数名などに使わないように気をつけましょう。

しかし、そのほとんどは、メソッド名である「if」や、return, varといったワードの類。ここにenumpackageinterface、さらには型の名前などが入っているので、およそ変な事をしなければ予約語にぶつかるということはないんじゃないかなぁと。ただ、importexportなどこれまで使えていたものは、使えなくなるようなものが出てきているので、ここは要注意です。

Xml.parseは使えません

地味に大きいのが、XMLデータのパースをする為のXml.parseが使えなくなったことです。RSSデータなどを処理するのにこれまでXml.parseやnew XML(要素)を使っていたようなコードは、全てXmlService.parseを利用する必要性があります。ただ、Namespaceを指定して取り出すだとかは、結構な面倒な作業なので、こちらのエントリーを参考にして、JSONに変換して取り出すほうがスマートかもしれません。

XML取り出し例としては以下のような感じになります。dc:identifierみたいなコロンを含んだものは、namespace指定をしないと取り出せないので、色々頑張る必要があります。

//XMLデータをparseする
var xmldocs = XmlService.parse(xmldata);

//itemノードを取得する
var items = xmldocs.getRootElement().getChildren('channel')[0].getChildren('item');
var length = items.length;

//namespaceを取得する
var namespaceDc = XmlService.getNamespace("dc", "http://purl.org/dc/elements/1.1/");

//ループでタイトルを取得
for(var i = 0;i<length;i++){
    //dc:identifierを取得
    var prop = items[i].getChildren('identifier', namespaceDc);

   ・・・・中略・・・・

}

その他

ES2017から加わったメソッドなどが普通に動くようになっています。Polyfillとかも対応しているのかも(サーバサイドだから意味ないけれど)。一方で、Object.prototype.toSource()といった古いメソッドが廃止されていたりもします。

バグ?やトラブル

V8ランタイムにする事で、Rhinoの時のコードの一部に不具合が発生する可能性は十分あります。またエンジンを変更している為、そもそも過去に廃止になってるものは、Rhino上では互換性のために維持していても、V8ではスッパリ切られているケースも。

  1. 2019年8月廃止されたFusion Tableのサービスは、コードを残しておくとエラーになります。UiAppなどのコード類も同義。
  2. ライブラリ側でもV8対応していないようなケースでは、エラーになるケースがある模様(Twitterライブラリで報告あり)
  3. HTML ServiceのcreateTemplateFromFileにて、HTML側でoutput.appendを利用している場合、TypeError: output.append is not a functionが出るバグが出ています。V8 Runtimeでoutputを使いたい場合は、「output._=('<b>test</b>');」を利用するとエラーになりません。
  4. V8にてJDBCサービスを利用するとReferenceError: Jdbc is not definedというエラーが出る報告があります。
  5. CSVを取り込む為のUtilities.parseCsvにてエラーが出る報告が上がっています。遭遇してしまったら、CSVToArrayを利用してみてください。stackoverflowでV8でも動く対応版コードも掲示されています。
  6. new Date()で生成した日付について、Logger.logに残る日付と表示する日付に時差分の誤差が生じているエラー報告あり
  7. ドキュメントにアクセスするようなWeb Appの場合、Exception: Document ****** is missingといったエラーが出て、Rhinoに戻してもアクセス出来ないケースがある(アクセス権はきちんと適用されているので、改めてGASを作り直すと動くパターン)
  8. claspでv8コードをpushするとエラーが出ている
  9. Logger.log等のログ出力の反映が異常に遅い。5〜6秒後に遅れて出力されている現象(しかも、遅れて一発で出ずに、何度か表示しないとすべて出ないケースがある)。console.logで出力し、ダッシュボード側で確認しましょう。
  10. バグじゃないけれど、2月に入ってから、UrlfetchAppにてGoogle DriveのファイルURLを取得しようとすると、403エラーが返り拒否されるようになったみたい。
  11. HtmlService.getUserAgent()にて、ユーザエージェントが取得できずnullが返ってくる。Rhinoだと「Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36,gzip(gfe),gzip(gfe)」といったユーザエージェントが帰ってくる=> 多分この影響かも。
  12. Googleスプレッドシートに於いて、onEditトリガーを設置、toastを表示するコードが、オーナー以外が編集した時に表示されないバグが発生しています。
  13. 拡張サービスであるDrive Activity APIを有効にしてDriveActivity.primaryActionDetailを取得すると、余計な情報まで全部ついてくるバグが発生しているようです。
  14. 動画ファイルからサムネイル画像を生成するgetThumbnail()が正しく動作せず、サービスエラーとなる。V8にすると一見関係ないメソッドでもGAS特有のメソッドに関しては致命的バグを抱えているみたい。
  15. また、同じエントリーにして画像ファイルをBlob⇒Base64⇒Blobで比較した際に、V8とRhinoとでエンコード結果が異なるというバグが報告されています。V8だとなぜかどんなファイルでも/9j/4A==が結果として返ってくる・・・以外と結構使うメソッドなので、画像関係やファイル関係のBlob取得系ではV8はしばらく避けたほうが良いかも。
  16. google.script.run.withSuccessHandler(OnSuccess).exefunction()にて、アップロード画面を制作した場合に、呼び出す関数であるexefunctionが実行されない致命的な問題があります。こちらでも同様なものが。ウェブアプリケーションを作る上では、V8はまだ利用は早い!!といった印象。
  17. よく利用されているOAuth2認証用ライブラリですが、現在はV8ではエラーが発生します。V8を無効にして利用するか?自前で認証をするためのコードを書いて回避しましょう。(OAuth2認証をするサンプル
  18. GMail Addon用コードにてV8の場合、CardServiceにてthrowExeptionにてエラーが発生するようになります。
  19. 一部のケースで単純なMailApp.sendEmailを使ったメール送信スクリプトが、スクリプトトリガーで発動した場合に動かないケースが報告されています。
  20. 古いGoogle Sitesに埋め込みのGoogle Apps ScriptではV8は利用できないThis action is not supported unless the runtimeVersion is set to DEPRECATED_ES5 in the appsscript.json file.といったメッセージが出たりします。DEPRECATED_ES5の指定が必要です。ただ旧サイトは既にDeprecated対象なので、早く新しいGoogle Sitesに移行すべきでしょう。
  21. MailAppに於いて、宛先を複数指定する場合、配列で指定するのですが、V8ですとこれが「number[],String,String,(class))が MailApp.sendEmail のメソッドのシグネチャと一致しません。」とエラーが出て送信出来ない。代替策としてGmailAppを利用すると、こちらは送信出来ました。GmailAppは添付ファイルの指定やパラメータの指定がちょっと違うので注意。
  22. ScriptApp.getService().getUrl()にて、WebアプリのURLが取得出来るのですが、V8だと/dev側のURLが取得され、ES5だと/exec側のURLが取得される状態。こちらで報告されています。こちらの回避方法についてはこちらのエントリーでまとめてみました。
  23. HTML Service上で作ったアップローダからバイナリファイルをアップロードしようとすると、Driveに生成時にファイルが破損する。V8オフであれば正常に動作します。
//問題が起きるコードの事例
function getStartDate(){
  var d = new Date();
  d.setDate(d.getDate() - 7);
  
  //ui.alertで出す値は正しい値
  SpreadsheetApp.getUi().alert(new Date(d.getFullYear(), d.getMonth(), d.getDate()))
  
  //Logger.logに残る値は誤った値
  Logger.log(new Date(d.getFullYear(), d.getMonth(), d.getDate()))
}

コード:同じ結果にならないバグ

図:まだまだバグがたくさん残っているようです

//これまでのコード(V8では動かない)
var data = Utilities.parseCsv(data, delimiter);

//バグ対応版コード
var data = Utilities.parseCsv(data, delimiter.charCodeAt(0));

もしくは

var data = Utilities.parseCsv(data, Utilities.newBlob(delimiter).getBytes());

コード:Utilities.parseCSVのバグ対応版コード

//動画からサムネイルを作る
function testDrive() {
  var fileID="ここに動画ファイルのIDを入れる";
  var folderid = "ここにサムネイル画像生成先フォルダのIDを入れる";
 
  var video_file=DriveApp.getFileById(fileID); //動画ファイルを取得する
  var blob=video_file.getThumbnail();
  blob.setName("test.jpg");

  var image_file=DriveApp.getFolderById(folderid).createFile(blob)
}

コード:無関係のように見えるメソッドでも動作しないケースが

//blob操作
function testBlob() {
  //画像ファイルを取得
  var fileid = "1uHrX6N5jPwhytfA-k0YSSVQBhkoRgQq4";
  var blobman = DriveApp.getFileById(fileid).getBlob();
  var ctype = blobman.getContentType();
  
  //Base64へエンコードする
  var base64 = Utilities.base64Encode(blobman.getBytes());
  
  //base64データを再度デコードする
  var blob=Utilities.newBlob(Utilities.base64Decode(base64), "image/jpg", "test.jpg");
 
  //ISO-8859-1を指定
  var content=blob.getDataAsString("ISO-8859-1");
 
  //再度Base64データをblobで取得
  var new_blob=Utilities.newBlob("", "image/jpg", "test.jpg").setDataFromString(content, "ISO-8859-1")
 
  //blobデータをデコードする
  var new_b64=Utilities.base64Encode(new_blob.getBytes());

  //V8だとfalseになり、Rhinoだとtrueになる
  //V8だとnew_b64の結果が、なぜか/9j/4A==となる。
  Logger.log(new_b64==base64);
}

コード:Blob操作なのかBase64デコードがおかしいのか・・・

スピード検証

以前高速化のエントリーで作成したテスト用スプレッドシートをV8にして、テストをしてみました。わかりやすいように「最悪なコード」で書いた場合と、「一番良い事例」で書いたコードでベンチマークしてみます。

このシートは1000行のデータの中から、1000というIDを持ったレコードを上から順番に探していき、見つけたらタイムを出すという仕様です。3回計測して一番良い値を取りました。

以前のGoogle Apps Scriptの場合

最悪なコードで書いた場合の検証結果は、「119.756(秒)」でした。

一番良い事例で書いた場合の検証結果は、「0.466(秒)」でした。

最悪なコードは無闇矢鱈にGASのAPI呼び出してるので、ものすごくオーバーヘッド掛かっているのに対して、一番良いコードは配列で取得して探索してるので、あっという間に終わります。

V8 Runtimeの場合

つづけて、同じデータで同じコードで、V8エンジンの場合の検証です。

最悪なコードで書いた場合の検証結果は、「97.078(秒)」でした。(18%ほど早かった

一番良い事例で書いた場合の検証結果は、「0.424(秒)」でした。(19%ほど早かった

最悪なコードで書いた場合も、一番良い事例で書いた場合も、いずれも18%程度高速化されているように思えます。

関連リンク

Google Apps ScriptのV8 Runtime対応を検証してみた【GAS】” に対して3件のコメントがあります。

  1. 上野 より:

    ファイルの順番ですが、Claspを使ってる場合、.clasp.jsonのfilePushOrderで指定した順になるようです。

    1. akanemaru2017 より:

      上野さん

      追加情報ありがとうございます。ブログにも追記しておこうと思います。

      1. 上野 より:

        ファイルの指定順ですが、
        ・派生クラスの場合は基底クラスの宣言が先のファイルでなければならない
        ・クラスのインスタンス化は前後関係ない
        ようです

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)