GASで使えるモダンJavaScript構文8選
|V8ランタイム

ネットで見つかるGASのコードは、いまだにvarと文字列結合だらけです。 現在のGASはChromeと同じV8エンジンで動くため、もっと短く安全に書けます。実務でそのまま使える8つの構文を、シート操作とAI連携のコードで紹介します。

|対象: GAS / V8ランタイム / JavaScript / コード改善

Table of Contents

V8ランタイムかどうかを最初に確認する

結論から言うと、いま新しく作るGASプロジェクトはV8で動いています。V8はChromeブラウザと同じJavaScript実行エンジン(コードを解釈して動かす仕組み)です。以前のGASはRhinoという古いエンジンを使っており、書けるJavaScriptがES5相当に制限されていました。ネット上のGAS記事にvarが多いのは、その時代の書き方が今も引用され続けているためです。

確認方法は簡単です。エディタ左メニューの「プロジェクトの設定」を開き、「Chrome V8 ランタイムを有効にする」にチェックが入っていればV8です。マニフェスト(appsscript.jsonというプロジェクトの設定ファイル)を表示している場合は、次の項目を見ます。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

// runtimeVersion が "V8" ならモダン構文が使える。
// "DEPRECATED_ES5" と書かれていれば古いRhinoエンジンで動いている。
// 「プロジェクトの設定 > Chrome V8 ランタイムを有効にする」で切り替えられる。

古いプロジェクトを引き継いだ場合はDEPRECATED_ES5になっていることがあります。切り替え自体はチェック1つですが、Rhino独自の構文を使っていると動かなくなります。該当する書き方は最後のセクションで扱います。まずは新規のスクリプトから、この記事の書き方を試してみてください。

var をやめて let / const にする

最初に変えるべきはこれです。varは宣言した変数が関数全体で生き続けるため、ループの外にまで影響が漏れます。letconst波かっこの中だけが有効範囲なので、変数の寿命が読んだとおりになります。

// 01. var をやめて let / const にする

function oldStyle() {
  for (var i = 0; i < 3; i++) {
    // var は関数全体が有効範囲。ループを抜けても i が生き残る
  }
  console.log(i);  // 3 が出てしまう
}

function newStyle() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("売上");
  const values = sheet.getDataRange().getValues();

  for (let i = 1; i < values.length; i++) {
    // let はブロックの中だけ。i はこのループの外では存在しない
    const row = values[i];
    console.log(row[0]);
  }
}

// 使い分けは単純。
// 再代入しないものは const、ループのカウンタなど再代入するものだけ let。
// const は「中身が変わらない」ではなく「その名前に別の値を入れ直さない」宣言。
// const rows = [...] の中身を push で増やすのは問題ない。

効果が一番わかりやすいのは、同じ名前を二重に宣言してしまったときです。varは黙って上書きしますが、letなら保存時にエラーで気づけます。長い関数を後から直すときほど効いてきます。

使い分けの基準は1つだけ。まず全部constで書き、再代入が必要になった変数だけletに変える。constは「中身が絶対に変わらない」という意味ではなく「その名前に別の値を入れ直さない」という宣言なので、const rows = []に対してrows.push(...)するのは問題ありません。

テンプレートリテラルで文字列を組み立てる

テンプレートリテラル(バッククォートで囲む文字列)は、GASで最も出番の多い機能かもしれません。メール本文やSlackへの通知文など、文字列を組み立てる処理がGASには大量にあるからです。

// 02. テンプレートリテラルで文字列を組み立てる

const name = "田中";
const count = 12;

// 旧: + でつなぐ。空白や全角の位置を間違えやすい
const oldText = name + "様\n本日の件数は" + count + "件です。";

// 新: バッククォートで囲み ${} に式を書く。改行もそのまま書ける
const newText = `${name}様
本日の件数は${count}件です。`;

// メール本文のような複数行の文字列と特に相性が良い
function sendDailyReport() {
  const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd");
  const total = 128000;

  MailApp.sendEmail({
    to: "team@example.com",
    subject: `【日次レポート】${today}`,
    body: `${today} の集計結果です。

  売上合計: ${total.toLocaleString()}円
  処理件数: ${count}件

  詳細はスプレッドシートをご確認ください。`,
  });
}

利点は2つあります。1つは+の連結が消えて、完成形がそのまま読めること。もう1つは改行をそのまま書けることです。\nを数えて配置する作業から解放されます。

注意点は、インデント(字下げ)もそのまま文字列に入ることです。関数の中で深くネストした位置に長文を書くと、メール本文の先頭に余計な空白が並びます。気になる場合は文字列を関数の外に切り出すか、行頭を左端に寄せて書いてください。

アロー関数・分割代入・スプレッド構文

