tacamy.blog

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

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♡

JavaScript のスコープチェーンとクロージャを理解する

前回JavaScript のスコープの基本がわかったので、今回はスコープチェーンとクロージャを勉強してみました。 Call オブジェクトとクロージャの理解がかなり大変でした・・。

変数オブジェクト

JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということです。

  • 変数オブジェクトというのは、key と value による変数管理専用のハッシュテーブルのこと
  • key が変数名、value が値のセットになっているテーブルで、変数の数だけレコードができるイメージ
  • 変数オブジェクトはプログラマが意識することのない、便宜的なオブジェクト

グローバルオブジェクト

JavaScript は、ブラウザが新しいページを読み込んだとき、内部的に新しいグローバルオブジェクトを生成して初期化します。 グローバルオブジェクトとは、グローバル変数やグローバル関数を管理するための、変数オブジェクトです。

つまり、グローバル変数を定義するということは、実際にはグローバルオブジェクトのプロパティを定義するということです。 ブラウザ上では window オブジェクトがグローバルオブジェクトなので、トップレベルで宣言した変数が window オブジェクトのプロパティとして扱えるようになるのも納得できます。

var n = 10;
console.log(window.n); // > 10

Call オブジェクト

ローカル変数もグローバルオブジェクトと同じように、関数が実行されたタイミングで変数オブジェクトができます。 それを Call オブジェクトといいます。目に見えないオブジェクトで、プログラマが直接操作することはできません。

関数が実行されたタイミングで、var で宣言された変数と実行時に渡された引数などが、Call オブジェクトに格納されます。

Call オブジェクトは関数実行が終了すると消えます。ローカル変数の記憶領域が関数実行終了とともに破棄されるのは、これが関係しているのかもしれません。

Call オブジェクトに含まれる情報

  • その関数内のローカル変数の値
  • 関数に渡された引数名とその値
  • 引数情報を管理するオブジェクト(arguments オブジェクト)
  • this
  • 親の Call オブジェクトの場所(アドレス情報)

スコープチェーン

変数 x がグローバル変数とローカル変数で名前がぶつかっている場合、値を調べる手順は次のとおりです(これを名前解決という)。

  1. チェーン先頭の Call オブジェクトから x というプロパティを探す。
  2. あればその値を返し、なければチェーンの次の Call オブジェクトを探す。
  3. あればその値を返し、なければチェーンの最後まで順に Call オブジェクトを探していく。
  4. グローバルオブジェクトにも見つからなければ、x はこのスコープ内に存在しないので、エラーになる。

チェーンの先頭は一番内側にある Call オブジェクトで、外側に行くにつれて優先度が下がります。この繋がりをスコープチェーンといい、Call オブジェクトに含まれる「親の Call オブジェクトの場所」の情報をたどっていくことによって、スコープチェーンが成り立ちます。

スコープチェーンを理解することによって、次のようなコードで名前解決がどのように行われるかをイメージできるようになりました。

var a = "Global";
var b = "Global";

function outer() {
  var a = "Outer";

  function inner() {
    var c = "Inner";

    console.log(c); // > "Inner" (自身の Call Obj で発見)
    console.log(b); // > "Global" (inner なし → outer なし → Global Obj で発見)
    console.log(a); // > "Outer" (inner なし → outer の Call Obj で発見)
  }
  inner(); // 呼び出し
}
outer();   // 呼び出し

クロージャ

クロージャとは、ローカル変数の状態を保持できる関数のことです。 通常ローカル変数は関数の呼び出しが終わると破棄されますが、クロージャはローカル変数を参照し続けられます。

クロージャのつくりかた

ローカル変数を参照している関数内の関数のことをクロージャといい、次のようにつくります。

  1. 関数の中にさらに関数を作る
  2. 外側の関数のスコープ内で変数を定義する
  3. 関数の中で入れ子になった関数を戻り値として返す
  4. 内側の関数から、外側の変数を参照する

クロージャでカウンタをつくる

function fn(n) {
    var cnt = n;
    return function() { // この無名関数がクロージャ
        return ++cnt;   // スコープチェーンにより、外側のローカル変数 cnt を参照
    }
}
var f = fn(0);    // fn 関数の実行
console.log(f()); // > 1
console.log(f()); // > 2
console.log(f()); // > 3

