読み込み中...

JavaScriptで遅延時間を指定して関数を待つ12の方法

JavaScriptのthisを使って遅延時間を指定し関数実行を待つモダンなテクニック JS
この記事は約19分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●JavaScriptでthisを使った遅延実行とは

JavaScriptでthisを使った遅延実行について、順を追って丁寧に解説していきます。

プログラミング初心者の方にもわかりやすいよう、例を交えながら説明しますので、ぜひ最後までお付き合いください。

JavaScriptの非同期処理では、しばしば遅延実行が必要になります。

遅延実行とは、ある処理を一定時間待ってから実行することです。

たとえば、ユーザーの入力を待つ場合や、サーバーからのレスポンスを待つ場合などに使われます。

しかし、JavaScriptの非同期処理では、関数内のthisの値が変わってしまうことがあり、初心者の方を悩ませるポイントの1つとなっています。

○遅延実行が必要になるケース

具体的に、どのような場面で遅延実行が必要になるのでしょうか。

・ボタンをクリックしてから、一定時間後にポップアップを表示させたい
・文字を入力し終わるまで待ってから、バリデーションチェックを行いたい
・APIリクエストを送信し、レスポンスが返ってくるまで待機したい

このように、ユーザーの操作や通信などを待つ必要がある場面では、遅延実行が欠かせません。

○thisを使うメリット

では、遅延実行の際になぜthisを使うのでしょうか。

JavaScriptではthisを使うことで、呼び出し元のオブジェクトを参照することができます。

遅延実行では、関数が非同期に呼び出されるため、関数内のthisが変わってしまう場合があります。

たとえば、setTimeoutを使って遅延実行する際、コールバック関数内のthisはwindowオブジェクトを参照してしまいます。

const obj = {
  value: 'hello',
  show: function() {
    setTimeout(function() {
      console.log(this.value); // undefinedになる
    }, 1000);
  }
};

obj.show();

このような問題を回避するために、thisを使って呼び出し元のオブジェクトを参照する必要があるのです。

●setTimeoutを使った遅延実行

さて、JavaScriptでthisを使った遅延実行を実現する方法の1つに、setTimeoutがあります。

setTimeoutは、指定した時間が経過した後に、関数を実行するメソッドです。

○サンプルコード1:基本的な使い方

まずは、setTimeoutの基本的な使い方から見ていきましょう。

こんな感じで使います。

setTimeout(function() {
  console.log("こんにちは");
}, 1000);

このコードを実行すると、1秒後に”こんにちは”とコンソールに表示されます。

setTimeoutの第一引数には、遅延実行したい関数を指定します。

第二引数には、遅延時間をミリ秒単位で指定します。

実行結果

// 1秒後に出力
こんにちは

ややこしいですが、setTimeoutに渡す関数は、通常の関数とは異なる特殊な環境で実行されます。

そのため、関数内でthisを使うと、思わぬ結果になることがあるのです。

○サンプルコード2:thisを使う

先ほどの例だと、thisを使っていないのでわかりにくいと思いますので、thisを使ったコードを見てみましょう。

const obj = {
  name: "Alice",
  greet: function() {
    setTimeout(function() {
      console.log(`こんにちは、${this.name}です`);
    }, 1000);
  }
};

obj.greet();

このコードでは、objオブジェクトのgreetメソッド内でsetTimeoutを使っています。

greetメソッドが呼び出されると、1秒後に”こんにちは、〇〇です”とコンソールに表示されることを期待しています。

しかし、実際に実行してみると…

実行結果

こんにちは、undefinedです

なんと、this.nameがundefinedになってしまいました!

これは、setTimeoutに渡した関数内のthisが、objオブジェクトではなくグローバルオブジェクト(ブラウザならwindow)を参照しているためです。

○サンプルコード3:アロー関数を使う

この問題を解決するには、アロー関数を使うのが簡単です。

アロー関数は、thisを囲むスコープの値を継承するという特徴があります。

先ほどのコードをアロー関数で書き換えてみましょう。

const obj = {
  name: "Alice",
  greet: function() {
    setTimeout(() => {
      console.log(`こんにちは、${this.name}です`);
    }, 1000);
  }
};

obj.greet();

アロー関数を使うことで、setTimeoutに渡した関数内のthisが、greetメソッド内のthisと同じ値を参照するようになります。

実行結果

こんにちは、Aliceです

無事、期待通りの結果が得られましたね!

