※スマホ対応はしてません。

タグ: Advent Calendar 2017

分割代入、画期的な機能。(現代的JavaScriptおれおれアドベントカレンダー2017 – 14日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 14日目

概要

オブジェクトとかでまとまってもらえる情報を、最初からバラバラにしちゃうやつです。

const data = {
  age: 99,
  id: id,
  name: 'Alice',
};

const { name, age } = data;
console.log(name);  // => "Alice"
console.log(age);  // => 99

配列もばらせます。別稿参照。

使い方

左辺ににょろ括弧 { … } で括って、欲しいプロパティの名前を書きます。

const data = { id: 123, name: 'Alice' };

const { id, age } = data;
console.log(id);  // => 123
console.log(age);  // => undefined

右辺のオブジェクトから合致する名前のプロパティの値を代入します。 (id)

右辺にないもの (age) を指定すると undefined になり、また右辺にあるもの (name) を必ずしも左辺に用意する必要はありません。

かんたん、かんたん。

初期値

= で、値が undefined だった場合の初期値を設定できます。

const data = { id: 123, name: undefined };

const { name = 'Anonymous', age = 0 } = data;
console.log(name, age);  // => "Anonymous", 0

null は普通に代入されます。

入れ子

入れ子になった情報を取り出すこともできます。その場合、途中のプロパティは値が取得されません。

const data = {
  detail: {
    phone: {
      number: '0120-000-000',
      type: 'free',
    },
  },
};

const { detail: { phone: { number, type } } } = data;
console.log(number);  // => "0120-000-000"
console.log(type);  // => "free"
console.log(detail);  // Exception: ReferenceError: detail is not defined

異なる名前で

APIで情報を取ってきたは良いけどなんかその命名違うんだよなーみたいな場合にも使えます。

{ original: renamed } のようにして original を renamed へ代入できます。

const data = { updated_at: '2017-12-14 00:00' };

const { updated_at: updatedAt } = data;
console.log(updatedAt);  // => "2017-12-14 00:00"

全部乗せ

初期値、入れ子、異名を全部組み合わせてもよろしい。

const data = { detail: { updated_at: '2017-12-14 00:00' } };

const {
  detail: {
    user_name: name = 'Anonymous',
    updated_at: updatedAt = 'NA',
  }
} = data;
console.log(name);  // => "Anonymous" (default value)
console.log(updatedAt);  // => "2017-12-14 00:00"

改行してもなお読みづらい感じはするね。うん、わかるよその気持ち。

関数の仮引数で

最初はちょっとぎょっとするかもしれないけど、仮引数の括弧の中でも使えます。

function sayHello({ message, delay = 1000 }) {
  setTimeout(() => console.log(message), delay);
}

sayHello({ message: 'Hello World!' });

必要なオプションが一目瞭然で良いかも。数が少ない間はね。

オプションオブジェクト option でざっくり受け取って、なんか中で処理を分けるとかすると良いかも?

function doGreatStuff(options) {
  doSomething(options);
  doAnything(options);
  doWhatever(options);
}

function doSomething({ foo }) {}
function doAnything({ bar }) {}
function doWhatever({ boo, hoge }) {}

その他

定義済み変数でSyntaxErrorになる

これはだめ。

const data = { foo: 'fufu' };

let foo;
{ foo } = data;  // Exception: SyntaxError: expected expression, got '='

行頭の { がブロックとみなされてしまうため。

括弧 ( … ) で括って式として判断させます。

const data = { foo: 'fufu' };

let foo;
({ foo } = data);
console.log(foo);  // => "fufu"

もちろん、普通にその場で変数宣言すれば問題ないです。

const data = { foo: 'fufu' };

const { foo } = data;
console.log(foo);  // => "fufu"

import

見た目は似てるけど違います。

import React, { Component } from 'react';
import { AppRegistry, Text, StyleSheet } from 'react-native';

雰囲気も似てるけど、動きも仕様も別物です。

destructuring assign

“destructure” は「破壊する」だそうです。あんまり一般的な語句ではないっぽい?

「分解代入」の方が意味的には適切なのかな。おれも分割代入って呼ぶけど。

デストラクタ destructor

じゃないです。コンストラクタの逆のやつ。

いや、まあこれはこれで欲しいことあるよね。いらない参照を明示的に切ったりとかイベント監視解除したりとかね。でも自動GCだからね。

参考

更新履歴

  • 2017-12-15 importは違うぞと言われてアアアーッッ!となって削除しました。たしかにいろいろ違います、すみません。

テンプレートを自作しよう。(現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目

概要

昨日のやつでJavaScriptのテンプレート記法をご紹介したわけなんだけども、そのテンプレート記法を使った処理を「タグ関数」を作って独自に拡張することができます。

// 連結するだけの(非現実的な)タグ関数
function tagger(template, ...substitutions) {
  console.log(template[0]);  // => "hoge "
  console.log(template[1]);  // => " fuga "
  console.log(template[2]);  // => " piyo"
  console.log(substitutions[0]);  // => 10
  console.log(substitutions[1]);  // => 25

  return template[0] + substitutions[0] + template[1] + substitutions[1] + template[2];
}

const n = 10;
console.log(      `hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"
console.log(tagger`hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"

...substitutions は可変長引数的なやつです。別稿参照。

タグ関数

テンプレート記法を分解した結果を受け取り、処理して返す関数です。

デフォルトで String.raw() というものがあります。

console.log(String.raw`foo\nbar\n${"boo"}`);  // => "foo\\nbar\\nboo"

この関数を自作することができます。

例えば普通に繋げるだけの「何もしない」タグ関数を作る場合、こんな感じになります。

function plain(strings, ...values) {
  // strings[0] === "hoge "
  // values[0] === 10
  // strings[1] === " fuga "
  // values[1] === 25
  // strings[2] === " piyo"

  // 文字列を連結してゆく変数
  // (初期値として先頭のもの "hoge " を与えておく。)
  let result = strings[0];

  // 先頭は既に追加済みなので飛ばし、
  // 残りを順次追加
  for (var i = 1; i < strings.length; i++) {
    result += values[i - 1] + strings[i];
  }

  return result;
}

const n = 10;
console.log(     `hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"
console.log(plain`hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"

values には ${ … } の部分が、 strings にはそうではない普通の部分がそれぞれ配列で格納されます。

strings.length は values.length + 1 に必ずなります。なるはず。

例えば `A${99}Z` の場合、 strings は ["A", "Z"] 、 values は [99] となります。

普通の文字列部分がない場合

文字がない場所は空文字列になります。

`${99}` の場合、 ${ … } の前後に空文字列があるとみなされます。つまり strings は ["", ""] 、 values は [99] となります。

同様に `${-1}${99}` の場合、 strings は ["", "", ""] 、 values は [-1, 99] となります。

一行で普通に繋げるやつ(おまけ)

こんな感じでどっすかね。

// 普通に結合
function plain(strings, ...values) {
  return strings.slice(1).reduce((result, s, i) => result + values[i] + s, strings[0]);
}

console.log(plain`hoge ${1} fuga ${2} piyo`)

Array#reduce()らくちんちょうべんり。(読みやすいかは別だけど。)

与えられた数値の小数点以下を揃える

${ … } には数値を与えられる前提で、各値の小数点以下二桁まで表示する、というやつをやってみたいと思います。

// decimal-regularized text
function drt(strings, ...values) {
  let result = '';

  for (let i = 0; i < strings.length; i++) {
    result += strings[i];

    const value = values[i];
    if (typeof value === 'number') {
      const sValue = Math.floor(value * 100).toString();  // 3.1415 -> 314.15 -> 314 -> "314"
      const srValue = `${sValue.slice(0, -2)}.${sValue.slice(-2)}`;  // "314" -> "3" + "." + "14"
      result += srValue;
    }
  }

  return result;
}

console.log(drt`πは${Math.PI}です。`);  // => "πは3.14です。"
console.log(drt`私の身長は${167.3}cmです。`);  // => "私の身長は167.30cmです。"
console.log(drt`私の体重は${66}kgです。`);  // => "私の体重は66.00kgです。"

タグ関数を返す関数

前項の「与えられた数値の小数点以下を揃える」やつ、何桁で揃えたいかってあるじゃないですか。そこを引数で受け取れるようにすると便利かもしれません。

というわけで、タグ関数を(クロージャを使って生成して)返す関数を用意してみました。

// decimal-regularized text
function drt(digits) {
  // この中身は、digitsを使ってる以外はさっきのと一緒
  return function(strings, ...values) {
    let result = '';

    for (let i = 0; i < strings.length; i++) {
      result += strings[i];

      const value = values[i];
      if (typeof value === 'number') {
        const sValue = Math.floor(value * 10 ** digits).toString();  // 3.1415 -> 314.15 -> 314 -> "314"
        const srValue = `${sValue.slice(0, -digits)}.${sValue.slice(-digits)}`;  // "314" -> "3" + "." + "14"
        result += srValue;
      }
    }

    return result;
  }
}

const drt1 = drt(1);
console.log(drt1`πは${Math.PI}です。`);  // => "πは3.1です。"

const drt5 = drt(5);
console.log(drt5`πは${Math.PI}です。`);  // => "πは3.14159です。"

文字列以外を返す

例えばHTML文字列を受け取ったら、それからDOMツリーを構築して返す、なんてのもありだと思います。

function html(strings, ...values) {
  // 普通に結合
  const html = strings[0] + strings.reduce((result, s, i) => values[i-1] + s, '');

  // DOMツリー生成
  // (サボってjQuery使います。)
  const tree = jQuery(html)[0];

  return tree;
}

const title = 'Hello World!';
document.body.appendChild(html`
  <div class="main">
    <h1>${title}</h1>
  </div>`)

テンプレート関数

関数を返すタグ関数なんてのもいけます。

さっきのを拡張して、任意のデータを受け取ってDOMツリーを形成するやつにしました。

// データ
const items = [
  { name: 'Sugoi pen', price: '3000' },
  { name: 'Sugokunai pen', price: '130' },
];

// 「DOMを生成する関数」を返すタグ関数
function html(strings, ...keys) {
  // DOMを生成する関数
  return function(data) {
    // 結合
    let html = strings[0];
    for (var i = 1; i < strings.length; i++) {
      const key = keys[i - 1];
      const value = data[key];
      html += value + strings[i];
    }

    // DOMツリー生成
    // (サボってjQuery使います。)
    const tree = jQuery(html)[0];

    return tree;
  };
}

// テンプレート
const itemTemplate = html`
  <li class="sugoi-item">
    ${'name'}
    <span class="price">(${'price'}円)</span>
  </li>
  `;

// 実行
const elList = document.querySelector('ul#yabai-list');
items.forEach(data => {
  elList.appendChild(itemTemplate(data));
});

Mustacheで <h1>{{title}}</h1> と書いていたところを <h1>${'title'}</h1> と書くようにした感じです。

デモ:

普通の文字列を解析してあれこれしてオブジェクト生成して返す

昔作ったのを思い出しました。

Vue.jsでコンポーネントが上位コンポーネントから値を受け取る (props) とき、その値の妥当性を検証する仕組みがあります。こんな感じで諸々指定していく感じです。

const MyComponent = {
  props: {
    simpleString: { type: String },
    requiredNumber: { type: Number, required: true },
    defaultBoolean: { type: Boolean, default: true },
  }
}

これをもうちょっと簡単に書けないかなーと思って作りました。こういうふうに書けます。

const pt = require('vue-props-template')
 
const MyComponent = {
  props: pt`
    string simpleString
    required number requiredNumber
    boolean defaultBoolean = ${true}
  `
}

文字列を元にオブジェクトを返しているわけです。

その後全然触ってないや。年末にまたやろうかな。

うっ頭が

その他

raw

String.raw() を普通に関数呼び出しすると、こんな感じだとエラーになってしまう。

String.raw(['foo', 'bar'], 1);  // Exception: TypeError: can't convert undefined to object

どうも実は第一引数の配列に raw というプロパティが追加されていて、そちらを見ているみたい。ので、それを与えてやれば大丈夫。

String.raw({ raw: ['foo', 'bar'] }, 1);  // => "foo1bar"

この raw はエスケープをものともしないやつらが格納されてます。下記別稿「生文字列」の項を参照。

自作のタグ関数から strings.raw を見るのも可能。使い道はぱっと思いつかないけど、 String.raw() 的なことをしたい場面で便利なはずです。

function rawSomething(strings, ...values) {
  const rawStrings = strings.raw;
  // ...
}

テンプレート記法の解析とオブジェクト生成

“12.2.9.3 Runtime Semantics: GetTemplateObject ( templateLiteral )” がその仕様だと思うんだけど、正直理解し切れてません。

Realmってなんじゃ……。

参考

テンプレート記法で簡単文字列組み立て。(現代的JavaScriptおれおれアドベントカレンダー2017 – 12日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 12日目

概要

back-tick (back-quote) ` … ` で括ると「テンプレートリテラル」となり、変数を埋め込んだりできます。

const user = { name: 'Alice', birthYear: 2000 };
const message = `${user.name} is ${new Date().getFullYear() - user.birthYear}-year-old.`;

他に改行したり、独自のテンプレート関数を適用させることも。

テンプレートリテラル

今まで動的な情報を使って文字列を組み立てる場合、 + で接続してあれこれしてきました。

console.log('timestamp: ' + Date.now() + ' at some place.');

テンプレートリテラルの場合、 ${ … } という書き方を使って文字列中に埋め込むことで、より読みやすく記述することができます。

console.log(`timestamp: ${Date.now()} at some place.`);

うは、Syntax highlightingが間に合ってないすね。

式

${ … } の中身には式を書くことができます。

console.log(`result: ${1 + 2}`);

+ 連結時と同様、 .toString() が呼び出され文字列になります。

式は書けるけど文は書けません。 for とかは駄目。

改行

テンプレートリテラル中では改行もそのまま使えます。

console.log(`Hello
             Beautiful
             World!!`);
Hello
             Beautiful
             World!!

インデントはアレなんだけど。

今まで通り \n での改行もできます。

生文字列

back tickの前に「タグ (tag) 」を置くことで、特別なテンプレート化を行うことができます。

String.raw() というのが用意されていて、これを使うと文字列を「そのまま」の状態で扱えます。

console.log("Ginpei \"Sushi-guy\" Takanashi");  // Ginpei "Sushi-guy" Takanashi
console.log(`Ginpei \"Sushi-guy\" Takanashi`);  // Ginpei "Sushi-guy" Takanashi
console.log(String.raw`Ginpei \"Sushi-guy\" Takanashi`);  // Ginpei \"Sushi-guy\" Takanashi

back slash \ でエスケープできるはずが、されなくなります。もちろん今まで通りの \n でも改行できなくなってしまいました。

改行や ${ … } による処理埋め込みは可能です。そして、当たり前だけど、そっちで普通の文字列にしたらエスケープされます。

console.log(String.raw`\'
${'\''}`);
\'
'

無駄にややこしくしちゃったかも。ごめんね。

普通の関数として使ってはいけない

これは駄目。

console.log(String.raw(`\n`));  // Exception: TypeError: can't convert undefined to object

この↑場合だと、先にテンプレートリテラルを普通に解釈して、その結果の文字列を与えることになります。

括弧なしで後ろに付けてください。(空白はあってもなくてもよろしい。)

console.log(String.raw `\n`);  // \n

自分でテンプレート関数を作るようになったらよくわかるかも。

タグ関数

String.raw() みたいなものをタグ関数 (tag function) 、タグ関数を伴うテンプレートリテラルの仕組みをタグ付きテンプレート (tagged template) と呼ぶようです。

加熱調理済み紐

ちなみにエスケープしない生の文字列のことを “raw strings” 、そうでないエスケープされるものを “cooked strings” と呼ぶみたいです。

独自テンプレート

前項 String.raw() は、使い方は特殊だけど結局は関数です。同じような関数を作成することで、独自のテンプレート処理を行うことができます。

別稿参照。

その他

この記号 ` の名前

MDN (en)だと “back-tick” と呼んでいる。

Wikipediaには “grave accent” の名前で項目があり、プログラミング界隈での別名として “backquote” 、 “backtick” が紹介されている。

Programmers use the grave accent symbol as a separate character (i.e., not combined with any letter) for a number of tasks. In this role, it is known as a backquote or backtick.

テンプレートリテラル vs テンプレート文字列

仕様的には “template literals” 。

MDNによると過去に「テンプレート文字列」と呼ばれていたみたいです。

ES2015 / ES6 仕様の以前のエディションでは、”template strings” と呼ばれていました。

参考

非同期やるならasync/awaitでもっとらくらく。(現代的JavaScriptおれおれアドベントカレンダー2017 – 11日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 11日目

概要

Promise を使った非同期の処理をさくさく書けるやつです。

簡単な例としては、まず Promise オブジェクトを返す関数↓があったとして、

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

このコード↓を

function sayAfterSleeping(message) {
    console.log('ぐー……');
    sleep(1000)
        .then(() => {
            console.log('むにゃ……');
            return sleep(1000);
        })
        .then(() => {
            console.log('あ、ごめん寝てた。えっと……');
            return sleep(1000);
        })
        .then(() => {
            console.log(message);
        })
}

sayAfterSleeping('Hello Async World!');

こう↓書けます。

async function sayAfterSleeping(message) {
    console.log('ぐー……');
    await sleep(1000);

    console.log('むにゃ……');
    await sleep(1000);

    console.log('あ、ごめん寝てた。えっと……');
    await sleep(1000);

    console.log(message);
}

sayAfterSleeping('Hello Async World!');

基本的な使い方

Promise を使ったコードを、前述のようにそれなりに簡単に置き換えることができます。

  1. Promise を返す関数の呼び出しに await を付ける
  2. await を使う関数に async を付ける

これだけです。

忘れないでね

async を付けるのは Promise オブジェクトを返す関数ではなく、 await を使う方の関数という点にご注意ください。逆に Promise オブジェクトを返すやつに async を付ける必要は(その中で await を使っていないなら)ないです。

あ、あと await を書けるのは async を付けた関数の中だけです。

ここら辺最初のころは忘れたり混同したりしがちでは。(個人の感想です。)

結果を受け取る

then() で登録するコールバック関数実行時に結果を受け取ることができましたが、 await の場合は単純な戻り値のような形で扱えます。

// fetch()とformat()はダミーです

function fetchDetail(id) {
    return fetch(`/api/foo/${id}`)  // Promiseを返す
        .then((res) => {
            return format(res);  // 結果を整形
        });
}

async function showDetail(id) {
    const detail = await fetchDetail(id);  // 待ってから結果を受け取る
    console.log(detail.name, detail.type);
}

showDetail(123);

Fetch APIの話じゃないです。

for 文

実践編。

間を置きながら n 回数えて終了する countdown() という処理を考えます。

真ん中が本体です。

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function countdown(n) {
    let p = Promise.resolve();
    for (let i = 0; i < n; i++) {
        p = p.then(() => {
            console.log(`${n - i}...`);
            return sleep(1000);
        });
    }
    return p;
}

countdown(3)
    .then(() => {
        console.log('done!')
    })

なんかややこしいすね。

単純な for 文だと駄目です。だって Promise 完了まで待たないとすぐ終わっちゃうから。なので、 Promise オブジェクトを変数 p で覚えておいて、さらにそこから then() の戻り値で上書きしながら連結します。(ちなみに let を var にするとだめです。)

これ↑を、 await を使うことで、こう↓めっちゃ簡単に書けるようになります。

// 指定ms待つ
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function countdown(n) {
  for (let i = 0; i < n; i++) {
    console.log(n - i, '...');
    await sleep(1000);
  }
}

countdown(3)
  .then(() => {
    console.log('done!')
  })

ほら、さっき駄目だった「ただの for 文」で済んでしまいました。読みやすーい。

宣言、式、メソッド

各種取り揃えております。

async function foo() {}

const bar = async function() {};

const obj = {
  async hoge() {},
};

class Klass {
  async fuga() {}
}

例外

Promise オブジェクトの catch() は、普通の try-catch で実現できます。

async function() {
  try {
    const result = await doAsync();
  }
  catch (err) {
    console.error(err);
  }
}

いろいろな await

途中に await

わりとどこにでも await を突っ込むことができるようです。

function returnLater(result) {
  return new Promise(resolve => setTimeout(() => resolve(result), 1000));
}

async function af () {
  console.log('...');
  console.log(await returnLater(1) + await returnLater(2));  // 3
}

af();

ただ await 出現の時点で待機を開始するので、複数書いても並列に実行はしません。上記の例では1000 ms待つのを二回呼んでるので、結果が出てくるまで2000 msかかります。

await new Promise()

というのもできます。要は右辺が Promise オブジェクトなら良いわけで、関数実行は結果としてそれを満たすだけです。

async function foo() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('おまたせ!');
}

await 123

というのもできます。右辺が Promise オブジェクトだったら待つんだけど、そうでなければそのまま流します。

await await await p

というようにたくさん並べることもできます。

できるけどしないね。(関数呼び出しの中身を追っていくと結果的にこうなる場面はいくらでもあるだろうけど。)

ES2017

ES2015 (ES6)じゃなくてES2017で追加されました。

ChromeとFirefoxではもう使えます。それ以外で使うならバベってください。

その他

async 後に改行は駄目

だめです。

async
function foo() {}
// Exception: ReferenceError: async is not defined

まあこんなのしないよね。

Function オブジェクトではない

async 付きで宣言したものは、コンストラクタが Function ではなく AsyncFunction になります。

console.log((async function(){}).constructor);  // => function AsyncFunction()

とはいえ AsyncFunction が Function を継承しているので、 instanceof は普通に動きます。ご安心ください。

console.log((async function(){}) instanceof Function);  // => true

AsyncFunction コンストラクタ

前項の通り、 AsyncFunction コンストラクタは存在するんだけど、グローバルオブジェクトにはなってません。

const af = new AsyncFunction();  // Exception: ReferenceError: AsyncFunction is not defined

どうしても使いたければ、インスタンスから探っていきます。

const AsyncFunction = (async function(){}).constructor;
const af = new AsyncFunction();

使い方は Function といっしょ。

“async” の読み方

「アシンク」より「エイシンク」くらいっぽいです。まあ日本語で会話してるところにエィスィ↑ンクとか言っても逆に通じないだろうけど。

英単語 “async”, “await” の意味

“async[hronous]” は「非同期」。皆様ご存知ですね。

“await” は「待ち受ける」だそうです。

参考

非同期やるならPromiseでらくらく。(現代的JavaScriptおれおれアドベントカレンダー2017 – 10日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 10日目

概要

メソッドチェインみたいにして、処理を順々に書いていけます。

// Promiseオブジェクトを返す関数
function doLater() {
  return new Promise((resolve, reject) => {
    const result = 123
    setTimeout(() => resolve(result), 1000);
  });
}

doLater()
  .then(result => {
    console.log('(1/2)', result);

    return doLater2();  // Promiseオブジェクトを返す関数
  })
  .then(result => {
    console.log('(2/2)', result);
  })
  .catch(err => {
    console.error(err);
  });

わかっちゃえば簡単だと思うんだけど、慣れないと戸惑うかも。

旧世代: コールバック

非同期処理やるとなると、コールバックを与えて終わったら呼び返してもらう、というのがかつては普通でした。

console.log('Ready.');

doLater(() => {
  console.log('Done.');
});

console.log('Working...');

doLater() が非同期に実行してコールバックするとして、出てくるのは “Ready.” → “Working…” → “Done.” の順になります。

Welcome to the Callback Hell!

まあちょっと使うくらいなら単純で何も問題ないんだけど、これが複数になると、やばい。

console.log('Start!');
doLater1(() => {
  doLater2(() => {
    doLater3(() => {
      doLater4(() => {
        doLater5(() => {
          console.log('Done!');
        });
      });
    });
  });
});

なかなかインデント・ハドウケンって感じで大変な趣き深さがあります。

こんなインデントが厳しくなるようなコールバックに次ぐコールバックを、コールバック地獄(rtヘル)と呼びます。

そしてプロミスへ

Promiseというのを使うと、インデントが深くならずに済みます。

console.log('Start!');
doLater1()
  .then(() => doLater2())
  .then(() => doLater3())
  .then(() => doLater4())
  .then(() => doLater5())
  .then(() => console.log('Done!'));

基本的な使い方

こんな感じ。実行順序にご注意ください。

// 1. `new Promise()` して新しい `Promise` オブジェクトを作成
const p = new Promise((resolve, reject) => {
  // 3. 非同期処理実行
  setTimeout(() => {
    // 4. 完了時に `resolve()` を実行
    const result = 123;
    resolve(result);
  }, 1000);
});

// 2. `Promise` オブジェクトに `then()` でコールバックを登録
p.then((result) => {
  // 5. `resolve()` に与えた情報を伴ってコールバック実行
  console.log(result)
});

前半、1と3、4のところが doLater() になる感じですね。

非同期処理といってもスレッドが分かれているわけではないので、 new Promise() の中で無限ループとかしたら、ブラウザは固まります。

戻り値を返す

まあ戻り値じゃないんだけど、処理をした「結果」として、 resolve() で何かひとつの情報を return することができます。その値は then() で実行されるコールバック関数へ引数として渡されます。

Promise オブジェクト

使い方はまあそこら中に教えてくれるひとがいると思うので、ちょっと仕様的な話の方を。

内部に持っている情報

new Promise() して戻ってくる値は、内部に以下の情報を持っています。

  • 状態。以下のいずれか:
    • pending … 待機
    • fulfilled … 満足(成功)
    • rejected … 拒否(失敗)
  • 結果
  • 満足(成功)時に実行するコールバックのリスト
  • 拒否(失敗)時に実行するコールバックのリスト

executer

new Promise() に与える関数を “executer” と呼ぶみたいです。

このexecuterには二つの引数、関数オブジェクトの resolve と reject が与えられます。

内部で持っている状態はもちろん “pending” から始まり、この resolve() ないし reject() を実行することで変化します。

ちなみに一度変化した後に resolve() 、 rejest() を呼んでも何も起こりません。

コールバック

then() や catch() を使って、コールバックリストへ関数を追加しておくと、状態が変化した際にいずれかのリストが実行されます。

then() で追加しようとした際に既に満足なり拒否なりの状態になっている場合は、その追加しようとしていた関数はすぐ実行されます。

then()

p.then().then()... と繋げることができますが、実は then() 実行のたびに新しい Promise オブジェクトが生成されています。

const p0 = new Promise(resolve => resolve())
const p1 = p0.then(() => undefined)
const p2 = p0.then(() => undefined)
const p3 = p1.then(() => undefined)

console.log(p1 === p0);  // false
console.log(p1 === p2);  // false
console.log(p3 === p1);  // false

複雑なコールバック連携

then() で何か値を return することで、結果として渡す情報を変更することができます。

さらにここで Promise オブジェクトを返すと、その完了(満足なり拒否なり)まで待ってから次へ進むことになります。

// 指定ms待ってから、指定の情報を伴ってコールバック
function sleep(ms, result) {
  console.log(`waiting for ${ms} ms`);
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(result);
    }, ms);
  });
}

// 1000 ms待って、100を返す
sleep(1000, 100)
  .then((result) => {
    // resolve()に与えられた情報を受け取る
    console.log(result);  // 100
  
    // 何もreturnしなければ、
  })
  .then((result) => {
    console.log(result);  // undefined

    return 123;  // 何か値を返すと、
  })
  .then((result) => {
    // 次の仮引数で新しい値の方を受け取れる
    console.log(result);  // 123

    // 新しいPromiseオブジェクトを返すと
    return sleep(1000, 234);
  })
  .then((result) => {
    // Promiseが解決されるまで待ってから、
    // 新しいPromiseの実行結果を得られる
    console.log(result);  // 234
  })

catch()

拒否(失敗)は catch() で拾います。 then() チェインのどこかで失敗した場合は catch() まで飛びます。

Promise.resolve()
  .then(() => {
    console.log(1);
    throw new Error('#1');
  })
  .then(() => {
    console.log(2);
  })
  .catch((error) => {
    console.error(error);
  })
  .then(() => {
    console.log(3);
  });

1 → Error → 3の順にコンソールに出力されます。 2 は、その前にエラーになったので実行されません。

try-catch みたいな感じ。

チェインさせなければ実行される

チェインという表現で良いのかわかんないですけど、前述の通り then() のたびに新しい Promise オブジェクトが生成されます。もし最初のオブジェクトに then() や catch() をぶら下げた場合は、その最初の Promise オブジェクトの結果だけが影響します。

const p0 = Promise.resolve()
p0.then(() => {
  console.log(1);
  throw new Error('#1');
});
p0.then(() => {
  console.log(2);
})
p0.catch((error) => {
  console.error(error);
})
p0.then(() => {
  console.log(3);
});

これなら 1 も 2 も 3 出力され、逆に catch() は 1 のところの例外を拾いません。

成功失敗に依らないコールバック

jQueryの deferred.always() 的なもの、 try-catch の finally 的なものは、ES2017までに存在しません。

代わりに catch() で枝分かれを収束させた後に then() すると、両方の場合に対応できます。

// くるくる表示
showLoading();

// サーバへ情報を送る
data.save()
  .catch(() => {
    // 失敗時の処理
  });
  .then(() => {
    // 成功しても失敗してもくるくる非表示
    hideLoading();
  })

一度catchするのがポイント。

finally()

まだないんだけど、proposalに出てるようです。ES2018で追加されるかも?

Chrome、Firefoxそれぞれ次のバージョンで入るっぽい。

// くるくる表示
showLoading();

// サーバへ情報を送る
data.save()
  .finally(() => {
    // 成功しても失敗しても、くるくる非表示
    hideLoading();
  });

繰り返しますけど、まだないです。

その他

then() の引数

実は第二引数に拒否(失敗)時のコールバックを指定できます。

p.catch(f) は p.then(undefined, f) と等価です。内部でそう実行してます。

resolvedな状態

仕様的には、 reject() すると”rejected” 状態になるのに、 resolve() すると “resolved” じゃなくて “fulfilled” 状態という呼び方になってます。なんでや。

ちなみに “full-” じゃなくて “ful-” 。

fulfilled/rejected 状態の Promise を一発で作る

Promise.resolve() 、 Promise.reject() というメソッドがあり、待機終了した状態のインスタンスを一発で作れます。試験時とかループしながら連結するときとか、ときどき便利。

複数の Promise オブジェクトを制御する

全て完了するまで待つ Promise.all() と、 ひとつでも完了したら進む Promise.race() というのもあります。

参考

更新履歴

  • 2017-12-10 満足(成功)、拒否(失敗)に依らない処理を、併記から catch().then() へ修正(ご指摘頂きました。ありがてえありがてえ)
  • 2017-12-10 「jQueryの “completed” 的なもの」という表現を「jQueryの deferred.alway() 」へ変更
  • 2017-12-10 「 catch() 」を追加
  • 2017-12-10 「チェインさせなければ実行される」を追加
  • 2017-12-10 Promise の静的メソッド各種に言及(ちょっとだけ)