コンテンツにスキップ

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にはmoveLeftmoveRightというメソッドが(定義・)実装されているので,これらを呼び出すことができる.


メソッドの呼び出し

オブジェクトのメソッドを呼び出すには

オブジェクト.メソッド();

のように,ピリオド(.)をつける.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(Document Object Model)要素,第2引数はウィンドウ上のx座標値である.

27行目で,第1引数であるelementをthis.elementに代入しており,これによってオブジェクト自身のelementプロパティに「<div id="chara">\(^^)/</div>」というDOM(Document Object Model)要素(への参照)が格納されることになる.同様に,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)要素(this.element)のleftスタイルの値(style.left)を更新して表示場所を変更しているだけである.ここで,「style.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);

window.onload で指定した関数は,windowオブジェクトが読み込まれたタイミングで実行される.setTimeout(arg1, arg2)では,arg2で指定した時間後に,arg1で指定した処理が実行される.


コールバック

引数として渡される関数のことをコールバック(またはコールバック関数)とよぶ.引数として渡すことで,何らかの任意のタイミングで関数を実行させられる.(代表的な用途としては非同期処理の実装)

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の機能を継承している,ということ).これがプロトタイプの仕組みである.

この例では,

  • taroHUMAN関数で作られたオブジェクト
  • 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;
  }
}