この3つはセットで覚えると効きます。アロー関数はfunctionの短縮記法、分割代入は配列やオブジェクトから値を取り出して変数に入れる書き方、スプレッド構文(...)は配列やオブジェクトを展開する書き方です。

// 03. アロー関数・分割代入・スプレッド構文

const sheet = SpreadsheetApp.getActive().getSheetByName("問い合わせ");
const [header, ...rows] = sheet.getDataRange().getValues();
// 1行目を header に、残りすべてを rows に分ける(スプレッド構文)

// アロー関数: function(row) { return ...; } の短縮形
const names = rows.map((row) => row[0]);

// 配列の分割代入。row[0] や row[2] という書き方をやめられる
const summaries = rows.map(([name, email, body]) => {
  return `${name}(${email}): ${body.slice(0, 20)}`;
});

// オブジェクトの分割代入。設定値の取り出しに便利
function callApi(options) {
  const { model = "claude-opus-5", maxTokens = 512, prompt } = options;
  // model と maxTokens は省略時のデフォルト値付きで受け取れる
  console.log(`${model} / ${maxTokens} / ${prompt}`);
}

// スプレッド構文はオブジェクトの上書きにも使える
const baseConfig = { model: "claude-opus-5", max_tokens: 512 };
const longConfig = { ...baseConfig, max_tokens: 4096 };  // 一部だけ差し替え

// 注意: 分割代入の変数名は「列の順番」に依存する。
// 列の並びが変わる可能性があるシートでは、ヘッダー行から
// 列名でオブジェクト化してから扱うほうが安全。

GASで特に効くのがconst [header, ...rows] = sheet.getDataRange().getValues()です。「1行目はヘッダー、残りがデータ」という定型処理が1行で書けますvalues.shift()と違い、元の配列を壊さないのも安心です。

一方で、([name, email, body]) =>のような配列の分割代入は列の並び順に依存します。運用中に列が1つ挿入されると静かに壊れるため、列が増減しうるシートでは、次のセクションのようにヘッダー行から列名でアクセスする形にしておくと安全です。

map / filter / reduce でシートのデータを処理する

GASの処理の中心はスプレッドシートの二次元配列です。ここをforループではなく配列メソッドで書くと、処理の意図がそのまま単語で残ります。「変換するならmap」「絞るならfilter」「まとめるならreduce」です。

// 04. map / filter / reduce でシートのデータを処理する

function summarizeSales() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("売上");
  const [header, ...rows] = sheet.getDataRange().getValues();

  // ヘッダー行をキーにしたオブジェクト配列へ変換
  const records = rows
    .filter((row) => row[0] !== "")                       // 空行を除く
    .map((row) => Object.fromEntries(header.map((key, i) => [key, row[i]])));

  // 条件で絞る
  const completed = records.filter((r) => r["ステータス"] === "完了");

  // 合計する(reduce の第2引数 0 が初期値)
  const total = completed.reduce((sum, r) => sum + Number(r["金額"] || 0), 0);

  // 担当者ごとに集計する
  const byOwner = completed.reduce((acc, r) => {
    const owner = r["担当者"];
    acc[owner] = (acc[owner] || 0) + Number(r["金額"] || 0);
    return acc;
  }, {});

  // 1件だけ探す。見つからなければ undefined
  const top = completed.find((r) => Number(r["金額"]) > 1000000);

  console.log(`完了 ${completed.length}件 / 合計 ${total.toLocaleString()}円`);
  console.log(JSON.stringify(byOwner));
  console.log(top ? top["案件名"] : "100万円超の案件なし");
}

// ポイント: これらはすべてメモリ上の配列に対する処理。
// ループの中で getValue() を呼ぶ書き方より圧倒的に速い。
// シートへのアクセスは「最初に一括read、最後に一括write」が基本。

Object.fromEntries()はV8で使えるようになったメソッドで、キーと値のペアの配列をオブジェクトに変換します。ヘッダー行と組み合わせると、row[3]のような列番号指定をr["金額"]に置き換えられます。列が入れ替わっても壊れません。

ここで強調したいのは速度面の前提です。これらのメソッドはすべてメモリ上の配列に対する操作であり、シートへのアクセスは発生しません。逆に言えば、ループの中でgetValue()setValue()を呼ぶ書き方が遅いのは構文の問題ではありません。最初に一括で読み、最後に一括で書くという形に寄せるのが、GAS高速化の本筋です。

class で共通処理をまとめる

class(関連するデータと処理をひとまとめにする仕組み)もV8で使えます。GASで出番が多いのは、APIキーやエンドポイントなど同じ設定値を複数の関数で使い回す場面です。

// 05. class で共通処理をまとめる

class ClaudeClient {
  constructor(apiKey, model = "claude-opus-5") {
    this.apiKey = apiKey;
    this.model = model;
    this.endpoint = "https://api.anthropic.com/v1/messages";
  }

