【GTM – dataLayer深掘りシリーズ】第1回:dataLayer.push()の引数は4種類ある

catch-img

dataLayer深掘りシリーズ(全4回)

第1回 / 第2回 / 第3回 / 第4回
本稿は第1回です。

dataLayer ってよく使いますよね。

GTMやGA4を日常的に触る人なら、コードを自分で書かなくても、実装指示やデバッグで目にする機会は多いと思います。

ところが、普段何気なく書いている dataLayer.push() は、単に配列へ値を追加しているだけではありません。渡した値の種類によって、内部では別々の処理へ振り分けられます。

このシリーズでは、実物のコードを見ながら、その仕組みを引数の型から深掘りします。
実装で使う書き方は、オブジェクト、配列、関数、arguments の4種類に整理できます。

厳密には文字列や数値なども渡せますが、それらは専用の処理を持たず、関数・配列・arguments のどれでもない最後の枝へ落ちるだけです。

  1. 第1回:渡した引数の型が、どの処理へ振り分けられるか
  2. 第2回:4つそれぞれで何ができて、何ができないのか
  3. 第3回:arguments 分岐から先にある、gtag() コマンドと送信の分岐
  4. 第4回:内部にある2つの置き場と、書き方・読み方による違い(データレイヤー変数のバージョン1と2の違いも!)

まず第1回では、この4種類が内部でどのように見分けられ、別々の処理へ振り分けられるのかを追います。
見た目はどれも dataLayer.push() ですが、同じ push の書き方というだけで、同じ処理をしているわけではありません。

1. 実装で使う dataLayer.push() の書き方は4種類ある

1-1. 普段接しているのは、そのうちの1つだけ

実装として押さえておきたいのは、次の4つです。

// ① オブジェクトを渡す ← 実装でほぼ毎回書くのはこれ
dataLayer.push({ event: 'add_to_cart', value: 1000 });
 
// ② 配列を渡す
dataLayer.push(['myWidget.open', 123]);
 
// ③ 関数を渡す
dataLayer.push(function () { this.set('user_id', 123); });
 
// ④ arguments を渡す
function gtag() { dataLayer.push(arguments); }   // ← gtag() の中身はこの1行だけ
gtag('event', 'add_to_cart');

いかがでしょうか?

日常的に直接書くのは、だいたい①だけだと思います。
②と③は、使ったことがないどころか、このような書き方ができること自体を知らない人も多いでしょう。

一方、④の gtag() は広く使われていますが、その正体が arguments を dataLayer.push() へ渡す関数であることまで意識される機会は、ほとんどありません。

④については、Googleタグ(gtag.js)の公式のインストールスニペットの中で、gtag() の正体として毎回使われています。

<!-- Googleタグ(gtag.js)の公式インストールスニペット -->
<script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'TAG_ID');
</script>
実際にGoogleタグをインストールする手順の画面
実際にGoogleタグをインストールする手順の画面

なので、④は自分で直接書くことはあまりないかもしれませんが、実際にはよく使われている形です。
gtag() を呼ぶと dataLayer.push(arguments) が実行されるわけですからね。

この4つは、同じ dataLayer.push() を呼んでいても、型によって最初の入口が分かれます。途中で同じ書き込み処理へ合流する枝はありますが、最初から同じものとして扱われるわけではありません。

端的に言うと、オブジェクトはデータモデルへの書き込み、配列はモデル上のメソッド呼び出し、関数は処理順が来た時点での実行、arguments はコマンド名の解釈へ進みます。

第1回では、この4つがどの条件で振り分けられるのかを追います。分岐した先で実際に何が起きるかは、第2回で1つずつ詳しく見ていきます。

1-2. 4つは互いに入れ替えられない

4種類あるということは、形を取り違える余地があるということでもあります。
そして取り違えたときに何が起きるかというと、自分の意図しない結果になってしまいます。
ただし、基本的にエラーは出ないので、コンソールを見ても気づきづらいです。

例えば、以下の例を考えましょう。

gtag('event', 'add_to_cart');                        // ④ arguments
dataLayer.push(['event', 'add_to_cart']);            // ② 配列
dataLayer.push({ 0: 'event', 1: 'add_to_cart' });    // ① オブジェクト

