第 8 回 Javascript

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

8-1. Java v.s. Javascript

Sun Microsystems は 組み込み機器をコントロールするため、 C++ の欠 点を解消する言語としてJavaを開発しました。 誕生時(1995年)、WWWの大流行もあり、Java対応のブラウザである HotJava と共に発表され、「動くホームページ」というような話題で、組 込系とは異なるユーザからも注目を浴びました。

同じく1995年にNetscape Communications 社のブラウザ Netscape Navigator 2.0 に LiveScript というブラウザのアプリケーション用の言語が搭載されました。 これも「動くホームページ」という話題で注目されました。 そのため、Sun Microsysems と協調して名前を JavaScript としました。 Microsoft は対抗して Internet Explorer 3.0 で、似たような JScript とい う言語を出し、ほぼ Netscape Navigator 2.0 とカタログ上では同じような機能 を実現しました。

Internet Explorer と Netscape Navigator のブラウザ戦争は、結局 Netscape Navigator の負けで終わってしまいました。 Netscape 社は最新ブラウザをオープンソースとして公開して、 Mozilla Foundation に引き継がれました。 2004年に Firefox ブラウザが発表され、一定のシェアを維持するブラウザに なりました。

Java はコンパイラ言語で、Sun Microsystems で開発が主導されました。

発表年 主要なバージョン 機能
1995年 Java 1.0 アプレット
1998年 Java2 (1.2) List, Map, Swing
2002年 Java1.4 正規表現など
2004年 Java5(1.5) ジェネリックス,オートボクシング、Enum, StringBuilder, Scanner
2014年 Java8(1.8) ラムダ式、FunctionalInterface, Stream

一方、Javascript はインタプリタ言語なので、プログラムを実行するに はインタプリタというプログラムが必要です。 これは、様々なブラウザで独自に実装されています。 さらに、近年は通常のプログラミング言語としても使用できるように、 Node.js のような単体のインタプリタも発表されています。

発表年 主要なバージョン 機能
1995年 LiveScript → JavaScript Netscape Navigator 2.0
1996年 JScript Internet Explorer 3.0
1997年 ECMAScript 1 標準化
2015年 ECMAScript 6(ES6, ES2015) Class, Map, Set, foreach, let, const など大幅な文法、機能変更

8-2. オブジェクト指向言語の特性

第一級オブジェクト

First-class object は、差別用語でもある First-class citizen から生 まれた用語です。被差別階級を意味する「二等市民」に対する言葉で、「全 ての権利が与えられている」ということを指します。 一般に、整数型というデータ型はあらゆるプログラミング言語において、 自由に使用できます。 一方で、関数を使用するには、特殊な構文である関数宣言をしなければな らないプログラミング言語が多々あり、整数を使用するようには使えない ことが多々あります。

第一級オブジェクトに求められる機能として次のようなことがあります。

  1. 変数で管理できる
  2. 式として定数が記述できる
  3. 名前がなくても管理できる
  4. 等価性の比較ができる
  5. 関数などに渡せる
  6. 関数から戻せる

このような機能が重要と注目されるような、ありふれた処理として、並び替 えがあります。 要素を左から右へ並び替える時、2つの要素のどちらが左かを決める仕組 みが用意できれば、並び替えをするための高速なアルゴリズムとして、ク イックソート、マージソート、ヒーブソートなどがあります。 つまり、数を並べ替えるのでも、大きい順、小さい順でも同じアルゴリズ ムが使えるということです。 そのため、大きい順、小さい順など、さらに、約数の少ない順、1を多く 含む順など、様々な「順序」が使えるように、並び替えのプログラムを書 けると良いです。 つまり、「順序」を並び替えのプログラムに渡すことが重要です。

古くは、COBOL の SORT では ASCENDING, DESCENDING と2種類の並べ替え のみを指定するようになっていました。 これは、並べ替えのプログラムの中に2種類の比較法が使われていて、さ らに変数による動的な並べ替えの切り替えはできないようになっていまし た。

C++では比較演算子の < を再定義できるので、オブジェクトに比較を 与えて、その順の並び替えが単純に < を使った式で並べ替えができます。


