tacamy--blog

JavaScriptを勉強中の人のブログです。

JavaScript で日付のフォーマットを整形する

Movable Type で日付のフォーマットをいじりたいときには、% を使って簡単に設定できます。 例えば、「2013年03月14日 (月)」と表示したいときは、次のように指定します。

<$MTDate format="%Y年%m月%d日 (%a)"$>

詳しくは、日付に関するテンプレートタグのモディファイアリファレンス を参照。

その感覚で、JavaScript でも同じようなことできると思っていたら、そういうものは無いらしい。 なので、自分でフォーマットを整形したいときは、次のように指定します。

// 今日の日付で Date オブジェクトを作成
var now = new Date();

// 「年」「月」「日」「曜日」を Date オブジェクトから取り出してそれぞれに代入
var y = now.getFullYear();
var m = now.getMonth() + 1;
var d = now.getDate();
var w = now.getDay();

// 曜日の表記を文字列の配列で指定
var wNames = ['日', '月', '火', '水', '木', '金', '土'];

// 「月」と「日」で1桁だったときに頭に 0 をつける
if (m < 10) {
  m = '0' + m;
}
if (d < 10) {
  d = '0' + d;
}

// フォーマットを整形してコンソールに出力
console.log(y + '年' + m + '月' + d + '日 (' + wNames[w] + ')');

コードの解説

「年」「月」「日」「曜日」を Date オブジェクトから取り出してそれぞれに代入

var y = now.getFullYear();
var m = now.getMonth() + 1;
var d = now.getDate();
var w = now.getDay();

date オブジェクトには、getFullYear() getMonth() getDate() getDay() というメソッドが用意されているので、取り出したらそれぞれ別の変数に格納しています。

なぜか getMonth() だけは、実際の月から -1 された数字が入るので、 +1 しています。

date オブジェクトのメソッドの詳細は Dateオブジェクト / JavaScriptリファレンス を参照しました。

曜日の表記を文字列の配列で指定

var wNames = ['日', '月', '火', '水', '木', '金', '土'];

曜日を取り出す getDay() では、0 から 6 の数字が取得できます。 0 が日曜日、1 が月曜・・・6 が土曜日という対応になっているので、曜日をどのように表記するかかを文字列の配列で持っておきます。

「月」と「日」で1桁だったときに頭に 0 をつける

if (m < 10) {
  m = '0' + m;
}
if (d < 10) {
  d = '0' + d;
}

「3月」を「03月」と 0 詰めして表記したい場合は、自分で処理しなければなりません。 入っている値が 1 〜 9 の場合は、頭に '0' という文字を追加することで、自動的に型変換されて 3 という数値が '03' という文字列になります。

フォーマットを整形してコンソールに出力

console.log(y + '年' + m + '月' + d + '日 (' + wNames[w] + ')');

あとは、それぞれ処理済みの変数を、好きなように整形して表示させるだけです。

関数化する

同じサイトの中では、日付の表記は普通は統一させると思います。でも、日付を扱う場所で都度上記の処理を行うのは面倒なので、フォーマットを整形する部分を関数化して、どこからでも呼び出せるようにしておくと便利です。

// Date オブジェクトを作成
var now = new Date();

// dateFormat 関数の実行
console.log(dateFormat(now));

// dateFormat 関数の定義
function dateFormat(date) {
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  var d = date.getDate();
  var w = date.getDay();
  var wNames = ['日', '月', '火', '水', '木', '金', '土'];

  if (m < 10) {
    m = '0' + m;
  }
  if (d < 10) {
    d = '0' + d;
  }

  // フォーマット整形済みの文字列を戻り値にする
  return y + '年' + m + '月' + d + '日 (' + wNames[w] + ')';
}

date オブジェクトを引数で渡して、整形済みの文字列を戻り値で受け取る関数にしてみました。

ゼロ埋めの方法を改善(2013-03-21 追記)

コメント欄で、ゼロパディング(ゼロ埋め)は .slice() メソッドを使えばできると教えてもらったので、dateFormat 関数を修正してみました。

// Date オブジェクトを作成
var now = new Date();

// dateFormat 関数の実行
console.log(dateFormat(now));