thisの値が変わってしまう問題は、JavaScriptの非同期処理でよく遭遇します。

●setIntervalを使った定期的な遅延実行

setTimeoutが一度だけ遅延実行するのに対し、setIntervalは一定間隔で繰り返し遅延実行します。

定期的にサーバーにデータを送信したり、アニメーションを実現したりする際に使われることが多いでしょう。

○サンプルコード4:基本的な使い方

setIntervalの使い方は、setTimeoutとほとんど同じです。

こんな感じで使います。

setInterval(function() {
  console.log("こんにちは");
}, 1000);

このコードを実行すると、1秒ごとに”こんにちは”とコンソールに表示され続けます。

setIntervalの第一引数には、繰り返し実行したい関数を指定します。

第二引数には、実行間隔をミリ秒単位で指定します。

実行結果

こんにちは
こんにちは
こんにちは
...

ただ、このままではずっと”こんにちは”が表示され続けてしまいます。

実行を止めるには、clearIntervalを使います。

○サンプルコード5:実行のキャンセル

setIntervalで開始した繰り返し処理を止めるには、clearIntervalを使います。

clearIntervalに、setIntervalの戻り値を渡すことで、対応する繰り返し処理をキャンセルできます。

const intervalId = setInterval(function() {
  console.log("こんにちは");
}, 1000);

// 5秒後に実行をキャンセル
setTimeout(function() {
  clearInterval(intervalId);
  console.log("キャンセルしました");
}, 5000);

このコードでは、setIntervalを使って1秒ごとに”こんにちは”を表示しています。

そして、setTimeoutを使って5秒後にclearIntervalを呼び出し、繰り返し処理をキャンセルしています。

実行結果

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
キャンセルしました

このように、clearIntervalを使えば、setIntervalで開始した繰り返し処理をキャンセルできます。

長時間続く繰り返し処理を止められないと、メモリリークなどの問題につながる恐れがあります。

必要なくなったタイミングで、きちんとキャンセルするようにしましょう。

●Promiseを使った遅延実行

ここまで、setTimeoutやsetIntervalを使った遅延実行について見てきましたが、このメソッドにはいくつか問題点があります。

たとえば、複数の非同期処理を順番に実行したい場合、setTimeoutをネストさせる必要があり、コードが読みにくくなってしまいます。

こうした問題を解決するために、ES2015でPromiseが導入されました。

Promiseを使えば、複雑な非同期処理を簡潔に書けるようになります。

○サンプルコード6:Promiseの基本

では早速、Promiseを使った遅延実行の基本的なコードを見ていきましょう。