class A {
 public:
  bool operator<(const A& b);
};

C言語では関数ポインタを使い、ビルドイン関数に比較関数を渡して、並び替えを行います。


int cmp(const void *a, const void *b){  
  ...
  return result;
}  
../
   qsort(array, num, sizeof(array[0]), cmp);

Java では java.util.Comparator インターフェイスを実装して、 比較子 を作成します。


import java.util.Comparator;
import java.util.Arrays;
public class Cmp implements Comparator<A>{      
  @override
  public int compare(A o1, A o2){
    ...
    return result;
  }
}

    A[] sortedArray = Arrays.sort(array, new Cmp());

さらに匿名クラスといい、一回だけ使用する名前をつけないオブジェクト を作成することができます。


import java.util.Arrays;
import java.util.Comparable;

A[] sortedArray = Arrays.sort(array, new Comparable<A>{
          @Override int compare(o1, o2){ ... return result;}});

Java 8 からはラムダ式と呼ばれる、関数オブジェクトが定義 できるようになりました。 ラムダ式とは、式の前に仮引数リストを付与したもので、これで無名の関 数を表すことができます。 例えば、入力 x に対して、出力 x+1 を返す関数をラムダ式で表すと、歴 史的に次のように表します。

λ x . ( x + 1 )

この場合λは仮引数リストを表しています。 Java の場合、任意の関数オブジェクトを作れるわけではありませんが、 指定されているオブジェクトの型である java.util.Comparable が FunctionalInterface と呼ばれる、未定義なメソッドがひとつだけの interface なので、 そのメソッドの型に合致する様に関数オブジェクトが作られます。


import java.util.Arrays;

A[] sortedArray = Arrays.sort(array, (o1,o2)->{ ... return result;});

Javascriptでは始めからラムダ式が使え、関数は式の中で記述できます。 さらに、近年簡略表現ができるように文法が改良されました。


x.sort(function(a,b){... return result;}); //legacy

y.sort((a,b)=>{ ... return result;}); //modern

なお、C++ でも C++11からはラムダ式が使え、C++14からは型推論も使えま す。


std::sort(x.begin(),x.end(),
          [](auto const& a, auto const& b){ ... return result;});

文法

Javascript は C 言語に似た文法を持っています。 従って、 if, while, for などの構造化プログラミング言語的な記述が可 能になっています。 しかし、オブジェクト指向のプログラミング言語としてはそのような構造 は必須ではありません。 SmallTalk という言語では、実際に構造化プログラミングの持つ構造は持っ ていません。

また、型として、具体的な値のみを持つプリミティヴ型と、 オブジェクト型があります。このうち、プリミティヴ型は必 須ではありません。 SmallTalk では0 や、 true もオブジェクトで、ユニークインスタンスのリテラルになって います。

実行前(コンパイル時)に全ての変数の型が確定しているのを 静的型付け言語と言い、 そうではない言語は動的型付け言語と言います。 C++やJavaは分割コンパイルが可能なのに、静的型付け言語なので、プロト タイプ宣言やimport などが必要で、ジェネリックスなどで複雑な型宣言が 必要です。 一方、SmallTalk, Objective C, JavaScript は動的型付け言語なので、変 数宣言は変数名だけで良いです。 動的型付け言語はプログラムは書きやすいですが、型違いによるプログラム ミスはそのプログラムが実行されるまで気づかないので、プログラムが完成 する過程で、どこが楽かが異なります。

API

BASIC言語では機能の全てはコマンドで実現されているため、コマンドの数 が言語の機能になります。 また、拡張もコマンドの追加により実現されます。

一方、オブジェクト指向言語は文法が非常にシンプルです。 なお、左辺の変数代入は不要なら省略できます。


変数 = 変数.メソッド(実引数); //C言語系
変数 ← 変数 メソッド 実引数.  "SmallTalk"

特に SmallTalk では論理値 true, false に ifTrue などのメソッドがあ り、 さらに、[](角括弧)内のプログラムは無名の手続きとして引数になり得ます。


(x < 3) ifTrue [ y ← 2].

つまり、オブジェクト指向では様々な機能は全て後付のプログラム(API)で 実現、拡張可能です。 逆に、オブジェクト指向言語を使いこなすには、APIの読解が必要になります。

