はっちんさんの「押すと上にスルスルっと戻る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);
質問
以下の書き方の意図は?
var Raise = function constructor() {};
クラスのコンストラクタを書くなら
function Raise() {};
と書くのが普通のはず。
以下の書き方の意図は?
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
が残り、ライブラリを組み合わせて使う場合などに名前の衝突が起こる可能性が高まる。即時関数パターンを使うことで、一時的に使った変数・関数の名前を名前空間に残さず、名前の衝突を起きにくくすることができる。
参考文献
- Douglas Crockford 著、水野貴明 訳「JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス」(オライリー・ジャパン、2008 年)
- 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
等(明示的に書かなくても)設定できるため。