上記のコードでは、次のようなことが内部で行われています。

  1. var f = fn(0); で fn 関数を実行
  2. fn 関数の戻り値は内側の無名関数なので、f には無名関数のアドレスが代入される
  3. このとき、無名関数が内部的に参照しているローカル変数 cnt も一緒に引っ張ってくる
  4. f(); とは、無名関数を実行すること(ここでは return ++cnt; )なので、cnt に渡した 0 がインクリメントされて 1 として返ってくる
  5. 次に f(); を実行したときも、匿名関数が cnt を参照し続けているため、先ほどの 1 をさらにインクリメントした 2 が返ってくる

内側の関数は、スコープチェーンによって、外側の関数の Call オブジェクトを参照し続けることができます。

独立したスコープチェーン

先ほどのカウンタで、fn 関数の実行を複数行った場合の挙動は次のとおりです。

function fn(n) {
    var cnt = n;
    return function() {
        return ++cnt;
    }
}
var f1 = fn(0);    // fn 関数の実行 1
var f2 = fn(10);   // fn 関数の実行 2
console.log(f1()); // > 1
console.log(f2()); // > 11
console.log(f1()); // > 2
console.log(f2()); // > 12

Call オブジェクトは関数の実行のタイミングで生成されます。よって、f1 と f2 で fn 関数を実行(インスタンス化)することで、別々の Call オブジェクトが生成され、スコープチェーンも別物になります。それぞれのローカル変数 cnt も別々の記憶領域として確保され、独立しています。

fn 関数は、インスタンス化するときに、一度だけ実行されるコンストラクタの役割をしています。 クロージャ自身はメソッド、クロージャから参照されるローカル変数 cnt はプロパティと考えられます。

クロージャによって、データの記憶領域をもったメソッドを作ることができます。

クロージャの使いどころ

クロージャでできることはなんとなく分かったけれど、実際にはどう使えばいいのかと考えていたところ、猿でもわかるクロージャ超入門 6 クロージャの応用例「注文ボタン」 のページで、なるほどという使い方が紹介されていたので、引用させていただきます。

ショッピングカートの「注文する」ボタンで、2重クリックすると2回決済されてしまうというサイトをたまに見かけます。 jQuery + クロージャを使うことで、これを防止してみましょう。

<form name="frm" id="frm">
<input type="submit" value="注文する" />
</form>
$(function(){
    var isClicked = false;

    $('#frm').submit(function(){
        if (isClicked) {
            alert('すでにクリック済みです。');
            return false;
        }
        isClicked = true;
    });
});

たしかにクロージャを使って、isClicked ローカル変数の状態を保存しておけば、2 重注文を防げますね。

まとめ

  • JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということ
  • 変数の名前解決は、スコープチェーンによって内側のローカル変数から順に探していく
  • スコープチェーンは、生成したインスタンスごとで別物になる
  • クロージャによって、ローカル変数の状態を保持できるメソッドをつくれる

参考サイト & 本 Thx♡

JavaScript のスコープを理解する

スコープとは、変数の有効範囲のことで、プログラムのどの場所から参照できるかを決める概念です。

スコープの種類

JavaScript のスコープには、グローバル変数とローカル変数の 2 種類あります。

グローバル変数 ローカル変数
関数の外(トップレベル)で宣言した変数 関数の中で宣言した変数, 関数の仮引数
プログラム全体から参照できる その関数の中でのみ参照できる

ブロックスコープは存在しない

Java などの言語では、if や for などの {} で囲まれたブロックごとにもブロックスコープがありますが、JavaScript には存在しません。 JavaScript でどうしてもブロックスコープを使いたい場合は、with 命令を使う方法や、無名関数を定義と同時に呼び出すなどの方法で、擬似的にブロックスコープを作ることは可能です。

補足 : let を使うとブロックスコープがつくれると教えてもらったのですが、ちゃんと調べてないので、調べたらここに追記します!

スコープの役割

  • 同じ名前の変数が、意図せず競合することを避ける。
    スコープが違う場合、同じ名前の変数であっても別物として扱われる。
  • ローカル変数の記憶領域は、関数の実行が終わり次第、破棄される。
    グローバル変数はプログラムが終了するまで記憶領域を確保するため、関数内でしか必要ない変数までグローバル変数にした場合、無駄にメモリを消費することになる。

宣言時の var の有無

var を省略して変数宣言をした場合、関数内での宣言であっても、その変数はグローバル変数になってしまいます。 混乱を避けるため、変数宣言の var は必ずつけること。

ローカル変数の有効範囲