一般的に、入出力も API で実現されます。 OSから実行されるのを前提としているプログラミング言語の場合、 標準入出力 とのやり取りのAPIは標準で付属してきます。 C言語の場合 stdio、Javaの場合 System.out など。 一方、ブラウザで動作することが前提の JavaScript の場合、標準出力自体 は前提ではありません。 JavaScript の場合文字を出力するだけでも、実行環境に依存することが、 後の演習で分かると思います。

副作用、変数宣言

プログラミングの始めに習う ループ変数は、ループの度に値を変化させ、ループを抜け出すための条件 を判定するのに使います。 また、配列変数の合計を求めるのに、合計値を計算するための変数を用意 して、繰り返し値を足しこみます。 しかし、そのような特定の定石以外を考えると、変数の値を頻繁に変化さ せることは余りありません。 回数を数えることと、集計をする以外は、 ループの中では、毎回計算をやり直すことが多く、ループの先頭で初期化 することも多いです。

Haskel のような関数型言語では変数の再代入は禁止されています。 また、一般のプログラムにおいて変数の再代入を許すと、その値の追跡や把握 が必要になるため、プログラムを完成させる手間が増えます。 そのため、C言語やJavaには再代入を禁止させるための変数宣言の修飾子 (const, final)があります。 Legacy JavaScript では型指定はしないので、変数宣言は var だけです。 宣言するとグローバルに使用できるため、 ループの中で宣言した変数がループを抜けても参照、再代入できます。 Modern JavaScript では更に let というスコープ内のみ有効な変数宣言 と、 const という再代入禁止の変数(定数)宣言が追加されました。

8-3. Javascriptの特徴

JavaScriptのデータ型

JavaScriptのObject型と関数

JavaScript の特徴として、以下があります。

  1. 連想配列と関数が第一級オブジェクトである
  2. Object という型が連想配列である
  3. 変数 x が参照する連想配列のキー y に対応する関数を呼び出す のが、オブジェクト指向の構文である x.y()

連想配列とは、変数名と文字列の対で値を取り出すもので、通常は配列と同 じ構文で使用します。 つまり 変数名[文字列] で値を取り出します。 JavaScript では、この構文と同等の表現として 変数名.名前 が使用できます。 名前は通常の変数名などと同等なので、数は含まれません。 一方で、角括弧を使用する場合は、文字列は式として与えます。

この x.y() を実現させる構文は次のようになります。


var x = {y: function(){}};  

var x = {};
x['y']=function(){};

var x = {};
x.y=function(){};

例8-1


var std={print:function(x){
    if(typeof console !== 'undefined'){
        console.log(x);
    }else  if(typeof WScript !== 'undefined'){
        WScript.Echo(x);
    }   
}};
var a = {"bc": 123, "de": function(){ return 456 }};
var x="bc";
var y="de";

std.print(a["bc"]);
std.print(a[x]);
std.print(a["b"+"c"]);
std.print(a.bc);
a[x]=a.bc+1;
std.print(a["b"+"c"]);

std.print(a["de"]());
std.print(a[y]());
std.print(a["d"+"e"]());
std.print(a.de());

さらに Modern JavaScript では、次のようにラムダ式も使用できます。


const x = {y: ()=>{}};  

const  x = {};
x['y']=function(){};

const x = {};
x.y=()=>{};

但し、この function で作る関数と、ラムダ式ではスコープが異なります。 オブジェクト指向でオブジェクトをthis で参照できるのは function だけです。

例8-2


var std={print:function(x){
    if(typeof console !== 'undefined'){
        console.log(x);
    }else  if(typeof WScript !== 'undefined'){
        WScript.Echo(x);
    }   
}};

var a={};
a.b=1;
a.c=function(){return this.b;};
a.d=()=>{return this.b;};
a.e=function(){return 2;};
a.f=()=>{return 2;};
std.print(a.c());
std.print(a.d());
std.print(a.e());
std.print(a.f());

8-4. Javascriptを使うには

推奨環境

JavaScript の開発環境として Node.js+Visual Studio Codeを推奨します。 但し、 Windows では Legacy でよければ、 cscript でも JavaScript のプログラムを実行できます。