結果は全く違います。

書き方

何が起きるか

gtag('event', 'add_to_cart')

event というコマンドとして解釈される

['event', 'add_to_cart']

通常は何も起きない。配列の枝に入り、そこで条件を満たせずに終わる

{ 0: 'event', 1: 'add_to_cart' }

"0" と "1" という名前の変数がデータモデルに書き込まれるだけ

3つとも、window.dataLayer の配列には積まれます。

CleanShot-2026-08-12-at-17.46.06-1024x643.png

GTMのプレビュー的には以下のような形になりますね。

CleanShot-2026-08-12-at-17.47.15.png

この違いを生む型判定は、このあとの「3. 型で4系統に振り分けられる」で確認します。分岐した先の詳しい挙動は、第2回で扱います。

ポイントは、arguments 専用の判定が入っていて、オブジェクトリテラルで添字と length を揃えただけでは通らないことです。

1-3. 見るべき場所を把握しよう

window.dataLayer はどのような要素が積まれているのか、時系列を見たい時などは役立ちますし、よくコンソールで確認する人も多いと思います。

しかし、実はGTM内部にはデータレイヤーの状態を保持するデータモデルがあり、実際の配列(window.dataLayer)とは別に管理されています(詳しくは第4回)。

ここで先に、今後使う呼び方だけ説明しておきます。
タグのスクリプト内部には、dataLayerの値を扱う2つの置き場 があります。本シリーズでは、キー名をそのまま扱う置き場を「辞書側」、ドットを階層として扱う置き場を「木側」と呼びます。

詳しい仕組みと違いは第4回で扱いますので、ここでは内部に2つの置き場があり、GTMプレビューと次の get() は「木側」を見るとだけ押さえてください。

分かりやすいのはGTMプレビュー画面の「このメッセージの後のデータレイヤー値」ですね。

CleanShot-2026-08-12-at-17.54.54-719x1024.png

ここに表示されるのは、window.dataLayer に蓄積されたメッセージを内部で処理したあとの、そのメッセージ時点のデータモデル(木側)です。

あるいは、コードから木側の現在値を読む場合は以下の通りです。GTMプレビューと同じ系統のモデル(木側)を見る経路ですが、プレビューは選択したメッセージ時点、get() は実行時点の現在値を読みます。

// キー指定して、内部のモデルが今どう見えているかを読む
window.google_tag_manager['G-XXXXXXX'].dataLayer.get('dataLayerのキーパス');
CleanShot-2026-08-12-at-17.58.46-1024x306.png

2. dataLayer.push は差し替えられている

ところで、 dataLayer を使うとき、最初の定義は次のように書くことが多いですよね。

window.dataLayer = window.dataLayer || [];

どーーう見てもただの配列ですね。
存在しなければ空配列を作り、あれば使いまわす。それだけのコードです。

そしてこの配列に push する。配列に要素が1つ増える。それだけのはずです。

では、なぜGTMがその値を認識し、処理を始められるのでしょうか?
配列に値を push しても、その瞬間にそれをGTMに知らせる仕組みなど、JavaScript には当然ありません。

答えは単純で、タグのスクリプトが、元の配列操作も残したラッパー関数へ push を差し替えているからです。

2-1. まずコンソールで確かめる

タグのスクリプトが読み込まれると、dataLayer の push メソッドはネイティブの Array.prototype.push ではなくなります。

// スクリプト読み込み前
String(window.dataLayer.push);
// → "function push() { [native code] }"
 
// 読み込み後
String(window.dataLayer.push);
// → "function () { var f; if (ps.SANDBOXED_JS_SEMAPHORE > 0) { ... } else f = [].slice.call(arguments, 0); ... }"
//    中の識別子はビルドごとに変わります。見るべきなのは [native code] でなくなっていることです

実際にはスクリプト読み込み前にコンソールで見るのは難しいと思いますので(方法はいくらでもありますが)、
分かりやすくするため、https://www.google.com/ にアクセスしてみてください。

執筆時点で筆者が確認したGoogleのトップページには、GTMが設置されていませんでした。

