ochalog

Ruby と MediaWiki が好きな電子・情報系の学生のブログ。

はっちんさんの「押すと上にスルスルっと戻るJavaScriptモジュール」について

はっちんさん(@hatchinee)の「押すと上にスルスルっと戻るJavaScriptモジュール」のコードの解説を頼まれて回答したので、それをまとめてみた。

コード

var ns = (function(exports) {
  var Raise = function constructor() {
    this._init_handler = function(func) {
      window.addEventListener('load', func);
    };
  };
 
  Raise.prototype.register = function(id) {
    var to_top = function(e) {
      var scroll_top = document.documentElement.scrollTop || document.body.scrollTop;
      if (scroll_top > 0) {
        var diff = Math.max(scroll_top / 2, 20);
        window.scrollTo(0, scroll_top - diff);
        window.setTimeout(to_top, 25, e);
      }
    };
 
    this._init_handler(function() {
      var target = document.getElementById(id);
      target.addEventListener('click', to_top);
    });
  };
 
  exports.Raise = Raise;
  return exports;
})({});
 
 
var RAISE_BUTTONS_ID = 'raise-top';
var raiser = new ns.Raise();
raiser.register(RAISE_BUTTONS_ID);

質問

  1. 以下の書き方の意図は?

    var Raise = function constructor() {};

    クラスのコンストラクタを書くなら

    function Raise() {};

    と書くのが普通のはず。

  2. 以下の書き方の意図は?

    var ns = (function(exports) {})({});

回答

1 番目

以下の 2 つの書き方はほぼ同等(function 文は省略記法[1]。細かな違いはいくつかある)。

// function 文
function Raise() {}
 
// function 式(関数オブジェクトを作る)
// FuncName の部分は何でもよい(省略してもよい)
var Raise = function FuncName() {};

どちらで書いても

var raise = new Raise();

のようにして Raise インスタンスを作ることができる。

2 番目

(グローバル)名前空間を汚さないための書き方。

JavaScript関数スコープを持つ。すなわち、関数の仮引数や、関数内で var で定義された変数は、その関数の外からはアクセスできない[1]。したがって、(function(exports) {}) 内で var で定義された変数や exports は、外部から参照できない。

function (args) {} のように名前を指定せず function 式で作る関数オブジェクトを無名関数 anonymous function と呼ぶ[1]

function 式で作った関数オブジェクトの直後に (args) を書いてその関数オブジェクトを呼び出す書き方は即時関数 immediate function と呼ばれる[2]。単純な例は以下の通り。

var sum = (function add(x, y) {
        return x + y;
    })(1, 2);
 
// function add() {} で和を返す関数オブジェクトを作り、
// その関数オブジェクトに実引数 1 と 2 を渡して呼び出す。
// sum にはその関数の戻り値 3 が代入される。
 
// function 式は変数定義文ではないので、呼び出した後 add という名前の
// 変数は残らない。

以上 3 つを組み合わせると、名前空間の汚染を最低限にして処理や定義を行うことができる。

最初のコードの ns 関連の部分を即時関数パターンを使わずに書くと、以下のようになる。

function f(exports) {
    /* Raise の定義 */
 
    exports.Raise = Raise;
    return exports;
}
 
var ns = f({});
// ns は Raise プロパティ(メソッド)を持つオブジェクト

このように書くと名前空間f が残り、ライブラリを組み合わせて使う場合などに名前の衝突が起こる可能性が高まる。即時関数パターンを使うことで、一時的に使った変数・関数の名前を名前空間に残さず、名前の衝突を起きにくくすることができる。

参考文献

  1. Douglas Crockford 著、水野貴明 訳「JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス」(オライリー・ジャパン、2008 年)
  2. Stoyan Stefanov 著、豊福剛 訳「JavaScriptパターン ―優れたアプリケーションのための作法」(オライリー・ジャパン、2011 年)

自分の書き方で書くと

「練習」と書いてあるので本番で使うコードではないと思うのだけれど、最初のコードは以下の点で不満。

  • 結局最後の部分でグローバル名前空間を汚している。
  • メソッドのみなので Raise クラスを作る必要がない。
  • Raise._init_handler() はおそらくプライベートメソッドにしたかったのだろうが、名前が '_' で始まるだけのパブリックメソッドである。
  • var ns の部分の即時関数に新しいオブジェクトを渡す意味がない。
  • window.scrollTo() より window.scrollBy() を使うべき。

あまり構造を変えずに自分の(古い)書き方で書くと、以下のようになった。デモはこちら

/*
 * 押すと上にスルスルっと戻る JavaScript モジュール 改造版
 * 2014-01-19 ocha
 */
 
/*jslint browser: true */
 
var MY_NAMESPACE = (function () {
    'use strict';
 
    // アニメーションの設定を変えられるようにした、
    // スルスル上昇設定のためのクラスのコンストラクタ
    //
    // scrollLengthDenom: スクロール量計算時の分母。デフォルト値は 4。
    // minScrollLength:   フレームあたりの最小スクロール量(px 単位)。
    //                    デフォルト値は 20。
    // interval:          アニメーションの間隔(ms 単位)。デフォルト値は 25。
    function Scroller(scrollLengthDenom, minScrollLength, interval) {
        this.scrollLengthDenom = scrollLengthDenom || 4;
        this.minScrollLength = minScrollLength || 20;
        this.interval = interval || 25;
    }
 
    Scroller.prototype.register = (function () {
        // クロージャで関数を隠蔽
 
        // ウィンドウ読み込み完了時のイベントハンドラを追加する
        function addWindowLoadEventHandler(func) {
            window.addEventListener('load', func);
        }
 
        // クリックするとスルスル上昇させる要素を登録する
        //
        // id: 要素の ID
        return function scrollerRegister(id) {
            var that = this, // this の保存
                target = document.getElementById(id),
 
                // 画面を少し上へスクロールする
                scrollToTop = function scrollToTop() {
                    // ここでの this は Scroller インスタンスではない
                    // Scroller インスタンスを参照するためには that を使う
 
                    var scrollTop = document.documentElement.scrollTop ||
                            document.body.scrollTop,
                        diff;
 
                    if (scrollTop > 0) {
                        diff = Math.max(
                            scrollTop / that.scrollLengthDenom,
                            that.minScrollLength
                        );
                        window.scrollBy(0, -diff);
 
                        // that.interval ms 後、再度呼び出す
                        window.setTimeout(scrollToTop, that.interval);
                    }
                };
 
            addWindowLoadEventHandler(function () {
                target.addEventListener('click', scrollToTop);
            });
        };
    }());
 
    // Scroller プロパティのみを持つオブジェクトを返す
    return {Scroller: Scroller};
}());
 
(function () {
    'use strict';
 
    var fastScroller = new MY_NAMESPACE.Scroller(4, 30, 30),
        slowScroller = new MY_NAMESPACE.Scroller(12, 5, 50);
 
    fastScroller.register('scroll-to-top-fast');
    slowScroller.register('scroll-to-top-slow');
}());

最近は、ECMAScript 5 で書ける(= IE 8 以下を切り捨てられる)場合、クラス定義で積極的に Object.defineProperties() を使うようにしている。プロパティディスクリプタを使って enumerable: false 等(明示的に書かなくても)設定できるため。