無駄と文化

実用的ブログ

【jQueryの基本の"き"】 パート1 - jQueryプラグインの使い方

f:id:todays_mitsui:20160826221908p:plain


jQueryプラグインの 作り方 ではなく、使い方 です。
Web上には多くのjQueryプラグインが公開されていますが、どのプラグインにも共通する基本の使い方を解説します。

普段、サンプルコードをなんとなくコピペするだけで済ませてしまっている人も、基本さえ押さえればjQueryプラグインをもっと使いこなせるようになるはずです。


使ってみる

具体的にjQueryプラグインを何か使ってみながらの方がイメージしやすいと思いますので、今回はカルーセルを作るための有名なjQueryプラグインである slick を使ってみます。

完成イメージはこんな感じです。


ベーシックな使い方

jQueryプラグインを使うための大まかな流れをおさらいしておきましょう。
どのようなプラグインであってもだいたい以下の流れになるはずです。

  1. jQuery本体を読み込む
  2. jQueryプラグインを読み込む
  3. (プラグインに付属のCSSがあれば、)CSSを読み込む
  4. プラグインを適用する HTML を書く
  5. プラグインの起動スクリプトを書く


1. jQuery本体を読み込む

jQueryプラグインは、その名の通りjQueryの機能を前提として作られています。
なのでプラグインだけを読み込んでも動きません。まずはjQueryの本体を読み込みましょう。

公式サイトのダウンロードのページからjQuery本体をダウンロードしてきます。
今回はバージョン3系列の最新版をダウンロードして使ってみます。

jquery.com

ダウンロードしてきたら最低限の記述と <script> タグでjQueryを読み込んだだけの HTML を書きましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8">
<title>Slick サンプルページ</title>

<link rel="stylesheet" href="./css/style.css">

<script src="./js/jquery-3.1.0.js"></script> <!-- JSファイルは <script> タグで読み込みます -->
</head>

<body>

<div class="container">
  <h1>Slick サンプルページ</h1>
</div><!-- /.container -->

</body>
</html>


さて、この HTML をブラウザで表示すると何が起こるでしょうか。
はい、何も起こりません。

jQueryを読み込むだけでは、表面上分かるような動作は何も行いません。
しかし、もしも貴方がブラウザのコンソールを開く方法を知っているならば、コンソールで以下の一文を実行してみて下さい。 (Google Chromeであれば、Ctrl+Shift+c または ⌘+option+j でコンソールが開きます)

jQuery().jquery

コンソールにjQueryのバージョン番号が表示されたはずです。
これでページの裏側で jQuery というオブジェクトが読み込まれたことが分かりました。

jQueryオブジェクトはページ上でHTMLを操作する便利な機能をたくさん詰め込んだオブジェクトです。
世の中にあるjQueryプラグインはjQueryオブジェクトの便利な機能を前提として、jQueryオブジェクトの機能をさらに拡張してくれます。


$ って何だろう

ところで、
ここでちょっと余談です。

jQuery関係でよく目にする $ という記号はいったい何者なんでしょうか?

$jQuery の別名です。 jQuery と6文字もタイピングするのが面倒な人のために、$ という記号を別名にしているんですね。

なので、

$('.foobar').fadeIn();

というように書いてある部分を、

jQuery('.foobar').fadeIn();

と書き換えても同じ意味になります。


2. jQueryプラグインを読み込む

次はjQueryプラグインの方を読み込んでみます。

今回は Slick を使うと言いましたね。こちらも公式サイトから最新版をダウンロードしてきましょう。 jQuery本体もプラグインも最新版を使う事で、バグが修正されたいちばんいい感じのものを使えるはずです。開発が活発なプラグインであれば毎月のようにバグ修正がされてバージョンアップされています。

kenwheeler.github.io

Slick は zip 形式で圧縮した状態で配布されています。
ダウンロードしたら zip を展開して、「slick.js」というファイルを見つけ出しましょう。

jQuery本体と同じフォルダに置き、先ほどと同じく <script> タグで読み込みます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8">
<title>Slick サンプルページ</title>