コンソールで dataLayer を定義して、上記のスクリプトを実行してみます。
(dataLayer が存在しないことは分かっているので、直接配列を代入しています)

CleanShot-2026-08-14-at-15.21.17.png

次に、どこでもよいので、GTMが設定されているサイトに行き、同様のことをします。
(基本的に dataLayer は定義済みだと思うので、上記のような window.dataLayer = [] は不要です)

CleanShot-2026-08-14-at-15.23.47.png

いかがでしょうか。

タグスクリプト読み込み後に、関数本体が読める形で返ってくるのは、それがGoogleのスクリプトが定義した関数だからです。

2-2. 差し替えの中身

差し替え方法については、よくある方法ではありますが、オリジナルを退避させておいて、自作のメソッドの中でオリジナルも呼ぶ、というようなシナリオになっています。

ざっくり類似コードを書くと次の形になります。

// ① 元の push を変数に退避する
var nativePush = dataLayer.push;
 
dataLayer.push = function () {
  // ② push に来た引数を本物の配列にする(push は複数引数を取れる)
  var pushed = [].slice.call(arguments, 0);
 
  // ③ 各要素を { message, messageContext } の封筒に包む
  var wrapped = pushed.map(function (item) { return wrapMessage(item); });
 
  // ④ 内部の処理キューへ積む
  processingQueue.push.apply(processingQueue, wrapped);
 
  // ⑤ 退避した本来の push も実行する(= window.dataLayer 配列にも要素が残る)
  var result = nativePush.apply(dataLayer, pushed);
 
  // ⑥ 処理エンジンを同期的に起動する
  var previousPushResultOk = (typeof result !== 'boolean') || result;   // 真偽値以外は真に均す
  return runQueue() && previousPushResultOk;
};

重要な点だけピックアップします。

⑤で本来の push も呼んでいる。
そのため、内部処理へ渡したあとも、window.dataLayer には元の要素が残ります。ここを省けば、配列を後から確認するコードや、同じ配列を参照する別の処理から見える結果も変わります。

⑤で apply を使って、第一引数に配列本体を渡している。
nativePush は「dataLayer の push メソッド」をただの変数に入れたものなので、この時点で「どの配列に push か」という結び付きが失われています。そのまま nativePush() と呼ぶと、内部の this が配列ではなくなり、成立しなくなります。だから、apply で対象配列を明示して束縛しています。
ここがピンとこない方は、JavaScriptの bind, call, apply などの束縛について調べてみてください。

⑥で処理エンジンが呼ばれている。
処理エンジンについては詳細は語りませんが、push されてきた要素を内部で処理し、いい感じに料理をしてくれます。

ちなみに、window.dataLayer 配列の長さには上限があります。⑤の直後に、配列が一定の長さを超えたら先頭から捨てる処理があります。上限値については、gtm.js のビルドによって変わりうるようですが、筆者の手元だと 1,000 が上限でした。

長寿命のSPAサイトなどで、window.dataLayer の先頭が消える経験をしたことがある人もいるかもしれませんが、これが理由の場合が多いです。

・・・というわけで、dataLayer.push(X) の1行は、(a)配列にXを積むと(b)処理エンジンを走らせるという二重構造になっています。DevToolsで眺める配列は(a)の結果で、いま動いているタグの発火やリクエスト送信を決めているのは(b)です。

2-3. この記事での用語

記事で現れる用語についてまとめておきます。
あくまでこの記事での呼び方で一般的に使われているものではありません。

この記事での呼び方

役割

処理エンジン

キューから1件取り出し、型で分岐する関数

コマンド表

config / event / set などのハンドラを名前で引くオブジェクト

データモデル

タグのスクリプトが内部に持つ変数保管庫。「完全一致の辞書」と「階層をたどれる木」の2つを持つ

完全一致の辞書 / 階層をたどれる木

データモデル内部の2つの置き場(第4回を参照)

外部から呼べるモデル操作用インターフェース

google_tag_manager[id].dataLayer として見えている get / set / reset。一般的なGTM実装で常用するための公開仕様という意味ではありません

コマンドエンジン

gtag() のコマンドを溜めて種別ごとに処理する層

3. 型で4系統に振り分けられる

