RitoLabo

JavaScript(jQuery)による継承とプロトタイプチェーンの基本と利点

  • 公開:
  • カテゴリ: JavaScript jQuery
  • タグ: JavaScript,jQuery,Prototype

JavascriptはES6でclassキーワードが導入されましたが、糖衣構文(シンタックスシュガー)であり現状ではプロトタイプベースのオブジェクト指向言語であると分類されています。

そんなJavaScriptのオブジェクトは、プロトタイプオブジェクトのプロパティを遡るようにして保持しています。

今回は、JavaScript&jQueryを使ってPrototypeチェーンを実装していきます。

アジェンダ
  1. 開発環境
  2. ベースコード
  3. オブジェクトに機能と処理を持たせる
  4. プロトタイプチェーンを用いる事の利点

開発環境

今回の開発環境は以下の通りです。

  • HTML 5
  • jQuery v3.3.1

尚、今回はコンパイルが不要(ES6不使用)の範囲でのPrototypeチェーンを実装していきます。

ベースコード

今回はプロトタイプチェーンのベースを示す事がメインなのでとりとめて何かの機能を作るものではありませんが、動作確認の為にボタンやリストの追加などを行います。

まずは基本となるHTMLコードを記します。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>jquery prototypeチェーン</title>
</head>
<body>
<div class="container">
<div class="main">
<ul class="list"></ul>
</div>
</div>

<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
crossorigin="anonymous"></script>
<script src="js/prototype_init.js"></script>
<script>

</script>
</body>
</html>

読み込んでいるのはjQueryのコアソース(CDN)と、今回のスクリプトを記述するJSファイルです。

また、HTMLはcontainerクラス内にあるmainクラスのを主軸としてJSを展開していきます。続いて、JSファイルへベースのJSを記述していきます。

js/prototype_init.js
/**
* コンストラクタ
* @param name string 機能の識別名
* @param target_el jQueryElement 操作対象要素
* @param data array データ配列
* @constructor
*/
Main = function(name, target_el, data) {
this.name = name;
this.target_el = target_el;
this.data = data;
};

/**
* インスタンス生成
* @param name string 識別名
* @param class_name string 対象クラス名
* @param data array データ配列
* @return {Main}
*/
function getInstance(name, class_name, data) {
var class_name = "." + class_name;

// インスタンス生成
var obj = new Main(name, $(class_name), data);

return obj;
}

変数Mainにコンストラクタとしての役割を持つ関数を代入しています。今回ここでは、内包するプロパティを設定しています。

そして、getInstance関数でnew演算子を用いて、Mainのコンストラクタを動作させます。ここで生成されるオブジェクトが機能のメインオブジェクトとなります。

ではここでHTMLに戻って、これらを使ってMainオブジェクトを生成します。

index.html
<script>
// サンプル用に渡すデータ配列
var data = [
'A', 'B', 'C', 'D', 'E', 'F', 'G'
];

$(function() {
 // Mainオブジェクトを生成
var main = getInstance('サンプル機能', 'main', data);
});
</script>

getInstanceメソッドを使ってMainオブジェクトを生成します。その際に、コンストラクタへ「機能名」「対象クラス名」「サンプルデータ配列」を渡しています。あとはJSで 定義した通り、これらを初期値として保持するMainオブジェクトが生成されます。

オブジェクトに機能と処理を持たせる

ベースとなるオブジェクトが作成できたら、このオブジェクトに機能を付与していきます。

今回は、ボタンをクリックしたらリストを表示する。という機能を付けるので、それぞれの処理メソッドや、イベントの登録を定義します。

Mainオブジェクトにメソッドを追加するには以下のように記述します。

Main.prototype.methodName = function() {
// 処理
};

プロトタイプチェーンの定義の仕方は何通りかありますが、今回は外部から定義していく形にしています。 この方式で機能の追加、そしてイベントの登録を行ったもの(完成形)が以下になります。