<link rel="stylesheet" href="./css/style.css">

<script src="./js/jquery-3.1.0.js"></script>
<script src="./js/slick.js"></script> <!-- 続けてプラグインも読み込みましょう -->
</script>

</head>

<body>

<div class="container">
  <h1>Slick サンプルページ</h1>
</div><!-- /.container -->

</body>
</html>


さて、先ほどと同じく HTML ファイルをブラウザで読み込ませて、何か変化がありますか?
またしても(表面上は)何も起こりませんね。

でもちゃんとプラグインが読み込まれているはずです。 コンソールに聞いてみましょう。

jQuery().slick

何か読み込まれている気配を感じ取れましたか?
実際に使ってみればもっと実感をもって感じられるはずです。続けましょう。


3. (プラグインに付属のCSSがあれば、)CSSを読み込む

プラグインによっては専用のCSSファイルが付属していることもあります。
モダンなWebサイトは HTML と CSS, JavaScript が3人4脚で働くので当然のことです。

Slick には「slick.css」という CSS が付属しているようです。お馴染みの <link> タグで読み込みましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8">
<title>Slick サンプルページ</title>

<link rel="stylesheet" href="./css/style.css">
<link rel="stylesheet" href="./css/slick.css"> <!-- Slick に付属の CSS です -->

<script src="./js/jquery-3.1.0.js"></script>
<script src="./js/slick.js"></script>
</script>

</head>

<body>

<div class="container">
  <h1>Slick サンプルページ</h1>
</div><!-- /.container -->

</body>
</html>

CSS と JavaScript のどちらを先に読み込んでも構わないのですが、一般的にCSS を先に記述することが多いですね。


4. プラグインを適用する HTML を書く

さて、ここまでは下準備でした。
jQueryプラグインを使うのはここからです。

プラグインを適用する HTML を書きましょう。
何のタグを使ってどんな風に組めばいいでしょうか?こういうときは公式サイトを見るに限ります。
最も情報が正確で充実しているのは公式サイトなので積極的に見に行きましょう。

Slick でいえば、Getting Started のあたりに基本的な組み方が書いてある見たいですね。

では組んでいきます。
今回は猫の画像でカルーセルを作るので、<img> タグを並べましょう。

画像は Flickr からクリエイティブコモンズのライセンスがついたものをダウンロードして「img」フォルダに入れておきました。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8">
<title>Slick サンプルページ</title>

<link rel="stylesheet" href="./css/style.css">
<link rel="stylesheet" href="./css/slick.css">

<script src="./js/jquery-3.1.0.js"></script>
<script src="./js/slick.js"></script>
</head>

<body>

<div class="container">
  <h1>Slick サンプルページ</h1>

  <!-- ほぼ公式のサンプル通り、class名は自由につけます -->
  <div class="slides">
    <div><img src="./img/01.jpg" alt=""></div>
    <div><img src="./img/02.jpg" alt=""></div>
    <div><img src="./img/03.jpg" alt=""></div>
    <div><img src="./img/04.jpg" alt=""></div>
    <div><img src="./img/05.jpg" alt=""></div>
    <div><img src="./img/06.jpg" alt=""></div>
  </div><!-- /.slides -->
</div><!-- /.container -->

</body>
</html>


はい、
<div> タグに class="slides" というクラス名を付けてみました。が、この時のクラス名は何でも好きなように命名することが出来ます
貴方のこだわりを詰め込んだいい感じのクラス名にしてあげましょう。


もう一度ブラウザで読み込んでみて確認しましょう。
画像が縦に並んで表示されていますね。これはプラグインが働く前のプレーンな表示です。


5. プラグインの起動スクリプトを書く

いよいよ大詰めです。起動スクリプト を書きます。

起動スクリプトとは、プラグインを実際に動かすためのスクリプトです。
これまでのjQuery本体とプラグインの読み込みのための <script> タグの記述ではあくまでも読み込むだけでした。
起動スクリプトは読み込んだプラグインを動かすための記述です。

