チェ・ゲバムラの日記

脱犬の道を目指す男のブログ

【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>

css

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);
});