Core JavaScript 4.コールバック関数(2)

この記事はweb開発に欠かせないJavaScriptのコアな概念をしっかり理解しようということでCore Javascript(韓国語)を要約した内容になります。

4. コールバック関数

目次

  1. コールバック関数とは
  2. 制御権
  3. コールバック関数は関数だ
  4. コールバック内部のthisにbindする方法
  5. コールバック地獄と非同期制御

4-3.コールバック関数は関数だ

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(this, v, i);
  }
};
obj.logValues(1, 2);
// { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues);
// window { ... } 4 0
// window { ... } 5 1
// window { ... } 6 2

thisが指すオブジェクトが呼び出し元であるobjからwindowに変わった。

コールバック関数として呼び出される場合には単純に関数として渡されるのでthisはグローバルオブジェクトを指すことになる。

4-4.コールバック内部のthisにbindする方法

コールバック関数でもthisを元のオブジェクトにしたい場合がある。

4-4-1.伝統的な方法

var obj1 = {
  name: 'obj1',
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  }
};
var callback = obj1.func();
setTimeout(callback, 1000);

self変数にthisを割り当てて、無名関数を宣言・返却することでobj1を出力することに成功した。 が、この方法は実際にthisを使っているわけでもないし、コードが冗長になる。

4-4-2.func関数の使い回し

var obj1 = {
  name: 'obj1',
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  }
};
var obj2 = {
  name: 'obj2',
  func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1000);

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

1秒後にobj2が、2秒後にobj3が出力される。 これで少しでも簡潔なコードにすることができるが、bindメソッドを使用するとさらに簡潔なコードにすることができる。 (コードの見通しとパフォーマンス両方改善される。)

var obj1 = {
  name: 'obj1',
  func: function () {
    console.log(this.name);
  }
};
setTimeout(obj1.func.bind(obj1), 1000);
var obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 2000);
var obj3 = { name: 'obj3' };
setTimeout(obj1.func.bind(obj3), 3000);

4-5.コールバック地獄と非同期制御

4-5-1.コールバック地獄(Callback hell)とは

callback-hell

関数の入れ子が積み重なっていき、コードがどんどん読みにくくなっていく様

JavaScriptではイベント処理や通信作業を行うコードがコールバック地獄になりがち。 コードが読みにくくなるし、それに伴って修正もしにくくなる。

4-5-2.非同期処理

4-5-3.JavaScriptの代表的な非同期処理

4-5-4.コールバック地獄の例

コーヒーの名前をリストに追加し出力する。

setTimeout(function (name) {
  var coffeeList = name;
  console.log(coffeeList);
  setTimeout(function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(function (name) {
      coffeeList += ', ' + name;
      console.log(coffeeList);
      setTimeout(function (name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);
      }, 500, 'ココア');
    }, 500, 'フラペチーノ');
  }, 500, 'ドリップコーヒー');
}, 500, 'コールドブリュー');

ネストが余計に深くなってしまったし値が渡される順番が逆順になっていて分かりにくい。

4-5-5. 解決方法1 - 名前付き関数に変換

var coffeeList = '';

var addColdBrew = function (name) {
  coffeeList = name;
  console.log(coffeeList);
  setTimeout(addDripCoffee, 500, 'ドリップコーヒー');
};

var addDripCoffee = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addFrappuccino, 500, 'フラペチーノ');
};

var addFrappuccino = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addCocoa, 500, 'ココア');
};

var addCocoa = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
};

setTimeout(addColdBrew, 500, 'コールドブリュー');

コードの見通しが少し改善されたが再利用できない関数を複数定義してしまい、 あまり望ましくないコードになってしまった。

ES6で導入されたPromiseと、ES2017で導入されたasync/awaitを活用してみよう。

4-5-6. 非同期作業の同期的な表現: Promise

new Promise(function (resolve) {
  setTimeout(function () {
    var name = 'コールドブリュー';
    console.log(name);
    resolve(name);
  }, 500);
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + ', ドリップコーヒー';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + ', フラペチーノ';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + ', ココア';
      console.log(name);
      resolve(name);
    }, 500);
  });
});

Promiseresolverejectが呼び出されるまでthencatchに進まない。 非同期作業を完了してからresolveを呼び出すことで同期的な表現をすることができる。

共通処理を関数化して短くすると以下のようになる。 (2行目と3行目で登場したクロージャー次の章で説明する予定)

var addCoffee = function (name) {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        var newName = prevName ? (prevName + ', ' + name) : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  };
};
addCoffee('コールドブリュー')()
  .then(addCoffee('ドリップコーヒー'))
  .then(addCoffee('フラペチーノ'))
  .then(addCoffee('ココア'));

4-5-6. 非同期作業の同期的な表現: Promise + async/await

Promiseのさらに進化した機能。 非同期処理を実行する関数にasyncを、必要な位置にawaitをつけことで Promiseを生成しthenで紐付けるような効果を得ることができる。

var addCoffee = function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
}

var coffeeMaker = async function () {
  var coffeeList = '';
  var _addCoffee = async function (name) {
    coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
  };
  await _addCoffee('コールドブリュー');
  console.log(coffeeList);
  await _addCoffee('ドリップコーヒー');
  console.log(coffeeList);
  await _addCoffee('フラペチーノ');
  console.log(coffeeList);
  await _addCoffee('ココア');
  console.log(coffeeList);
};
coffeeMaker();

4-6. まとめ

出典・参考文献