これについても公式サイトを参考にしましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta charset="UTF-8">
<title>Slick サンプルページ</title>

<link rel="stylesheet" href="./css/style.css">
<link rel="stylesheet" href="./css/slick.css">

<script src="./js/jquery-3.1.0.js"></script>
<script src="./js/slick.js"></script>

<!-- これがいわゆる「起動スクリプト」です -->
<script>
jQuery(function($) {
  $('.slides').slick();
})
</script>

</head>

<body>

<div class="container">
  <h1>Slick サンプルページ</h1>

  <div class="slides">
    <div><img src="./img/01.jpg" alt=""></div>
    <div><img src="./img/02.jpg" alt=""></div>
    <div><img src="./img/03.jpg" alt=""></div>
    <div><img src="./img/04.jpg" alt=""></div>
    <div><img src="./img/05.jpg" alt=""></div>
    <div><img src="./img/06.jpg" alt=""></div>
  </div><!-- /.slides -->
</div><!-- /.container -->

</body>
</html>


起動スクリプトは必ずjQuery本体とプラグインとを読み込んだ後に記述しましょう。
「読み込んだ後に起動」これは鉄の掟です。


では、今度こそブラウザで読み込んで…、何か起きましたか?
先ほどまでただの縦並びになっている画像がちゃんとカルーセルの形にレイアウトされているはずです。
写真をめくるためのボタンも表示されるようになりましたね。

もっともベーシックな使い方ですが、以上がjQueryプラグインを動かすための一部始終です。


オプションを指定してカスタマイズする

設置したプラグインが微妙に思い通りの動作じゃないとき、イメージ通りにカスタマイズしたいですよね。

プラグインによって様々なオプションが用意されています。
このオプションを指定することで、jQueryプラグインを要望に合わせて細かくカスタマイズすることができます。

とりあえず Slick を読み込んで動かしてみましたが、ちょっと不満なことがあります。

  • ボタンを押さなければ次の画像が見れないので、一定時間で次の画像に切り替わるようにしたい
  • ボタンの表記が「Previous」「Next」となっているのを「前へ」「次へ」としたい

このような要望をオプションで解決してみましょう。


では指定することができるオプションを調べるにはどうすればいいでしょう?
はい、公式サイトですね。Settingsというセクションに指定可能なオプションが解説されているみたいです。
実にありがたい。


f:id:todays_mitsui:20160826230324p:plain

オプションの指定は起動スクリプトの中で行います。

先ほどまでの起動スクリプトをもとに、

<script>
jQuery(function($) {
  $('.slides').slick();
})
</script>

書き替えます。
.slick の後の括弧() がオプションのためのスペースです。

<script>
jQuery(function($) {
  $('.slides').slick(
    { autoplay: true }
  );
})
</script>

{ autoplay: true } という記述を足しました。
これでボタンを押さなくても画像がめくられるようになったはずです。

続いて行きましょう。
ボタンの表記を変えるには prevArrownextArrow というオプション項目があるみたいですよ。
デフォルト値が <button type="button" class="slick-prev">Previous</button> だと書いてるので、これをコピって部分的に書き替える感じでやりましょう。

<script>
jQuery(function($) {
  $('.slides').slick(
    { autoplay: true, prevArrow: '<button type="button" class="slick-prev">前へ</button>', nextArrow: '<button type="button" class="slick-next">次へ</button>' }
  );
})
</script>

はい。

できました…。


と言いつつ、これではあまりにも見づらいのでスペースや改行を入れ込んで整えます。

<script>
jQuery(function($) {
  $('.slides').slick({
    autoplay: true,
    prevArrow: '<button type="button" class="slick-prev">前へ</button>',
    nextArrow: '<button type="button" class="slick-next">次へ</button>',
  });
})
</script>

JavaScriptのルールでは見やすさを整えるためのスペースや改行は(一部例外はあるものの)自由に入れていいことになっています。
オプションが増えてくると適当に改行を入れておいかないと見通しが悪くなります。

あとで読んだとき訳が分からなくなって困るのは自分です。
起動スクリプトを美しく書くことにもこだわってみましょう。