// dateFormat 関数の定義
function dateFormat(date) {
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  var d = date.getDate();
  var w = date.getDay();
  var wNames = ['日', '月', '火', '水', '木', '金', '土'];

  m = ('0' + m).slice(-2);
  d = ('0' + d).slice(-2);

  // フォーマット整形済みの文字列を戻り値にする
  return y + '年' + m + '月' + d + '日 (' + wNames[w] + ')';
}

if が取れてスッキリしました。うれしい。GAVAScript さん、吉田さん、どうもありがとうございます。

ライブラリを使う(2013-03-21 追記)

自分で書かずにライブラリを使うのが便利らしいです。教えてくれてあざます!

参考サイト Thx ♡

jQuery の on() と off() を理解する

初心者向けの本とかだと、イベントを jQuery オブジェクトの後に直接指定する、 $('.foo').click(); のような書き方で説明されている場合が多いけど、少し複雑なことをしようとするとそれだと困ることが出てきます。そんなときに便利なのが on() を使ったイベント設定です。

on() ひとつで bind() live() delegate() を表せる

jQuery 1.7 で、bind() live() delegate() がすべて on() に統合されたそうです。 つまり on() の書き方によって、3 パターンの使い方ができるということです。

.foo という要素をクリックしたら何かするという例で、イベントの設定と削除の方法をそれぞれ書いてみます。

bind()

$('.foo').bind('click', function(){...});$('.foo').click(function(){...}); と同じ意味で、ごく普通の基本的なイベント設定方法です。 その要素に対してイベントが発生したら何かするという感じ。

$('.foo').bind('click', function(){...});
$('.foo').unbind('click');

これを on() で書くとこうなります。

$('.foo').on('click', function(){...});
$('.foo').off('click');

一緒だったらわざわざ on() なんて使わずに click() でいいじゃんと思うかもしれませんが、このあとの話で on() にするメリットが出てきます。

delegate()

delegate() では、イベントを設定するのは親の要素に対して行います。delegate() の 1 番目の引数で、実際にクリックする要素 .foo を指定しますが、イベントが張り付いているのは親の .parent です。そして、 .parent の中にある .foo だけがクリックイベントの発生する対象となります。このように、親要素に対してイベントを設定することを、イベントデリゲートと言います。

最初に HTML が描画された後に、JavaScript によって要素を追加した場合、bind() でイベントを設定している場合は、後から追加された要素にはそのイベントが効きません。でも、delegate() の場合は、後から追加した要素にもちゃんとイベントを適用できます。

$('.parent').delegate('.foo', 'click', function(){...});
$('.parent').undelegate('.foo', 'click');

これを on() で書くとこうなります。

$('.parent').on('click', '.foo', function(){...});
$('.parent').off('click', '.foo');

live()

live() も delegate() と同じように、JavaScript を使って後から追加した要素に対してもイベントを設定できます。delegate() との違いは、イベントを設定する対象が document になります。でも、書き方は次のように .foo に対してイベントを設定しているように書きます。でも実際にイベントが設定されているのは document です。分かりにくい・・!

$('.foo').live('click', function(){...});
$('.foo').die('click');

これを on() で書くとこうなります。

$(document).on('click', '.foo', function(){...});
$(document).off('click', '.foo');

on() の書き方の方が、実際の挙動と見た目が一致していて分かりやすいです。あと、die() って名前、live() と対になってるのが分かりにくい気がする。英語できないからでしょうか・・。

ひとつの要素に複数のイベントを設定する方法

同じ処理をする複数のイベントを設定

on() のひとつ目の引数を、スペース区切りで複数指定することで、どのイベントが発生しても同じ処理をさせることができます。

$('.foo').on('click blur', function(){...});

別々の処理をする複数のイベントを設定

on() の中身を連想配列にすることで、イベントごとに異なる処理を記述できます。

$('.foo').on({
  'mouseenter': function(){...},
  'mouseleave': function(){...}
});

イベントデリゲートで複数イベントを設定する

イベントの連想配列の後に、カンマで区切ってイベント対象の要素を指定します。

$('.parent').on({
  'mouseenter': function(){...},
  'mouseleave': function(){...}
}, '.foo');

live() は廃止

live() と die() は jQuery 1.9 で廃止されました。 delegate() は今のところ廃止の予定はありませんが、on() が推奨なので、今から on() で書いておいたほうがよさそうです。

参考サイト & 本 Thx♡

Node.js + Express を Heroku で動かすまでの手順まとめ