Macintosh でも Node.js + Visual Studio Code を推奨します。 ただし、Mac で Legacy でよければ、アプリケーション→ユーティリティにあ るスクリプトエディタで開発ができます。 なお、Console.log で情報を表示するために、メニューで「表示→ログを表示」 を選択して下さい。

インストール(Windows,Macとも)

  1. nodejs で検索して、自分の環境に合わせて、ダウンロード、インストー ルする
  2. Visual Studio Code を検索して、自分の環境に合わせて、ダウンロー ド、インストールする

Eclipseの利用

Eclipse は、Node.js をインストールした後で、 Eclipse のWeb の開発環 境を次のようにインストールすれば開発することできる。

  1. Help→Install New Software... を選択
  2. Work with で -- All Available Sites -- を選択する。
  3. その下の検索欄に web を入れる
  4. Web, XML, Java EE and OSGI Enterprise Development に属する全てのソ フトウェアをインストールするため、その項目名をチェックし、Next ボタ ンを押した後、メッセージに従ってインストール、Eclipse の再起動をする。

但し、現状では実行が完了した後で何らかのプロセスが37%で実行中のまま になるというバグが存在する。

動くプログラムを作るには

Java は主要な開発元が Sun Microsystems や Oracle なので、ドキュメン トは開発元のを参照して作り、開発元のコンパイラを使ってコンパイルし、 開発元のランタイムを使用して動作確認をすればよかったです。 実際のユーザは異なるバージョンのランタイムを使用することも想定される が、建前上は動作することになっています。 一方で、ドキュメントに違反していても必ずしもエラーは生じず、動作することもありえますが、このような実装をすると将来のバージョンで動作不能になることもあります。 日本社会では、特定のランタイムのみでしか動作を保証しないプログラムを官 公庁が公開し、セキュリティを無視して、古いランタイムを動作させるよう に迫っていた事例もありました。

JavaScript は大元の開発元であった Netscape Communication が既に無く、 当初はよく似た別のものである JScript と混用されました。 インタプリタ言語なので、実行に使用されるインタプリタは多種多様です。 単一環境での動作確認は、その環境での動作を保証するだけに過ぎません。 そのため、ブラウザ戦争の時も互換性が問題になり、 ECMAが 1997年より規格化しました。 但し、規格化されたものが全て動作するわけではありません。 規格がおかしい場合もあれば、インタプリタ側で実装されていなかったり、 バグが含まれている場合もあります。

自分が書いたプログラムが相手の環境で動作するには、バグだらけの環境 は無視するとしても、まともな環境なら動くようなプログラムにする必要 があります。 そのためには、基本的には規格書に準拠したプログラムを書くべきです。 但し、規格書に載っていても実装状況は調べるべきで、トラブルが起きる ような規格はさせるべきだと思います。

8-5. 演習問題

画面に情報を表示するのに、以下のプログラムを共通に使用することとします。 そのために、プログラムの冒頭に常に置くこととします。 なお、出力の抽象化に興味が無く、複数のJavascriptを取り扱う必要が無い場 合は、console.log などをそのまま使って解答して良いです。


var std={print:function(x){
    if(typeof console !== 'undefined'){
        console.log(x);
    }else  if(typeof WScript !== 'undefined'){
        WScript.Echo(x);
    }   
}};

演習8-1

二次方程式を3つ要素を持つ配列で表すことを考える。 配列を受け取って、それを表示する関数 printNiji(a) を書きなさい。 但しa[0]は定数項、a[1]は一次の係数、a[2]は二次の係数とする。

なお、文字列は Java と同様に + 演算子で結合でき、さらに文字列と数 値を + 演算子で結合すると、全て文字列に変換して結合されます。

テストプログラム


var std={print:function(x){
    if(typeof console !== 'undefined'){
        console.log(x);
    }else  if(typeof WScript !== 'undefined'){
        WScript.Echo(x);
    }   
}};

function printNiji(a){
// ここを書く
}

var mondai=[[2,3,1],[4,4,1],[1,1,1]];