少々話がそれました。
では、オプションを指定したバージョンのカルーセルをご覧ください。

いい感じですね。


jQueryプラグインを使うときの注意点

さて、基本的な使い方は以上。次は基本的な注意点です。

  1. jQuery本体は一番最初に一度だけ読み込む
  2. jQuery本体とプラグインはなるべく最新バージョンを使う
  3. jQuery本体やプラグインを書き換えようとしない


1. jQuery本体は一番最初に一度だけ読み込む

jQueryの本体は必ず一番最初に読み込みましょう。 そして、ページ内で複数のプラグインを使う場合でも1度だけ読み込めば大丈夫だと覚えましょう。


ちなみに、jQuery本体を2個3個読み込んだらどうなるか知りたいですか?
私の経験では2個目のjQuery本体を読み込んだ瞬間にそれまでに読み込んだプラグインがすべてリセットされて、まぁ厄介なことになります。

jQuery本体を複数読み込むメリットはありません。


2. jQuery本体とプラグインはなるべく最新バージョンを使う

悪いことは言わないので最新を使いましょう。
発見されたバグが修正されている、便利な機能が追加されているなど、新しいバージョンを使うメリットがきっとあります。

プラグインによってはjQuery本体の最低バージョンを明記しているものもあります。
参考までに Slick の公式サイトには、

requires jQuery 1.7 +

と書いてあります。
これは「jQuery バージョン1.7以上じゃないと動作を保証しないよ」という意味ですね。


逆にjQuery本体を最新にしたらプラグインが動かなくなった場合はどうしましょうか?

はっきり言います。
jQuery本体のバージョンアップに追いついていないようなメンテナンスの行き届いていないプラグインを使い続けるのは危険です。
ほかのプラグインへの乗り換えを検討する絶好のチャンスですよ。


3. jQuery本体やプラグインを書き換えようとしない

jQuery本体もプラグインも所詮はただの JS ファイルなのでエディタで開いて書き換えることができます。
ですが、プラグインの動作が微妙に望み通りでないときでもjQuery本体やプラグインを書き換えようとしないでください。壊れます。


jQueryプラグインの中身を書き換えなくても動作をカスタマイズできるように、プラグインにオプションを指定できる仕組みが備わっています。
まずは希望する動作を実現するようなオプションが用意されているかどうか調べましょう。

どうやって調べるか。もちろんプラグインの公式サイトを見るわけです。
プラグインの作者が想定している範囲で想定している使い方をするのが、トラブルを起こさないために重要です。


まとめ

jQueryプラグインの大まかな使い方と、基本的な注意点を解説してきました。
もう一度書き並べてみましょう。

大まかな流れ

  1. jQuery本体を読み込む
  2. jQueryプラグインを読み込む
  3. (プラグインに付属のCSSがあれば、)CSSを読み込む
  4. プラグインを適用する HTML を書く
  5. プラグインの起動スクリプトを書く

基本的な注意点

  1. jQuery本体は一番最初に一度だけ読み込む
  2. jQuery本体とプラグインはなるべく最新バージョンを使う
  3. jQuery本体やプラグインを書き換えようとしない


これらはjQueryプラグインを使う上で毎回くりかえす工程です。
基本の部分だからこそ、コピペでなんとなくではなく基礎を押さえてコーディングしていきましょう。


もっと使いこなすために

jQueryは JavaScript を理解していなくても使えてしまうのが良いところでもあり、悪いところでもありますね。
jQueryとjQueryプラグインとをもっと使いこなすために、合わせて学ぶといいことを紹介して終わりたいと思います。

  • JavaScript の基本文法を覚える
    jQuery以前の部分で躓かないために。MDNのチュートリアルがおすすめです

  • プラグインのデフォルトを知ってみる
    オプションが指定できるということは、オプションを指定しないときのデフォルトの値があるはずです
    調べてみましょう

  • 英語を読もう
    jQueryに限らず、情報源は英語で書かれたものが大半です。英語は苦手?大丈夫、コードだけ拾い読みすればいいんですよ