さて、ここまで実用上の書き方を4種類に整理しました。
オブジェクト、配列、関数、arguments ですね。

ここからはそれらを渡したときにどう分岐するのかを見ていきましょう。

3-1. 判定に使われているのは3つの式だけ

処理エンジンがキューから取り出した値を、次の順で判定します。

typeof value === 'function'                                    // 関数か
Array.isArray(value)                                           // 本物の配列か
Object.prototype.toString.call(value) === '[object Arguments]'  // arguments か
  || Object.prototype.hasOwnProperty.call(value, 'callee')      // (旧環境向けの二重判定)
// ※実物には先頭に null/undefined を弾くガードが付いています。ここでは判定の本体だけを示しました

ここで効いてくるのは、Array.isArray(arguments) が false になるというJavaScriptの仕様です。
arguments は添字と length を持つ配列のようなオブジェクトですが、配列ではありません。この仕様があるおかげで、「配列を渡した場合」と「arguments を渡した場合」を、確実に切り分けられます。

gtag() が dataLayer.push(arguments) と書かれているのは明確に理由があるわけですね。
配列で渡してしまうと別の分岐に吸い込まれるから、arguments のまま渡す必要があります。

渡した値

どの判定が true か

行き先

function(){}

typeof === 'function'

処理キューで順番が来た時点で実行される

[...](本物の配列)

Array.isArray

モデル上のメソッド呼び出し窓口

arguments

[object Arguments] または callee を持つ

第1引数をコマンド名として表を引く

{...}、文字列・数値など
(null / undefined を除く)

どれも false

最後の枝へ進み、値が truthy ならデータモデルへの書き込み処理へ合流

最後の行には、もう1段だけ注意が必要です。null / undefined は手前のガードで除外され、それ以外でも false、0、空文字、NaN のような falsy なプリミティブ値は、合流点の truthy 判定を通らないため書き込みまで進みません。一方、42 や 'purchase' のような truthy な値は最後の分岐に着地します。

3-2. もう少し詳しく見てみる

引数は4種類ですが、コードの構造としては if / else if / else の3分岐です。
arguments と素オブジェクトは同じ else の中の2択になっています。

if (isFunction(pushed)) {                        // 関数枝
  try { pushed.call(modelApi); } catch (ignored) {}
}
else if (Array.isArray(pushed)) {                // 配列枝
  if (isString(pushed[0])) {
    var path = pushed[0].split('.');
    var methodName = path.pop();                 // 末尾 = 呼ぶメンバー名
    var args = pushed.slice(1);                  // 残り = 引数
    var target = readModel(path.join('.'), TREE_SIDE);   // 2つある置き場のうち木側から(→ 第4回を参照)
    if (target != null) {
      try { target[methodName].apply(target, args); } catch (ignored) {}
    }
  }
}
else {
  var toMerge = undefined;
  if (isArguments(pushed)) {                     // arguments 枝
    if (pushed.length && isString(pushed[0])) {
      var handler = commandTable[pushed[0]];      // 第1引数をコマンド名として引く
      if (handler && ...) { toMerge = handler(pushed, context); }
    }
  } else {
    toMerge = pushed;                            // 素オブジェクト枝
  }
 
  if (toMerge) {                                 // ★ arguments 枝と素オブジェクト枝がここで合流する
    Object.keys(toMerge).forEach(function (key) {
      var overwrite = toMerge._clear || context.overwriteModelFields;
      key !== '_clear' && (overwrite && model.set(key, undefined), model.set(key, toMerge[key]));
    });
    toMerge.event ? fireTriggers(toMerge) : /* 何もしない */ void 0;
  }
}

上記のとおり、arguments の枝と素オブジェクトの枝は、最後に同じ書き込みブロックへ合流します。arguments 枝のハンドラがオブジェクトを返した場合、その戻り値もここでキーごとにモデルへ書かれます。

ただし、ハンドラが上書き用のフラグも立てる場合があるため、合流点が同じでも、通常のオブジェクトの push と更新結果まで必ず同じとは限りません。