function delay(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

console.log("開始");

delay(1000).then(function() {
  console.log("1秒経過");
  return delay(2000);
}).then(function() {
  console.log("さらに2秒経過");
});

このコードでは、delayという関数を定義しています。

delayは、指定したミリ秒後にresolveを呼び出すPromiseを返します。

そして、delay(1000)で1秒のディレイを作り、そのあとにthenメソッドで続く処理を記述しています。

thenメソッドには、Promiseが解決された時に実行される関数を渡します。

ここでは、”1秒経過”とコンソールに表示し、さらにdelay(2000)で2秒のディレイを作っています。

delay(2000)で作られたPromiseにも、thenメソッドで続く処理を記述しています。

実行結果は次のようになります。

開始
1秒経過
さらに2秒経過

このように、Promiseとthenメソッドを使えば、複数の非同期処理を順番に実行できます。

コードも、setTimeoutをネストさせるよりも読みやすくなりました。

○サンプルコード7:thisを使う

Promiseを使う際も、thisの値には気をつける必要があります。

Promiseのコールバック関数内でthisを使うと、グローバルオブジェクトを参照してしまいます。

const obj = {
  value: "hello",
  show: function() {
    delay(1000).then(function() {
      console.log(this.value); // undefinedになる
    });
  }
};

obj.show();

この問題は、アロー関数を使えば解決できます。

先ほどのコードをアロー関数で書き換えてみましょう。

const obj = {
  value: "hello",
  show: function() {
    delay(1000).then(() => {
      console.log(this.value); // "hello"が表示される
    });
  }
};

obj.show();

アロー関数を使うことで、コールバック関数内のthisが、showメソッド内のthisと同じ値を参照するようになりました。

実行結果

hello

Promiseを使った遅延実行でも、thisの値に注意が必要ですが、アロー関数を使えば簡単に解決できます。

●async/awaitを使った遅延実行

ES2017で導入されたasync/awaitを使えば、Promiseをさらに簡潔に書くことができます。

async/awaitは、Promiseをベースに構築されたシンタックスシュガーで、非同期処理を同期処理のように書けるようにしてくれます。

コードの見通しが良くなり、処理の流れが追いやすくなるでしょう。

○サンプルコード8:async/awaitの基本

では早速、async/awaitを使った遅延実行の基本的なコードを見ていきましょう。

function delay(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

async function main() {
  console.log("開始");

  await delay(1000);
  console.log("1秒経過");

  await delay(2000);
  console.log("さらに2秒経過");
}

main();

このコードでは、Promiseを返すdelay関数は先ほどと同じです。

そして、main関数をasyncで宣言し、その中でawaitを使ってdelay関数を呼び出しています。

awaitを使うと、Promiseが解決されるまで処理を待ち、解決された値を返します。

ここでは、await delay(1000)で1秒待ち、await delay(2000)でさらに2秒待っています。

実行結果

開始
1秒経過
さらに2秒経過

このように、async/awaitを使えば、Promiseの解決を待つ処理を同期処理のように書くことができます。

コードの流れが追いやすくなり、可読性が向上するでしょう。

○サンプルコード9:Promiseと組み合わせる

async/awaitは、Promiseと組み合わせて使うこともできます。

たとえば、複数のPromiseを並行して実行し、すべての結果を待ちたい場合は、Promise.allとasync/awaitを組み合わせると簡潔に書けます。

async function main() {
  console.log("開始");

  const [result1, result2] = await Promise.all([
    fetch("https://api.example.com/data1"),
    fetch("https://api.example.com/data2")
  ]);

  const data1 = await result1.json();
  const data2 = await result2.json();

  console.log(data1, data2);
}

main();

このコードでは、2つのAPIエンドポイントからデータを取得しています。

fetch関数は、HTTPリクエストを送信してResponseオブジェクトを返すPromiseを返します。

ここでは、Promise.allを使って2つのfetchを並行して実行し、両方のResponseが返ってくるまで待っています。

そして、awaitを使ってResponseオブジェクトからJSONデータを取り出しています。

実行結果は次のようになります(APIレスポンスに依存)。

開始
{...} {...}

このように、async/awaitとPromiseを組み合わせることで、複雑な非同期処理をすっきりと書くことができます。

コードの見通しが良くなり、バグも入り込みにくくなるでしょう。

●よくあるエラーと対処法

JavaScriptでthisを使った遅延実行を実装する際、よくつまずくポイントがいくつかあります。

ここでは、そうしたエラーの原因と対処法を詳しく見ていきましょう。

遅延実行でエラーに遭遇しても、落ち着いて原因を突き止められるようになると安心ですよね。

○thisがundefinedになる

setTimeoutやsetIntervalの中でthisを使おうとすると、undefinedになってしまうことがあります。

こんな感じのコードです。

const obj = {
  name: "Alice",
  greet: function() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}!`);
    }, 1000);
  }
};

obj.greet();

実行結果

Hello, undefined!

これは、setTimeoutに渡したコールバック関数内のthisが、グローバルオブジェクトを参照しているためです。

この問題は、アロー関数を使えば簡単に解決できます。

アロー関数は、thisを囲むスコープの値を継承するからです。

const obj = {
  name: "Alice",
  greet: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}!`);
    }, 1000);
  }
};

obj.greet();

実行結果

Hello, Alice!

このように、setTimeoutやsetInterval内でthisを使う場合は、アロー関数を使うようにしましょう。

○遅延時間が思った通りにならない

遅延実行の第二引数に指定した時間通りに実行されないことがあります。

それでも、だいたいの目安にはなるので、ミリ秒単位の厳密なタイミングが必要な場合以外は問題ありません。

ただ、ブラウザのタブがバックグラウンドになっている場合は、時間の精度が落ちるので注意が必要です。

○実行がキャンセルできない

setTimeoutやsetIntervalで登録した関数の実行をキャンセルする方法を知らないと、無限ループに陥ってしまう危険性があります。

clearTimeoutやclearIntervalを使えば、遅延実行をキャンセルできます。

const timerId = setInterval(() => {
  console.log("Hello!");
}, 1000);

setTimeout(() => {
  clearInterval(timerId);
  console.log("Stopped.");
}, 5000);