そんなわけでjQueryプラグインそのものの使い方を解説してみました。
こういうのってありそうで無かったですよね。

この調子でパート4まで続きます。


私からは以上です(続く)


【jQueryの基本の"き"】シリーズ

blog.mudatobunka.org

blog.mudatobunka.org

blog.mudatobunka.org

React で this.props.children に新しい Props を渡す

f:id:todays_mitsui:20160814183151p:plain

React でカスタムコンポーネントを作るとき、コンポーネントの子要素には this.props.children でアクセスできます。
この this.props.children はそのままレンダリングすることもできるのですが、何かしらの Props を渡したくなったらどうするのでしょうか。

ざっくり調べた感じ Stack Overflow とか海外のブログにしか情報が無いようだったのでまとめてみます。


TL;DR

いきなり結論から、
this.props.children に直接 Props を渡すことはできません。
代わりの方法として、React.cloneElement() で React要素をクローンする時に Props を渡すことができるので、this.props.children をクローンしつつ Props を渡せばいいようです。

デモを用意しました。


以下のようにすればローカルでデモをいじりつつ試せます。

$ git clone https://github.com/todays-mitsui/passing-props-to-children-sample.git
$ cd passing-props-to-children-sample
$ npm install
$ npm start


要素に Props を渡す

this.props.children に限らず React要素は React.cloneElement() という API でクローンできます。
そのとき第2引数にオブジェクトを渡すと、React要素が持っている既存の Props とマージされた後、新しい Props として設定されるとのことです。

let elementWithProps = React.cloneElement(element, { foo: 'bar' })

公式ドキュメントに解説があるので、詳しくはそちらをどうぞ。


this.props.children に応じた処理をする

ところで this.props.children は状況によって様々な型の値になります。
複数の子要素を持っている場合には this.props.childrenReact要素の配列 になります。
その他、子要素がただのテキストノードだった場合には string に、子要素を持たない場合は undefined と、まぁ場合によっていろいろみたいです。

そんな this.props.children を上手く扱うために React.Children というユーティリティクラスが用意されています。
使うときには React.Children で参照するほかに、ES2015 で記述しているなら、

import { Children } from 'react'

というように個別にインポートしてもいいでしょう。


今回は React.Children.map() を使って子要素一つひとつに Props を渡します。

const newProps = { foo: 'bar' }

const childrenWithProps = React.Children.map(
  this.props.children,
  (child) => {
    // 各子要素をクローンしつつ newProps を渡す
    return React.cloneElement(child, newProps)
  }
)

さて、実はこれだけではいけません。
子要素がテキストノードを含んでいる場合には、上記の child に string が渡されます。そして React.cloneElement() は string を受け取ってくれないので、そこでエラーが発生します。

なので child の type を判別しつ上手いこと処理を分岐しましょう。
string が渡ってきた場合には何もせず、そのまま返すようにします。

const newProps = { foo: 'bar' }

const childrenWithProps = React.Children.map(
  this.props.children,
  (child) => {
    console.info(typeof child, child)

    switch (typeof child) {
      case 'string':
        // 子要素がテキストノードだった場合はそのまま return
        return child

      case 'object':
        // React要素だった場合は newProps を渡す
        return React.cloneElement(child, newProps)

      default:
        // それ以外の場合はとりあえず null 返しとく
        return null
    }
  }
)

これで this.props.children を複製しつつ Props を渡した childrenWithProps を作ることができました。


React.Children.map() についても公式ドキュメントの解説が親切でした。


簡単なデモ

やり方の説明としては以上なんですが、上記のコードは単体では動かないので実際に動作する簡単なデモを書きました。

ロジック部分は1ファイルでこのようになっています。

/* main.js */

import React, { Children } from 'react'
import ReactDom from 'react-dom'


class Child extends React.Component {
  render() {
    const parentName = this.props.parentName || 'UnknownParent'
    const name = this.props.name || 'UnknownChild'

    return (
      <p>
        ParentName: <em>{parentName}</em> > ChildName: <em>{name}</em>
      </p>
    )
  }
}

