あもんノート TOP
速習 JavaScript
JavaScript はブラウザ上で動くスクリプト言語で、
通常、HTML (ハイパーテキスト・マークアップ言語)の script 要素に記述されることで実行されます。
高速かつシンプルな仕組みでオブジェクト指向を実現する、非常に優れた言語です。
動的なホームページやウェブアプリの多くがこの JavaScript で書かれています。
一方、
JS 実行 は JavaScript をオンラインで簡単に実行するアプリで、
JavaScript のテストや学習に適したものです。
ここでは JS 実行のコードを多数例示し、
JavaScript の基本事項を解説していきます。
上のリンクをクリックして JS 実行で試しながら学習を進めてください。
JS 実行のコードを書けるようになれば、JavaScript は当然のこと、
近年の多くのプログラミング言語の理解に役立つでしょう。
また、JS 実行が用意しているオブジェクトの利用により、
高度な数値計算やグラフ描画をオンラインで簡単に行えるようになります。
※ インターネットの記事などを見ると、
Java や C# などのクラスベース言語の常識に囚われて
JavaScript を正しく理解できていない方が多いです。
JavaScript は基本的にはプロトタイプベースの言語です。
クラスベース言語に慣れている方は、
一旦その常識を忘れて下さい。
目次
数値と演算
JS 実行のコードエリアに以下のコードを書いてみてください
(コピペでも良い)。
a = 3; b = 4
c = a / b
Text.putln( c )
実行ボタンを押すと、出力エリアに c の値である
0.75 が表示されるはずです。
変数への代入は = で行います。
複数の命令文は ; (セミコロン)で区切ります。
算術演算子には、
+ (加算)、- (減算)、* (乗算)、/ (除算)、% (剰余)、** (べき)
があります。
Text.putln は JS 実行が用意した関数(メソッド、命令)で、
引数の値を出力エリアに行出力します。
JavaScript では変数の型を宣言する必要がありません。
代入された値によって自動的かつ動的に型が決まります。
このような言語は
動的型付け言語 と呼ばれます。
変数の型を調べるには
typeof という演算子を使います。
a = 3.2
Text.putln( typeof a ) // number と表示される
整数と小数の区別はありません。
ひっくるめて
数値型 (number)とみなされます。
// 以下、改行まではコメント(注釈、REM 文)とみなされ、
動作に影響を与えません。
コメントの始まりと終わりを自由に指定したい場合は、
/* 〜 */ を使います。
数値型の変数に .toFixed( n )
を付けると小数点以下 n 桁の近似表記が返されます。
a = 35.223567
Text.putln( a.toFixed( 3 ) ) // 35.224 と表示される
また、数値型の変数に .toExponential( n )
を付けると小数点以下 n 桁の指数表記が返されます。
a = 123456789
Text.putln( a.toExponential( 4 ) ) // 1.2346e+8 と表示される
. (ドット)以下に記述することで呼び出される関数を、
その変数の
メンバ関数 といいます。
toFixed や toExponential は数値型の変数が持つメンバ関数です。
文字列
文字列 は " (ダブルクォーテーション)または
' (シングルクォーテーション)で囲って表します。
s = "Taro"
Text.putln( "My name is " + s ) // My name is Taro と表示される
Text.putln( typeof s ) // string と表示される
文字列は + 演算子で連結されます。
数値と文字列を + で繋ぐと自動的に数値が文字列に変換され、連結されます。
強制的に数値を文字列に変えるには
String、
文字列を数値に変えるには
Number という関数を使います。
a = 123; b = "456"
Text.putln( a + b ) // 123456 と表示される
Text.putln( a + Number( b ) ) // 579 と表示される
文字列型変数のメンバ関数は色々あり、次のようなものがよく用いられます。
s = "Hello"
Text.putln( s.length ) // 5 (文字列の長さ)
Text.putln( s.indexOf( "l" ) ) // 2 ( "l" が最初に現れる位置)
Text.putln( s.indexOf( "a" ) ) // -1 ( 存在しない場合は -1 になる)
Text.putln( s.substr( 1, 3 ) ) // ell (1番目から3文字抜き出す)
Text.putln( s.substr( 2 ) ) // llo (2番目以下を抜き出す)
Text.putln( s.charCodeAt( 1 ) ) // 101 (1番目の文字 'e' のコード)
Text.putln( s.replace( "l", "x" ) ) // Hexlo (最初の "l" を "x" に置換)
Text.putln( s.replaceAll( "l", "x" ) ) // Hexxo ( "l" を全て "x" に置換)
t = " xyz "
Text.putln( t.trim() ) // xyz (前後の空白やタブを除去)
JavaScript では最初の文字を 0 番目と数えるので注意。
length に () は必要ありません。
これは実はメンバ関数ではなく、
メンバ変数 という扱いだからです。
下のコードを実行するとどうなるか、
予想して試してみてください。
a = 1
Text.putln( typeof typeof a )
Text.putln( typeof a.toFixed( 3 ) )
予想は当たりましたか?
真理値
true と
false は
真理値型
(ブーリアン型)におけるただ2つの値です。
それぞれ真(正しい)と偽(正しくない)を意味します。
a = 1; b = 2
Text.putln( a == b ) // false
Text.putln( a != b ) // true
Text.putln( a < b ) // true
Text.putln( a >= b ) // false
Text.putln( typeof true ) // boolean
== は等号で、!= は ≠ (等しくない)を意味します。
代入が = だったのに対し、等号は == なので注意してください。
論理演算子 には、
&& (論理積、and )、|| (論理和、or )、! (否定、not )があります。
Text.putln( true && false ) // false
Text.putln( true || false ) // true
Text.putln( ! false ) // true
関数
Boolean は引数の値(文字列の場合はその長さ)が 0 のとき false、
そうでない場合は true を返します。
Text.putln( Boolean( 0 ) ) // false
Text.putln( Boolean( 1 ) ) // true
また、関数 Number.isInteger は引数が整数のとき true、
そうでない場合は false を返します。
Text.putln( Number.isInteger( 3 ) ) // true
Text.putln( Number.isInteger( 3.2 ) ) // false
条件分岐
条件分岐は
if で行います。
基本的な文法を例示しておきます。
Text.clear()
a = 5
if( a > 0 ) Text.putln( "a は正" )
a = 5 のとき a > 0 は true を与えるので、後ろの文が実行され、
「 a は正」と表示されます。
もし a = 5 という文を a = -2 と書き換えると何も表示されなくなります。
Text.clear は JS 実行が用意した関数で、出力エリアをクリアします。
複数の命令を書くときは
{} を用いてブロック化します(ブロック if 文)。
Text.clear()
a = 2
if( a > 0 ){
a = a + 1
Text.putln( "a の値を増やしました" )
}
Text.putln( "a = " + a )
字下げ(タブ)はコードの読みやすさ(可読性)のために行っていて、
動作には影響を及ぼしません。
if 文の後に
else を書くと、"そうでない場合" という意味になります。
Text.clear()
a = 2
if( a < 0 ){
Text.putln( "負" )
} else if( a > 0 ){
Text.putln( "正" )
} else{
Text.putln( "零" )
}
変数 a の値を変えて動作を確認してみてください。
ちなみに、if 文の条件式が真理値を与えない場合は、
自動的にその値の関数 Boolean の返り値として解釈されます。
if( 3 ) Text.putln( "A" )
Boolean( 3 ) が true なので、「A」という文字が表示されます。
繰り返し処理
条件を満たす間だけ繰り返し処理を行うには
while を用います。
i = 0
while( i < 10 ){
Text.put( i )
i++
}
// 0123456789 と表示される
Text.put は JS 実行が用意した関数で、
Text.putln と異なり表示後改行を行わない単純出力です。
i++ は i = i + 1 の略記で、変数 i の値を 1 だけ増やします。
これを
インクリメント といいます。
同じことは i += 1 と書くこともできます。
同様に、i-- は i = i - 1 の略記で、
デクリメント といいます。
上の例は
for を用いて次のように書くこともできます。
for( i = 0; i < 10; i++ ){
Text.put( i )
}
次のコードは九九の表を出力します。
for( i = 1; i < 10; i++ ){
for( j = 1; j < 10; j++ ){
Text.put( ( i * j ) + " " )
}
Text.put( "\n" )
}
"\n" は改行コードです。
while や for から強制的に抜けるには、
break というキーワード(コマンド)を使います。
i = 0
while( true ){
Text.put( i )
i++; if( i > 9 ) break
}
// 0123456789 と表示される
また、while や for のループからは抜けないが、
今回の処理をキャンセルするキーワードに
continue があります。
for( i = 0; i < 10; i++ ){
if( i >= 3 && i <= 7 ) continue
Text.put( i )
}
// 01289 と表示される
ここまでの構文は、Java や C# など、C 系の言語とそう変わりません。
異なるのは変数の型宣言が無用であるということと、
行末においては ; を省略できるということです。
他の C 系の言語との併用の関係から、
行末の ; を省略しない人が多いのですが、ここでは省略します。
関数
関数 (メソッド、プロシージャ)は一連の手続きをまとめたものです。
引数を持たせたり、返り値を得ることもできます。
f = function( x ){
return x ** 2 + 1
}
Text.putln( f( 3 ) ) // 10
Text.putln( typeof f ) // function
function は関数であることの宣言、
引数があるときは続けて () 内に書きます。
return は関数を終えるキーワードで、
もしその後に式があればその値が返されます。
上の例で変数 f が
関数型
(function)に値をとっていることがわかりますが、
関数がデータ型の一種であることは、
他の言語ではあまり見られない JavaScript の特徴です。
ちなみに上のコードは次のように書き換えることもできます。
Text.putln( f( 3 ) ) // 10
Text.putln( typeof f ) // function
function f( x ){
return x ** 2 + 1
}
このように function から書き始める文法では、
その関数はコードの先頭で宣言されたものと解釈されます。
これを関数の
巻き上げ (ホイスティング)といいます。
ただしこの文法は、
関数をサブルーチンのようにみなす従来の言語のそれの踏襲(互換性)として用意されたものと考えられ、
関数がデータ型であるという JavaScript の理念にはそぐわないので注意。
順列(permutation)を与える関数を作ってみましょう。
permutation = function( n, m ){
var k = 1
for( var i = n; i > n - m; i-- ) k *= i
return k
}
Text.putln( permutation( 5, 3 ) ) // 60
var は
ローカル変数 の宣言です。
ローカル変数は宣言した関数の中だけで有効な変数で、
関数を抜けると無効になります。
このような変数の
スコープ (有効範囲)は、
関数のブラックボックス的な性質(独立性)のために必要です。
対して、var で宣言せずに用いた変数は
グローバル変数 とみなされ、
どこでも有効ということになります。
関数の引数もローカル変数の一種です。
k *= i は k = k * i の略記です。
ここまでのおさらいを兼ねて、少し本格的なサンプルコードを示します。
// 素数の一覧表示
isPrime = function( n ){
if( n < 2 ) return false
var max = n ** 0.5
for( var i = 2; i <= max; i++ ){
if( n % i == 0 ) return false
}
return true
}
putPrimesInRange = function( a, b ){
var k = 0
for( var n = a; n <= b; n++ ){
if( isPrime( n ) ){
Text.put( n + " " )
k++
}
}
Text.putln( "\n以上 " + k + " 個" )
}
Text.clear()
putPrimesInRange( 1, 1000 )
関数名は少し長くても適切なものにしましょう。
そうすることでコードの可読性が高まります。
また、何気ない処理でも関数化し名前を付けることで、
やはり可読性が高まります。
このようなプログラミングの手法は
構造化プログラミング
と呼ばれます。
例えば、ある関数の記述が20行以上になってしまったなら、
その関数の処理の一部を別の関数として抽出できないか、
検討してみるべきです。
配列
プリミティブ(原始的)なデータの集合体を
オブジェクト といいます。
配列 (アレイ、リスト)はオブジェクトの一種で、
特に番号づけられた変数(メンバ)の集合体です。
x = [ "a", "b", "c" ]
Text.putln( x[ 0 ] ) // a
Text.putln( x[ 1 ] ) // b
Text.putln( x[ 2 ] ) // c
Text.putln( x.length ) // 3
Text.putln( typeof x ) // object
length は配列が自動的に持つメンバ変数で、
メンバの数を記憶しています。
空の配列にメンバを追加していくことで配列を作ることもできます。
x = []
x[ 0 ] = "a"
x[ 1 ] = "b"
x[ 2 ] = "c"
Text.putln( x[ 1 ] ) // b
配列の各メンバをさらに配列とすることで高階の配列を作ることができます。
a = []
for( i = 0; i < 10; i++ ){
a[ i ] = []
for( j = 0; j < 10; j++ ){
a[ i ][ j ] = i * j
}
}
Text.putln( a[ 3 ][ 7 ] ) // 21
文字列の節では紹介しませんでしたが、
文字列のメンバ関数の一つに、
文字列を指定された区切り文字で分割する
split があります。返り値は分割された文字列の配列になります。
s = "ab, cde, fg, hij"
ss = s.split( "," )
for( i = 0; i < ss.length; i++ ){
ss[ i ] = ss[ i ].trim()
}
Text.putln( ss[ 0 ] ) // ab
Text.putln( ss[ 1 ] ) // cde
また、関数の中で
arguments は引数を保持する配列になります。
sumOf = function(){
var k = 0
for( var i = 0; i < arguments.length; i++ ){
k += arguments[ i ]
}
return k
}
Text.putln( sumOf( 4, 5, 6, 7 ) ) // 22
一方、関数の引数列を配列で与えたい場合は、
... を使います。
norm = function( a, b ){
return ( a ** 2 + b ** 2 ) ** 0.5
}
a = [ 3, 4 ]
Text.putln( norm( ... a ) ) // 5
ただし ... は近年 ES2015 (ES6) から導入された仕様であるため、
古いブラウザだとエラーになります。
ちなみに文字列に [ n ] を付けると n 番目の文字が抜き出されます。
s = "あいうえお"
Text.putln( s[ 3 ] ) // え
これは文字列を文字の配列とみなす C 言語の仕様の踏襲と考えられます。
連想配列
番号の代わりに文字列をインデックス(添字、キー)とした配列を、
連想配列 (ハッシュマップ、辞書)といいます。
x = { name: "太郎", age: 16 }
Text.putln( x.name ) // 太郎
Text.putln( x.age ) // 16
この例では x が連想配列で、
メンバに name と age という名の変数を持っています。
このコードは下のように書いても同じです。
x = { name: "太郎", age: 16 }
Text.putln( x[ "name" ] ) // 太郎
Text.putln( x[ "age" ] ) // 16
配列と同様、
空の連想配列にメンバを追加していくこともできます。
x = {}
x.name = "太郎"
x.age = 16
Text.putln( x.age ) // 16
プリミティブな変数は、代入の際、
値渡し になるのに対し、
オブジェクトは
参照渡し になります。
下の例を見てください。
a = 1
b = a // b には a の値 1 が渡される
a = 2 // a の値を変更
Text.putln( b ) // 1 --- 当たり前ですね
x = { fox: 1 } // x の "値" はこのオブジェクトのアドレス
y = x // y にはそのアドレスが渡される
x.fox = 2 // よって x のメンバの値を変えると
Text.putln( y.fox ) // 2 --- y のメンバの値も変わる
a と b は別の変数になりますが、
x と y は同一のオブジェクトを参照することになります。
このことをちゃんと理解していないと、本格的なプログラムを作った際、
予期せぬ挙動に悩まされることになります。
ちなみにアドレスはデータのメモリ上における位置情報を意味します。
C や C++ など、低レベルな処理(機械寄りな処理)が可能な言語では、
これをポインタ型の変数であらわに扱うことができますが、
一般に低レベル処理は暴走(ハングアップ、フリーズ)の原因になるため、
近年よく用いられる高級言語の多くは、
このような低レベル処理をあえてできないようにしています。
オブジェクトを複製したい場合は Object.assign を用います。
x = { fox: 1 }
y = Object.assign( {}, x )
// {} に x の全てのメンバを追加したオブジェクトを y で参照
x.fox = 2
Text.putln( y.fox ) // 1
オブジェクトの for 文
for( n of a ) ... は配列 a のメンバに対して繰り返し処理を行います。
このときループ変数 n は配列 a の各メンバの値に値を取ります。
a = [ "x", "y", "z" ]
for( n of a ) Text.put( n ) // xyz
先に書いた関数 sumOf は、
以下のように書くこともできます。
sumOf = function(){
var k = 0
for( var n of arguments ) k += n
return k
}
Text.putln( sumOf( 4, 5, 6, 7 ) ) // 22
一方、for( n in a ) ... はオブジェクト a が持つメンバに対して繰り返し処理を行います。
このときループ変数 n は a の各メンバ名(インデックス、キー)に値を取ります。
a = { x: 1, y: 2, z: 3 }
for( n in a ) Text.put( n ) // xyz
for( n in a ) Text.put( a[ n ] ) // 123
of と in の違いに注意。
of は配列専用ですが、
in は配列でも連想配列でも使用できます。
ただし配列を in でループさせたとき、
ループ変数は数値の文字列
"0", "1", "2", … に値を取るので注意してください。
複合的なオブジェクト
配列と連想配列を組み合わせてデータベースを作れます。
ourList = [
{ name: "太郎", age: 16 },
{ name: "次郎", age: 14 },
{ name: "花子", age: 12 }
]
Text.putln( ourList[ 1 ].name ) // 次郎
Text.putln( ourList[ 2 ].age ) // 12
OS におけるフォルダ(ディレクトリ)がいい例ですが、
箱の中に箱が複数あって、
それぞれの箱の中にまた箱が複数あるというような構造は、
ツリー構造 と呼ばれます。
木の幹と枝が成すような構造だからです。
配列や連想配列で作られるオブジェクトも一般にツリー構造を成します。
その末端部分、
すなわち一番小さい箱がプリミティブな変数で、
値を持つことになります。
プリミティブな変数はファイル、
オブジェクトはフォルダのようなものだと考えるとわかりやすいでしょう。
オブジェクトのメンバに関数を与えることで、
機能を内包したデータベースを作ることもできます。
ourList = {}
ourList.no = [
{ name: "太郎", gender: "M", age: 16 },
{ name: "次郎", gender: "M", age: 14 },
{ name: "花子", gender: "F", age: 12 }
]
ourList.get = function( name, p ){
for( var k of this.no ){
if( k.name == name ) return k[ p ]
}
}
Text.putln( ourList.get( "次郎", "age" ) ) // 14
Text.putln( ourList.get( "花子", "gender" ) ) // F
this はその関数を呼んだオブジェクトを指すキーワードです。
今の例では ourList.get( ... ) と書いて、
オブジェクト ourList が関数 get を呼んでいるので、
この場合の this は ourList を指します。
さらに、データを追加、編集、検索する関数などを与えることで、
より本格的なデータベースにすることも可能です。
特にゲームやアプリなど、大きなプログラムを書く場合、
このようにデータと機能がパッケージングされたオブジェクトを必要に応じて用意し、
それらをプログラムの部品(モジュール)とみなしてコーディングするのが、
効率の良い方法になります(
オブジェクト指向 )。
ちなみにプログラミング言語における "オブジェクト" という用語は、
一般的には、「自分の振る舞いを知っているデータ」と定義されます。
JavaScript ではそれは関数をメンバに持つ連想配列として簡単に実現されるわけですが、
関数がデータ型でない言語ではこのような実現はできず、
クラスを用いて実現されることになります。
クラスについては後述。
ビルトインオブジェクト
JavaScript のメモリ領域は
window という名のオブジェクトで与えられています。
これを
グローバルオブジェクト といいます。
その配下に色々な変数やオブジェクトが(少なくとも仮想的に)置かれています。
例えば Number や String という関数ですが、
その正式名称は window.Number、window.String です。
window. は省略できるため、単に Number および String で使用できたわけです。
ユーザーが用いたグローバル変数や関数も window の配下に作られます。
fox = 1
Text.putln( window.fox ) // 1
この仕様により、ローカル変数とグローバル変数が同じ変数名でも構わないことになります。
グローバル変数の方は window. を付ければ区別可能だからです。
x = 1
f = function( x ){
Text.clear()
Text.putln( window.x + x )
}
f( 3 ) // 4
window の配下にあって最初から用意された変数やオブジェクトを、
ビルトインオブジェクト といいます。
Number、String の他、よく使うところでは、
document、Math、Date 等があります。
例えば Text.put の代替を次のように書くことができます :
p = function( s ){
document.getElementById( "text" ).value += s
}
p( "Hello" )
document は HTML
ドキュメントに関してブラウザが提供する機能( Web API )をまとめたオブジェクトで、
そのメンバに getElementById という関数があります。
この関数は ID で指定された HTML ドキュメントの要素を返します。
今の場合、
"text" という ID を持つ出力用テキストエリアがオブジェクトとして返されます。
そのメンバ変数 value の値、
すなわちテキストエリアの内容に、数値や文字列を追加してやれば、
それが出力されることになるわけです。
Math オブジェクトは数学に関する変数や関数を収容しています。
Text.clear()
pl = function( n ){ Text.putln( n ) }
pl( Math.PI ) // 3.14159... (円周率)
pl( Math.E ) // 2.71828... (自然対数の底)
pl( Math.floor( 2.3 ) ) // 2 (切り捨て)
pl( Math.ceil( 2.3 ) ) // 3 (切り上げ)
pl( Math.round( 4.7 ) ) // 5 (四捨五入)
pl( Math.abs( -1.2 ) ) // 1.2 (絶対値)
pl( Math.sign( -1.2 ) ) // -1 (符号)
pl( Math.sqrt( 2 ) ) // 1.41421... (平方根)
pl( Math.exp( 2 ) ) // 7.38905... (指数関数)
pl( Math.log( 2 ) ) // 0.69314... (自然対数)
pl( Math.log10( 100 ) ) // 2 (常用対数)
pl( Math.sin( 0.3 ) ) // 0.29552... (三角関数)
pl( Math.asin( 0.5 ) ) // 0.52359... (逆三角関数)
pl( Math.random() ) // 0〜1 の間の乱数
pl( Math.max( 3, 4, 5 ) ) // 5 (最大値)
pl( Math.min( 3, 4, 5 ) ) // 3 (最小値)
JS 実行では、加えて、
数値計算やグラフ描画等に便利なオブジェクトを、
ビルトインオブジェクトの形で提供しています。
Infinity、NaN、undefined
JavaScript では 0 で除算してもエラーになりません。
a = 1 / 0
b = 0 / 0
Text.putln( a ) // Infinity
Text.putln( b ) // NaN
Text.putln( typeof a ) // number
Text.putln( typeof b ) // number
Infinity は無限大、
NaN は不定や非数を意味する数値です( Not a Number の略)。
数値とみなせない文字列を数値化しても NaN が返されます。
a = Number( "hello" )
Text.putln( a ) // NaN
NaN は自身と等しくないという風変わりな性質を持つため、
NaN の判定には関数
isNaN を用います。
a = NaN
Text.putln( isNaN( a ) ) // true
一方、定義されていない変数を参照するとエラーになります。
また、定義されていないメンバの値は
undefined になります。
undefined は undefined 型の値です。
a = [ 0, 10, 100 ]
Text.putln( a[ 3 ] ) // undefined
Text.putln( typeof a[ 3 ] ) // undefined
定義されていない変数の型も undefined 型とみなされるので、
このことは変数の有無の判定に用いることができます。
x = 1
Text.putln( typeof x == "undefined" ) // false
Text.putln( typeof y == "undefined" ) // true
Infinity、NaN、undefined、isNaN はビルトインオブジェクトです。
グラフィックス
JS 実行ではサイズ 800×800 のグラフィックスエリア(
Canvas )を用意しています。
その2次元環境(2次元コンテキスト)のオブジェクトは、
ctx = document.getElementById( "canvas" ).getContext( "2d" )
で参照できます。例えば全体を黄色く塗りつぶしたかったら、
ctx = document.getElementById( "canvas" ).getContext( "2d" )
ctx.fillStyle = "yellow"
ctx.fillRect( 0, 0, 800, 800 )
とします。
fillStyle はコンテキストのメンバ変数(プロパティ)で、
塗りつぶしのための色を保持します。
また、fillRect はコンテキストのメンバ関数(メソッド)で、
矩形塗りつぶしを行います。
引数は順に、左上頂点の横座標、縦座標、矩形の横幅、高さです。
線や円を描く場合は、それぞれ次のように書きます。
ctx = document.getElementById( "canvas" ).getContext( "2d" )
// 赤で ( 200, 100 ) から ( 600, 700 ) に線を描く
ctx.strokeStyle = "red"
ctx.beginPath()
ctx.moveTo( 200, 100 )
ctx.lineTo( 600, 700 )
ctx.stroke()
// 色 rgb( 0, 100, 200 ) で中心 ( 400, 200 )、半径 60 の円を描く
ctx.strokeStyle = "rgb( 0, 100, 200 )"
ctx.beginPath()
ctx.arc( 400, 200, 60, 0, 2 * Math.PI )
ctx.stroke()
// 緑で中心 ( 200, 400 )、半径 100 の塗りつぶされた円を描く
ctx.fillStyle = "green"
ctx.beginPath()
ctx.arc( 200, 400, 100, 0, 2 * Math.PI )
ctx.fill()
色 rgb( r, g, b ) において、
引数 r (赤の強さ), g (緑の強さ), b (青の強さ) の値の範囲は、
それぞれ 0〜255 です。
透明度(アルファ値) a を含めて指定する場合は rgba( r, g, b, a ) と書き、
a の値の範囲は 0〜1 で、0 で完全に透明、1 で完全に不透明になります。
また、文字列(テキスト)を描く場合は次のように書きます。
ctx = document.getElementById( "canvas" ).getContext( "2d" )
ctx.fillStyle = "brown" //茶色
ctx.font = "32px sans-serif" //フォントのサイズとファミリー
ctx.textAlign = "left" //基準点の横位置。他、"center", "right"
ctx.textBaseline = "top" //基準点の縦位置。他、"middle", "bottom"
ctx.fillText( "Hello", 200, 300 )
sans-serif はゴシック体っぽいフォントを指定する
論理フォント名 で、
明朝体の場合は serif、
等幅フォントの場合は monospace とします。
ctx.fillText( "Hello", 200, 300 ) により、
基準点を ( 200, 300 ) としてテキスト "Hello" を描きます。
イベント駆動
JavaScript ではキー操作やマウスクリック等の各イベントに対し、
実行する処理を登録できます。
イベントによってプログラムが呼び出される仕組みを、
イベント駆動 (イベントドリブン)といいます。
例えば、キー操作については、
document.onkeydown および
document.onkeyup が実行される関数になります。
このような関数を
イベントハンドラ といいます。
document.onkeydown = function( e ){
Text.clear()
Text.putln( e.key + " のキーが押されました" )
}
document.onkeyup = function( e ){
Text.putln( e.key + " のキーが離されました" )
}
引数 e にはキーイベントの情報が入ったオブジェクトが渡ってきます。
押されたキーのキー名はメンバ変数 key, キーコードはメンバ変数 keyCode
が持っています。
また、一定時間ごとに呼び出される関数の登録もできます(インターバル)。
下は 0.1 秒ごとに数字を出力するコードです。
putFrame = function(){
Text.put( count + " " )
count++
if( count > 20 ) clearInterval( iid )
}
Text.clear()
count = 0
iid = setInterval( "putFrame()", 100 )
setInterval で実行する関数および何ミリ秒ごとに実行するかを指定します。
その返り値はインターバルの ID になるので、
適当な変数名でこれを受け取ります。
インターバルを止める場合は ID を引数として
clearInterval を実行します。
これらの機能を使えば、
アニメーションやゲームを作ることができるでしょう。
ただしイベント駆動を用いたプログラミングには多少のコツが必要です。
下はグラフィックス画面に表示された黒丸(プレイヤー)をカーソルキーにより移動させるコードで、
アクションゲームの基礎(土台)となります。
良かったら参考にしてください。
if( typeof main == "object" ) clearInterval( main.iid )
canvas = {}
canvas.init = function(){
this.e = document.getElementById( "canvas" )
this.ctx = this.e.getContext( "2d" )
}
canvas.clear = function(){
this.ctx.fillStyle = "white"
this.ctx.fillRect( 0, 0, this.e.width, this.e.height )
}
key = {}
key.init = function(){
this.in = {}
document.onkeydown = function( e ){
key.in[ e.key ] = true
}
document.onkeyup = function( e ){
key.in[ e.key ] = false
}
}
player = {}
player.init = function(){
this.x = 400
this.y = 400
this.radius = 15
this.speed = 10
}
player.draw = function(){
var c = canvas.ctx
c.fillStyle = "black"
c.beginPath()
c.arc( this.x, this.y, this.radius, 0, 2 * Math.PI )
c.fill()
}
player.move = function(){
var s = this.speed
if( key.in.ArrowLeft ) this.x -= s
if( key.in.ArrowRight ) this.x += s
if( key.in.ArrowUp ) this.y -= s
if( key.in.ArrowDown ) this.y += s
}
main = {}
main.init = function(){
canvas.init()
key.init()
player.init()
this.iid = setInterval( "main.frame()", 20 )
}
main.frame = function(){
canvas.clear()
player.move()
player.draw()
}
main.init()
このコードではグローバル領域に canvas、key、player、main
という4つのオブジェクト(連想配列)が定義されているだけで、
この他にグローバル変数は一切書かれていません。
OS のデスクトップにファイルが沢山散らかっていたら嫌でしょう?
これと同じことで、
データと機能の集合体をオブジェクトとしてモジュール化(パッケージ化)し、
グローバル変数をできるだけ用いないことが、
オブジェクト指向プログラミングの基本になります。
クラスとインスタンス
JavaScript には数値や文字列がプリミティブな型として用意されていますが、
ベクトルの型は用意されていません。
そこで、2次元のベクトルの型を作ってみたいとします。
それは x 成分と y 成分をメンバ変数に持ち、
さらに大きさ( abs )や加算( add )をメンバ関数として持つものとします。
JavaScript では通常これを次のように実現します。
Vector = function( x, y ){
this.x = x
this.y = y
}
Vector.prototype.abs = function(){
return ( this.x ** 2 + this.y ** 2 ) ** 0.5
}
Vector.prototype.add = function( v ){
return new Vector( this.x + v.x, this.y + v.y )
}
a = new Vector( 1, 4 )
b = new Vector( 3, -1 )
c = a.add( b )
Text.putln( c.x + ", " + c.y + ", " + c.abs() )
new はオブジェクトを生成するためのキーワードで、
空のオブジェクトを作り、
そのオブジェクトから指定された関数を呼び出し、
作られたオブジェクトにメンバ、
__proto__: 指定された関数.prototype
を加えて返します。
例えば、a = new Vector( 1, 4 ) により、a は、
{ x: 1, y: 4, __proto__: Vector.prototype }
というオブジェクトへの参照になります。
このように new によって作られたオブジェクトを
インスタンス (実体)といい、
インスタンスを生成することを
インスタンス化 といいます。
さて、JavaScript では呼び出されたメンバが見つからない場合、
次に
__proto__ により指定された場所を捜索します。
このため Vector.prototype 配下にメンバ関数を与えることで、
それらは Vector のインスタンス全てに共通のメンバ関数
(
インスタンスメソッド) とみなされることになるわけです。
JavaScript の関数は、
実はメンバを有することができ、
オブジェクトの一種とみなされます。
特に prototype は関数生成時に自動的に与えられるオブジェクト型のメンバです。
関数 Vector は、
prototype 配下のメンバも含め、
ベクトルの "型の設計図" を意味するオブジェクトになっています。
このようなオブジェクトを
クラス といいます。
関数 Vector 自体は
コンストラクタ と呼ばれます。
Vector.prototype 配下に様々なメンバ関数を追加していくことで、
本格的なベクトルのクラスを作成することができるでしょう。
( "構造体" という用語を知っている人は、
クラスが構造体の機能の拡張であることがわかるでしょう。)
ビルトインオブジェクトとして用意されたクラスとしては、
例えば日時に関するクラス Date があります。
Text.clear()
d = new Date()
Text.putln( d.getTime() ) // ある瞬間からの経過ミリ秒
Text.putln( d.getFullYear() ) // 年
Text.putln( d.getMonth() ) // 月 (0〜11)
Text.putln( d.getDate() ) // 日 (1〜31)
Text.putln( d.getDay() ) // 曜日 (0〜6)
Text.putln( d.getHours() ) // 時 (0〜23)
Text.putln( d.getMinutes() ) // 分 (0〜59)
Text.putln( d.getSeconds() ) // 秒 (0〜59)
Text.putln( d.getMilliseconds() ) // ミリ秒 (0〜999)
d = new Date() により現在の日時のインスタンスが生成されます。
Date クラスのインスタンスメソッドによりその日時に関する様々な情報を取得できます。
月は 0〜11 に値を取るので注意。0 は1月を意味します。
特定の日時のインスタンスを生成する場合は、
d = new Date( 2017, 3, 26, 9, 41, 0 )
のように書きます。
これは 2017年4月26日9時41分0秒 を意味します。
また、JS 実行においては、複素数のクラス Complex、複素行列のクラス CMatrix
が用意されています。
ラッパーオブジェクト
すでに述べたように、関数はオブジェクトの一種で、
作成時自動的にいくつかのメンバが付与されます。
f = function(){}
Text.putln( f.name ) // f
Text.putln( typeof f.prototype ) // object
Text.putln( f.prototype.constructor.name ) // f
関数のメンバ name は関数名に値を持ち、
関数のメンバ prototype はオブジェクト、
さらにそのメンバ constructor は関数そのものを参照していることが理解できるでしょう。
この機能により、
インスタンスからそれを生成したコンストラクタを知ることができます。
d = new Date()
Text.putln( d.constructor.name ) // Date
ところで、
数値型や文字列型の変数はオブジェクトでなくプリミティブなはずだったのに、
メンバ変数やメンバ関数を持っていることを不思議に思った人がいるかもしれません。
この疑問はもっともで、
実はこれらプリミティブな型は、そのメンバが呼ばれると、
一時的にインスタンスに変換されて扱われます。
このようなインスタンスを
ラッパーオブジェクト といいます( wrap = 包む )。
数値型変数のラッパーオブジェクトのコンストラクタは Number、
文字列型変数のそれは String です。
a = 3
Text.putln( a.constructor.name ) // Number
s = "abc"
Text.putln( s.constructor.name ) // String
例えば toFixed や toExponential は Number.prototype 配下にあり、
それゆえ数値型変数のメンバ関数のように振る舞っていたわけです。
そうすると、Number.prototype 配下に関数を記述することで、
数値型変数のメンバ関数を自作することもできるはずです。
Number.prototype.putln = function(){
Text.putln( this )
}
a = 3
a.putln() // 3 と表示される
ただしビルトインオブジェクトに手を入れることは行儀が悪いことと考えられるので、
このようなコーディングは実際にはすべきではないでしょう。
プリミティブな変数を、少なくとも仮想的にインスタンスとみなせるということは、
「データは全てオブジェクト」と考えることができることを意味します。
この性質は広く
純粋なオブジェクト指向 と呼ばれます。
実際、全てのデータは、
次のようなインスタンス化により生成することができます。
a = new Number( 3 )
s = new String( "taro" )
b = new Boolean( true )
f = new Function( "x", "y", "return x + y" )
r = new Array( "taro", "jiro" )
d = new Object( { name: "taro", age: 14 } )
Function,
Array,
Object はそれぞれ、
関数、配列、連想配列のコンストラクタで、
ビルトインオブジェクトです。
クラスの継承
ベクトルのクラス Vector をすでに作りましたが、
その機能を
継承 (内包)した新しいベクトルのクラス VectorS を作りたい場合、
例えば次のようにします。
Vector = function( x, y ){
this.x = x
this.y = y
}
Vector.prototype.abs = function(){
return ( this.x ** 2 + this.y ** 2 ) ** 0.5
}
Vector.prototype.add = function( v ){
return new this.constructor( this.x + v.x, this.y + v.y )
}
VectorS = function( x, y ){
this.x = x
this.y = y
}
Object.setPrototypeOf( VectorS.prototype, Vector.prototype )
VectorS.prototype.toString = function(){
return "(" + this.x + "," + this.y + ")"
}
v = new VectorS( 1, -2 )
Text.putln( v.abs() )
Text.putln( v.toString() )
ここで、
Object.setPrototypeOf( VectorS.prototype, Vector.prototype )
は、
VectorS.prototype.__proto__ = Vector.prototype
の意味です。
__proto__ の直接的な記述は非推奨であるため、
このようにアクセッサーを用いて書きます。
こうすることで、VectorS のインスタンス(上の例では v )の指定されたメンバが
VectorS.prototype においても見つからない場合、
次に Vector.prototype を捜索することになり、
VectorS は Vector を継承したクラスとみなされるわけです。
このような __proto__ の機構は幾重にも重ねて連鎖させることができ、
プロトタイプチェーン と呼ばれます。
例えば、更に新しいクラスとして VectorSS を作り、
VectorS を継承すれば、
VectorSS は Vector の機能も有するわけです。
関数 Vector.prototype.add において、
コンストラクタを Vector でなく、
一般的に this.constructor と書いていますが、
継承される可能性のあるクラスではこのような記述が望ましいことになります。
プロトタイプチェーンがある場合、
関数 Vector.prototype.add を呼び出すオブジェクトが
Vector のインスタンスとは限らないことに注意。
クラス構文とクラスベース言語
ちなみに近年、
class Vector{
constructor( x, y ){
this.x = x
this.y = y
}
abs(){
return ( this.x ** 2 + this.y ** 2 ) ** 0.5
}
add( v ){
var t = this
return new t.constructor( t.x + v.x, t.y + v.y )
}
}
class VectorS extends Vector{
constructor( x, y ){
super( x, y )
}
toString(){
return "(" + this.x + "," + this.y + ")"
}
}
v = new VectorS( 1, -2 )
Text.putln( v.abs() )
Text.putln( v.toString() )
という構文も用意されました( ES2015 より)。
これを
クラス構文 といいます。
クラス構文は Java、C#、Python 等の
クラスベース の言語に似せた糖衣構文です。
クラス構文ではキーワード
extends によりクラスの継承を行えます。
super は親クラスのコンストラクタを呼び出す関数で、
子クラスのコンストラクタ内で少なくとも一度実行される必要があります。
一般に、クラスベースの言語では、クラスが構文として先にあって、
オブジェクトはクラスによって作られます。
一方、JavaScript は
プロトタイプベース の言語であり、
オブジェクト(連想配列)が先にあって、
クラスはオブジェクトと __proto__ の機構によって実現されます。
構成的で敷居が低いのは
JavaScript のような関数がデータ型のプロトタイプベース言語でしょうが、
近年よく普及しているのはクラスベースの言語です。
クラス構文の導入によって、
JavaScript はプロトタイプベースとクラスベースのハイブリッドに位置付けられることになります。
HTML ファイル
JS 実行を用いず、独自の HTML ファイルを作成したい場合、
メモ帳などのテキストエディタを用いて例えば次のように記し、
文字コード UTF-8、拡張子を .html にして保存すれば良いでしょう。
<!DOCTYPE html>
<html>
<script>
onload = function(){
var e = document.getElementById( "text" )
e.style.fontFamily = "serif"
e.style.fontSize = "28px"
e.style.fontWeight = "bold"
e.style.color = "blue"
e.innerText = "Hello, world!"
}
</script>
<body>
<br>
<div id="text" align="center"></div>
</body>
</html>
<body> 〜 </body> の中身が HTML ドキュメント、
<script> 〜 </script> の中身がスクリプト要素( JavaScript のコード)で、
onload はドキュメント読み込み終了時に呼ばれる関数です。
このような関数は一般に
エントリーポイント と呼ばれ、
イベントハンドラの一種です。
ドキュメント要素配下の style は要素の装飾のためのプロパティを集めたオブジェクトで、
HTML の CSS (カスケーディング・スタイルシート)に対応します。
よってどのようなプロパティがあるかは、
CSS について調べることで分かります。
以下は、シューティングゲームの基礎となる HTML ファイルです。
良かったら参考にしてください。
<!DOCTYPE html>
<html>
<title>Shooting Sample</title>
<meta name="viewport" content="width=device-width">
<script>
canvas = {}
canvas.init = function(){
this.e = document.getElementById( "graphics" )
this.width = this.e.width
this.height = this.e.height
this.ctx = this.e.getContext( "2d" )
this.bgColor = this.e.style.backgroundColor
}
canvas.clear = function(){
this.ctx.fillStyle = this.bgColor
this.ctx.fillRect( 0, 0, this.width, this.height )
}
key = {}
key.init = function(){
this.in = {}
this.buf = ""
document.onkeydown = function( e ){
if( e.key.substr( 0, 5 ) == "Arrow" ) e.preventDefault()
key.in[ e.key ] = true
key.buf = e.key
}
document.onkeyup = function( e ){
key.in[ e.key ] = false
}
}
key.get = function(){
var buf = this.buf
this.buf = ""
return buf
}
sound = {}
sound.init = function(){
this.mode = "off"
}
sound.turnOn = function(){
if( this.mode == "on" ) return
if( this.ctx != undefined ) return
this.ctx = new AudioContext()
this.osc = new OscillatorNode( this.ctx )
this.gain = new GainNode( this.ctx )
this.osc.type = "square"
this.gain.gain.value = 0
this.osc.start()
this.mode = "on"
}
sound.turnOff = function(){
this.mode = "off"
}
sound.play = function( volume, interval, frequencyArray ){
if( this.mode != "on" ) return
if( this.iid != undefined ) clearInterval( this.iid )
this.arr = frequencyArray
this.volume = volume
this.count = 0
this.frame()
this.iid = setInterval( "sound.frame()", interval )
}
sound.frame = function(){
if( this.arr[ this.count ] == undefined ){
this.gain.gain.value = 0
clearInterval( this.iid )
} else{
this.gain.gain.value = this.volume / 1000
this.osc.frequency.value = this.arr[ this.count ]
this.osc.connect( this.gain ).connect( this.ctx.destination )
}
this.count++
}
charaImage = function( dotWidth, dotHeight, colorList, dotPattern ){
this.colorList = colorList
this.dotPattern = dotPattern
this.dotWidth = dotWidth
this.dotHeight = dotHeight
this.width = dotWidth * dotPattern[ 0 ].length
this.height = dotHeight * dotPattern.length
}
charaImage.prototype.draw = function( x, y ){
var ptn = this.dotPattern
var iMax = ptn[ 0 ].length, jMax = ptn.length
var eps = 0.25
var x0 = x - this.width / 2 - eps, y0 = y - this.height / 2 - eps
var dw = this.dotWidth, dh = this.dotHeight
var dw2 = dw + 2 * eps, dh2 = dh + 2 * eps
var c = canvas.ctx
var j, i
for( j = 0; j < jMax; j++ )
for( i = 0; i < iMax; i++ ){
if( ptn[ j ][ i ] > 0 ){
c.fillStyle = this.colorList[ ptn[ j ][ i ] - 1 ]
c.fillRect( x0 + i * dw, y0 + j * dh, dw2, dh2 )
}
}
}
player = {}
player.init = function(){
this.x = canvas.width / 2
this.y = canvas.height / 4 * 3
this.speed = 6
this.image = new charaImage( 4, 4,
[ "white", "mediumseagreen", "red" ],
[
[0,0,0,0,1,0,0,0,0,],
[0,0,0,0,1,0,0,0,0,],
[0,0,0,0,1,0,0,0,0,],
[0,0,0,1,2,1,0,0,0,],
[0,0,1,1,2,1,1,0,0,],
[0,1,1,1,3,1,1,1,0,],
[1,1,1,1,1,1,1,1,1,],
[0,0,0,1,1,1,0,0,0,],
[0,0,1,1,0,1,1,0,0,],
]
)
var w = this.image.width
var h = this.image.height
this.topLine = h / 2
this.bottomLine = canvas.height - h / 2
this.leftLine = w / 2
this.rightLine = canvas.width - w / 2
}
player.update = function(){
var s = this.speed
if( key.in[ "ArrowLeft" ] ) this.x -= s
if( key.in[ "ArrowRight" ] ) this.x += s
if( key.in[ "ArrowUp" ] ) this.y -= s
if( key.in[ "ArrowDown" ] ) this.y += s
if( this.x < this.leftLine ) this.x = this.leftLine
if( this.x > this.rightLine ) this.x = this.rightLine
if( this.y < this.topLine ) this.y = this.topLine
if( this.y > this.bottomLine ) this.y = this.bottomLine
var k = key.get()
if( k.length > 0 ) sound.turnOn()
if( k == "z" || k == " " ){
bullet.shoot( this.x, this.y - this.image.height / 2 )
}
}
player.draw = function(){
this.image.draw( this.x, this.y )
}
bullet = {}
bullet.init = function(){
this.speed = 10
this.image = new charaImage( 2, 2,
[ "red", "yellow" ],
[
[0,1,1,0,],
[1,2,2,1,],
[1,2,2,1,],
[0,1,1,0,],
]
)
this.max = 10
this.wait = 6
this.count = 0
this.no = []
for( var i = 0; i < this.max; i++ ){
this.no[ i ] = { x: 0, y: 0, exist: false }
}
this.next = 0
}
bullet.shoot = function( x, y ){
if( this.count < this.wait ) return
if( this.no[ this.next ].exist ) return
var n = this.no[ this.next ]
n.x = x
n.y = y
n.exist = true
sound.play( 20, 20, [ 300, 400, 450 ] )
this.next = ( this.next + 1 ) % this.max
this.count = 0
}
bullet.update = function(){
for( var i = 0; i < this.max; i++ ){
this.no[ i ].y -= this.speed
if( this.no[ i ].y < 0 ) this.no[ i ].exist = false
}
this.count++
}
bullet.draw = function(){
for( var i = 0; i < this.max; i++ ){
if( this.no[ i ].exist ) this.image.draw( this.no[ i ].x, this.no[ i ].y )
}
}
main = {}
main.init = function(){
canvas.init()
key.init()
sound.init()
player.init()
bullet.init()
this.count = 0
this.iid = setInterval( function(){ main.frame() }, 20 )
}
main.frame = function(){
this.count++
player.update()
bullet.update()
canvas.clear()
player.draw()
bullet.draw()
this.putCount()
}
main.putCount = function(){
var ctx = canvas.ctx
ctx.fillStyle = "white"
ctx.font = "bold 16px sans-serif"
ctx.fillText( this.count, 15, 25 )
}
onload = function(){ main.init() }
</script>
<body style="width: 500px; margin: 0px auto; text-align: center;">
<canvas width=500 height=500
id="graphics" style="background-color: midnightblue;"></canvas>
<div style="margin: 10px; font-weight: bold;">
■ Shooting Sample ■<br>
Move ... Cursor keys<br>
Shoot ... [Z] or space key<br>
</div>
</body>
</html>
ここで、canvas, key, sound, charaImage
はゲームのための汎用的な(他で使いまわしが可能な)オブジェクトで、
ライブラリとみなされる部分です。
すなわち、このコードを改良してブラウザゲームを作る場合、
これらオブジェクトに手をつける必要はほとんどないと考えられます。
言い換えるなら、player = {}
以下のコードがこのプログラムの本体とみなされます。
汎用性や可読性を無視するなら、
同じプログラムをもっと短いコードで記すことも可能ですが、
機能をどんどん追加していくことが前提となる大きなプログラムにおいては、
コードの短さより汎用性や可読性が重要になってきます。
参考記事
MDN web docs - JavaScript
Microsoft Docs - JScript