  // 静的メソッド: インスタンスを作らずに呼べる
  static fromProperties() {
    const key = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");
    if (!key) throw new Error("ANTHROPIC_API_KEY が未設定です");
    return new ClaudeClient(key);
  }

  ask(prompt, { system = "", maxTokens = 512 } = {}) {
    const res = UrlFetchApp.fetch(this.endpoint, {
      method: "post",
      contentType: "application/json",
      headers: { "x-api-key": this.apiKey, "anthropic-version": "2023-06-01" },
      payload: JSON.stringify({
        model: this.model,
        max_tokens: maxTokens,
        ...(system ? { system } : {}),   // system が空なら送らない
        messages: [{ role: "user", content: prompt }],
      }),
      muteHttpExceptions: true,
    });

    if (res.getResponseCode() !== 200) {
      throw new Error(`Claude APIエラー: ${res.getResponseCode()} ${res.getContentText()}`);
    }
    const json = JSON.parse(res.getContentText());
    return json.content?.[0]?.text?.trim() ?? "";
  }
}

// 使う側はこれだけ
function classifyRow() {
  const claude = ClaudeClient.fromProperties();
  const answer = claude.ask("この問い合わせを1語で分類してください。\n配送が遅れています", {
    system: "見積 / 不具合 / その他 のいずれか1語だけを返してください。",
    maxTokens: 32,
  });
  console.log(answer);
}

// 注意: class は「複数の関数が同じ設定値を共有する」ときに効く。
// 1つしか関数が無いなら、素直に function のままで十分。

引数に{ system = "", maxTokens = 512 } = {}と書いているのは、オブジェクト引数の分割代入とデフォルト値の組み合わせです。呼び出し側は必要な項目だけ渡せばよく、第2引数を丸ごと省略することもできます。引数が増えても呼び出し側が壊れないため、業務スクリプトの共通関数と相性がよい書き方です。

ただしclassにすれば良いというものではありません。呼び出す関数が1つしかないなら、ただの関数のほうが読みやすいです。判断の目安は「同じ初期化コードを3か所以上で書いているか」。当てはまるならclassか、ライブラリとしての切り出しを検討する段階です。

オプショナルチェーンとNull合体演算子

外部APIのレスポンスを扱うGASで、最も事故が減る構文がこの2つです。?.(オプショナルチェーン)は途中のプロパティが無ければそこで止まってundefinedを返し、??(Null合体演算子)は左側がnullundefinedのときだけ右側の値を使います。

// 06. オプショナルチェーン(?.)とNull合体演算子(??)

const json = JSON.parse(res.getContentText());

// 旧: 途中が無いと「Cannot read property of undefined」で落ちる
const oldText = json.content[0].text;

// 旧の対策: 毎回チェックが必要で読みづらい
const safeOld =
  json && json.content && json.content[0] && json.content[0].text
    ? json.content[0].text
    : "";

// 新: ?. は途中が null / undefined ならそこで undefined を返して止まる
// ?? は左が null / undefined のときだけ右の値を使う
const text = json.content?.[0]?.text ?? "";

// || との違いに注意。
const count1 = 0 || 10;   // 10(0 は偽とみなされる)
const count2 = 0 ?? 10;   // 0(null/undefined のときだけ右を使う)
// 「0 や空文字も正しい値」として扱いたい場面では ?? が正しい。

// シートの値の取り扱いでも役立つ
function readConfig() {
  const props = PropertiesService.getScriptProperties();
  const raw = props.getProperty("CONFIG");
  const config = raw ? JSON.parse(raw) : {};

  const retry = config.api?.retry ?? 3;          // 未設定なら 3
  const label = config.gmail?.label ?? "処理済み";
  console.log(`retry=${retry} / label=${label}`);
}

意外と知られていないのが||との違いです。||0や空文字も「無い」と判定するため、在庫数0や単価0が勝手にデフォルト値に置き換わるバグが起きます。0を正しい値として扱いたい場面では??を使ってください。

なお?.は「落ちない」だけで、値が取れなかった事実は残りません。AIの応答が空だったときに気づけるよう、空文字ならログを残すなどの分岐は別途書いておくほうが運用は安全です。

V8でも使えない・気をつける書き方

// 07. V8でも使えない・気をつける書き方

// × Rhino時代の for each。V8では構文エラーになる
// for each (var value in values) { ... }

// ○ for...of に書き換える
function loopValues(values) {
  for (const value of values) {
    console.log(value);
  }
  // index も欲しいときは entries()
  for (const [i, value] of values.entries()) {
    console.log(`${i}: ${value}`);
  }
}

// △ async / await は「書けるが意味がない」
async function notReallyAsync() {
  // UrlFetchApp は同期的に結果を返す。await しても速くならない
  const res = UrlFetchApp.fetch("https://example.com");
  return res.getContentText();
}