class Parent extends React.Component {
  render() {
    const newProps = { parentName: 'foo' }

    const childrenWithProps = Children.map(
      this.props.children,
      (child) => {
        console.info(typeof child, child)

        switch (typeof child) {
          case 'string':
            return child

          case 'object':
            return React.cloneElement(child, newProps)

          default:
            return null
        }
      }
    )

    return (
      <div>
        {childrenWithProps}
      </div>
    )
  }
}


ReactDom.render(
  (
    <Parent>
      ### Text Node ###
      <Child name='hoge' />
      <Child name='fuga' />
      <Child name='piyo' />
    </Parent>
  ),
  document.querySelector('.container')
)

<Child> コンポーネントは name, parentName という2つの Props を <p> タグの中で表示するだけの簡単なものです。
今回は parentName の方を親要素の <Parent> コンポーネントから渡してあげています。


今回のデモのソースは全て GitHub に置いてあります。


まとめ

React はもともと API が少なくて学習コストが少ないと思っているんですが、少ないなりに覚えておくと便利な API もありますね。
React.cloneElement()this.props.children をカスタマイズする以外にもいろいろな用途で使えるはずです。


私からは以上です。

【今日のバグ取り】 JavaScript でコールスタックが溢れていたのをどうにかした話

lazex さまのはてブコメントを受けて、animation プロパティを使った改良版を書きました。完全にこっちの方が良いので、参考にするならばどちらかというと新実装の方で。
lazex さま、ご指摘ありがとうございます。
JavaScript のコールスタックが溢れていたのをどうにかしたら JS 要らなくなった話


f:id:todays_mitsui:20160626123718p:plain


先日、とあるサイトを見ていたら JavaScript でエラーが出ているっぽいのを見つけました。

Chromeで見たときのエラーの内容はこんな感じ、

jquery.min.js:2 Uncaught RangeError: Maximum call stack size exceeded

どうやらコールスタックがいっぱいになって溢れているようですね。


スタックトレースを辿っていくと、以下の箇所がエラーの原因のようでした。

$(document).ready(function() {
  setAutoColorChange();
});
 
function setAutoColorChange(index) {
  var colorlist = ["#346caa", "#3386c8", "#4aa45d", "#8fb84f", "#debc1c", "#e09532", "#eb7889"];
  if (!index || index > (colorlist.length - 1)) {
    index = 0;
  }
  var color = colorlist[index];
  $('.top').animate({
    backgroundColor: color
  }, 5000);
  setAutoColorChange(++index);
}

この処理では「とある要素の背景色をゆっくりと変化させ続ける」というようなことをやっています。
5秒に一度、背景色を変化させるアニメーションを呼び出し続けることで、背景色を変化させ続ける演出をしているようです。


このコード、やりたいことはすごくわかるんです。
ロジックとしてもそこまで破綻していないと思います。

一方で、このコードがエラーを引き起こす理由もパッと見えてきます。
おそらく、JavaScript 以外の言語での経験が長い人のコードなのではないかと思います。

というわけで今回はこのコードの解説をしつつ、JavaScript らしく書き直していきたいと思います。


エラーの解説 - コールスタックとは何か

JavaScript のプログラム中で関数が実行されると、関数が実行された場所やそのときの状況などとの情報が コールスタック と呼ばれるメモリ領域に積まれます。

エラーが出ているコード中では setAutoColorChange() 関数の最後の部分で、再び自分自身である setAutoColorChange() を呼び出しています。

function setAutoColorChange(index) {
  /* ... 中略 ... */

  setAutoColorChange(++index); // <- 再び自分自身を呼び出す
}

これによってループを実現しているのですが、そのせいで setAutoColorChange() の処理が1周するたびにコールスタックが1段ずつ積まれてしまいます。

setAutoColorChange() が自分自身を呼ばずに関数内での計算が終わることは無いためコールスタックが無限に積まれ続けて、そのうちに「これ以上スタックを積むことが出来ない!」という点に達してエラーを引き起こしている訳です。


