JavaScript応用編
このページでは,(C言語しか学んでこなかった)学生諸君にとって,おそらく分かりにくい,あるいは多少,新しい(と思われるであろう)事項について取り上げる.
オブジェクト?
基礎編のページでは,特に断りなくオブジェクト(Object)という単語を用いていた.
そもそもJavaScriptはオブジェクト指向言語であり,変数に代入できるものはほぼ全てがオブジェクト,という特徴を持つ.(厳密には,undefinedなどの特殊な値や,true/falseなどのプリミティブ値はオブジェクトではないが)
「オブジェクト」は数値や文字列のように分かりやすいものではなく,定義が難しいが,さしあたっては
- プロパティ(property)
- メソッド(method)
- インタフェース(interface)
を持つもの,とでも覚えておくと良いかもしれない.(HTML基礎編のページにも書いたように,C言語の範囲内の知識だと「構造体を拡張したもの」に近い)
JavaScriptにおけるオブジェクト実装例
とにかく,具体例で見てみよう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>オプジェクト実装サンプル</title> <style> div { position: absolute; top: 50px; } </style> <script> var chara; function init() { window.addEventListener("keydown", callback_keydown); chara = new CHARACTER(document.getElementById("chara"), 100); } function callback_keydown(event) { if(event.keyCode == 37) { // Left-arrow key chara.moveLeft(); } else if (event.keyCode == 39) { // Right-arrow key chara.moveRight(); } } function CHARACTER(element, xpos) { this.element = element; this.xpos = xpos; this.moveLeft = function () { this.xpos -= 10; this.element.style.left = this.xpos + "px"; } this.moveRight = function () { this.xpos += 10; this.element.style.left = this.xpos + "px"; } this.element.style.left = this.xpos + "px"; } </script> </head> <body onload="init()"> <div id="chara">\(^^)/</div> </body> </html> |
このHTMLをブラウザに読み込ませると,\(^^)/
というキャラクター?が表示され,左右の矢印キーを押すとウィンドウ上を動くはずである.
以下,このプログラムを少しずつ解説する.
Bodyタグのonload属性
この属性が指定されると,HTML文書の読み込みが終わった段階でその関数が実行される.つまり,今回の場合はinit()
関数が実行される.
14 15 16 17 | function init() { window.addEventListener("keydown", callback_keydown); chara = new CHARACTER(document.getElementById("chara"), 100); } |
15行目でkeydownイベントに対するハンドラを登録している.つまり,キーが押される度にcallback_keydown
関数が実行されるようになる.
16行目が,実際にオブジェクトを生成している部分である.以下ではさらに細かく見る.
オブジェクトの作り方
JavaScriptでは
オブジェクト = new 関数(引数1, 引数2, ...) { ... }
という形でオブジェクトを作る.関数定義と間違いやすいが,こちらにはキーワードnewがついている.オブジェクトを生成する関数のことを特に「コンストラクタ」(constructor)と呼ぶ.コンストラクタは,他の言語では分かりやすく(普通の関数とは)区別されていることが多いが,JavaScriptでは非常に紛らわしいので注意すること.(頑張って慣れて!)
この16行目で,chara
という名前のオブジェクトが生成され,(そのオブジェクトを操作するための)メソッドを呼び出せるようになる.今回の場合,charaにはmoveLeft
とmoveRight
というメソッドが(定義・)実装されているので,これらを呼び出すことができる.
メソッドの呼び出し
オブジェクトのメソッドを呼び出すには
オブジェクト.メソッド();
のように,ピリオド(.
)をつける.charaオブジェクトのmoveLeftメソッドを呼ぶにはchara.moveLeft()
と書く.18行目からのcallback_keydown関数を見ると
18 19 20 21 22 23 24 25 | function callback_keydown(event) { if(event.keyCode == 37) { // Left-arrow key chara.moveLeft(); } else if (event.keyCode == 39) { // Right-arrow key chara.moveRight(); } } |
となっており,押されたキーが左矢印(キーコード37)ならchara.moveLeft()を呼び,右矢印(キーコード39)ならchara.moveRight()を呼び出している.
このプロパティ呼び出しの際,呼び出した側(オブジェクトを使う・利用する側)は実際にcharaオブジェクト内で何が起きているかを知る必要はなく,ただ結果としてcharaが右なり左なりに動いてくれれば良いだけ,である.この考え方がオブジェクト指向プログラミングの真髄であり,特に今回のようなグループ開発では重要な意味を持つ.中身(実装の具体的方法)を知らなくてもオブジェクトを使える,という事実が重要である.よく肝に銘じておこう.
オブジェクトの宣言(定義)
さて,ではオブジェクトを提供する側の方も見ていこう.
26 27 28 29 30 31 32 33 34 35 36 37 38 | function CHARACTER(element, xpos) { this.element = element; this.xpos = xpos; this.moveLeft = function () { this.xpos -= 10; this.element.style.left = this.xpos + "px"; } this.moveRight = function () { this.xpos += 10; this.element.style.left = this.xpos + "px"; } this.element.style.left = this.xpos + "px"; } |
プログラム中のthis
は,「オブジェクト自分自身」を表す(他言語ではself
などというキーワードになっていることもある).16行目の
16 | chara = new CHARACTER(document.getElementById("chara"), 100); |
という命令でchara
オブジェクトが作成されるが,この第1引数はDOM要素,第2引数はウィンドウ上のx座標値である.
27行目で,第1引数であるelementをthis.element
に代入しており,これによってオブジェクト自身のelement
プロパティに「<div id="chara">\(^^)/</div>
」というDOM要素(への参照)が格納されることになる.同様に,28行目ではオブジェクトにxpos
というプロパティを登録し(てその初期値を第2引数で受け取ったxposにし)ている.このように,オブジェクトには自由にプロパティを追加することができる.
29行目以降はメソッドの定義部分である.
29 30 31 32 | this.moveLeft = function () { this.xpos -= 10; this.element.style.left = this.xpos + "px"; } |
functionに名前がついていないが,JavaScriptではこのような無名(匿名)関数が利用できる.ここでは,moveLeft
というプロパティに無名関数が代入されることでメソッドが実現されている.関数の中身に関する詳細は省略するが,単にelement(DOM)要素のleftスタイルの値を更新して表示場所を変更しているだけである.
ワンポイント
プロパティが値でメソッドが関数,というだけで,JavaScriptのプログラム上(の見た目)ではこれらの明確な区別はない.そのせいで最初はかなり戸惑うかも知れないが,たくさん手を動かしてまずは慣れ親しみ,徐々に理解を深めていこう.
組み込みオブジェクト
JavaScriptには,最初から用意されている便利な関数やオブジェクトがある.
タイマー
文字通りのタイマー機能.一定時間後に関数を実行したり,定期的に(一定間隔で)関数を実行したりできる.
メソッド | 機能 |
---|---|
setTimeout(関数名,ミリ秒) |
ミリ秒後に関数を1回だけ呼び出す |
clearTimeout(timerId1) |
setTimeoutの処理を停止する.timerId1 はsetTimeoutの戻り値 |
setInterval(関数名,ミリ秒) |
ミリ秒間隔で関数を定期的に呼び出す |
clearInterval(timerId2) |
setIntervalの処理を停止する.timerId2 はsetIntervalの戻り値 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>タイマーサンプル</title> <script> var timerId; function timerStart() { timerId = setTimeout(what_time_isitnow, 3000); // 3秒後に1回 } function timerStop() { clearTimeout(timerId); } function intervalStart() { clearInterval(timerId); timerId = setInterval(what_time_isitnow, 2000); // 2秒ごとに定期的に } function intervalStop() { clearInterval(timerId); } function what_time_isitnow() { document.getElementById("timeinfo").textContent = new Date(); } </script> </head> <body> <button onclick="timerStart()">3秒タイマー</button> <button onclick="timerStop()">3秒タイマー停止</button> <button onclick="intervalStart()">2秒間隔タイマー</button> <button onclick="intervalStop()">2秒間隔タイマー停止</button> <p id="timeinfo"></p> </body> </html> |
Math
各種計算を行うためのメソッドを提供している.
メソッド | 機能 |
---|---|
Math.min(a, b) |
aとbの小さい方を返す |
Math.max(a, b) |
aとbの大きい方を返す |
Math.random() |
0以上1未満の乱数を返す |
Math.floor(n) |
nを切り捨てた整数値を返す |
Math.ceil(n) |
nを切り上げた整数値を返す |
Math.round(n) |
nを四捨五入した整数値を返す |
Math.sqrt(n) |
nの平方根を返す |
Math.PI |
円周率(3.1415926535... )を返す |
Array
基礎編のページに記載した「配列」は,実はArrayオブジェクト.
関数
JavaScriptの関数は「第一級オブジェクト」であり,通常のオブジェクトと同じ振る舞いが可能である.この特徴こそが開発者を混乱させる要因にもなっているが,まずはこの性質に完全に慣れる必要がある.
// 関数宣言 function func1(a, b) { // 省略 } // 変数に代入して呼び出し var f = function() { console.log('f is called.'); } f(); // 'f is called.' // パラメータ的な使い方 var f2 = function() { console.log('f2 is called.'); } function func2(func) { func(); } func2(f); // 'f is called.' func2(f2); // 'f2 is called.'
さらに,関数は「通常のオブジェクトと同様に扱える」ので,関数自身にも固有のプロパティやメソッドを動的に追加できる.
var myFunc = function() { // 省略 } // この時点ではプロパティiは存在しない console.log(myFunc.i); // undefined // プロパティiを追加 myFunc.i = 'Software Design'; console.log(myFunc.i); // 'Software Design'
また,通常のオブジェクトには無い,関数だけの特徴として,カッコ演算子で処理を呼び出せるというものがある.カッコ演算子をつけずに呼び出すと,自身の定義内容が参照される.
var f = function() { return 'csd'; } console.log(f()); // 'csd' console.log(f); // '[Function: f]'
関数の引数
他の言語が「関数の定義通りに引数を渡さないとエラーになる」ことに対し,JavaScriptでは引数チェックが一切行われない.つまり,定義済み引数の個数より多く渡しても少なく渡しても,また,個々の引数の型が違っていたとしても,何事もなかったかのように動作してしまう.この自由すぎる特徴は「グループ開発では致命傷となり得る」ので十分に注意しよう.
function func(a, b, c) { console.log(a, b, c); } // 少なく渡す func('a'); // 'a undefined undefined' // 多く渡す func('a', 'b', 'c', 'd'); // 'a b c' // 渡さない func(); // 'undefined undefined undefined'
無名関数
JavaScriptでは,関数名が必要なければ省略することができる.この名前の無い関数を無名関数という.無名関数は,イベントハンドラやコールバックのように「複数回呼び出すことがない関数」を定義する際によく利用される.
// イベントハンドラで window.onload = function () { // 略 } // コールバックで setTimeout( function() = { // 略 }, 1000);
コールバック
引数として渡される関数のことをコールバック(またはコールバック関数)とよぶ.引数として渡すことで,何らかの任意のタイミングで関数を実行させられる.(代表的な用途としては非同期処理の実装)
function func(callback) { console.log('func'); callback(); } // コールバック func( function() { console.log('aaa'); // func aaa という順で出力 }); // コールバック func( function() { console.log('bbb'); // func bbb という順で出力 });
JavaScriptを使う場合,このような「任意のタイミングで実行したい処理(関数)を,引数として渡す」ということが当たり前に考えられるようになる必要がある.(イベントハンドラは,まさにこの考え方)
巻き上げ
まずは例をみてみる.
var a = 'software'; var f = function () { console.log(a); // undefined var a = 'design'; console.log(a); // 'design' } f();
なぜ最初のログが undefined になるのか,を理解するためには,巻き上げについての認識が必要となる.JavaScriptでは,関数内のどの位置でも変数宣言できるものの,それらの宣言は全て「その関数の先頭で宣言された」とみなされる.要は,上のプログラムは以下と同じことである.
var a = 'software'; var f = function () { var a; console.log(a); // undefined a = 'design'; console.log(a); // 'design' } f();
これも「グループ開発を行う上では致命傷となりかねない特徴」なので,十分に注意すること.なお,巻き上げによる予期せぬ動作を防ぐには
- 変数は使用前の宣言,初期化を徹底する
- 自動的にブロックスコープとなる変数宣言
let
を使用する(ES2015から利用可) - 関数内でグローバル変数を参照しなければならないときは,引数として渡してしまう
などの対処法が考えられる.
即時関数
JavaScriptでは,関数を定義すると同時に実行することができる.これを即時関数,あるいは即時呼び出しという.
無名関数を定義し,そのまま即実行することで,グローバルスコープを汚すこと(不要なプロパティの追加とか)を防げる.
// 基本的な使い方 (function() { console.log('software'); }()); // <- この行の()によって無名関数が実行される,ということ.softwareと表示される // 引数の渡し方 (function(a,b) { console.log(a + b); }('software','design')); // softwaredesign
即時関数は「全体をカッコで括る」必要があるので注意すること.
プロトタイプ
JavaScriptでは,「プロトタイプ」(prototype)という特徴的な概念が使われる.中規模以上のソフトウェア開発では(当然のように)様々なオブジェクトを数多く扱う必要があるが,新しいオブジェクトを必要とする度にいちいち最初から作り直していたのでは,あまりにも非効率的である.オブジェクト指向言語では,大抵,オブジェクトを再利用するための仕組みが準備されており,JavaScriptの場合はそれがprototype
である.
クラス
ES2015からは正式にクラスが導入されており,今後はクラスが広く使われると思われる
簡単な例で見てみよう.
1 2 3 4 5 6 7 8 9 | function HUMAN(name) { this.name = name; // nameプロパティ this.sayName = function () { // sayNameメソッド console.log("My name is " + this.name + "."); } } var taro = new HUMAN("Taro"); taro.sayName(); // "My name is Taro." |
オブジェクトtaro
を作り,sayName
メソッドを呼び出しているだけの簡単な例であるが,これを元に(少し機能を追加した)新たなNeoHUMAN
を作ってみる.
10 11 12 13 14 15 16 17 18 19 20 21 | NeoHUMAN.prototype = taro; function NeoHUMAN(age) { this.age = age; // ageプロパティを追加 this.sayAge = function () { // sayAgeメソッドを追加 console.log("I am " + this.age + " years old."); } } neotaro = new NeoHUMAN(10); neotaro.sayAge(); // "I am 10 years old." neotaro.sayName(); // "My name is Taro." |
オブジェクトtaro
には年齢を話す機能がないが,neotaro
の方は年齢を話すsayAge()
メソッドが追加されている.
NeoHUMANからHUMANへは,10行目でprototypeという関連付けが行われている.20行目ではsayAge()
メソッドが呼び出されているが,sayAge()自体はNeoHUMANで定義されているので(そのオブジェクトである)neotaro
で処理が行われる.しかし,21行目のメソッド呼び出しでは,NeoHUMANにsayName()
が定義されていないので,prototypeを辿ってHUMANオブジェクトのsayName()メソッドを実行する形になる(親元であるHUMAN
の機能を継承している,ということ).これがプロトタイプの仕組みである.
この例では,
taro
はHUMAN
関数で作られたオブジェクトNeoHUMAN
はオブジェクトを作るための関数
であることに注意しよう.つまり,10行目は
コンストラクタ.prototype = オブジェクト;
と指定している.ややこしいが,新たなオブジェクトのprototype属性に親元のオブジェクトを代入すると継承が実現できる,ということである.
JavaScriptでは,再利用のためにこのようなルールが設けられている,と理解して欲しい.
例外処理
try〜catch
文を使うと,スクリプトの「通常処理部分」と「エラー処理部分」を分けて書くことができるため,可読性がよくなる.
try
ブロックに通常処理のコードを,catch
ブロックにエラー処理部のコードを書く.throw
を使うと自ら例外を発生させられる(Errorオブジェクトを投げる)
console.log("計算値: " + div(5, 0)); function div(num1, num2) { try { if (num2 == 0) { throw new Error("0で割ろうとしました"); } return (num1/num2); } catch( err ) { console.error("エラー!:", err.message); return 0; } }