実行結果

Hello!
Hello!
Hello!
Hello!
Hello!
Stopped.

このように、不要になった遅延実行はきちんとキャンセルするようにしましょう。

メモリリークの原因にもなりますからね。

●遅延実行の応用例

ここまで、JavaScriptでthisを使った遅延実行の基本的な方法を見てきました。

それでは実際に、遅延実行はどのようなシーンで役立つのでしょうか。

実務でも遭遇しそうな場面ばかりなので、ぜひイメージを膨らませてみてください。

○サンプルコード10:ボタンクリック時の遅延実行

ユーザーがボタンをクリックしてから、一定時間後に処理を実行したい場合があります。

たとえば、ダブルクリックを防止するために、2回目のクリックを無視したい場合などです。

こんな感じのコードになります。

const button = document.getElementById("myButton");
let isWaiting = false;

button.addEventListener("click", function() {
  if (isWaiting) return; // 処理中なら何もしない

  isWaiting = true; // 処理中フラグを立てる

  console.log("処理を開始します");

  setTimeout(() => {
    console.log("処理が完了しました");
    isWaiting = false; // 処理中フラグを下ろす
  }, 1000);
});

このコードでは、ボタンがクリックされたら、isWaitingフラグをチェックしています。

処理中であれば何もせず、そうでなければ処理を開始します。

そして、setTimeoutを使って1秒後に処理が完了したことを通知し、isWaitingフラグを下ろしています。

こうすることで、連続クリックによる意図しない重複処理を防ぐことができます。

○サンプルコード11:アニメーション効果

要素のフェードインやフェードアウト、スライド移動などのアニメーション効果を実装する際にも、遅延実行が活用できます。

こんな感じのコードになります。

const box = document.getElementById("box");

function fadeOut() {
  box.style.opacity = 1;

  const timer = setInterval(() => {
    if (box.style.opacity > 0) {
      box.style.opacity -= 0.1;
    } else {
      clearInterval(timer);
      box.style.display = "none";
    }
  }, 100);
}

fadeOut();

このコードでは、setIntervalを使って100ミリ秒ごとにボックスの不透明度を下げています。

不透明度が0以下になったら、setIntervalをクリアしてボックスを非表示にしています。

このように、一定間隔で少しずつ処理を実行することで、滑らかなアニメーション効果を実現できます。

○サンプルコード12:非同期処理の順次実行

複数の非同期処理を順番に実行したい場合にも、遅延実行が役立ちます。

Promiseやasync/awaitを使えば、より簡潔に書けますが、setTimeoutでも実現できます。

こんな感じのコードになります。

function asyncProcess1(callback) {
  setTimeout(() => {
    console.log("非同期処理1が完了しました");
    callback();
  }, 1000);
}

function asyncProcess2(callback) {
  setTimeout(() => {
    console.log("非同期処理2が完了しました");
    callback();
  }, 500);
}

function asyncProcess3(callback) {
  setTimeout(() => {
    console.log("非同期処理3が完了しました");
    callback();
  }, 1500);
}

function execSequentially() {
  asyncProcess1(() => {
    asyncProcess2(() => {
      asyncProcess3(() => {
        console.log("すべての非同期処理が完了しました");
      });
    });
  });
}

execSequentially();

このコードでは、asyncProcess1、asyncProcess2、asyncProcess3という3つの非同期処理を順番に実行しています。

各処理は、setTimeoutを使って一定時間後にコールバック関数を呼び出します。

execSequentially関数内で、各処理のコールバック関数をネストすることで、処理の順番を制御しています。

実行結果は次のようになります。

非同期処理1が完了しました
非同期処理2が完了しました
非同期処理3が完了しました
すべての非同期処理が完了しました

このように、遅延実行を使えば、複雑な非同期処理の流れを制御できます。

ただ、ネストが深くなるとコードが読みにくくなるので、Promiseやasync/awaitを使うのがおすすめです。

まとめ

JavaScriptでthisを使った遅延実行について、基本から応用まで詳しく解説してきました。

非同期処理は、JavaScriptの醍醐味であり、エンジニアとして身につけておくべき重要なスキルです。

今回学んだことを土台に、さらに深く学んでいきましょう。

遅延実行をマスターすれば、より洗練されたコードを書けるようになるはずです。

頑張って学んできたあなたなら、きっとすぐに遅延実行をものにできるでしょう。