関数の中で自分自身を呼び出すことを再帰呼び出しといいます。

再帰呼び出しそのものがエラーの原因という訳ではありません。
無限に再帰呼び出しし続けてしまい、その結果コールスタックが積まれ続けてしまうことがエラーの原因なのです。


しかし、矛盾するようですが、このコードが正常に動く場合もあるだろうと思ったりします。
JavaScript以外の言語で同様の処理を実装すれば正常に動いてくれる場合もあります。

なぜ、JavaScriptではうまくいかないのか。それを理解するためには JavaScript のいくつかの特徴について知らなければいけません。


JavaScriptの特徴 1 - JavaScriptに「末尾再帰最適化」は無い

今回のコードのように関数の最後の行で自分自身を呼び出すようなパターンを 末尾再帰 といいます。
リスト構造や木構造、数列などのいくらでも長くなっていく可能性のあるデータを順々に辿って処理していくときによく使われる結構ポピュラーなパターンです。

ゆえに末尾再帰に対して 末尾再帰最適化 と呼ばれる最適化が施される言語もあります。
ざっくり言うと「再帰呼び出しが関数の最終行だったときには、コールスタックに積まずに済ませる」という処理です。

この末尾再帰最適化が効いていれば、最終行での再帰呼び出しを無限に繰り返してもコールスタックが溢れることはありません。
なんせ末尾再帰であればスタックに積まれない訳ですからね。


が、しかし、

JavaScript に末尾再帰最適化はありません。


では、どうすればいいかというと「無限に再帰するような処理は避けて、whileループなどを使いましょう」というのが一つの答えです。


JavaScriptの特徴 2 - JavaScript の処理は非同期に進む

元のコードにはjQueryを利用した5秒間のアニメーションが含まれています。

  $('.top').animate({
    backgroundColor: color
  }, 5000);

この部分です。
再帰呼び出しはこの後の行なので、感覚的には「5秒かけてアニメーションしてから、自分自身を呼び出して繰り返す」という処理に見えます。

が、実際に実行してみると、
無限の再帰呼び出しでコールスタックが溢れるまでにかかる時間は一瞬です。
1周するのに少なくとも5秒はかかりそうなループなのに、これはどういうことでしょうか。


これは JavaScript の 非同期 を基本とした処理に起因するものです。

多くのプログラミング言語では途中に時間のかかる処理が現れたら、その処理の完了を待ってから続きを再開します。あえてネガティブに言うと、時間のかかる処理が完了するまでプログラム全体がブロックされるのです。

しかし、JavaScript はそこらの言語とは違い、非同期処理が基本です。 途中に時間のかかる処理が現れても、完了を待つことなどせずどんどん進めます。

結果、光の速さで再帰呼び出しが掛かります。一瞬のうちに大量のスタックが積まれ、最終的に溢れます。


よくある解決策は callback関数 を渡すことです。.animate() メソッドの終了を待つ代わりに、「アニメーションが終わったタイミングでこの関数を実行しておいて」という感じで関数を渡します。

  $('.top').animate({
    backgroundColor: color
  }, 5000);
  setAutoColorChange(++index);

これを、

  $('.top').animate({
    backgroundColor: color
  }, 5000, function() {
    setAutoColorChange(++index);
  });

このように変えます。

これにて再帰呼び出しで setAutoColorChange() が呼び出されるのは少なくとも5秒に一度になりました。
コールスタックは16000段くらいなら積まれても溢れないようなので、この実装であれば80000秒間 = 約22時間はスタックが溢れずに持ちこたえられそうです。


書き直す

これまでの事を踏まえると、①末尾再帰最適化が効いて、②同期的に処理が進む言語であれば、そもそもエラーにならずに済みそうな気がしました。
が、しかし、JavaScriptはそうではないのです。無いものねだりしていても仕方ありません。

書き直しましょう。


というわけで書き直したデモとソースコードがこちらにあります。

github.com


大事な部分だけ抜き出すとこんなコードになっています。

まずは CSS と、

