【JS】オブジェクト指向〜Chapter1 ブレイクスルーJavaScript フロントエンジニアとして越えるべき5つの壁〜
今回のChapter1の概要と達成できること
優れたプログラムのための機能について理解する。
すなわち
・プロトタイプ
・クロージャ
・オブザーバー
・this
を理解し、最終的にはリアルタイムバリデーションを実装するところまで本記事にてまとめた。
オブジェクト指向プログラミングとは
一連の処理から同じ処理を抜き出して1箇所にまとめ、
適切にグループ化、部品化した上で組み合わせながらプログラムを完成させること。
また部品同士はなるべく依存しないようにする(疎結合)
プロトタイプ
prototypeメソッドを使うことでメモリ節約になる。
コンストラクタにはあまりたくさんのものを入れないこと
ダメな例↓
function Human(name){ this.name = name; this.greet = function(){ ........ } } var alice = new Human("Alice"); alice.greet();
良い例↓
function Human(name){ this.name = name; } Human.prototype.greet = function(){ .......... } var alice = new Human("Alice"); alice.greet();
つまりコンストラクタに書くとインスタンス化のたびにgreetが作られてしまうのに対して、
prototypeで後から追加してあげることでインスタンス化した時はgreetがない(nameだけ)の状態となってメモリ節約。
クロージャ
クロージャとは
実行時の環境ではなく、変数自身が定義された環境が保持されること。
実装としては、関数内に関数をつくり、その中で変数を定義することで変数を隠す。
クロージャなし↓
var count1 = 0; function counter1() { count1++; console.log(count1); } var count2 = 0; function counter2() { count2++; console.log(count2); } counter1(); // 1 counter1(); // 2 counter2(); // 1 counter2(); // 2 count1 = 100; counter1(); // 101 counter1(); // 10
クロージャあり↓
function createCounter() { var count = 0; //ローカル変数のため、外部からアクセス不可 。宣言なので、2回目以降呼ばれてもスルーしてreturnにいく。 return function() { count++; console.log(count); }; } var counter1 = createCounter(); counter1(); // 1 counter1(); // 2 counter1(); // 3 var counter2 = createCounter(); counter2(); // 1 counter2(); // 2 count = 100; //関数内のcountにはアクセス不可のため、動作に影響しない counter1(); // 4
オブザーバー
オブザーバーとは
JavaScriptデザインパターンの一つで、最もよく使用される。
あるオブジェクトの状態が変化した時に、あらかじめ登録しておいた監視者に対して通知を行う。
①Observerオブジェクトに監視者を登録
↓
②通知者がイベントを通知
↓
③監視者がイベント通知を受け取り、目的を実行する
※ちょっとわかりにくいので例に出すと↓
出版社:通知者
読者:監視者
読者は出版社が発売する本を購入したいが、出版社は一人一人に通知するのが面倒。
出版社は本がでたら通知をし、通知を受け取った読者がすぐに買いに行くことができる。
オブザーバーのコード↓
function Observer() { this.listeners = {}; } Observer.prototype.on = function(event, func) { if (! this.listeners[event] ) { this.listeners[event] = []; } this.listeners[event].push(func); }; Observer.prototype.off = function(event, func) { var ref = this.listeners[event], len = ref.length; for (var i = 0; i < len; i++) { var listener = ref[i]; if (listener === func) { ref.splice(i, 1); } } }; Observer.prototype.trigger = function(event) { var ref = this.listeners[event]; for (var i = 0, len = ref.length; i < len; i++) { var listener = ref[i]; if(typeof listener === "function") listener(); } }; var observer = new Observer(); var greet = function () { console.log("Good morning"); }; observer.on("morning", greet); observer.trigger("morning"); //Good morning var sayEvening = function () { console.log("Good evening"); }; observer.on("evening", sayEvening); observer.trigger("evening"); // Good evening
これはおそらく最初のコンストラクタlisnersオブジェクトに
morning: greet(),
evening: sayEvening();
というのが登録されていき、triggerで指定して呼び出されるのだと思う。
ともあれ、上記コードで複数イベントに対応したオブザーバーが完成した。
thisを理解する
下記の場合はmikeがこの関数内のthisとなる
function Human(name) { this.name = name; }; Human.prototype.greet = function() { console.log("Hello " + this.name); }; var mike = new Human("Mike"); mike.greet(); // Hello Mike
thisを操作(束縛)する
thisは呼び出し元で決まるが、下記3つのメソッドで操作することができる
・call(object,arg1,arg2.....)
・apply(object,Array)
・bind(object,arg1,arg2.....)
callとapplyは関数をすぐ実行。第二引数の渡し方が違うだけで機能は同じ。
bindは、呼び出された時に新しい関数を生成し、値を束縛する。
callの例
function Human(name) { this.name = name; } function greet(arg1, arg2) { console.log(arg1 + this.name + arg2); } var mike = new Human("Mike"); greet.call(mike, "Hello ", "!!"); // Hello Mike!
mikeオブジェクトをcallメソッドを使って渡してあげることで、thisを決められる。というか決まる。
applyの例
function Human(name) { this.name = name; } function greet(arg1, arg2) { console.log(arg1 + this.name + arg2); } var mike = new Human("Mike"); greet.apply(mike, ["Hello ", "!!"]); // Hello Mike!!
上に同じ。
bindの例
function Human(name) { this.name = name; } function greet(arg1, arg2) { console.log(arg1 + this.name + arg2); } var mike = new Human("Mike"); var greetMorning = greet.bind(mike); greetMorning("Good Morning ", "!!"); // Good Morning Mike!!
上に同じといえば同じだが、
greetの関数をmikeバージョンとしてつくるので、thisがmikeになる。
リアルタイムバリデーションを作る
フォームを作るものと仮定する。
data-●●と書いて独自データ属性を使う。
今回はdata-errorとかにする。
Modelとは
データ構造を扱うコード。
慣例的に「種類+Model」という名前にする。UserModelなど。
今回の役割
・Viewから値を受け取ってその値に対してバリデーションを実行する
・バリデーションの結果に応じてイベント通知
View
見た目の部分。
慣例的に「種類+View」という名前にする。
html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>realtime-validation</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="styles/style.css"> </head> <body> <div class="container"> <div class="page-header"> <h1>オブジェクト指向</h1> </div> <div class="row"> <div class="col"> <label for="">アカウントを作成</label> </div> <div class="col"> <input type="text" placeholder="4文字以上8文字以内で入力してください" data-minlength="4" data-maxlength="8" required> <ul> <li data-error="required">必須項目です</li> <li data-error="minlength">4文字以上で入力してください</li> <li data-error="maxlength">8文字以内で入力してください</li> </ul> </div> </div> <div class="row"> <div class="col"> <label for="">パスワードを作成</label> </div> <div class="col"> <input type="text" placeholder="4文字以上8文字以内で入力してください" data-minlength="4" data-maxlength="8" required> <ul> <li data-error="required">必須項目です</li> <li data-error="minlength">4文字以上で入力してください</li> <li data-error="maxlength">8文字以内で入力してください</li> </ul> </div> </div> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <script src="scripts/app.js"></script> </div> </body> </html>
ul { margin: 0; padding: 0; list-style: none; } #container { padding: 40px; } h1 { font-size: 16px; font-weight: bold; } .row { margin-top: 2em; display: table; width: 100%; } .row .col { display: table-cell; } .row .col:first-child { width: 200px; } .row .col label { font-weight: bold; } .row .col input { font-size: 100%; outline: none; border: none; border-bottom: 1px solid #9e9e9e; padding: 0.5em 0; min-width: 400px; } .row .col input.error { border-color: #e51c23; } .row .col ul { margin-top: 0.5em; } .row .col li { display: none; color: #e51c23; background: url("../images/error.png") no-repeat left center; -webkit-background-size: 12px 12px; -moz-background-size: 12px 12px; background-size: 12px 12px; padding-left: 20px; margin-top: 0.5em; } .row .col li:first-child { margin-top: 0; }
js
//Modelのコンストラクタ function AppModel(attrs) { this.val = ""; this.attrs = { //使用するバリデーションパターンオブジェクトを追加 required: attrs.required || false, maxlength: attrs.maxlength || 8, minlength: attrs.minlength || 4 }; this.listeners = { //オブザーバーの機能 valid: [], invalid: [] }; } //引数で受け取ったvalとthis.valを比較して変化がなければ何もしない。 AppModel.prototype.set = function(val) { if (this.val === val) return; //変化があったらthis.valにvalを代入 this.val = val; this.validate(); }; //上で変化があったらバリデーション実行 AppModel.prototype.validate = function() { var val; this.errors = [];//バリデーションでエラーが出たら保存するための配列 for (var key in this.attrs) { val = this.attrs[key]; if (val && !this[key](val)) this.errors.push(key);//keyは0-2まで3つあり、エラーがある場合は順々に格納されていく。minlengthとか //つまりthis["maxlength"]となったりするので、AppModel.prototype.maxlengthを参照する。 } //バリデーション後、this.errorsが空ならvalidイベント通知、 //空でないならinvalidイベントを通知する this.trigger(!this.errors.length ? "valid" : "invalid"); }; //オブザーバーの通知に追加 AppModel.prototype.on = function(event, func) { this.listeners[event].push(func); }; //オブザーバーに登録されている通知を反復実行 AppModel.prototype.trigger = function(event) { $.each(this.listeners[event], function() { this(); }); }; //パターン1 値が空かどうか判定 AppModel.prototype.required = function() { return this.val !== ""; }; //パターン2 文字数がnum以上かどうか判定 AppModel.prototype.maxlength = function(num) { return num >= this.val.length; }; //パターン3 文字数がnum以下かどうか判定 AppModel.prototype.minlength = function(num) { return num <= this.val.length; }; //Viewをつくる function AppView(el) { this.initialize(el); this.handleEvents(); //インスタンス化したらhandleEvents実行 } AppView.prototype.initialize = function(el) { this.$el = $(el); this.$list = this.$el.next().children();//this.$elの隣のliをthis.$listに代入 var obj = this.$el.data(); //this.$elには<input type=.....required>がはいる想定 if (this.$el.prop("required")) { obj["required"] = true; } //AppModelにobjを渡してインスタンス化したものをthis.modelでmodelメソッドを使用できるようにする this.model = new AppModel(obj); }; AppView.prototype.handleEvents = function() { var self = this; this.$el.on("keyup", function(e) {//keyupイベントでonKeyupを登録 self.onKeyup(e); }); this.model.on("valid", function() {//modelイベント(validイベント)にonValidを登録 self.onValid(); }); this.model.on("invalid", function() {//同上 self.onInvalid(); }); }; AppView.prototype.onKeyup = function(e) { var $target = $(e.currentTarget); this.model.set($target.val());//inputの値をmodelにセットする }; AppView.prototype.onValid = function() { this.$el.removeClass("error"); this.$list.hide(); }; //エラーの場合はthis.$listの中で該当のエラーだけ表示する AppView.prototype.onInvalid = function() { var self = this; this.$el.addClass("error"); this.$list.hide(); $.each(this.model.errors, function(index, val) { self.$list.filter("[data-error=\"" + val + "\"]").show(); }); }; //inputを引数としてインスタンス化する $("input").each(function() { new AppView(this); });