ローカル変数は「関数全体で有効」なので、同じ関数内であれば、変数宣言より前のコードからアクセスできます。 これをホイスティング(巻き上げ)と呼びます。

var scope = "Global";

function getValue() {
    console.log(scope);  // > undefined
    var scope = "Local";
    return scope;
}

console.log(getValue()); // > "Local" (ローカル変数を参照)
console.log(scope);      // > "Global" (グローバル変数を参照)

getValue 関数の console.log(scope); は、変数宣言 var scope = "Local"; より前にありますが、ホイスティングによってグローバル変数の scope ではなく、ローカル変数の scope を参照しています。 ただし、変数宣言はホイスティングされますが、代入はされないために undefined が返っています。

ホイスティングとは、JavaScript の内部で、上記コードの getValue 関数を、下記のように書き換えているようなイメージです。

function getValue() {
    var scope;           // ホイスティングで関数宣言だけを先頭に移動
    console.log(scope);  // > undefined
    scope = "Local";     // 代入は元の位置にそのまま残る
    return scope;
}

Java などのブロックスコープがある言語では、できるだけスコープを小さくするために、必要になった場所で変数宣言をしますが、JavaScript にはブロックスコープがないので、関数内の先頭に変数宣言を持っていった方が、直感的なスコープと実際のスコープが一緒になるので分かりやすいです。

関数の入れ子

関数を入れ子にして定義すると、それぞれの関数ごとに独自のローカルスコープを持ちます。 内側と外側のローカル変数が同じ名前の場合は、内側のローカル変数が優先されます。

仮引数のスコープ

仮引数もローカル変数です。

var n = 0;

function incrementValue(n) {
    n++;
    return n;
}

console.log(incrementValue(n)); // > 1 (ローカル変数を参照)
console.log(n);                 // > 0 (グローバル変数を参照)

incrementValue 関数の仮引数は、グローバル変数と同じ名前ですが、incrementValue 関数内のみで有効で、グローバル変数に影響は与えません。 また、incrementValue 関数の外から変数 n を参照した場合は、グローバル変数を参照します。 スコープの外から、スコープの中の変数を参照することはできません。

同じ名前でも別の変数として扱われるというのはつまり、仮引数の名前を変えた下記のコードと同じことです。

var n = 0;

function incrementValue(m) {    // n を m にするのと同じ意味
    m++;
    return m;
}

console.log(incrementValue(n)); // > 1 (ローカル変数を参照)
console.log(n);                 // > 0 (グローバル変数を参照)

引数で値を渡すということは、渡した先の関数のローカル変数に、渡した値をコピーしているようなイメージです。 よって、コピー先(ローカル変数)をいくら変更しても、コピー元(グローバル変数)に影響は与えません。

仮引数で参照型を渡したときのスコープ

参照型とは、値そのものではなく、その値があるメモリ上の番地(アドレス)が格納される変数のことです。 関数 function、配列 array などは参照型なので、これらの変数には実際のオブジェクトが格納されているわけではなく、別の場所にあるオブジェクトのアドレス情報が格納されているだけです。

引数で参照型を渡すということは、オブジェクトそのものをコピーしているわけではなく、アドレス情報をコピーしているだけなので、元々のオブジェクトはひとつのままです。

var ary = [1, 2];

function addValue(ary) {
    ary.push(3);
    return ary;
}

console.log(addValue(ary)); // > [1, 2, 3] (ローカル変数を参照)
console.log(ary);           // > [1, 2, 3] (グローバル変数を参照)

仮引数で渡されたアドレスにあるオブジェクトの値を関数内で変更すると、グローバル変数からも同じオブジェクトを参照するので、どちらも値が変わっています。仮引数はローカル変数ですが、参照渡しの場合はスコープの話からは外れます。

まとめ

  • グローバル変数はプログラム全体から参照でき、ローカル変数はその関数の中でのみ参照できる。
  • ローカル変数は var の有無でスコープが変わるので、変数宣言の var は必ずつける。
  • ホイスティングによる混乱を避けるため、関数内の先頭に変数宣言をまとめて書く。
  • 仮引数もローカル変数のスコープだが、参照渡しの場合は関数外から参照するオブジェクトにも影響を与えるので気をつける。

長くなったので、スコープチェーンとクロージャについては分割して書きます。たぶん今日中に。

→ この話の続き : JavaScript のスコープチェーンとクロージャを理解する

参考にした本 Thx♡