Comments
Description
Transcript
最強オブジェクト指向言語 JavaScript 再入門!
最強オブジェクト指向言語 JavaScript 再入門! 自己紹介 ‣ ノジマユウジ @yuka2py ‣ 株式会社フォーエンキー 代表取締役 ‣ ‣ 絶賛 お仕事募 集中 システム開発、 グラフィックデザイン、 DTPや印刷なども PythonとJavaScriptが大好 き(Dartに興味深々) ‣ 様々なWebアプリケーション 設計・構築・運用。またWP によるシステム開発、プラグ イン開発などが主なお仕事 ‣ おしゃれも大好き☆ リボンやお花が好き☆ ‣ 参加コミュニティ ● ● ● WordBench 神戸 HTML5-WEST.jp 日本Androidの会 神戸支部 去年のボク Python 1% Design お嫁 Windows(C#) Web(PHP/JS) Android iOS Design Python 2012年11月2日 株式会社フォーエンキー調べ 15% iOS 4% Android 10% Web(PHP/JS) 10% Windows(C#) 20% お嫁 40% 最近のボク お嫁 Web/WordPress Windows Android Design Python Python Android 1% 5% Design Windows 5% 15% お嫁 50% Web/WordPress 25% 2013年6月1日 株式会社フォーエンキー調べ 愛 恐 はじめに 本セッションでは、JavaScript でオブジェクト指向プログ ラミングを行う際に備えておくことが望ましい、基礎知識 や概念について解説します。 対象者 ・JavaScript でアプリケーションを構築できる方 ・JavaScript におけるオブジェクト指向プログラミングの 実現手法や原理への理解を深めたい方 ・Java 的なクラスベースの言語との違いに違和感や混乱を 感じてらっしゃる方 Simple and Powerfull JavaScript はとてもシンプルな言語仕様でありながらも、 とても強力にオブジェクト指向プログラミングをサポートしています。 var obj = {} JavaScript はオブジェクトが基本です。 var obj = { key: value } オブジェクトの構造は、単純なキーと値の組み合わせによるハッシュテーブルのよ うなものです。とても大切な概念です。難しく考え過ぎないでください。 var f = function() {...} 関数は、JavaScript において「第一級オブジェクト」…簡単に言うと「値」です。 値ですから、変数に代入したり、取り出したり、受け渡しが制約なく行えます。 var obj = { key: function() {...} } もちろん、関数をオブジェクトのプロパティに格納する事もできます。オブジェクトが 単純なハッシュテーブルで、関数も値であるならば、それはとても自然なことですね。 var obj = { key: function() {...} } 言い換えると、オブジェクトのメソッドはそれをメソッドとして持つオブジェクト に束縛されているのではなく、 オブジェクトによってただ参照されているだけです。 おしながき ‣ ‣ クラスはあるか? プロトタイプチェイン ● ‣ ‣ JSでのオブジェクト指向プ ログラミングの基礎概念 ‣ オブジェクトの生成 ● ● newとコンストラクタ関数 プロトタイプチェインとの 関連 スコープチェイン ● var の意味 ● スコープチェインの正体 クロージャ ● ‣ 定義された環境を束縛する this ● thisの決定 ● thisのコントロール クラスはあるか? 追記。本資料では、いわゆる「Java言語的なクラス」を「クラス」と定義してお話しています。それは、こ のスライドを書きはじめた動機が、Web上でJava言語的OOPで解釈しようとする記事が多くあり、それが JSを学ぼうとされる方の理解を混乱させているように思えたので、その混乱を解消したかったからです。 また、ES.next の class 構文も説明するには早そうなこと、かつさらに理解を難しくするものとして割愛。 なお、これらの省略や定義付けに違和感を覚えられる方は既に本資料の内容は理解済みとの認識です! (*'-'*) Q JavaScript で クラスは作れますか? (?_?) A 無理ポ。 Q では、クラスっぽいものは 作れますか? (?_?) A だから、無理ポ。 だって「オブジェクト」しか 無いんだもん。 「クラス」って何それ? おいしいの? (・ ・) クラスっぽいものは出来るっていう方も いらっしゃると思いますが… と僕は思う それが理解を難しくする クラスが無いのに、クラスの概念を持ち込んで 理解しようとしたら、メンドクサクなりそうでないっすか? そもそも… オブジェクト指向に クラスなんて要らない。 クラスや型は、オブジェクト指向を 扱い易くするためのテクニックのひとつに過ぎません。 必要なのはオブジェクト。 だって「オブジェクト指向」だしぃ∼ だから JavaScript は 代わりに、 もっと面白い方法を選んだ。 (↑あくまでも個人的主観) それが… プロトタイプチェイン 「クラス」とは? の前に 似たオブジェクトを 量産するための仕組み クラス Class (型) new new new クラスという「型」を元にして、 オブジェクトを生成するイメージ クラスから生成されたオブジェクトは クラスの特性(属性や機能)を受け継ぐ Object(実体) Object(実体) Object(実体) でも、 クラスを使わなくても、 同じ機能や属性(性質)を持った オブジェクトを量産できれば OKですよね。 では、JavaScript では? Object (実体) (prototype) dele gate dele gate dele gate プロトタイプ オブジェクトは、自分自身が備えて いない特性(機能や属性)を、別の オブジェクトにお願い(委譲)する イメージ。この時、お願いする先を 「プロトタイプ」と言ったりします。 Object(実体) Object(実体) Object(実体) …な、感じで実現します。 コードで見てみる。 var objA = { name : 'Yome', say : function () { alert('I Love ' + this.name); } }; var objB = { name : 'Nikole' }; objB.__proto__ = objA; var objC = { }; objC.__proto__ = objB; objC.say(); I Love Nicole 3つのオブジェクトが登場し ています。 ここで、3つ目の、objC は __proto__ 属性以外何も持っ ていませんが、I Love Nicole と表示されます。 var objA = { name : 'Yome', say : function () { alert('I Love ' + this.name); } 所有 }; var objB = { name : 'Nikole' }; objB.__proto__ = objA; 所有 var objC = { }; objC.__proto__ = objB; objC.say(); ここでポイントになるのは、 __proto__という属性。 こ の _ _ p r o t o _ _ を 介 して 、 objC が objB を経て objA ま での参照を保持できているこ とを確認してください。 I Love Nicole が表示される仕組み objC.say() メソッドのコール objC.say 無い objC.__proto__.say 無い (objB) objC.__proto__.__proto__.say (objB) アッタ! (objA) objC に対して say() がコールされた時、JavaScript は、まず objC 自身が「say」というキーの 値を持っているか調べます。しかし、見つからないので、次にその__proto__ …つまり objB を検 索します。また見つからないので、objB の __proto__ = objA を検索し、say を見つけます。 objC.name 属性の参照 objC.name 無い objC.__proto__.name アッタ! (objB) objC.__proto__.__proto__.say (objB) 使われ ない (objA) 同様に、objC の name を参照した時、JavaScript は objC 自身が name を持っているか確認し ます。しかし、見つからないので、次にその__proto__ …つまり objB を検索し、name を見つ けました。必要な名前が見つかった時点で、検索は終了します。 obj.__proto__.__proto__.property このように、自分に無い機能や属性を、 __proto__ に入っている別のオブジェクトから 連鎖的に検索して探す仕組みが… obj.__proto__.__proto__.property プロトタイプチェイン では、同じ特性を持った オブジェクトを量産するには? var objA = { name : 'Yome', say : function () { alert('I Love ' + this.name); } }; 同じプロト var objB = { name : 'Nikole' }; タイプを所有 すればOK objB.__proto__ = objA; var objC = { name : 'Gyu-Ri' }; objC.__proto__ = objA; objB.say(); // I Love Nikole objC.say(); // I Love Gyu-Ri クラスベースと 較べてみる。 ‣ クラスベース class A ‣ プロトタイプベース クラスから オブジェクト を作成 extends class B new obj A of B extends class C new obj A オブジェクト だけの世界 (delegete) obj B (delegete) (delegete) obj D obj C (delegete) new obj B of C obj C of C (delegete) obj E obj F クラスベースではクラスという型を拡張し、そこからインスタンスを作成するイメージです。 一方プロトタイプベースでは、必要な特性を持った他のオブジェクトを利用するイメージです。 プロトタイプチェインは、 以上です。 あれ? prototype 忘れてない? いえいえ、以上っすぅ。 だって prototype 無くても プロトタイプチェインできたし (・ ・) 分けて 考えると イイらしい 実は、prototype 属性は、 プロトタイプチェインではなく、 オブジェクトの生成で使われるもの。 ネット上の情報で、ここを一緒に解説している例を見掛けますが、 それが「ちょっと分かり難いなー」と個人的には思っています。 プロトタイプチェインはあくまでも__proto__の連鎖的検索であって、 prototype は検索対象では無いからです。 では… オブジェクトの生成 とはいえ… あまり見たコトない… obj.__proto__ = otherObj 実は __proto__は標準仕様ではありません。 つまり、__proto__ が利用できない環境もあり、だから通常、このようなことはしません。 ここまでは、プロトタイプチェインの概念を分かり易くするために __proto__ を使って 説明していましたが、そういうことなので、実際のプログラミングではこんなことしな いでくださいねー。 (・ ・)/ では、逆に、良く見かけるのは… こっちのが、 よく見る感じ! var o = new Person('Nicole') これは new 演算子とコンストラクタ関数によるオブジェクトの生成です。 JavaScript におけるオブジェクトの生成で、よく見られるコードです。 このあたりの記法が、クラスベースの言語を学んだ方を惑わしますね♪ この手法による オブジェクト生成について 見てみます。 よく見るコードの例 準備 var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } var person = new Person('Nicole'); 利用 準備して、利用しているのは、間違いありません。 よく見るコードの意味 クラス 定義? var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } var person = new Person('Nicole'); 利用 クラスは存在しないので、もちろん「クラス定義」ではありません。 よく見るコードの意味 コンスト ラクタ関数 の定義 var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { prototype alert('Hello ' + this.name); の拡張 } var person = new Person('Nicole'); 利用 とこんな感じで、ちゃんと分けて理解しておくのが良いと思います。 コンストラクタ関数とは? オブジェクトの初期化に 使われる関数。 new 演算子と組み合わせて関数をコールすることで、関数はコンストラクタ関数 として実行され、オブジェクトの生成に利用されます。 new 演算子は? オブジェクトの 生成・継承・初期化を行う 指示をしてます。 こんなふうに new した時は… var o = new Person('Nicole') このようなコードを実行した時、 実際にはどんな意味合いになるか? だいたいこんな意味 var newObj = {}; newObj.__proto__ = Person.prototype; Person.apply(newObj, arguments); return newObj; 擬似 コード ※分かり易さの為に詳細を割愛しています。 だいたいこんな意味 var newObj = {}; newObj.__proto__ = Person.prototype; ①新しい Object が 生成される Person.apply(newObj, arguments); return newObj; new する度に、新しい個別のオブジェクトが生成されます。 だいたいこんな意味 var newObj = {}; newObj.__proto__ = Person.prototype; Person.apply(newObj, arguments); return newObj; ②プロトタイプのオブ ジェクトの参照を代入 コンストラクタ関数の Person.prototype を、新しいオブジェクトの __proto__ に 参照を代入し、プロトタイプチェインの仕組みで、その特性を継承します。 だいたいこんな意味 var newObj = {}; ③ newObj を this に、コンストラクタ 関数を実行 newObj.__proto__ = Person.prototype; Person.apply(newObj, arguments); return newObj; これによって、コンストラクタ関数の中で、生成される新しいオブジェクトを this と して参照し、その属性をセットしたりできるようになります。 だいたいこんな意味 var newObj = {}; newObj.__proto__ = Person.prototype; ④完成した オブジェクトを返す Person.apply(newObj, arguments); return newObj; Person.prototype の特性を継承し、コンストラクタ関数によって初期化済みの、 新しいオブジェクトが返され、プログラムで利用できるようになります。 ポイント はココ newObj.__proto__ = Person.prototype; Person.prototype オブジェクトの機能が、 newObj で利用できるようになる! (プロトタイプチェインの仕組み) 整理すると… ‣ __proto__ ● プロトタイプチェインで 検索されるプロトタイプ オブジェクトが入ってる プロトタイプチェインで 使われるもの ‣ prototype ● ● プロトタイプオブジェク トの控え室 new とコンストラクタ 関数でオブジェクトを生 成する時に、__proto__ に代入される オブジェクト生成で 使われるもの コードで見てみる。 余計分かり難くなったらごめんなさいですが、 __proto__ と prototype の動きをなるべく丁寧に書いてみました。 ゆっくりじっくり読んでみてください。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); // true p.say(); // I Love Nicole p.name = 'Gyu-Ri'; p.say(); // I Love Gyu-Ri Person.prototype.name = 'Ha-Ra'; p.say(); // I Love Gyu-Ri delete p.name; p.say(); // I Love Ha-Ra })(); SAMPLE CODE (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } この準備で、右側のようなオブジェ クトが出来ます(コンストラクタ 関数の実行部は省略)。 ここで、Person.prototype に 入っているオブジェクトは、name と say の2つの属性を持ちます。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); した時の擬似コード new p.say(); p.name = 'Gyu-Ri'; p.say(); Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } 代入 var newObj = {}; newObj.__proto__ = Person.apply(newObj, ['Nicole']) return newObj; Person を newすると、擬似コードにあるような初期化が行われ、新しい オブジェクトの __proto__ に Person.prototype のオブジェクトが代入されます。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } 参照 p: { name:'Nicole' __proto__: } 結果、新しいオブジェクト p の __proto__ は、 Person.prototype に入っているオブジェクトへの参照を持ちます。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } True 同じ p: { name:'Nicole' __proto__: } Person.prototype が p.__proto__ に代入された直後なので、 当然ですが、同一性比較は true になります。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); 参照 I Love Nicole Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } p: { name:'Nicole' __proto__: } p.say() がコールされると、p 上に say を探すが見つかりません。 __proto__を って、say を見つけます。name は p 上で見つかります。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); 参照 I Love Gyu-Ri Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'nanashi' say: functon () {…} } } p: { name:'Gyu-Ri' __proto__: } 先ほどと同じですが、p 自身が持つ name が変更されたため、 当然、表示される内容は変化します。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); 参照 I Love Gyu-Ri Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'Ha-Ra' say: functon () {…} } } p: { name:'Gyu-Ri' __proto__: } Person.prototype.name を変更しましたが、 name は p 自身で見つかるので、結果に変化はありません。 (function() { var Person = function (name){ this.name = name; }; Person.prototype.name = 'nanashi'; Person.prototype.say = function () { alert('I Love ' + this.name); }; var p = new Person('Nicole'); alert( p.__proto__ === Person.prototype ); p.say(); p.name = 'Gyu-Ri'; p.say(); I Love Ha-Ra Person.prototype.name = 'Ha-Ra'; p.say(); delete p.name; p.say(); })(); Person: { prototype: { name:'Ha-Ra' say: functon () {…} } } 参照 削除されて無くなった p: { name:'Gyu-Ri' __proto__: } p 自身の name を削除すると、p 自身は name を持たなくなるので、 say も name も __proto__ から検索されて、結果、表示内容が変わります。 こんな感じです。 この解説で prototype 属性と __proto__ 属性それぞれの 役割や位置づけ、またその原理などがしっくりと理解でき たら嬉しいです。 それでは、次。 JavaScriptの もう一つの鎖などについて。 (チェイン) スコープチェイン スコープとは? 任意の変数に アクセスできる範囲。 …で、いいか。 (;゚ ゚) JavaScriptの2つのスコープ ‣ グローバルスコープ ● ● ● 文字通り「グローバル」 プログラム全体からアク セスできる ● ‣ ローカルスコープ ● ある特定の範囲でだけで アクセスできる ● JSでは基本的に、個々 の関数の中がローカルス コープになる ある関数の中(=ローカ ルスコープ)で作成され た変数は、その関数の 内側でのみ参照できる 関数の引数もその関数の 内側でのみで参照できる 例を見てみます。 function myfunc() { var name = 'Gyu-Ri'; } ローカル 変数 myfunc(); alert(name); //ReferenceError 外からは 見えないか らエラー でも逆に… var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); //738 } func2(); } func1(); 外側の変数 期待通り は見える 計算される ポイント JavaScriptのスコープは、 外から内は見えなくて、 内から外は見える。 「自分より外側は見える」ぐらいの、とてもシンプルな概念です。 もう少し見てみます。 var name = 'Nicole'; function myfunc() { 名前が同じでも スコープが違えば 別の変数 var name = 'Gyu-Ri'; } myfunc(); alert(name); //Nicole 別の変数なので、関数内 での代入は外側の name に影響しない。 var name = 'Nicole'; じゃ、これは? function myfunc() { name = 'Gyu-Ri'; } myfunc(); alert(name); //??? var name = 'Nicole'; function myfunc() { 同じ変数!! name = 'Gyu-Ri'; } myfunc(); alert(name); //Gyu-Ri 同じ変数なので、関数内 で代入したら外側の name も変わる。 なにが違った? var name = 'Nicole'; function myfunc() { var name = 'Gyu-Ri'; } Before var name = 'Nicole'; function myfunc() { name = 'Gyu-Ri'; } After var name = 'Nicole'; function myfunc() { var name = 'Gyu-Ri'; } var が無い!! var は、変数宣言。 「新しく作るよ」宣言。 var a = 123 新しく変数を作る時は必ず必要です。 言い換えると、宣言してない時は、 「既に有るよ」的な意味になる。 var a = 123; function func1() { var b = 3; さっきの例の 変数 a の場合 function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 外側の変数 は見える var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 このローカルスコープで、宣言さ れているのは c だけなので、 a と b は 既にあると解釈される …でも、func2 のローカルスコー プには、a と b は見つからない。 var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 そこで JavaScript は、一つ外側 の func1 のスコープで a と b を 検索する。 そして、b を見つけるが、 まだ a は見つからない。 var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 そこで JavaScript は、さらに 外側のスコープで a を検索し、 そして a を発見する。 こんな感じで、順繰りに 内側から外側へと探して行く。 なんとなく… プロトタイプチェインに 似てなくない? 変数 a への参照をちょっと オブジェクトっぽく書いてみた scope.a 無い (func2) scope.__scope__.a (func2) 無い (func1) scope.__scope__.__scope__.a (func2) (func1) (global) アッタ! やっぱり、 プロトタイプチェインに そっくり。 scope.__scope__.__scope__.identifier このように、今のスコープに無い識別子を、 より外側のスコープから連鎖的に 検索して探す仕組みが… scope.__scope__.__scope__.identifier スコープチェイン この事から、「グローバルスコープ」も、何も特別なものではなくて、単に「最も外側にあるローカルス コープ」という解釈もできます。実際のところ、グローバルスコープには幾つかの特別な役割も課せられてい ますが、感覚的なその理解は概ね正しいです。 難しく考えないことがポイント。 原理的には内から外への連鎖的なスコープの検索ですが、利用上は単に、「関数が 定義された環境(ソースコード上の見かけ)において、その外側は見える」とい うことに過ぎません。プログラマにとっては、とても直感的な仕様です。 余談ですが、 実はJavaScriptでは、変数のスコープでさえも、内部的にはオブジェクトで構成されています(=変数オブ ジェクト)。つまり、つまるところ、「スコープチェイン」も「プロトタイプチェイン」も、オブジェクトの特 殊なプロパティによる連結リストによるデータ構造と、それを終端に向かって連鎖的に検索するという、よく 似たシンプルな仕組みに過ぎません。またこれは、JavaScriptにおけるスコープの実体が、グローバルスコー プ(グローバル変数オブジェクト)をrootとする、変数オブジェクトが連なった巨大なオブジェクトのツ リー構造だということでもあります。興味深いですね。 残念ながら、それらのスコープを構成するオブジェクトのツリーを、プログラマが直接参照することは出来ま せん。しかし、プロトタイプチェインを構成する__proto__属性については、ECMA Script標準では無いも のの、幾つかのブラウザ実装では実際に目にしてその仕組みを確認することが出来ます。これを良く観察す ることで、「スコープチェイン」と「プロトタイプチェイン」両方の理解を深めることが出来るはずです。 さて、JavaScript のスコープには もうひとつ大事な概念があります。 クロージャ 早速のクロージャの サンプルコード。 var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 (・∀・)? サッキト オナジジャネ? そうです。 さっきのもクロージャ。 クロージャという言葉には広義なものと狭義なものとあります。 そして「広義」にはこれもクロージャです。 クロージャ の意味 引数以外の変数を実行時の環境ではなく、 自身が定義された環境(静的スコープ)において 解決することを特徴とする。 by Wikipedia(いつもありがとう) ようするに、 var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 この関数が 定義された 環境とは… var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 この関数を含 む全てのスコー プのこと スコープチェインによって、 関数定義の外側の変数まで 参照できましたよね! var a = 123; function func1() { var b = 3; function func2() { var c = 2; alert(a * b * c); } func2(); } func1(); //738と表示 さっきの 内側から外側は 見えるって コト! スコープチェインによって、 関数定義の外側の変数まで 参照できましたよね! スコープチェイン => クロージャ つまり JavaScript では、スコープチェインの仕組みが、 そのままクロージャとして機能することになります。 JavaScript を使っていると、ごくごく当たり前かも知れませんが…、 これが出来る汎用プログラミング言語は少し前までそう多くはありませんでした。 PHPでは 5.3 から、Java でも Java 8 から利用できるようになります。 では、どんなふうに使えるか? 変数の隠 、別名の利用。 みんなもう普通に使ってるよ。 (*'-'*) var Person = (function($) { 引数を利用して オブジェクトの 別名を利用 スコープの外から は見えない。 =隠 されてる var privateValue = 'Hogeta'; function Person(name) { ... } Person.prototype.xxxx = ... return Person; })(jQuery); 即時関数で ラップしてローカル スコープを作成 ここで、変数 privateValue や $ は、即時関数の外からは見 えない変数になりますが、こ のスコープの内側で定義され る関数からは参照することが できます。 即時関数は文字通り即時実行 さ れてそ の 処 理 を 抜 け ま す が、privateValue や $ と いった変数が、引き続きその 中で定義された関数から参照 できることがポイントです。 変数の永続化。 とても重要でパワフルな概念。 function newCounter() { var i = 0; return function() { i = i + 1; return i; } } 返された クロージャは i をずっと 参照 var c1 = newCounter(); alert(c1()); // 1 alert(c1()); // 2 alert(c1()); // 3 呼び出す毎 に i が1ずつ 増える ※コードはWikipediaのクロージャのページからの引用です。 newCounter は、新しい関数 オブジェクトを生成して返却 します。newCounter が実行 されるたび、変数 i が初期化 され、その i を参照するク ロージャが返却されます。 newCounter が返却するク ロージャを変数 c1 に受け 取って実行すると、実行毎に 1ずつ増えた値が得られます。 これは、newCounter が返し た関数オブジェクトが、変数 i を参照し続けていることで 実現されています。 このようにクロージャは、定 義された時点の環境を束縛し ています。 クロージャを使いこなすには? とりあえず、書く。 たくさんコードを書いて慣れるのが一番です。 (*'-'*) とてもシンプルで直感的な仕様なのですが、上手く利用するには訓練が必要です。 クロージャの利用シーンはとても広範囲なので、たくさん書いて少しずつ用法を 身につけていくと良いと思います。 では、最後に、 例のややこしいやつですよ。 this this も、ワリと簡単。 これこそ、クラスベース言語によくある考え方で理解しようとすると、 落とし穴にはまります。一旦、頭を真っ白してください。 this を使った例。 var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } var person = new Person('Nicole'); var btn = $('button#say-hello'); btn.on('click', function() { person.sayHello(); }); これは OK よくあるパターンです。イベントにコールバック関数をバインドしています。 ここで、btn.on に渡している関数はクロージャで、クロージャ変数として person を参照しています。 var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } var person = new Person('Nicole'); var btn = $('button'); btn.on('click', person.sayHello); これは NG 前の例では、呼び出したい関数をコールバックとして登録しているだけだったので、 sayHello 関数を直接バインドしたらどうだろう?とやってみると NG です…。 結果として、(多くの場合)「Hello undefined」と表示されます。 なぜか? ポイント 常識! 関数はオブジェクトに 束縛されない var obj = { key: function() {...} } 繰り返しになりますが、オブジェクトのメソッドはそれを持つオブジェクトに束縛され ているのではなく、その時そのオブジェクトによってただ参照されているだけです。 この意味に注意してください。 オブジェクトのメソッドは、 「オブジェクトによってただ参照されているだけ」 つまり… person の sayHello メソッドを 渡してる btn.on('click', person.sayHello); 「person の sayHello」を渡しているのではない。 sayHelloの中の関数オ ブジェクトを渡してる btn.on('click', person.sayHello); 「sayHello」に入っている、関数オブジェクトを渡している、が正解。 関数オブジェクトだけが渡されている、という理解が大切です。 では、this ってなに? ポイント 関数コール時にその関数が 所属していたオブジェクト。 = this 所属 this person.sayHello(); 呼び出し時に、所属していたオブジェクトが、 関数内での this になる では、 ただの関数呼び出しの時は? 所属 ??? ...nothing... sayHello(); ただの関数呼び出しで、関数がオブジェクトに所属していない時、 関数内の this は何になるのか? function sayHello() { alert('Hello ' + this.name); } sayHello(); global object = window 関数のコール時にレシーバーが無い時、this はグローバルオブジェクトになります。 ブラウザ実装の場合、これは通常 window オブジェクトです。 つまり ポイント this は、 呼び出し時に決定される ところで、 this が呼び出し元で 決定されるなら、 実は、呼び出し元で、 thisを操ることもできます。 this を操る thisを操る メソッド call apply bind 関数オブジェクトには、this をコントロールできる3つのメソッドがあります。 (関数オブジェクトだけに存在する) this call(object, arg1, arg2, ...) this apply(object, Array) 関数に渡す引数 this bind(object, arg1, arg2, ...) これらのメソッドの第1引数に与えた object が、関数内での this になります。 また、その後の引数は、それぞれ関数呼び出し時に関数に与える引数になります。 3つのメソッドの利用法。 call と apply は 関数を実行する call の例 function say(arg1, arg2) { alert(arg1 + this.name + arg2); } var person = new Person('Nicole'); say.call(person, 'Hello ', ' chan'); person を this として実行 call の第1引数が this に、 第2引数以降の引数が、 say関数の引数に。 apply の例 function say(arg1, arg2) { alert(arg1 + this.name + arg2); } var person = new Person('Nicole'); say.apply(person, ['Hello ', ' chan']); person を this として実行 call の第1引数が this に、第2引数の 配列の内容が、say関数の引数になる。 引数の渡し方以外は call と同じ var p1 = { name: 'Gyu-Ri', say: function (arg1, arg2) { alert(arg1 + this.name + arg2); } }; var person = new Person('Nicole'); p1.say.call(person, 'Hello ', ' chan'); 他のオブジェクト のメンバだったら? var p1 = { name: 'Gyu-Ri', say: function (arg1, arg2) { alert(arg1 + this.name + arg2); } }; var person = new Person('Nicole'); p1.say.call(person, 'Hello ', ' chan'); 他のオブジェクト のメンバでも関係ない 関数はオブジェクトに「束縛されていない」 ことを思い出してください。 ここで、関数 say が p1 に属していても、 指定した person が this になります。 bind は 関数に値を束縛する function say(arg1, arg2) { alert(arg1 + this.name + arg2); } var person = new Person('Nicole'); var say2 = say.bind(person); say2('Hello ', ' chan'); personをthisに束縛した 新しい関数オブジェクトを返す say2 関数の呼び出しでは、 常に person が this となる 結果:Hello Nicole chan と表示される bind の利用例 JavaScript OOP では非常に便利。 var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } var person = new Person('Nicole'); var btn = $('button'); btn.on('click', person.sayHello); さきほど NG だった例ですが... これは NGでした var Person = function (name) { this.name = name; } Person.prototype.sayHello = function() { alert('Hello ' + this.name); } こうすれば var person = new Person('Nicole'); var btn = $('button'); OK btn.on('click', person.sayHello.bind(person)); bind で関数に this を束縛できるので、そのまま渡せるようになる。 person を2回使ってるように見えるけど、そういう意味でないことに注意。 apply とクロージャの利用例 すこし長いし、元ネタは…だけど。 // 絶対 this を離さないユーティリティメソッド function __bind (self, func) { return function () { }; return func.apply(self, arguments); } // コンストラクタ関数とプロトタイプを用意 元のオブジェクト(self)と関 数(func)をクロージャで保持 し、必ず self に対して関数が 適用されるようにしている。 引数の数はまちまちなの で、apply を使って実行する function Person (name) { this.name = name; this.say = __bind(this, this.say); } Person.prototype.say = function (prefix) { alert(prefix + this.name); } コンストラクタ関数の中で、 __bind を実行し、返された 新しい関数をオブジェクト のメンバーとして保持する (関数の差し替え) // オブジェクト生成 var person = new Person('Nicole'); // 普通に利用 nicole.say('I Love '); // I Love Nicole // 他の値をバインドしてもOK setTimeout(person.say.bind(null, 'I Just Realy Love to '), 2000); say には prefix も渡したい ので、bind を使っているが、 第1引数に person でなく null を渡しても、this は変 更されない ということで… JavaScript でオブジェクト指向プログラミ ングを行う際に備えておくことが望ましい、 基礎知識や概念について解説してきました。 ちょっと欲張り過ぎましたが、 どれも一度理解したらとても簡単な概念です。 JavaScript にはクラスが無いこと、 オブジェクトが 単純なハッシュテーブルであること。 この2つの理解を深めると、 たとえば this の挙動も納得できます。 JavaScript では、このように単純なデータ構 造を幾つかのルールに沿って組み合わせるこ とで、とても柔軟で強力なオブジェクト指向 プログラミングを実現しています。 JavaScript が強力なのは、 このシンプルさの所以です。 しかしある場面では、 そのシンプルさが冗長なソースコードの記述 を要求するかも知れません。 それらの問題は、例えば CoffeeScript や TypeScript といった、JavaScript を基本と する別の技術で解決できるかも知れません。 ただ、そういった新しい技術を利用するにあ たっても、そのベースとなる JavaScript そ のものの基礎理解は、あなたの開発を大いに 助けてくれるはずだと、僕は信じています。 var obj = {} JavaScript はオブジェクトが全てです。 ご清聴、ありがとうございました! @yuka2py 書籍を執筆しました。 JavaScript についても基礎から 応用・発展まで詳しく書かれています。 良かったら書店で手に取ってください。 http://www.amazon.co.jp/dp/4798129682 この文書は クリエイティブ・コモンズ 表示 - 継承 2.1 日本 ライセンスの下に提供されています。