普通の JavaScriptjQuery もまともに書けないけど、はじめての Web アプリを Node.js でつくってみるという奮闘記。

環境つくるだけなのに何も分からなすぎてハマりすぎて、この一連の流れだけで丸 2 日潰れるという大惨事だったので、ちゃんとブログに残しておく。

Node.js のインストール

Node.js の INSTALL ボタンから、インストーラを使って入れることもできるけど、Node.js のバージョンを切り替えて使える方が便利だと思うので、前回のエントリを参考に nodebrew を使ってインストールするのがオススメ。

インストールが正しくできているか確認のため、Node.js のバージョンを表示。

$ node -v

npm のインストール

Node.js にはたくさんのモジュールパッケージが用意されていて、それらを簡単に導入できるのが npm という Node.js のパッケージ管理ツールです。

最近のバージョンの Node.js では、Node.js と一緒に npm もインストールされるようです。念のため、自分の Mac に npm がインストールされているかの確認のため、バージョンを表示。

$ npm -v

Express のインストール

Express は 動的なWebサイトをつくるための基本機能が揃ったフレームワークです。有名らしいのでこちらを使うことにしました。ただ、現在のバージョンは 3.1.0 なのですが、ブログで書かれている内容は 2.x 系の場合が多く、3.x 系でだいぶ変わったそうなので、本家のドキュメントを参考にするといいそうです。英語だけど・・。

Express はグローバル環境にインストールすると、express コマンドがターミナルで使えるようになるそうなので、下記のように実行してインストールします。-g はグローバルという意味です。

$ npm install -g express

終わったら念のため、バージョンを表示してインストールの成功を確認します。

$ express --version

プロジェクトをつくる

express コマンドを使うことで、プロジェクトを生成できます。例として「sample」プロジェクトをつくってみます。

まず、プロジェクトを生成したいディレクトリの一つ上まで移動します。

$ cd ~/works/project/

express コマンドとプロジェクト名を指定して実行します。

$ express sample

~/works/project/sample にフレームワークのファイルが一式展開される形でプロジェクトが生成され、ターミナルには次のように表示されます。

create : sample
--- 省略 ---
create : sample/views/index.jade

install dependencies:
  $ cd sample && npm install

run the app:
  $ node app

この「install dependencies:」に書いてあるコマンドを実行しないと、node app の実行がエラーとなりサーバを起動できません。私はここでハマりました。プロジェクトのディレクトリに移動して、モジュールをインストールします。

$ cd sample && npm install

モジュールがインストールできたら、以下のコマンドで Node.js を起動できるようになります。

$ node app

Express のデフォルトのポート番号は 3000 番なので、ブラウザで http://localhost:3000/ にアクセスしてみます。「Express - Welcome to Express」と表示されていればインストール成功です。Thank you!!!(Welcome to への返事)

ちなみに Node.js は、Ctrl + C で終了できます。

Heroku Toolbelt のインストール

Heroku Toolbelt をインストールすると、ターミナルで heroku コマンドが使えるようになります。インストールは簡単です。

  1. Heroku Toolbelt の「Heroku Toolbelt for Mac OS X」ボタンから、インストーラをダウンロードします。
  2. ダウンロードした「heroku-toolbelt.pkg」をダブルクリックしてインストールします。
  3. ~/.bashrc などの環境設定ファイルに、自動的に PATH が追加されます。もし追加されてなければ、次の一文を追加して PATH を通すと heroku コマンドがターミナルから使えるようになります。
export PATH="/usr/local/heroku/bin:$PATH"

ターミナルから Heroku にログイン

※あらかじめ、Heroku のサイトでユーザアカウントを登録(Sign Up)しておきます。

ターミナルから、Heroku の自分のアカウントにログインします。

$ heroku login

続けて、登録時の Email アドレスとパスワードを聞かれるので入力します。成功すると「Authentication successful.」と表示されます。

Heroku へデプロイ

cd でプロジェクトのディレクトリに移動して、下記のコマンドを実行します。「tacamy-sample」の部分は「http://tacamy-sample.herokuapp.com/」といった具合に URL の一部になります。指定なしで $ heroku create とした場合は、自動的に生成されたランダムな名前が割り当てられます。

$ heroku create tacamy-sample

完了すると、ターミナルにアプリの URL と Git のリポジトリ URL が表示されます。

http://tacamy-sample.herokuapp.com/ | git@heroku.com:tacamy-sample.git

