Canvas (Javascriptのオブジェクト,プロトタイプ,委譲,クロージャ,ローカル変数の寿命)
演習5-6
- マウスで大きさを指定して緑の三角形,赤の矩形,青の円を順に描画するスクリプトを作成せよ.(一度描画した図形はそのまま表示).継承を使ってスクリプトをスリムにせよ.
オブジェクトのプロトタイプ,委譲による継承
Javascriptはプロトタイプ型のオブジェクト指向言語である.全てのオブジェクトは,プロトタイプオブジェクトとリンクしており,そこからプロパティを継承している.オブジェクトを新たに生成する際に,プロトタイプとするオブジェクトを選択することができる.すなわち,オブジェクトは他のオブジェクトのプロパティを直接継承する仕掛けをもつ.
オブジェクトからあるプロパティの値を取得する際,そのオブジェクト自身に指定された名前のプロパティがなければ,そのプロトタイプオブジェクトのプロパティの値を取得しようとする.そこにもプロパティがなければ更にそのオブジェクトのプロトタイプのプロパティを探す.この仕組みを委譲と呼ぶ.
Javascriptでは,さまざまなスタイルで継承を記述できる.この演習では,次の2つのスタイル(パターン)による継承の例を示す.
- プロトタイプ型パターン
- 関数型パターン(モジュールパターン)
プロトタイプ型パターン(Object.createメソッド)による継承
プロトタイプ型パターンの継承は次の手順で行う.
- ひな形となるオブジェクトを作成する.
- Object.createメソッドを利用して,ひな形オブジェクトをプロトタイプとするオブジェクトを作成する.
- 作成したオブジェクトを個別にカスタマイズする.
- (必要なら)更にObject.createメソッドを利用して,そのオブジェクト(3でカスタマイズしたオブジェクト)をプロトタイプとするオブジェクトを作成する.
解答例
マウスで大きさを指定して緑の三角形,赤の矩形,青の円を順に描画します.
ソース
- <!DOCTYPE html>
- <html>
- <head>
- <style>
- canvas {
- border: solid 1px #000066;
- }
- </style>
- <meta charset="utf-8" />
- <title>描画ツールのようなもの</title>
- <script>
- "use strict";
- // プロトタイプ型の継承を利用
- /****************************************************************************
- shapeを生成するnewShape
- newShapeを継承した,newRect, newTri, newCircle
- それぞれ,矩形,三角形,円のオブジェクト
- ****************************************************************************/
- // 図形のプロトタイプ shapeProto
- // drawShapeメソッドを追加しないと描画できない.
- var shapeProto = {
- x : 0,
- y : 0,
- w : 0,
- h : 0, // 位置と大きさ
- color : "#0000FF", // デフォルトの色
- tmpColor : "AAAAAA", // 一時的に色を変えて描画するとき(ドラッグ中)の色
- // 描画(セットされている色で描画)
- draw : function (ctx) {
- ctx.strokeStyle = this.color;
- this.drawShape(ctx);
- },
- // 一時的に色を変えて描画(ドラッグ中)
- tmpDraw : function (ctx) {
- ctx.strokeStyle = this.tmpColor;
- this.drawShape(ctx);
- }
- };
- // 矩形のプロトタイプ rectProto
- // newShapeを継承して,drawShapeメソッドを追加
- var rectProto = Object.create(shapeProto);
- rectProto.color = "#FF0000";
- rectProto.drawShape = function (ctx) {
- ctx.beginPath();
- ctx.strokeRect(this.x, this.y, this.w, this.h);
- };
- // 三角形のプロトタイプ triProto
- // newShapeを継承して,drawShapeメソッドを追加
- var triProto = Object.create(shapeProto);
- triProto.color = "#00FF00";
- triProto.drawShape = function (ctx) {
- ctx.beginPath();
- ctx.moveTo(this.x + this.w / 2, this.y);
- ctx.lineTo(this.x + this.w, this.y + this.h);
- ctx.lineTo(this.x, this.y + this.h);
- ctx.closePath();
- ctx.stroke();
- };
- // 円のプロトタイプ circleProto
- // newShapeを継承して,drawShapeメソッドを追加
- var circleProto = Object.create(shapeProto);
- circleProto.color = "#0000FF";
- circleProto.drawShape = function (ctx) {
- var r;
- ctx.beginPath();
- r = Math.min(Math.abs(this.w), Math.abs(this.h)) / 2;
- ctx.arc(this.x + this.w / 2, this.y + this.h / 2, r, 0, 2 * Math.PI, true); // 0度から360度左回りに円弧を描画
- ctx.stroke();
- };
- /****************************************************************************
- 図形のリスト(配列),図形領域の描画
- ****************************************************************************/
- var shapeListProto = {
- x : 0,
- y : 0,
- w : 100,
- h : 100, // 描画領域の位置とサイズ
- shapes : [],
- // 全図形の描画
- draw : function (ctx) {
- var i;
- ctx.clearRect(this.x, this.y, this.w, this.h);
- for (i = 0; i < this.shapes.length; i += 1) {
- this.shapes[i].draw(ctx);
- }
- },
- push : function (shape) { this.shapes.push(shape); }
- };
- /****************************************************************************
- イベント処理
- ****************************************************************************/
- window.addEventListener("load", function () {
- var canvas,
- context, // カンバスとコンテクスト
- shapeList, // 図形のリスト
- drawing, // 描画中か否か(ドラッグ中か否か)
- shape, // 描画中の図形オブジェクト (ShapeListに加える前)
- kindofShape, // 選択された図形の種類 't' 'r' 'c' のいずれか
- prototypes; // 三角形,矩形,円のコンストラクタの配列
- // canvasとcontextの取得,大きさの設定
- canvas = document.getElementById("canvas1");
- context = canvas.getContext("2d");
- // 図形リストの生成と描画領域の設定
- shapeList = Object.create(shapeListProto);
- shapeList.w = canvas.width;
- shapeList.h = canvas.height;
- drawing = false;
- prototypes = [triProto, rectProto, circleProto];
- kindofShape = 0;
- // mousedownの処理
- canvas.addEventListener("mousedown", function (e) {
- var bcr;
- shape = Object.create(prototypes[kindofShape]);
- bcr = e.target.getBoundingClientRect();
- shape.x = e.clientX - bcr.left;
- shape.y = e.clientY - bcr.top;
- drawing = true;
- }, false);
- // mousemoveの処理
- canvas.addEventListener("mousemove", function (e) {
- var bcr;
- if (!drawing) {
- return;
- }
- bcr = e.target.getBoundingClientRect();
- shape.w = e.clientX - bcr.left - shape.x;
- shape.h = e.clientY - bcr.top - shape.y;
- shapeList.draw(context);
- shape.tmpDraw(context);
- }, false);
- // mouseupの処理
- canvas.addEventListener("mouseup", function (e) {
- var bcr;
- if (!drawing) {
- return;
- }
- bcr = e.target.getBoundingClientRect();
- drawing = false;
- kindofShape = (kindofShape + 1) % prototypes.length;
- shapeList.push(shape);
- shapeList.draw(context);
- }, false);
- });
- </script>
- </head>
- <body>
- <canvas id="canvas1" width="300" height="200"></canvas>
- </body>
- </html>
解説
このソースは上記の手順と次のように対応する.
- 図形のひな形となるオブジェクトshapeProtoを作成する(オブジェクトリテラルとして記述:23行目から40行目).
- Object.createメソッドを利用して,shapeProtoをプロトタイプとする3つのオブジェクトrectProto(44行目),triProto(53行目),circleProto(66行目)を作成する.
- 3つのオブジェクトを,それぞれカスタマイズする(45行目から49行目,54行目から62行目,67行目から74行目).
- マウス操作に応じて,Object.createメソッドを利用して,rectProto,triProto,circleProtoの何れかをプロトタイプとするオブジェクトを作成する(125行目).
関数型パターン(モジュールパターン)による継承
上記のプロトタイプ型パターンによる継承では,情報隠蔽(カプセル化)できない.オブジェクトの外から,全てのプロパティにアクセスできてしまう.これが問題になるときは,関数型パターン(モジュールパターン)の継承を用いるとよい.
Ⅰ.この継承では,以下の4つのステップを含む「オブジェクトを生成する関数」を作成する.
- 新しいオブジェクトを生成する.(どのように生成しても良い.オブジェクトリテラルを使っても,new演算子を使っても,Object.createメソッドを使っても,オブジェクトを返す関数を使っても)
- 必要に応じて,変数,関数を定義する.(ここで定義された変数や関数は,オブジェクト外からはアクセスできないプライベート変数,プライベートメソッドとなる)
- 生成したオブジェクトにメソッドを追加する.
- このメソッドは関数内で定義され,クロージャとなる.すなわち,2.で定義した変数や関数にアクセスできる.このメソッドを通してのみ2.で定義された変数や関数にアクセスできる.
- これにより情報隠蔽(カプセル化)が可能になる.
- 生成したオブジェクトを戻り値として返す.
- これにより,2.で定義された変数や関数,および,「オブジェクトを生成する関数」の引数の寿命は,戻り値のオブジェクトが削除されるまで延期される.
Ⅱ.この関数により生成されたオブジェクトを更にカスタマイズしたオブジェクトを返す関数を作成することにより,多段の継承が可能になる.
解答例
マウスで大きさを指定して緑の三角形,赤の矩形,青の円を順に描画します.
ソース
- <!DOCTYPE html>
- <html>
- <head>
- <style>
- canvas {
- border: solid 1px #000066;
- }
- </style>
- <meta charset="utf-8" />
- <title>描画ツールのようなもの</title>
- <script>
- "use strict";
- // 関数型パターン(モジュールパターン)の継承を利用
- /****************************************************************************
- shapeを生成するnewShape
- newShapeを継承した,newRect, newTri, newCircle
- それぞれ,矩形,三角形,円のオブジェクト
- ****************************************************************************/
- var newShape = function () {
- var
- that = {}, // 生成する図形オブジェクト
- x = 0, y = 0, w = 0, h = 0, // 位置と大きさ
- color = "#0000FF", // デフォルトの色
- tmpColor = "AAAAAA"; // 一時的に色を変えて描画するとき(ドラッグ中)の色
- // 位置と大きさの設定と取得
- that.setXYWH = function (x1, y1, w1, h1) { x = x1; y = y1; w = w1; h = h1; };
- that.getXYWH = function () { return {x : x, y : y, w : w, h : h}; };
- that.setColor = function (c) { color = c; };
- that.setTmpColor = function (c) { tmpColor = c; };
- // 右下の座標をセット
- that.setX2Y2 = function (x2, y2) {
- w = x2 - x;
- h = y2 - y;
- };
- // 描画(セットされている色で描画)
- // drawShapeメソッドを追加しないと描画できない.
- that.draw = function (ctx) {
- ctx.strokeStyle = color;
- that.drawShape(ctx);
- };
- // 一時的に色を変えて描画(ドラッグ中)
- that.tmpDraw = function (ctx) {
- ctx.strokeStyle = tmpColor;
- that.drawShape(ctx);
- };
- return that;
- };
- // newShapeを継承して,drawShapeメソッドを追加
- var newRect = function () {
- var that = newShape();
- that.setColor("#FF0000");
- that.drawShape = function (ctx) {
- var xywh = that.getXYWH();
- ctx.beginPath();
- ctx.strokeRect(xywh.x, xywh.y, xywh.w, xywh.h);
- };
- return that;
- };
- // newShapeを継承して,drawShapeメソッドを追加
- var newTri = function () {
- var that = newShape();
- that.setColor("#00FF00");
- that.drawShape = function (ctx) {
- var xywh = that.getXYWH();
- ctx.beginPath();
- ctx.moveTo(xywh.x + xywh.w / 2, xywh.y);
- ctx.lineTo(xywh.x + xywh.w, xywh.y + xywh.h);
- ctx.lineTo(xywh.x, xywh.y + xywh.h);
- ctx.closePath();
- ctx.stroke();
- };
- return that;
- };
- // newShapeを継承して,drawShapeメソッドを追加
- var newCircle = function () {
- var that = newShape();
- that.setColor("#0000FF");
- that.drawShape = function (ctx) {
- var r,
- xywh = that.getXYWH();
- ctx.beginPath();
- r = Math.min(Math.abs(xywh.w), Math.abs(xywh.h)) / 2;
- ctx.arc(xywh.x + xywh.w / 2, xywh.y + xywh.h / 2, r, 0, 2 * Math.PI, true); // 0度から360度左回りに円弧を描画
- ctx.stroke();
- };
- return that;
- };
- /****************************************************************************
- 図形のリスト(配列),図形領域の描画
- ****************************************************************************/
- var newShapeList = function () {
- var
- x, y, w, h, // 描画領域の位置とサイズ
- that = []; // 生成するオブジェクト(newShapeListの戻り値)
- // 位置とサイズの設定と取得
- that.setXYWH = function (x1, y1, w1, h1) { x = x1; y = y1; w = w1; h = h1; };
- that.getXYWH = function () { return {x : x, y : y, w : w, h : h}; };
- // 全図形の描画
- that.draw = function (ctx) {
- var i;
- ctx.clearRect(x, y, w, h);
- for (i = 0; i < that.length; i += 1) {
- this[i].draw(ctx);
- }
- };
- return that;
- };
- /****************************************************************************
- イベント処理
- ****************************************************************************/
- window.addEventListener("load", function () {
- var canvas,
- context, // カンバスとコンテクスト
- shapeList, // 図形のリスト
- drawing, // 描画中か否か(ドラッグ中か否か)
- shape, // 描画中の図形オブジェクト (ShapeListに加える前)
- kindofShape, // 選択された図形の種類 't' 'r' 'c' のいずれか
- constructors; // 三角形,矩形,円のコンストラクタの配列
- // canvasとcontextの取得,大きさの設定
- canvas = document.getElementById("canvas1");
- context = canvas.getContext("2d");
- // 図形リストの生成と描画領域の設定
- shapeList = newShapeList();
- shapeList.setXYWH(0, 0, canvas.width, canvas.height);
- drawing = false;
- constructors = [newTri, newRect, newCircle];
- kindofShape = 0;
- // mousedownの処理
- canvas.addEventListener("mousedown", function (e) {
- var bcr, x, y;
- bcr = e.target.getBoundingClientRect();
- x = e.clientX - bcr.left;
- y = e.clientY - bcr.top;
- // 描画の開始
- drawing = true;
- shape = constructors[kindofShape]();
- shape.setXYWH(x, y, 0, 0);
- }, false);
- // mousemoveの処理
- canvas.addEventListener("mousemove", function (e) {
- var bcr, x, y;
- if (!drawing) {
- return;
- }
- bcr = e.target.getBoundingClientRect();
- x = e.clientX - bcr.left;
- y = e.clientY - bcr.top;
- shapeList.draw(context);
- shape.setX2Y2(x, y);
- shape.tmpDraw(context);
- }, false);
- // mouseupの処理
- canvas.addEventListener("mouseup", function (e) {
- var bcr;
- if (!drawing) {
- return;
- }
- bcr = e.target.getBoundingClientRect();
- drawing = false;
- kindofShape = (kindofShape + 1) % constructors.length;
- shapeList.push(shape);
- shapeList.draw(context);
- }, false);
- });
- </script>
- </head>
- <body>
- <canvas id="canvas1" width="300" height="200"></canvas>
- </body>
- </html>
解説
このソースは上記の手順と次のように対応する.
Ⅰ.図形のひな形となるオブジェクトを返す関数newShapeを定義する(19行目から51行目).そのなかで,以下のことを行っている.
- 新しいオブジェクトを生成し,thatという変数に代入(24行目).
- 位置,大きさ,色などを表す変数を宣言(22行目から24行目).
- 生成したオブジェクトthatに,位置や色などの設定・取得のメソッド(26行目から36行目),描画メソッド(38行目から42行目),ドラッグ中の描画用のメソッド(44行目から48行目)を定義.
- 2で宣言した変数は隠蔽されるので,外部からアクセスするためのメソッドを提供する.
- 生成したオブジェクトthatを戻り値として返す(50行目).
Ⅱ.newShapeにより生成されたオブジェクト(図形)を更にカスタマイズしたオブジェクト(矩形,三角形,円)を返す関数newRect(53行目から63行目), newTri(65行目から79行目), newCircle(82行目から94行目)を作成.
マウス操作に応じて,これらの関数を呼出し,矩形オブジェクト,三角形オブジェクト,円オブジェクトを生成する(155行目).
※ プロトタイプ型パターン,関数型パターンなどの用語・手順は,参考文献で挙げた「JavaScript: The Good Parts」より.
ローカル変数の寿命
上述のように,関数で生成したオブジェクトを戻り値として返すと,関数内で定義された変数や関数の寿命は,戻り値のオブジェクトが削除されるまで延期される.この節では,簡単な例で,関数のローカル変数が,関数の読み出し後も存在することを確かめる.
何も表示されません.
動作はブラウザのコンソールを開いて確認してください.
ソース
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8" />
- <title>Javascriptのクロージャを利用したカプセル化</title>
- <script type="text/javascript">
- "use strict";
- var newObj1 = function () {
- var x = 'x0';
- var that = {};
- that.y = 'y0';
- that.setx = function (x1) {
- x = x1;
- };
- that.getx = function () {
- return x;
- };
- that.sety = function (y1) {
- this.y = y1;
- };
- that.gety = function () {
- return that.y;
- };
- return that;
- };
- var newObj2 = function () {
- var z ='z0';
- var that = newObj1();
- that.setz = function (z1) {
- z = z1;
- }
- that.getz = function () {
- return z;
- }
- return that;
- };
- var obj1 = newObj1();
- var obj2 = newObj1();
- var obj3 = newObj2();
- console.log("1:obj1 x", obj1.getx())
- console.log("1:obj1 y", obj1.gety())
- console.log("1:obj1 y", obj1.y)
- console.log("1:obj2 x", obj2.getx())
- console.log("1:obj2 y", obj2.gety())
- console.log("1:obj2 y", obj2.y)
- console.log("1:obj3 x", obj3.getx())
- console.log("1:obj3 y", obj3.gety())
- console.log("1:obj3 y", obj3.y)
- console.log("1:obj3 z", obj3.getz())
- obj1.setx('x1');
- obj1.sety('y1');
- obj2.setx('x2');
- obj2.sety('y2');
- obj3.setx('x3');
- obj3.sety('y3');
- obj3.setz('z3');
- console.log("2:obj1 x", obj1.getx())
- console.log("2:obj1 y", obj1.gety())
- console.log("2:obj1 y", obj1.y)
- console.log("2:obj2 x", obj2.getx())
- console.log("2:obj2 y", obj2.gety())
- console.log("2:obj2 y", obj2.y)
- console.log("2:obj3 x", obj3.getx())
- console.log("2:obj3 y", obj3.gety())
- console.log("2:obj3 y", obj3.y)
- console.log("2:obj3 z", obj3.getz())
- </script>
- </head>
- <body>
- </body>
- </html>
解説
- 関数newObj1()で生成されたオブジェクトobj1はyというプロパティを持つ.12行目
- obj1.gety()でyの値を得ることができる.45行目,64行目
- obj1.yでもアクセスできる.46行目,65行目
- yはクラス型のオブジェクト指向言語のパブリック変数のように扱うことができる.
- 関数newObj1()のローカル変数xは,newObj1の呼び出しが終了した後も存在する.
- obj1.getx()でxの値を得ることができる.44行目,63行目
- obj1.xでアクセスすることはできない.そもそもobj1のプロパティではない.
- xはクラス型のオブジェクト指向言語のプライベート変数のように扱うことができる.
- これにより情報隠蔽(カプセル化)が可能となる.
- 関数newObj1()のローカル変数は,newObj1()の呼び出し毎に別のオブジェクトが生成される.40行目と41行目
- obj1とobj2のそれぞれのxに別の値をセットし(55行目と57行目),getx()で値を得ると別々の値が返る(63行目と66行目).
- 28行目から38行目のように定義された関数newObj2()で生成したオブジェクトはnewObj1()で生成したオブジェクトの性質を継承する.
- メソッド(getx(), gety(), setx(), sety())も継承.50行目,51行目,59行目,60行目,69行目,70行目
- メンバー変数(y)も継承.52行目,71行目
- 継承元のnewObj1のローカル変数(x)も継承.50行目,59行目,69行目