// ○ 複数リクエストを並列にしたいなら fetchAll を使う
function parallelFetch(urls) {
  const requests = urls.map((url) => ({ url, muteHttpExceptions: true }));
  const responses = UrlFetchApp.fetchAll(requests);
  return responses.map((r) => r.getResponseCode());
}

// × setTimeout / setInterval は存在しない(ブラウザやNode.jsの機能)
// ○ 待つなら Utilities.sleep(1000)、定期実行はトリガーで行う

1. Rhino独自の for each は構文エラーになる

古いスクリプトをV8に切り替えるとき、最初にぶつかるのがこれです。for each (var v in list)はRhino独自の拡張で、V8では動きません。for...offorEachに書き換えてください。同じくRhino時代のE4X(XMLをそのまま書く記法)も使えないため、XMLの解析はXmlServiceに置き換えます。

2. async / await は書けても速くならない

構文としては通りますが、GASのAPIはすべて同期的に結果を返します。awaitを付けても待ち時間は1件ずつ積み上がるだけです。複数のHTTPリクエストを並列化したいならUrlFetchApp.fetchAll()を使ってください。こちらはGASが用意した本物の並列実行です。

3. setTimeout は存在しない

setTimeoutsetIntervalはJavaScriptの言語仕様ではなく、ブラウザやNode.jsが提供している機能です。GASには存在しません。少し待ちたいときはUtilities.sleep(1000)、定期的に動かしたいときはトリガーを使います。

4. 切り替え後は日付とソートの挙動を確認する

RhinoとV8では、文字列から日付への変換や、比較関数を指定しないときのソート結果が一致しないことがあります。既存スクリプトをV8へ切り替えたら、日付を扱う処理と並べ替えの処理は本番データのコピーで一度通しで実行して確認してください。エラーにならず結果だけ変わるタイプの差分は、後から気づくと厄介です。

5. 構文を新しくしても実行時間の上限は変わらない

V8にしても1回の実行が6分(Workspaceアカウントは30分)で止まる制限はそのままです。処理が重いなら、構文ではなくシートアクセスの回数や、処理の分割・トリガーでの再開といった設計側の対処が必要になります。

まとめ

GASはChromeと同じV8エンジンで動きます。ネット上のサンプルがvarと文字列結合ばかりなのは、単に古い記事が残り続けているからです。今日から新しい書き方に切り替えられます。

優先順位をつけるなら3つです。const中心にするメール本文などの文字列をテンプレートリテラルにするAPIレスポンスの取り出しに?.??を使う。この3つだけで、バグの数と読みにくさは目に見えて減ります。

一方で、構文を新しくしても処理は速くなりません。速度に効くのはシートアクセスの回数を減らすことであり、そこは別の話です。モダン構文の価値は半年後の自分や引き継いだ人がコードを読めることにあります。業務で動かし続けるスクリプトほど、この差が効いてきます。

よくある質問

GASのコードを実行するエンジンのことです。以前はRhinoという古いエンジンが使われていて、書けるJavaScriptがES5相当に限られていました。現在の新規プロジェクトはChromeと同じV8エンジンで動くため、let/constやアロー関数、classといったモダンな構文がそのまま使えます。

エディタの「プロジェクトの設定」を開き、「Chrome V8 ランタイムを有効にする」にチェックが入っているかを見ます。appsscript.jsonを表示している場合は runtimeVersion が V8 になっていれば有効です。DEPRECATED_ES5 と書かれていればRhinoで動いています。

構文としては書けますが、実務ではほぼ意味がありません。GASのUrlFetchAppやSpreadsheetAppはすべて同期的に動き、setTimeoutも存在しないためです。複数のAPIを並列で呼びたい場合は、async/awaitではなくUrlFetchApp.fetchAllを使います。

Rhino独自の構文を使っている場合は壊れます。代表例は for each (var x in list) 形式のループとE4XのXMLリテラルで、V8では構文エラーになります。切り替え前に該当箇所を検索し、for...ofやforEachへ書き換えてください。日付の文字列解析やソートの挙動が変わるケースもあるため、切り替え後は一度通しで動作確認するのが安全です。

現在のV8ランタイムでは使えます。APIのレスポンスのように、途中のプロパティが無いかもしれない値を安全に取り出すときに便利です。もし構文エラーになる環境では、条件式でのnullチェックに書き換えれば同じ結果が得られます。

構文そのもので速くなることはほとんどありません。GASの実行時間の大半は、シートへのアクセスや外部APIの通信が占めるためです。速度を上げたいなら、getValuesとsetValuesでのまとめ読み書きなど、呼び出し回数を減らす工夫のほうが効きます。モダン構文の価値は、あくまで読みやすさと事故の減少にあります。

読みやすく、
壊れにくいGASを。

古いスクリプトの書き換えから、AI連携を含む新規開発まで。引き継げるコードで業務自動化をご支援します。