/* 背景色をゆっくりと変化させるために transiton を5sで設定する */
body { transition: background-color 5s linear; }

/* 切り替えの基点になる色を設定 */
body.color0 { background-color: #346caa; }
body.color1 { background-color: #3386c8; }
body.color2 { background-color: #4aa45d; }
body.color3 { background-color: #8fb84f; }
body.color4 { background-color: #debc1c; }
body.color5 { background-color: #e09532; }
body.color6 { background-color: #eb7889; }

JavaScript はこんな感じに、

// 背景色を設定したクラスを切り替えるためのクロージャを生成
// 現在設定されている色は index で保持する、
function initAutoColorChange($el, colorCount) {
  var index = 0;

  return function() {
    index = (index + 1) % colorCount;

    // "color*"にマッチするクラス名を全て削除
    // 今回の組み方では色数が10色以上になった場合に対応していない
    $el.removeClass(function(i, classNames) {
      return classNames.split(' ').map(function(className) {
        return className.match(/color\d+/);
      }).filter(Boolean).join(' ');
    });

    // 次のクラス名を付与
    $el.addClass('color'+index);
  }
}

jQuery(function($) {
  // 切り替え関数を取得
  // 対象は<body>、色数は7色
  var autoColorChange = initAutoColorChange($('body'), 7);

  autoColorChange()
  setInterval(autoColorChange, 5000); // 5000ms 毎に切り替えされるよう設定
});


変更点 1 - 再帰を辞めて setInterval

一定時間毎に関数を実行する方法として、JavaScript では setInterval() というメソッドが用意されています。
月並みですが、ループは setInterval() を使いましょう。

setInterval(autoColorChange, 5000);

このように。


変更点 2 - アニメーションは CSS transition に任せる

確かに jQuery の .animate() メソッドは便利なんですが、今回のように単純に背景色を変えるだけなら CSS アニメーションで充分だと感じます。

今回は <body> の背景色を5秒かけて変化させたいので、その旨を CSS に記述しています。

body { transition: background-color 5s linear; }


変更点 3 - 現在の色番号の保持はクロージャでやる

今回はあらかじめ7つの色を決めておいて、それをアニメーションさせながらぐるぐると変化させています。
そのために、処理の中でも「現在どの色を表示しているか」という情報を index という変数で保持しています。

元のコードは関数型の影響を色濃く受けているようで、再帰呼び出しの際に渡す 引数として 色番号を保持しています。


今回書き直すにあたって、同じようにしても良かったのですが、せっかくなら JavaScript らしいやり方でと思い クロージャ を使うことにしました。 具体的には、クラス名を貼り替える関数を返す関数 initAutoColorChange() を定義しています。

function initAutoColorChange($el, colorCount) {
  var index = 0;

  return function() {
    index = (index + 1) % colorCount;

    // "color*"にマッチするクラス名を全て削除
    // 今回の組み方では色数が10色以上になった場合に対応していない
    $el.removeClass(function(i, classNames) {
      return classNames.split(' ').map(function(className) {
        return className.match(/color\d+/);
      }).filter(Boolean).join(' ');
    });

    // 次のクラス名を付与
    $el.addClass('color'+index);
  }
}

initAutoColorChange() によって返される関数は、自身の外側で定義された変数 index を使用しています。
内側の関数が定義される時点で、 index の値は0。その後、関数が呼び出されるたびに index の値は1ずつインクリメントされますが、関数は index の値を保持し続けます。


まとめ

今回コードを書き直すにあたってやったのは、結局のところ「再帰を辞めて setInterval を使った」というだけです。

それだけ聞くと簡単に聞こえますが、
なぜ再帰ではだめなのか、なぜ setInterval を使う事が JavaScript らしいのか、それについて理解しようとするとバックグラウンドに多くの知識が必要になります。

今回、説明したコールスタックや末尾再帰最適化についてもっと詳しく知りたい方は、以下の記事を根気強く読み解いていくといいかも知れません。

つくづく、複数の言語を学んでこそプログラミングの知識がより深まるなぁ、と感じますね。


私からは以上です。