確認のため、http://tacamy-sample.herokuapp.com/ にブラウザからアクセスし、「Heroku | Welcome to your new app!」と表示されれば成功です。Thank you!!!

Procfile をつくる

Procfile とは、Heroku で必要となる、アプリケーションの起動に関するファイルです。私はこれがないと Heroku での表示で「Web じゃないよ」みたいなエラーになりました。詳しいことは procfile · herokaijp/devcenter Wiki に書かれているようですが、私は理解できませんでした/(^o^)\

とりあえず動くようにするには、次の一文だけ書いて、ファイル名を「Procfile」とし、プロジェクト直下に置きます。

web: node app.js

.gitignore をつくる

プロジェクトのファイルは Git を使って Heroku に Push しますが、node_modules ディレクトリ以下は Heroku 上で展開する(?)ので、ローカルから Push する必要はありません。そのため、.gitignore ファイルをつくってその旨を指定します。

node_modules
.DS_Store

ついでに、.DS_Store もコミットされないようにしてみました。

package.json に追記

Express で生成された sample/package.json ファイルに、Heroku 用の記述を追加します。これを書かないと Heroku に Push したときにエラーになるので注意!(はい、ハマりました。)

Express で生成された状態では、"dependencies" の記述はありますが、"engines" の記述がありません。そのため下記のように、"dependencies" のあとに "engines" を追加して、Node.js と npm のバージョンを記述します。

--- 省略 ---
"dependencies": {
  "express": "3.1.0",
  "jade": "*"
},
"engines": {
  "node": "0.8.19",
  "npm":  "1.2.10"
}
--- 省略 ---

Git リポジトリの作成とコミット

  1. リポジトリつくって、
  2. ローカルのファイルを全部 Index に追加して、
  3. コメントつけてコミット

という、普通の Git の操作をします。

$ git init
$ git add .
$ git commit -m "initial commit"

SSH の鍵認証

ここ少しあやしいのですが・・。

まず、Heroku の公開鍵を確認します。

$ heroku keys

Heroku に登録した Email アドレスで、公開鍵が表示されれば大丈夫です。この鍵が、~/.ssh/id_rsa.pub の鍵と同じであることを確認し、下記のコマンドを実行して Heroku に公開鍵を登録します。

$ heroku keys:add ~/.ssh/id_rsa.pub

私の場合は、heroku keys で登録した Email アドレスでの鍵が表示されなかったので、Heroku と SSH 通信するための公開鍵をつくりました。

$ ssh-keygen -t rsa -C "xxxxx@gmail.com"

上記のコマンドを実行したところ、~/.ssh 以下に、id_rsa と id_rsa.pub ファイルが新たに作成され、$ heroku keys コマンドの結果にも表示されるようになり、晴れて Heroku に公開鍵を登録できました。このあたりの詳しいことは、下記エントリを参照しました。

Heroku へ Push

最初だけ、下記のコマンドを実行しないと、Push できずにエラーで怒られます。

$ git remote add heroku git@heroku.com:tacamy-sample.git

あとは普通に heroku master に Push します。

$ git push heroku master

Heroku で表示確認

Heroku 上のプロジェクトの URL にアクセスして、ローカルと同じものが見れてれば成功です。長い道のりおつかれさまでした!!!

http://tacamy-sample.herokuapp.com/

余談

この流れは、普通に新規でプロジェクトをつくる流れなのですが、私の場合は Facebook アプリをつくりたくて、Facebook にアプリ登録するときに Heroku を選択したらプロジェクトが Heroku に登録されてしまったのです。でも、ローカルにはまだ何もないし、Heroku と Facebook にはプロジェクトが存在するという状況で、この新規でつくる流れより混乱しました。その場合の方法は、また別のエントリで書くつもりです。

あと、Heroku 以外のところは、JavaScript徹底攻略 (WEB+DB PRESS plus) が本当に役立ちました。これがなかったら未だにできてないと思います・・。ありがとうございます!!

参考サイト Thx♡

node.js 入れるなら nodebrew が超簡単

node をバージョン別に使い分けたりしたかったので、nodebrew なるものを使うことにした(hokaccha++)。

最初は nvm 使ったんだけど、なんかうまくできなかった。 ~/.bashrc に設定書いたら、ターミナル起動するたびに nvm use v0.8.19 とか出てくるのがイラっとしたのでやめた。