for(var i=0; i<mondai.length; i++){
    printNiji(mondai[i]);
}
出力例
1x^2+3x+2=0
1x^2+4x+4=0
1x^2+1x+1=0

演習8-2

二次方程式を3つ要素を持つ配列で表すことを考える。 配列を受け取って、それの解を表示する関数 printNijiAns(a)を書きなさい。 但し配列の 0 番目は定数項、 1 番目は一次の係数、2番めは二次の係数とする。

なお、平方根は Math.sqrt(x) で求める

テストプログラム


var std= ...;

function printNiji(a){
// 演習8-1の解
}

function printNijiAns(a){
// ここを書く
}

function printNijiKaihou(a){
    printNiji(a);
    std.print("の解は");
    printNijiAns(a);
}

var mondai=[[2,3,1],[4,4,1],[1,1,1]];

for(var i=0; i<mondai.length; i++){
    printNijiKaihou(mondai[i]);
}
出力例
1x^2+3x+2=0
の解は
-1, -2
1x^2+4x+4=0
の解は
-2
1x^2+1x+1=0
の解は
-0.5±0.8660254037844386i

演習8-3

二次方程式のオブジェクト niji を考える。 niji.coef に配列を代入すると、 niji.desc() でその二次方程式を表す文 字列を返し、 niji.ans() でその二次方程式の解を表す文字列を返すように、niji を作成 しなさい。

なお、オブジェクトを作るには var niji={}; と宣言する。 また、関数オブジェクトでオブジェクトを参照するには function を用い、 this を使用する。 定数項の値を参照するには this.coef[0] とする。

テストプログラム


var std=...;

var niji={};
niji.desc= /* ここを書く */ ;
niji.ans=  /* ここを書く */ ;

var mondai=[[2,3,1],[4,4,1],[1,1,1]];

for(var i=0; i<mondai.length; i++){
    niji.coef=mondai[i];
    std.print(niji.desc());
    std.print(niji.ans());
}
出力例
1x^2+3x+2=0
-1, -2
1x^2+4x+4=0
-2
1x^2+1x+1=0
-0.5±0.8660254037844386i

8-6. 演習解答

演習8-1

二次方程式を3つ要素を持つ配列で表すことを考える。 配列を受け取って、それを表示する関数 printNiji(a) を書きなさい。 但しa[0]は定数項、a[1]は一次の係数、a[2]は二次の係数とする。


function printNiji(a){
    std.print(a[2]+"x^2+"+a[1]+"x+"+a[0]+"=0");
}

演習8-2

二次方程式を3つ要素を持つ配列で表すことを考える。 配列を受け取って、それの解を表示する関数 printNijiAns(a)を書きなさい。 但し配列の 0 番目は定数項、 1 番目は一次の係数、2番めは二次の係数とする。


function printNijiAns(a){
    var d=a[1]*a[1]-4*a[0]*a[2];
    if(d>0){
        std.print((-a[1]+Math.sqrt(d))/2/a[2]+", "+(-a[1]-Math.sqrt(d))/2/a[2]);
    }else if(d==0){
        std.print(-a[1]/2/a[2]);
    }else{
        std.print(-a[1]/2/a[2]+"±"+Math.sqrt(-d)/2/a[2]);
    }
}

演習8-3

二次方程式のオブジェクト niji を考える。 niji.coef に配列を代入すると、 niji.desc() でその二次方程式を表す文 字列を返し、 niji.ans() でその二次方程式の解を表す文字列を返すように、niji を作成 しなさい。


var niji={};
niji.desc=function(){return this.coef[2]+"x^2+"+this.coef[1]+"x+"+this.coef[0]+"=0";}
niji.ans=function(){
        var d=this.coef[1]*this.coef[1]-4*this.coef[0]*this.coef[2];
        if(d>0){
            return (-this.coef[1]+Math.sqrt(d))/2/this.coef[2]+", "+(-this.coef[1]-Math.sqrt(d))/2/this.coef[2];
        }else if(d==0){
            return ""+(-this.coef[1]/2/this.coef[2]);
        }else{
            return -this.coef[1]/2/this.coef[2]+"±"+Math.sqrt(-d)/2/this.coef[2];
        }
    };

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科