js/prototype_init.js
/**
* コンストラクタ
* @param name string 機能の識別名
* @param target_el jQueryElement 操作対象要素
* @param data array データ配列
* @constructor
*/
Main = function(name, target_el, data) {
this.name = name;
this.target_el = target_el;
this.data = data;
this.browser = this.findBrowser();
};
/**
* 自身の識別名を返却する
* @return string
*/
Main.prototype.getName = function() {
return this.name;
};
/**
* 自身の操作対象要素を返却する
* @return jQueryElement
*/
Main.prototype.getElementTarget = function() {
return this.target_el;
};
/**
* データ配列を返却する
* @return array
*/
Main.prototype.getListData = function() {
return this.data;
};
/**
* ブラウザ名を返却する
* @return string
*/
Main.prototype.getBrowser = function() {
return this.browser;
};
/**
* ブラウザを判定する
* @return string
*/
Main.prototype.findBrowser = function() {
var userAgent = window.navigator.userAgent.toLowerCase();
var ret;

switch(true) {
case (userAgent.indexOf('msie') !== -1):
case (userAgent.indexOf('trident') !== -1):
ret = 'ie';
break;
case (userAgent.indexOf('edge') !== -1):
ret = 'edg';
break;
case (userAgent.indexOf('chrome') !== -1):
ret = 'chrome';
break;
case (userAgent.indexOf('safari') !== -1):
ret = 'safari';
break;
case (userAgent.indexOf('firefox') !== -1):
ret = 'firefox';
break;
case (userAgent.indexOf('opera') !== -1):
ret = 'opera';
break;
default: ret = false; break;
}

return ret;
};
/**
* データからリストを生成する
* @return void
*/
Main.prototype.addList = function() {
// コールバック
function callback(self) {
return function(i, param) {
self.getElementTarget().find('ul.list').append('<li>' + param + '</li>');
}
}

// ループ
$.each(this.getListData(), callback(this));

// ついでに使用ブラウザもリストに加える
this.getElementTarget().find('ul.list').append('<li>' + this.getBrowser() + '</li>');
};

/**
* 初期化処理
* @return void
*/
Main.prototype.init = function() {
// ボタンを追加する
this.getElementTarget().append('<button class="btn">ボタン</button>');
};

/**
* インスタンス生成
* @param name string 識別名
* @param class_name string 対象クラス名
* @param data array データ配列
* @return {Main}
*/
function getInstance(name, class_name, data) {
var class_name = "." + class_name;

// インスタンス生成
var obj = new Main(name, $(class_name), data);

// 初期化処理
obj.init();

// イベント登録
$(document).on('click', class_name + ' button', function(e) {
obj.addList();
});

return obj;
}

イベントの登録は、getInstance関数の中で生成したオブジェクトを用いて行います。

このようにして、定義された機能は、Mainオブジェクトを生成した後に、内部でも外部でも参照でき利用する事が可能です。つまりは、関数の中で別の関数を呼んだり、Mainオブジェクトを取得した変数(外部)からもコールできます。

$(function() {
 // Mainオブジェクトを生成
var main = getInstance('サンプル機能', 'main', data);

// 外部から利用
alert(main.getBrowser()); // ←Mainオブジェクトで定義したファンクションを外部からでも使用できる

});

プロトタイプチェーンを用いる事の利点

JSを書く上でプロトタイプチェーンを使う事の利点の1つは、ソースコードを1つの機能として収束できる点だと思います。

フレームワークを用いないJavascriptや、jQueryでもそうですが、WEBアプリケーションの規模が大きくなればなるほどソースコードも増え、気が付けばJSだけで何千・何万という行数になる事も少なくありません。

最近ではReactやAngularJSも出てきて体系だったソース管理(MVW・MVVM・MVCという意味で)が行えるようになっていますが、jQueryなどは工夫しないとアプリケーションの規模拡大と共にソースコードがただただ肥大化していくので、そこを嫌うエンジニアも少なくありません。(チーム開発をガンガン行うところではやはり管理が難しいのもわかる)

ただし、まだまだレガシーとして活用され、現在も開発が進められているjQueryは無下にできない優秀なJSフレームワークの1つだと私は考えています。

プロトタイプチェーン自体は別にjQueryのものではありませんが、Javascriptを効率よく書く為に有効な手段の1つがこのプロトタイプチェーン(継承)になります。

また、プログラマ的な利点の1つとして、thisスコープの明確化も上げられます。

Javascriptを定義していくと、thisのスコープを見失う場合(このthisが何を指しているかわからなくなる・このthisが意図しているスコープじゃなかった問題)がありますが、Prototypeチェーンで定義するとthisのスコープが同一オブジェクト内で自身(のオブジェクト)に向き、スコープが揃うので、1つの機能として迷子にならずにプログラミングを進められるという利点もあります。

そして、jQueryに至ってはやはり直接DOM操作を行う上でその対象が決まっていたりする事が多いので、対象のjQueryElementを自身に内包しておけば、それを呼び出すだけで良くなり、実装効率も向上します。

今回のデモでは何かの機能を作ったわけではないのであまりソースコード自体の解説を行いませんでしたが、対象となる要素の取り回しやデータの方向などはある程度記述しておいたので、是非読み解いて自分でも書いてみてください。