例えば、gtag('set', 'currency', 'JPY') など、gtag で set コマンドを使ったときに、GTMのプレビューに表示されるのを見たことはないでしょうか?これは、set ハンドラが返したオブジェクトも、通常のオブジェクトと同じ書き込みブロックへ届くためです。set が既存値をどう更新するかは、通常のオブジェクト push とは異なります。その違いは第4回で整理します。

補足:素オブジェクトと arguments が同じではない例

上記で説明した「まったく同じ扱い」ではない例の1つです。プレビュー画面は呼び出し種別を区別して表示します。

既存値との統合方法も異なりますが、詳しくは第4回で扱います。

実行したもの

プレビューのイベント名

「API 呼び出し」の表示

gtag('set', 'currency', 'JPY')

Set

gtag("set", {currency: "JPY"})

dataLayer.push({currency: 'JPY'})

メッセージ

dataLayer.push({currency: "JPY"})

CleanShot-2026-08-14-at-18.00.23.png
CleanShot-2026-08-15-at-00.04.58.png

この、「GTMプレビューの表示のされ方」についても色々分岐があって面白いところではあるのですが、ここでは割愛します。

3-3. 最後の枝は正確には「素オブジェクトの枝」ではない

もう一度状況を整理しましょう。

dataLayer.push(X) の実用上の書き方を4種類(オブジェクト・配列・関数・arguments)に整理するという話をしていました。

分岐としては、if / else if / else で、関数の場合→配列の場合→それ以外という条件分岐をしています。

さらに、else の中で、以下のように arguments とそれ以外の分岐があります。

// else ブロックを再掲
else {
  var toMerge = undefined;
  if (isArguments(pushed)) {                     // arguments 枝
    if (pushed.length && isString(pushed[0])) {
      var handler = commandTable[pushed[0]];      // 第1引数をコマンド名として引く
      if (handler && ...) { toMerge = handler(pushed, context); }
    }
  } else {
    toMerge = pushed;                            // 素オブジェクト枝
  }

上記のコメントでは分かりやすく「素オブジェクト」と書いていますが、実際には「オブジェクト判定」は入っていません。条件は、先頭の null / undefined ガードを通ったうえで、関数でも、配列でも、arguments でもないというだけです。

例えば、以下のようなプリミティブ値もこの分岐に入ります。(こんな書き方をするシーンはないと思いますが)

dataLayer.push('purchase');   // 文字列もこの枝に落ちる
dataLayer.push(42);           // 数値もこの枝に落ちる

実際、文字列を渡すと、Object.keys('purchase') が、["0", "1", "2", ...] を返すため、1文字ずつのキーがデータモデルに書き込まれます。

CleanShot-2026-08-15-at-00.20.38.png
CleanShot-2026-08-15-at-00.20.51.png

42 のような truthy な数値も最後の枝までは進みますが、Object.keys(42) は空の配列なので、モデルの実体としては何も変わりません。なお、0 は falsy なので、その手前の合流判定で止まります。

CleanShot-2026-08-15-at-00.26.25.png
CleanShot-2026-08-15-at-00.26.39.png

実害があるかどうかは別として、弾かれるのではなく通ってしまうことは知っておくと良いかもしれません。
「渡す型を間違えたらエラーになるだろう・・・」と思いきやここではその期待は裏切られます。

第2回以降で、この「それ以外」の枝を実用上のオブジェクト push として説明するときは、プリミティブ値などを除き、{ prop1: 'value1', prop2: 'value2', prop3: 'value3' } のような素オブジェクトを指すことにします。

4. 第1回のまとめ

第1回で確認したのは、dataLayer.push() が渡された値をひとまとめに処理するのではなく、内部の判定順に、関数、配列、arguments、それ以外の4系統へ振り分けているということです。

同じ dataLayer.push() に見えても、入る枝が違えば、値の書き込み、メソッド呼び出し、コマンド解釈、関数実行と、起きることは変わります。しかも、形を間違えても多くの場合はエラーになりません。

次の第2回では、オブジェクト、配列、関数、arguments の4種類を1つずつ取り上げ、それぞれで何ができ、何ができないのかを具体的に見ていきます。

第1回は以上です、Bye, bye.

Tomonaga
Tomonaga

人気記事ランキング

タグ一覧