1. nodebrew のインストール

ターミナルに以下の 1 行コピペするだけ。超簡単。

curl https://raw.github.com/hokaccha/nodebrew/master/nodebrew | perl - setup

2. 環境設定ファイルにパスを通す

いまだに「パスを通す」っていう意味が分かってないけど、これをやらないと、ターミナルを再起動したときに、せっかく入れたツールが使えなくなるっていう認識でおります・・。

私は ~/.bashrc を使ってるので、以下の1行を追記した。

export PATH=$HOME/.nodebrew/current/bin:$PATH

~/.zshrc 使ってる人はそっちに同じもの書けばいいらしい。超簡単。

nodebrew がちゃんと入ってるか確認するために、ターミナルを再起動したら、以下の 1 行を実行してみる。

nodebrew -v

ちゃんと nodebrew 0.6.2 って表示されたのでできてるぽい。

追記

「PATHを通す」っていう意味は、教えてもらったページ読んだら分かった気がする!

OS にデフォルトで用意されていないコマンドを使うには、そのコマンドのプログラムがある場所を教えてあげないと OS は自力で探せないらしい。だから PATH を通してないと、「そんなコマンドないよ」って怒られるぽい。

@hokaccha @cipher さんありがとう〜(∩´∀`)∩

3. node.js をインストール

まず、nodebrew でインストールできる node.js のバージョンを確認するために、以下の 1 行を実行。

nodebrew ls-remote

バージョンの一覧が表示されるけど、この中からどれを入れようって迷う。nodejs.org のトップページで、「Current Version: v0.8.19」って書いてあったので、きっとこれが安定版の最新なんだろうと思い、以下の 1 行を実行してインストール。

nodebrew install-binary v0.8.19

インストールが一瞬で終わる。はやい。

ちなみに、nodebrew install v0.8.19 としてもインストールできるけど、コンパイルを自分でするからインストールにすごく時間がかかる。だから、バイナリからインストールするとよいです。最初、install コマンドを使ったら、あまりにも時間がかかるし、意味不明なコードがターミナル上で延々と表示されて、壊れたんじゃないかと不安になった。

ちなみに、v0.8.6 以前のバージョンは、バイナリからインストールできないので、install-binary じゃなくて install コマンドでやるしかないらしい。

他に入れたいバージョンがあったら同じように、どんどんインストールしていきます。

インストールした node.js のバージョンの一覧は、以下のコマンドで確認できます。

nodebrew ls

node のバージョンを切り替えるのは、以下のコマンドで。

nodebrew use v0.8.19

これで、node.js がインストールできました。簡単すぎてすごい。

今回参考にしたサイト Thx♡

jQuery : キューの stop とfadeIn / fadeOut の謎

超大作は書くのが大変すぎて続かないので、どうでもいいエントリも織り交ぜつつ、ゆるく続けていくことにしました。

今回は、jQuery の勉強でツールチップ書いたときに、謎の現象で困ったのでメモ。

つくったもの

謎の現象その1 - チカチカする

アニメーションする速度より速く、複数のリスト上を通るようにマウスを高速に動かすと、ツールチップが点滅する・・。 .fadeIn() / .fadeOut().show() / .hide() にすれば大丈夫なんだけど・・ん・・?

「あっ!これ、zudo 本で出た問題だ!」

複数の命令を順番に処理するしくみを、キュー(待ち行列)と言います。
キューに貯まった吹き出しの表示・非表示アニメーションの処理は、マウスの操作に追いつきません。
その結果、マウスの動きを止めてからも順番に処理されることになります。
この問題を解消するために、stop メソッドを利用します。
これは現在進行中のアニメーションを即座に止めるメソッドです。
Webデザイナーのための jQuery入門 より引用)

ということで、.fadeIn() / .fadeOut() の前に .stop() を入れてみることにしたのが次のコード。

謎の現象その2 - ツールチップの色が薄くなる

.stop() を追加して、一見できたように思えたのですが、また複数リストの上を通るようにしてマウスを動かしてみると、だんだんとツールチップの色が薄くなっていく・・。 ナニコレ/(^o^)\

よく分からなかったので、zudo 本に書いてある通りに、.fadeIn() / .fadeOut() を使わずに、.animate() を使うことにした。

完成

やったーー!!でもなんでなのかは、よくわからん。まあいいか。

このあと、オプションでカスタマイズできるように機能追加してゆく予定。

追記

@5509 さんに、.stop() の引数使えばうまくいくって教えてもらったので、やってみたらできた・・!ありがとうございます。

.fadeIn() / .fadeOut() の前に .stop(true, true) を指定しています。

引数の指定が必要な理由

以下のエントリがすごい分かりやすかったです。

.stopが単純にアニメーションを停止させるだけなので、.slideDownの途中で呼び出された場合、その時点のheightが次回以降のheightとして認識されることが原因です。

今回の場合は、opacity の値が次回の opacity として認識されてしまっていたようです。

引数の渡し方の種類

引数の渡し方の違いにより、どう変わるのかのサンプルが分かりやすかったです。

今回の場合は、.stop(true, false) だとうまくいかなかったので、.stop(true, true) にしたらうまくいきました。

参考にした本 Thx♡

jQuery プラグインの書き方メモ

勉強のために超簡単な jQuery プラグインを書いてみたら、凄腕 JSer の方々に色々とご指導ご鞭撻いただいたので、忘れないようにメモる。

オレオレ jQuery プラグインテンプレート

(function($) {

    $.fn.pluginName = function(options) {
        
        /**
         * Option
         */
        options = $.extend({
            opt1: 'val',
            opt2: 'val'
        }, options);

        /**
         * Core
         */
        return this.each(function() {
            // Process
        });

        /**
         * Method
         */
        function _methodName() {
        };
    };
}(jQuery));

/**
 * Execute
 */
$(function() {
    $(".foo").pluginName();
});

メソッドのまとまりを、コンストラクタの前と後ろのどっちに書くべきか、未だに迷い続けてる・・。関数リテラルだと前に書かないといけないし、function文なら前でも後ろでもいいぽいけど。

$.fn ってそもそも何なの・・

$.fn$.prototypeエイリアスなのだそうです。

$.fn === $.prototype //=> true

jQuery オブジェクトをつくるときの $('.foo') とかいうのも、実は jQuery が裏で new をしていて、セレクタを引数にとって jQuery クラスのインスタンスを返していたらしい。これによって jQuery オブジェクトは、jQuery クラスで用意されている便利なメソッドが使えるようになってたわけですね。

なので、$.fn.pluginName というのは、jQuery クラスのプロトタイプに pluginName というメソッドを追加しているということになります。プラグインを読み込んで $(".foo").pluginName(); とか実行するっていうのも意味が分かった。なるほど。

(function($) {...}(jQuery)); で囲む

別の JavaScript のコードと変数名や関数名がぶつからないように、一番外を (function($) {...}(jQuery)); で囲むことにした。$.fn.pluginName = function(options){...}; の中ですべて完結するなら別に書かなくてもいいんだけど、もし何かを外に出したくなったときのために、あらかじめカプセル化することにしてみたけどしつこいかな・・?

return this; の位置

よくある jQuery プラグインだと、コードの最後に return this; って書かれてる場合が多いと思うけど、これはコンストラクタthis.each(function() {...}); の前に return をつけて、return this.each(function() {...}); ってしてる。

この方法だと、プラグインの頭の方で this を返していることが分かるメリットがあるそうです。

関数はループの外に

関数が .each の中にあると、ループ毎に関数が作られてしまうので、ループの外に置くようにします。

子要素から検索する

this を each で回して、その子要素から検索するという場合、書き方がいくつかあって、私はもともと $(".foo", this); って書いていたのだけど、.find() を使う方がおすすめらしい。this の書き忘れによる思わぬバグを防ぐというメリットがあるそうです。

.click よりも .on

まだこのへんちゃんと理解できてないけど .click(function(){...}); よりも .on('click', functiin() {...}); の方がいろいろ応用がきくそうです。.on 以外にも、.bind.delegate とかあるけど、jQuery1.7 からは .on が推奨らしいのでコレ使ってみました。

そのあたりの詳しいことは、下記のページでわかりやすくまとめてあったすごい。

jQuery 1.7の更新内容をまとめたよ。 | Ginpen.com

教えてくれてありがとう!!!

JavaScript の this を理解する

this はインスタンス自身を指す、ただそれだけの話でしょう?
そんなふうに考えていた時期が私にもありました。そう、JavaScript の this に出会うまでは・・。

用語について

私は Java 脳で書いてるので、言葉遣いが JavaScript と若干違う部分があると思います。 下記のようなイメージで言葉を使っています。

用語 意味
クラス インスタンスオブジェクトの元となる設計図
コンストラクタ クラスのコード部分で、new したときにコンストラクタの内容で初期化する
インスタンス クラスを元に実体化したオブジェクト
メンバ変数 クラスやインスタンスに属するローカル変数
メソッド クラスやインスタンスに属する命令(関数)

オブジェクト指向とプロトタイプと this

JavaScriptオブジェクト指向開発では、元になるクラスを new することで、インスタンスを生成します。

同じクラスを元に作られたインスタンス間において、メンバ変数の値はインスタンスにごとに異なり、メソッドは共通で使える場合が多いです。そのため、基本的にはメンバ変数はコンストラクタ部分に、メソッドはプロトタイプ部分に記述します。 (詳しい内容は プロトタイプチェーンをもっと理解する を参照。)

function Bird(voice) {
    this.voice = voice;
}
Bird.prototype.sing = function() {
    console.log(this.voice);
}

var cock = new Bird("cocka-a-doodle-doo");
cock.sing();  // > "cocka-a-doodle-doo" (this が cock を指している)

var duck = new Bird("quack");
duck.sing();  // > "quack" (this が duck を指している)

上記のコードでは、2 箇所に this を使っています。

  • Bird クラスのコンストラクタthis.voice = voice;
  • Bird クラスのプロトタイプ sing メソッド内 console.log(this.voice);

それぞれの this の使われ方について見ていきます。

メンバ変数に値を代入する this

Bird クラスをインスタンス化するときに引数で渡した値を、this を使ってインスタンス自身にセットすることにより、そのインスタンスのメンバ変数として値を代入できます。 このメンバ変数は、インスタンスごとに個別に作られ、それぞれ別々のメモリ領域に保管されます。

このとき this はインスタンス自身(cock, duck)を指しており、それぞれのインスタンスで voice 変数を持っています。

メソッドからメンバ変数を参照する this

クラスのプロトタイプで定義されたメソッドは、そのクラスを元にして作ったインスタンスから実行できます。しかし、実際にはメソッド本体はひとつしかなく、そのメソッドのあるアドレス(番地)を参照しているだけで、インスタンス化するときにメソッドが複製されているわけではありません。

sing メソッドが定義されているのは Bird クラスですが、「this はインスタンス自身を指す」ので、console.log(this.voice); の this は Bird クラスではなくそれぞれのインスタンスを指しています。

関数内の this が指すもの

関数は独立したオブジェクトであり、関数を定義したオブジェクトに所属するものではありません。 これを念頭に置いておくと、関数内の this が何を指すのかがイメージしやすくなります。

関数を変数に代入して呼び出す場合

次のコードは、obj1 で定義した fn 関数を obj1 から呼び出しているので、this は obj1 を見ています。これは簡単ですね。

var obj1 = {
    str: "obj1",
    fn: function() {            // obj1 で fn 関数を定義
        console.log(this.str);  // > "obj1"
    }
}
obj1.fn();  // obj1 から fn 関数を呼び出す

次のコードでは、obj2 の fn 変数に、obj1 で定義した fn 関数を代入後、obj2 から fn 関数を呼び出しています。 この場合、this は関数を定義した obj1 ではなく、呼び出した obj2 を見ています。

var obj1 = {
    str: "obj1",
    fn: function() {            // obj1 で fn 関数を定義
        console.log(this.str);  // > "obj2"
    }
}
var obj2 = {
    str: "obj2",
    fn: obj1.fn  // obj2.fn に obj1.fn の関数を代入
}
obj2.fn();       // obj2 から obj1 で定義した fn 関数を呼び出す

function() { console.log(this.str); } という関数オブジェクトは obj1 に所属しているわけではなく、独立したオブジェクトとして存在しています。

そもそも、「obj1 で fn 関数を定義」というのは、関数オブジェクトのアドレス情報を obj1.fn に代入しているだけです。 つまり、「obj2.fn に obj1.fn の関数を代入」というのも、obj1.fn に代入されている関数オブジェクトのアドレス情報を obj2.fn に代入しているだけのことです。 obj1.fn に関数が所属しているように見えるけれど、実際は obj1.fn も obj2.fn も、単に独立した関数オブジェクトへのアドレス情報を持っているだけで、どちらも同じことなのです。

こう考えると、関数内の this が、定義されたオブジェクトとは関係なしに、呼び出されたオブジェクトを指すというのも納得できます。

グローバルオブジェクトから呼び出す場合

グローバルスコープの範囲から呼び出された this は、グローバルオブジェクトを指します。

console.log の引数の str に this をつけない場合は、内側の関数の変数オブジェクトから順に名前解決をするため、ローカル変数 str を見にいきます。 (この挙動については、JavaScript のスコープチェーンとクロージャを理解する を参照。)

var str = "global";
function fn() {
    var str = "local";
    console.log(str);  // > "local" (自身の関数内のローカル変数を参照)
}
fn();

this をつけた場合は、this が呼び出し元であるグローバルオブジェクト(window)を指すので、グローバル変数の str を見にいきます。 this.strwindow.str に置き換わるようなイメージです。

var str = "global";
function fn() {
    var str = "local";
    console.log(this.str);  // > "global" (this が window を指す)
}
fn();  // グローバルスコープの範囲から呼び出す

イベントハンドラで関数を呼び出す場合

ボタンをクリックしたら、メッセージを表示する showMsg 関数を実行するようにしてみます。

HTML は次のとおりです。

<input type="button" id="btn" name="btn" value="Click Me">

まずは、イベントなしの場合の挙動を見てみます。this はグローバルオブジェクトを指しているため、this.msg はグローバル変数の msg を参照します。

var msg = "Hello";
function showMsg() {
    alert(this.msg);  // > "Hello" (this は グローバルオブジェクトを指す)
}
showMsg();

次に、input 要素のクリックイベントによって showMsg を呼び出してみます。すると this.msg が undefined を返すようになってしまいました。

var msg = "Hello";
function showMsg() {
    alert(this.msg);  // > undefined
}
window.onload = function() {
    document.getElementById("btn").onclick = showMsg;
}

this が何を指しているか確認するために、alert(this); に変えてみたところ、object HTMLInputElement と表示されました。イベントを元に関数を呼び出した場合、関数内の this はイベントの発生元の要素を指します。

var msg = "Hello";
function showMsg() {
    alert(this);  // > [object HTMLInputElement] (this は input 要素を指す)
}
window.onload = function() {
    document.getElementById("btn").onclick = showMsg;
}

call メソッドと apply メソッド

関数の呼び出し方によって this の指すオブジェクトがこれだけ変わると混乱してしまいますが、JavaScript のプロトタイプには、call と apply というメソッドが用意されています。 call と apply は、this の指すオブジェクトを自由にコントロールできます。

call も apply もほぼ同じ機能ですが、関数に渡す引数の指定方法が違います。 call は引数を個別に指定し、apply は配列でまとめて指定します。

call も apply の話だけで 1 エントリできてしまいそうなので、ひとまず参考サイトを貼っておきます。

考察

メソッド内の this は、メソッド呼び出し時にどのオブジェクトのプロパティとして呼び出されたかによって、this が何を指すのかが変わります。

なぜそんなややこしい仕様になっているのでしょうか。

Java の場合、メソッドはクラスであらかじめ定義しておき、それをインスタンス化することで初めて使えるようになります。インスタンス化した後に変更することはできません。しかし、JavaScript は柔軟な言語のため、あるオブジェクトで定義した関数を、他のオブジェクトの変数に代入して呼び出したりするなど、自由に再利用ができます。

もし this が「関数(メソッド)を定義したオブジェクトを指す」という仕様だとしたら、メソッドをインスタンス間で共有できても、メソッドを呼び出したインスタンスではなく、関数を定義したオブジェクトのメンバ変数を見に行ってしまいます。this が呼び出し元のオブジェクトによって指すものが変わるからこそ、同じメソッドを再利用できる仕組みになっているのだと思います。たぶん。

まとめ

  • クラスをインスタンス化するときに引数で渡した値を、this を使ってインスタンス自身にセットすることにより、そのインスタンスのメンバ変数として値を代入できる。
  • 関数内の this は、呼び出し時にどのオブジェクトのプロパティとして呼び出されたかによって、this が何を指すのかが変わる。
  • イベントハンドラで関数を呼び出した場合の this は、イベントの発生源のオブジェクトを指す。

今回参考にしたサイト & 本 Thx♡